diff --git a/__pycache__/__init__.cpython-311.pyc b/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index a838ab3..0000000 Binary files a/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/build/__pycache__/sync_version.cpython-311.pyc b/build/__pycache__/sync_version.cpython-311.pyc new file mode 100644 index 0000000..323d8d7 Binary files /dev/null and b/build/__pycache__/sync_version.cpython-311.pyc differ diff --git a/build/features.json.backup b/build/features.json.backup deleted file mode 100644 index d71fe6e..0000000 --- a/build/features.json.backup +++ /dev/null @@ -1,67 +0,0 @@ -{ - "features": { - "basic": { - "name": "Basic Text Generation", - "description": "Core text texture generation functionality", - "enabled_by_default": true, - "components": [ - "core.text_processor", - "core.generation_engine", - "ui.panels.basic" - ] - }, - "preview": { - "name": "Live Preview", - "description": "Real-time preview of text textures", - "enabled_by_default": true, - "components": ["ui.preview", "core.text_fitting"] - }, - "advanced_styling": { - "name": "Advanced Styling Options", - "description": "Extended styling and customization options", - "enabled_by_default": false, - "components": ["ui.panels.advanced", "core.advanced_styling"] - }, - "normal_maps": { - "name": "Normal Map Generation", - "description": "Generate normal maps from text", - "enabled_by_default": false, - "components": ["core.normal_maps", "ui.panels.normal_maps"] - }, - "presets": { - "name": "Preset Management", - "description": "Save and load text style presets", - "enabled_by_default": false, - "components": [ - "presets.storage_system", - "operators.preset_ops", - "ui.panels.presets" - ] - }, - "batch_processing": { - "name": "Batch Processing", - "description": "Process multiple text textures at once", - "enabled_by_default": false, - "components": ["operators.batch_ops", "ui.panels.batch"] - } - }, - "version_configs": { - "free": { - "enabled_features": ["basic", "preview"], - "max_texture_size": 1024, - "max_concurrent_operations": 1 - }, - "full": { - "enabled_features": [ - "basic", - "preview", - "advanced_styling", - "normal_maps", - "presets", - "batch_processing" - ], - "max_texture_size": 4096, - "max_concurrent_operations": 4 - } - } -} diff --git a/build/file_exclusions.json b/build/file_exclusions.json index 605465a..cb04e9f 100644 --- a/build/file_exclusions.json +++ b/build/file_exclusions.json @@ -6,11 +6,11 @@ "description": "Free version - core text texture generation only", "exclude_files": [ "core/normal_maps.py", - "operators/preset_ops.py", + "presets/", "operators/utility_ops.py", - "presets/" + "operators/preset_ops.py" ], - "exclude_patterns": ["*batch*", "*advanced*"], + "exclude_patterns": ["*batch*", "*advanced*", "*overlay*", "*utility*"], "max_texture_size": 1024, "max_concurrent_operations": 1 }, @@ -25,6 +25,10 @@ "feature_to_files_mapping": { "normal_maps": ["core/normal_maps.py"], "presets": ["presets/", "operators/preset_ops.py"], - "batch_processing": ["operators/utility_ops.py"] + "batch_processing": ["operators/utility_ops.py"], + "image_overlays": ["operators/utility_ops.py"], + "basic_utilities": ["operators/utility_ops.py"], + "advanced_utilities": ["operators/utility_ops.py"], + "advanced_styling": [] } } diff --git a/build/sync_version.py b/build/sync_version.py index 92852f1..56a448a 100755 --- a/build/sync_version.py +++ b/build/sync_version.py @@ -66,6 +66,12 @@ def create_template_variables(config: Dict, version: str, version_type: str = "f # Get version-specific configuration version_config = exclusions_config['version_configs'][version_type] + # Define features based on version type + if version_type == "free": + enabled_features = ["basic", "preview", "image_overlays", "basic_utilities"] + else: # full version + enabled_features = ["basic", "preview", "image_overlays", "basic_utilities", "advanced_styling", "normal_maps", "presets", "batch_processing", "advanced_utilities"] + # Parse Blender version blender_version = config['project']['blender_version_min'] blender_parts = blender_version.split('.') @@ -98,6 +104,7 @@ def create_template_variables(config: Dict, version: str, version_type: str = "f 'BLENDER_VERSION_MINOR': str(blender_minor), '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']) } diff --git a/build/templates/constants.py.template b/build/templates/constants.py.template index bf08e10..75d1d23 100644 --- a/build/templates/constants.py.template +++ b/build/templates/constants.py.template @@ -2,26 +2,39 @@ Constants and metadata for the Text Texture Generator addon. """ +# Version information +VERSION_TYPE = "{{VERSION_TYPE}}" # "free" or "full" + bl_info = { - "name": "{{PROJECT_NAME}}", + "name": "{{PROJECT_NAME}}" + (" Free" if VERSION_TYPE == "free" else " Full"), "author": "{{PROJECT_AUTHOR}}", "version": ({{VERSION_MAJOR}}, {{VERSION_MINOR}}, {{VERSION_PATCH}}), "blender": ({{BLENDER_VERSION_MAJOR}}, {{BLENDER_VERSION_MINOR}}, {{BLENDER_VERSION_PATCH}}), "location": "{{PROJECT_LOCATION}}", - "description": "{{PROJECT_DESCRIPTION}}", + "description": "{{PROJECT_DESCRIPTION}}" + (" - Free Version (Limited Features)" if VERSION_TYPE == "free" else " - Full Version"), "category": "{{PROJECT_CATEGORY}}", } -# Version information -VERSION_TYPE = "{{VERSION_TYPE}}" # "free" or "full" +# Feature configuration based on version type +ENABLED_FEATURES = ["basic", "preview"] if VERSION_TYPE == "free" else [ + "basic", "preview", "presets", "normal_maps", "batch_processing", + "image_overlays", "basic_utilities", "advanced_utilities", "advanced_styling", + "stroke_effects", "blur_effects", "shadow_glow_effects", "shader_generation" +] # Version-specific limits (applied at build time through file exclusions) def get_version_limits(): """Get version-specific limits.""" - return { - "max_texture_size": {{MAX_TEXTURE_SIZE}}, - "max_concurrent_operations": {{MAX_CONCURRENT_OPERATIONS}} - } + 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 + } def is_free_version(): """Check if this is the free version.""" @@ -29,4 +42,118 @@ def is_free_version(): def is_full_version(): """Check if this is the full version.""" - return VERSION_TYPE == "full" \ No newline at end of file + return VERSION_TYPE == "full" + +# UI Helper Functions for Version Display +def get_version_badge_text(): + """Get version badge text for UI display.""" + return "FREE" if is_free_version() else "PRO" + +def get_version_badge_icon(): + """Get appropriate icon for version badge.""" + return 'GIFT' if is_free_version() else 'STAR' + +def get_upgrade_hint_text(feature_name): + """Get contextual upgrade hint text for specific features.""" + if not is_free_version(): + return "" + + return f"Upgrade to Pro version to access {feature_name}" + +# Feature availability functions +def has_feature(feature_name): + """Check if a specific feature is available.""" + return feature_name in ENABLED_FEATURES + +def has_image_overlays(): + """Check if image overlay features are available""" + return has_feature("image_overlays") + +def has_basic_utilities(): + """Check if basic utility operations are available""" + return has_feature("basic_utilities") + +def has_presets(): + """Check if preset features are available""" + return has_feature("presets") + +def has_normal_maps(): + """Check if normal map generation is available""" + return has_feature("normal_maps") + +def has_batch_processing(): + """Check if batch processing is available""" + return has_feature("batch_processing") + +def has_advanced_utilities(): + """Check if advanced utility operations are available""" + return has_feature("advanced_utilities") + +def has_advanced_styling(): + """Check if advanced styling options are available""" + return has_feature("advanced_styling") + +def has_stroke_effects(): + """Check if stroke effects are available""" + return has_feature("stroke_effects") + +def has_blur_effects(): + """Check if blur effects are available""" + return has_feature("blur_effects") + +def has_shadow_glow_effects(): + """Check if shadow and glow effects are available""" + return has_feature("shadow_glow_effects") + +def has_shader_generation(): + """Check if shader generation is available""" + return has_feature("shader_generation") + +def get_max_resolution(): + """Get maximum texture resolution based on version""" + limits = get_version_limits() + return limits.get("max_texture_size", 1024) + +def should_show_feature_lock(feature_name): + """Check if feature lock indicator should be shown.""" + # Map feature names to their availability functions + feature_checks = { + 'presets': has_presets, + 'normal_maps': has_normal_maps, + 'batch_processing': has_batch_processing, + 'advanced_utilities': has_advanced_utilities, + 'advanced_styling': has_advanced_styling, + 'image_overlays': has_image_overlays, + 'basic_utilities': has_basic_utilities, + 'stroke_effects': has_stroke_effects, + 'blur_effects': has_blur_effects, + 'shadow_glow_effects': has_shadow_glow_effects, + 'shader_generation': has_shader_generation + } + + if is_full_version(): + return False + + # For free version, show lock if feature is not available + if feature_name in feature_checks: + return not feature_checks[feature_name]() + + return True # Show lock for unknown features + +def get_feature_availability_text(): + """Get text describing current feature availability.""" + if is_free_version(): + return "Free version - Basic features available" + return "Pro version - All features available" + +def get_version_info_details(): + """Get detailed version information for display.""" + limits = get_version_limits() + info = { + 'version_type': VERSION_TYPE.upper(), + 'max_texture_size': limits.get('max_texture_size', 'Unlimited'), + '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 diff --git a/build/validate_package.py b/build/validate_package.py index 6adeb3f..e6d2823 100755 --- a/build/validate_package.py +++ b/build/validate_package.py @@ -186,6 +186,92 @@ def validate_constants(zip_path: Path, config: Dict, version_type: str) -> Valid return result +def validate_version_differences(zip_path: Path, config: Dict, version_type: str) -> ValidationResult: + """Validate version-specific content and file exclusions.""" + result = ValidationResult() + addon_id = config['project']['id'] + + # Load exclusions configuration + exclusions_path = get_project_root() / "build" / "file_exclusions.json" + try: + with open(exclusions_path, 'r') as f: + exclusions_config = json.load(f) + except Exception as e: + result.add_error(f"Could not load file exclusions config: {e}") + return result + + version_config = exclusions_config['version_configs'].get(version_type, {}) + excluded_files = version_config.get('exclude_files', []) + excluded_patterns = version_config.get('exclude_patterns', []) + + try: + with zipfile.ZipFile(zip_path, 'r') as zip_file: + file_list = zip_file.namelist() + + # Check that excluded files are actually excluded in free version + if version_type == "free": + excluded_found = [] + for excluded_file in excluded_files: + full_path = f"{addon_id}/{excluded_file}" + if full_path in file_list or any(f.startswith(full_path) for f in file_list): + excluded_found.append(excluded_file) + + if excluded_found: + result.add_error(f"Free version contains excluded files: {excluded_found}") + else: + result.add_info(f"Correctly excluded {len(excluded_files)} premium files from free version") + + # Check for excluded patterns + pattern_violations = [] + for file_path in file_list: + for pattern in excluded_patterns: + if pattern.strip('*').lower() in file_path.lower(): + pattern_violations.append((file_path, pattern)) + + if pattern_violations: + result.add_warning(f"Files matching excluded patterns found: {pattern_violations}") + + elif version_type == "full": + # Full version should contain premium files + missing_premium = [] + for premium_file in excluded_files: + full_path = f"{addon_id}/{premium_file}" + if full_path not in file_list and not any(f.startswith(full_path) for f in file_list): + missing_premium.append(premium_file) + + if missing_premium: + result.add_error(f"Full version missing premium files: {missing_premium}") + else: + result.add_info(f"Full version correctly includes all {len(excluded_files)} premium features") + + # Validate UI helper functions are present in constants.py + constants_path = f"{addon_id}/utils/constants.py" + if constants_path in file_list: + constants_content = zip_file.read(constants_path).decode('utf-8') + ui_functions = [ + 'get_version_badge_text', + 'get_version_badge_icon', + 'get_upgrade_hint_text', + 'should_show_feature_lock', + 'get_feature_availability_text', + 'get_version_info_details' + ] + + missing_ui_functions = [] + for func in ui_functions: + if func not in constants_content: + missing_ui_functions.append(func) + + if missing_ui_functions: + result.add_error(f"Missing UI helper functions in constants.py: {missing_ui_functions}") + else: + result.add_info("All UI helper functions present in constants.py") + + except Exception as e: + result.add_error(f"Error validating version differences: {e}") + + return result + def validate_feature_flags(zip_path: Path, config: Dict) -> ValidationResult: """Validate the feature flags system.""" result = ValidationResult() @@ -278,6 +364,14 @@ def validate_package(zip_path: Path, version_type: Optional[str] = None) -> bool features_result.print_results() overall_valid &= features_result.is_valid + print("\n" + "─" * 60) + + # Version differences validation + print("Validating version-specific content and exclusions...") + version_diff_result = validate_version_differences(zip_path, config, version_type) + version_diff_result.print_results() + overall_valid &= version_diff_result.is_valid + print("\n" + "=" * 60) print(f"Overall Validation: {'✅ PASS' if overall_valid else '❌ FAIL'}") diff --git a/dist/text_texture_generator_v1.0.0.zip b/dist/text_texture_generator_v1.0.0.zip index 4eb9b1c..e6d2cec 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 bbc5db1..68cf2e2 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/src/__init__.py b/src/__init__.py index cea4842..e1cb70e 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -16,14 +16,41 @@ from .utils import bl_info # Import utility functions from .utils import install_pillow, get_system_fonts, get_font_enum_items -# Import preset system functions -from .presets import ( - initialize_presets, - refresh_presets, - refresh_presets_sync, - ensure_presets_available, - migrate_presets_if_needed -) +# Import preset system functions (conditionally for free version) +try: + pass + from .presets import ( + initialize_presets, + refresh_presets, + refresh_presets_sync, + ensure_presets_available, + migrate_presets_if_needed + ) + PRESETS_AVAILABLE = True +except ImportError: + pass + # Free version - no presets functionality + PRESETS_AVAILABLE = False + def initialize_presets(): + pass + """Mock function for free version""" + pass + def refresh_presets(): + pass + """Mock function for free version""" + pass + def refresh_presets_sync(): + pass + """Mock function for free version""" + return True + def ensure_presets_available(): + pass + """Mock function for free version""" + return True + def migrate_presets_if_needed(): + pass + """Mock function for free version""" + return {'migrated_count': 0} # Import core generation functions from .core import generate_texture_image, generate_preview @@ -35,31 +62,90 @@ from .properties import ( TEXT_TEXTURE_Properties ) -# Import operators +# Import operators - generation operators are always available from .operators import ( TEXT_TEXTURE_OT_generate, + TEXT_TEXTURE_OT_preview, TEXT_TEXTURE_OT_generate_shader, - TEXT_TEXTURE_OT_refresh_preview, - TEXT_TEXTURE_OT_view_preview_zoom, - TEXT_TEXTURE_OT_save_preset, - TEXT_TEXTURE_OT_save_preset_enter, - TEXT_TEXTURE_OT_load_preset, - TEXT_TEXTURE_OT_delete_preset, - TEXT_TEXTURE_OT_refresh_presets, - TEXT_TEXTURE_OT_add_overlay, - TEXT_TEXTURE_OT_remove_overlay, - TEXT_TEXTURE_OT_duplicate_overlay, - TEXT_TEXTURE_OT_delete_overlay, - TEXT_TEXTURE_OT_move_overlay, - TEXT_TEXTURE_OT_open_panel, - TEXT_TEXTURE_OT_refresh_fonts, - TEXT_TEXTURE_OT_set_anchor_point, - TEXT_TEXTURE_OT_sync_margins, - TEXT_TEXTURE_OT_export_presets, - TEXT_TEXTURE_OT_import_presets, - TEXT_TEXTURE_OT_show_migration_report ) +# Try to import utility operators (might not be available in free version) +try: + pass + from .operators import ( + TEXT_TEXTURE_OT_refresh_preview, + TEXT_TEXTURE_OT_view_preview_zoom, + TEXT_TEXTURE_OT_add_overlay, + TEXT_TEXTURE_OT_remove_overlay, + TEXT_TEXTURE_OT_duplicate_overlay, + TEXT_TEXTURE_OT_delete_overlay, + TEXT_TEXTURE_OT_move_overlay, + TEXT_TEXTURE_OT_open_panel, + TEXT_TEXTURE_OT_refresh_fonts, + TEXT_TEXTURE_OT_set_anchor_point, + TEXT_TEXTURE_OT_sync_margins, + ) + UTILITY_OPERATORS_AVAILABLE = True +except ImportError: + pass + # Free version - create mock operators for utility functionality + UTILITY_OPERATORS_AVAILABLE = False + + class MockOperator: + pass + bl_idname = "" + bl_label = "" + bl_description = "" + def execute(self, context): + pass + return {'CANCELLED'} + + TEXT_TEXTURE_OT_refresh_preview = MockOperator + TEXT_TEXTURE_OT_view_preview_zoom = MockOperator + TEXT_TEXTURE_OT_add_overlay = MockOperator + TEXT_TEXTURE_OT_remove_overlay = MockOperator + TEXT_TEXTURE_OT_duplicate_overlay = MockOperator + TEXT_TEXTURE_OT_delete_overlay = MockOperator + TEXT_TEXTURE_OT_move_overlay = MockOperator + TEXT_TEXTURE_OT_open_panel = MockOperator + TEXT_TEXTURE_OT_refresh_fonts = MockOperator + TEXT_TEXTURE_OT_set_anchor_point = MockOperator + TEXT_TEXTURE_OT_sync_margins = MockOperator + +# Conditionally import preset-related operators +if PRESETS_AVAILABLE: + pass + from .operators import ( + TEXT_TEXTURE_OT_save_preset, + TEXT_TEXTURE_OT_save_preset_enter, + TEXT_TEXTURE_OT_load_preset, + TEXT_TEXTURE_OT_delete_preset, + TEXT_TEXTURE_OT_refresh_presets, + TEXT_TEXTURE_OT_export_presets, + TEXT_TEXTURE_OT_import_presets, + TEXT_TEXTURE_OT_show_migration_report + ) +else: + pass + # Create mock classes for free version + class MockOperator: + pass + bl_idname = "" + bl_label = "" + bl_description = "" + def execute(self, context): + pass + return {'CANCELLED'} + + TEXT_TEXTURE_OT_save_preset = MockOperator + TEXT_TEXTURE_OT_save_preset_enter = MockOperator + TEXT_TEXTURE_OT_load_preset = MockOperator + TEXT_TEXTURE_OT_delete_preset = MockOperator + TEXT_TEXTURE_OT_refresh_presets = MockOperator + TEXT_TEXTURE_OT_export_presets = MockOperator + TEXT_TEXTURE_OT_import_presets = MockOperator + TEXT_TEXTURE_OT_show_migration_report = MockOperator + # Import UI panels and functions from .ui import ( TEXT_TEXTURE_PT_panel, @@ -74,11 +160,15 @@ from .ui import ( # REGISTRATION # ============================================================================ +# Global list to track successfully registered classes +_registered_classes = [] + classes = ( TEXT_TEXTURE_ImageOverlay, TEXT_TEXTURE_Preset, TEXT_TEXTURE_Properties, TEXT_TEXTURE_OT_generate, + TEXT_TEXTURE_OT_preview, TEXT_TEXTURE_OT_generate_shader, TEXT_TEXTURE_OT_refresh_preview, TEXT_TEXTURE_OT_view_preview_zoom, @@ -104,30 +194,57 @@ classes = ( TEXT_TEXTURE_PT_panel_properties, ) +def _is_mock_class(cls): + pass + """Check if a class is a mock operator (not a real Blender class).""" + # Mock classes don't inherit from Blender's base classes and lack bl_rna + return ( + not hasattr(cls, 'bl_rna') and + hasattr(cls, 'bl_idname') and + cls.bl_idname == "" and + hasattr(cls, 'execute') and + cls.__name__ == 'MockOperator' + ) + def register(): - print("[TTG DEBUG] ========== ADDON REGISTRATION START ==========") - for cls in classes: - 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}") + pass + + + # Clear the registered classes list + global _registered_classes + _registered_classes.clear() + + for cls in classes: + pass + + # Skip mock classes - they can't be registered as Blender classes + if _is_mock_class(cls): + pass + continue + + try: + pass + bpy.utils.register_class(cls) + _registered_classes.append(cls) + except Exception as e: + pass + print(f"Failed to register {cls.__name__}: {e}") + # Continue with other classes even if one fails - 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: + pass # Update all areas to ensure panels are properly displayed for window in bpy.context.window_manager.windows: + pass for area in window.screen.areas: + pass area.tag_redraw() - print("[TTG DEBUG] UI refresh completed successfully") except Exception as e: - print(f"[TTG DEBUG] UI refresh failed: {e}") + pass + print(f"UI refresh failed: {e}") # Add to menus bpy.types.NODE_MT_add.append(menu_func_node_editor) @@ -137,24 +254,27 @@ def register(): # Add to Image menu if available if hasattr(bpy.types, 'IMAGE_MT_image'): + pass bpy.types.IMAGE_MT_image.append(menu_func_image) # Initialize presets using reliable synchronization - no timer chains needed - print("[TTG DEBUG] Registration completed - initializing presets with reliable system") # Use the new reliable initialization system try: + pass # Perform migration check first migration_result = migrate_presets_if_needed() if migration_result['migrated_count'] > 0: - print(f"[TTG DEBUG] Migrated {migration_result['migrated_count']} presets during registration") + pass + print(f"Migrated {migration_result['migrated_count']} presets during registration") # Initialize presets using the simplified system initialize_presets() - print("[TTG DEBUG] Preset initialization completed successfully") + print("Preset initialization completed successfully") except Exception as e: - print(f"[TTG DEBUG] Error during preset initialization: {e}") + pass + print(f"Error during preset initialization: {e}") # Don't fail registration if preset initialization fails import traceback traceback.print_exc() @@ -162,47 +282,63 @@ def register(): # Add handler to reload presets when blend file changes @bpy.app.handlers.persistent def on_file_load(dummy): + pass """Reload presets when a blend file is loaded""" try: - print("[TTG] Reloading presets after file load...") + pass + + print("Reloading presets after file load...") # Use reliable synchronous refresh instead of timer success = refresh_presets_sync() if success: - print("[TTG] Presets reloaded successfully after file load") + pass + print("Presets reloaded successfully after file load") else: - print("[TTG] Warning: Preset reload failed after file load") + pass + print("Warning: Preset reload failed after file load") except Exception as e: - print(f"[TTG] Error reloading presets: {e}") + pass + print(f"Error reloading presets: {e}") import traceback traceback.print_exc() # Register the file load handler if on_file_load not in bpy.app.handlers.load_post: + pass bpy.app.handlers.load_post.append(on_file_load) - print("[TTG DEBUG] ========== ADDON REGISTRATION END ==========") def unregister(): + pass # Clean up preview try: + pass if bpy.context.scene: + pass props = bpy.context.scene.text_texture_props if props and props.preview_image: + pass bpy.data.images.remove(props.preview_image) except Exception: pass + pass if "TextTexturePreview" in bpy.data.images: + pass bpy.data.images.remove(bpy.data.images["TextTexturePreview"]) # Remove file load handler try: + pass for handler in bpy.app.handlers.load_post[:]: + pass if handler.__name__ == 'on_file_load' and 'TTG' in str(handler): + pass bpy.app.handlers.load_post.remove(handler) except Exception as e: + pass print(f"Error removing file load handler: {e}") # Remove from menus @@ -212,12 +348,26 @@ def unregister(): bpy.types.NODE_MT_node.remove(menu_func_node_editor) if hasattr(bpy.types, 'IMAGE_MT_image'): + pass bpy.types.IMAGE_MT_image.remove(menu_func_image) - for cls in reversed(classes): - bpy.utils.unregister_class(cls) + # Only unregister classes that were successfully registered + + for cls in reversed(_registered_classes): + pass + try: + pass + bpy.utils.unregister_class(cls) + except Exception as e: + pass + print(f"Failed to unregister {cls.__name__}: {e}") + # Continue with other classes even if one fails + + # Clear the registered classes list + _registered_classes.clear() del bpy.types.Scene.text_texture_props if __name__ == "__main__": + pass register() \ No newline at end of file diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc index a00fb75..71d5f3e 100644 Binary files a/src/__pycache__/__init__.cpython-311.pyc and b/src/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/core/__init__.py b/src/core/__init__.py index e476afa..5b1e2db 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -3,8 +3,19 @@ Core logic modules for Text Texture Generator addon. Phase 2 refactoring: Independent core functionality modules. """ -# Normal map generation functionality -from .normal_maps import generate_normal_map_from_alpha +# Normal map generation functionality (conditionally for free version) +try: + pass + from .normal_maps import generate_normal_map_from_alpha + NORMAL_MAPS_AVAILABLE = True +except ImportError: + pass + # Free version - no normal maps functionality + NORMAL_MAPS_AVAILABLE = False + def generate_normal_map_from_alpha(*args, **kwargs): + pass + """Mock function for free version""" + return None # Core texture generation functions from .generation_engine import generate_texture_image, generate_preview diff --git a/src/core/__pycache__/__init__.cpython-311.pyc b/src/core/__pycache__/__init__.cpython-311.pyc index c8af4e5..98a5526 100644 Binary files a/src/core/__pycache__/__init__.cpython-311.pyc and b/src/core/__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 1e0491b..cdf83cc 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/__pycache__/normal_maps.cpython-311.pyc b/src/core/__pycache__/normal_maps.cpython-311.pyc index 31969aa..3746294 100644 Binary files a/src/core/__pycache__/normal_maps.cpython-311.pyc and b/src/core/__pycache__/normal_maps.cpython-311.pyc differ diff --git a/src/core/__pycache__/text_fitting.cpython-311.pyc b/src/core/__pycache__/text_fitting.cpython-311.pyc index 9b61164..51b67c6 100644 Binary files a/src/core/__pycache__/text_fitting.cpython-311.pyc and b/src/core/__pycache__/text_fitting.cpython-311.pyc differ diff --git a/src/core/__pycache__/text_processor.cpython-311.pyc b/src/core/__pycache__/text_processor.cpython-311.pyc index db7bca9..1aba4a3 100644 Binary files a/src/core/__pycache__/text_processor.cpython-311.pyc and b/src/core/__pycache__/text_processor.cpython-311.pyc differ diff --git a/src/core/generation_engine.py b/src/core/generation_engine.py index 7225f07..f3b5dd3 100644 --- a/src/core/generation_engine.py +++ b/src/core/generation_engine.py @@ -6,6 +6,7 @@ Core functionality for generating texture images with text overlays. import bpy def generate_texture_image(props, width, height): + pass """ Generate the actual texture image with overlays. @@ -19,60 +20,61 @@ def generate_texture_image(props, width, height): Returns (None, None) on error """ try: - print(f"[TTG DEBUG] Starting texture generation at {width}x{height}px") + pass - # TODO: Implement actual texture generation logic - # This is a minimal stub to resolve the import error - # The full implementation should include: - # - Background color/image handling - # - Text overlay processing and rendering - # - Normal map generation if enabled - # - Image composition and final output + # Enhanced texture generation with proper error handling and logging + print(f"DEBUG: Starting texture generation {width}x{height}") - print(f"[TTG DEBUG] Step 1: Creating Blender image object") # Create Blender image directly without large numpy arrays img_name = f"TextTexture_{width}x{height}" if img_name in bpy.data.images: - print(f"[TTG DEBUG] Step 1a: Removing existing image: {img_name}") + pass bpy.data.images.remove(bpy.data.images[img_name]) - print(f"[TTG DEBUG] Step 1b: Creating new Blender image: {img_name}") blender_img = bpy.data.images.new(img_name, width, height, alpha=True) - print(f"[TTG DEBUG] Step 1c: Blender image created successfully") - print(f"[TTG DEBUG] Step 2: Creating text texture with PIL") # Use PIL to create a proper text texture instead of solid color + # PIL dependency is declared in blender_manifest.toml and should be auto-installed by Blender try: + pass from PIL import Image, ImageDraw, ImageFont - print(f"[TTG DEBUG] Step 2a: PIL imported successfully") + print("DEBUG: PIL imported successfully") # Create PIL image with transparent background if props.background_transparent: + pass pil_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) - print(f"[TTG DEBUG] Step 2b: Created transparent PIL image") else: + pass bg_color = tuple(int(c * 255) for c in props.background_color[:3]) + (255,) pil_img = Image.new('RGBA', (width, height), bg_color) - print(f"[TTG DEBUG] Step 2b: Created PIL image with background color: {bg_color}") # Draw text if provided (including prepend/append) full_text_parts = [] if props.prepend_text.strip(): + pass full_text_parts.append(props.prepend_text.strip()) if props.text.strip(): + pass full_text_parts.append(props.text.strip()) if props.append_text.strip(): + pass full_text_parts.append(props.append_text.strip()) if full_text_parts: + pass draw = ImageDraw.Draw(pil_img) + print(f"DEBUG: Drawing text parts: {full_text_parts}") # Combine text based on layout mode if props.prepend_append_layout == 'HORIZONTAL': + pass # For horizontal layout, use simple space separation with margin control if len(full_text_parts) == 1: + pass full_text = full_text_parts[0] else: + pass # Calculate number of spaces based on margin (but reasonable amounts) prepend_spaces = max(1, int(props.prepend_margin / 10)) if props.prepend_text.strip() else 0 append_spaces = max(1, int(props.append_margin / 10)) if props.append_text.strip() else 0 @@ -80,36 +82,42 @@ def generate_texture_image(props, width, height): # Build text with controlled spacing text_parts_with_spacing = [] for i, part in enumerate(full_text_parts): + pass if i == 0 and prepend_spaces > 0: + pass # Add spacing after prepend text text_parts_with_spacing.append(part + " " * prepend_spaces) elif i == len(full_text_parts) - 1 and append_spaces > 0: # Add spacing before append text text_parts_with_spacing.append(" " * append_spaces + part) else: + pass text_parts_with_spacing.append(part) full_text = " ".join(text_parts_with_spacing) layout_mode = "horizontal" else: + pass # Vertical layout - keep parts separate for individual positioning full_text = "\n".join(full_text_parts) layout_mode = "vertical" - print(f"[TTG DEBUG] Step 2c: Full text composed: '{full_text}' (layout: {layout_mode})") # Determine font size (with text fitting if enabled) font_size = props.font_size if props.enable_text_fitting: - print(f"[TTG DEBUG] Step 2d: Text fitting enabled, calculating optimal size") + pass # Try to load font for sizing try: + pass if props.use_custom_font and props.custom_font_path: + pass test_font_path = props.custom_font_path elif props.font_path != "default": test_font_path = props.font_path else: + pass test_font_path = None # Binary search for optimal font size @@ -121,9 +129,12 @@ def generate_texture_image(props, width, height): optimal_size = min_size for test_size in range(min_size, max_size + 1, 2): # Step by 2 for performance try: + pass if test_font_path: + pass test_font = ImageFont.truetype(test_font_path, test_size) else: + pass test_font = ImageFont.load_default() bbox = draw.textbbox((0, 0), full_text, font=test_font) @@ -131,35 +142,38 @@ def generate_texture_image(props, width, height): test_height = bbox[3] - bbox[1] if test_width <= target_width and test_height <= target_height: + pass optimal_size = test_size else: + pass break except (OSError, IOError): + pass break font_size = optimal_size - print(f"[TTG DEBUG] Step 2d: Optimal font size calculated: {font_size}") except Exception as e: - print(f"[TTG DEBUG] Step 2d: Font fitting failed, using default size: {e}") + pass font_size = props.font_size # Load final font try: + pass if props.use_custom_font and props.custom_font_path: + pass font = ImageFont.truetype(props.custom_font_path, font_size) - print(f"[TTG DEBUG] Step 2e: Using custom font: {props.custom_font_path}, size: {font_size}") elif props.font_path != "default": font = ImageFont.truetype(props.font_path, font_size) - print(f"[TTG DEBUG] Step 2e: Using system font: {props.font_path}, size: {font_size}") else: + pass font = ImageFont.load_default() - print(f"[TTG DEBUG] Step 2e: Using default font, size: {font_size}") except (OSError, IOError) as e: - print(f"[TTG DEBUG] Step 2e: Font loading failed, using default: {e}") + pass font = ImageFont.load_default() # Calculate text position and draw if layout_mode == "vertical" and props.prepend_append_layout == 'VERTICAL': + pass # Draw each part separately for vertical layout y_offset = 0 total_height = 0 @@ -167,6 +181,7 @@ def generate_texture_image(props, width, height): # Calculate total height first part_heights = [] for part in full_text_parts: + pass bbox = draw.textbbox((0, 0), part, font=font) part_height = bbox[3] - bbox[1] part_heights.append(part_height) @@ -174,6 +189,7 @@ def generate_texture_image(props, width, height): # Add spacing between parts if len(full_text_parts) > 1: + pass spacing = font_size // 4 # 25% of font size as line spacing total_height += spacing * (len(full_text_parts) - 1) @@ -185,18 +201,19 @@ def generate_texture_image(props, width, height): text_color = tuple(int(c * 255) for c in props.text_color[:4]) for i, part in enumerate(full_text_parts): + pass bbox = draw.textbbox((0, 0), part, font=font) part_width = bbox[2] - bbox[0] x = (width - part_width) // 2 # Center horizontally draw.text((x, current_y), part, font=font, fill=text_color) - print(f"[TTG DEBUG] Step 2f: Drew part '{part}' at ({x}, {current_y})") current_y += part_heights[i] if i < len(full_text_parts) - 1: # Add spacing except after last part current_y += font_size // 4 else: + pass # Horizontal layout or single text text_bbox = draw.textbbox((0, 0), full_text, font=font) text_width = text_bbox[2] - text_bbox[0] @@ -209,57 +226,85 @@ def generate_texture_image(props, width, height): # Draw text text_color = tuple(int(c * 255) for c in props.text_color[:4]) draw.text((x, y), full_text, font=font, fill=text_color) - print(f"[TTG DEBUG] Step 2f: Full text '{full_text}' drawn at ({x}, {y}) with size {font_size}") + print(f"DEBUG: Text drawn at position ({x}, {y}) with color {text_color}") else: - print(f"[TTG DEBUG] Step 2d: No text provided, using solid background") + pass + print("DEBUG: No text provided, using solid background") # Convert PIL image to Blender format (flip vertically to fix mirroring) - print(f"[TTG DEBUG] Step 2f: Converting PIL to Blender format with vertical flip") pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM) # Fix mirroring issue pil_pixels = list(pil_img.getdata()) blender_pixels = [] for r, g, b, a in pil_pixels: + pass blender_pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) + print(f"DEBUG: Converting {len(pil_pixels)} PIL pixels to Blender format") blender_img.pixels[:] = blender_pixels - print(f"[TTG DEBUG] Step 2g: Pixel data conversion completed with flip correction") - - except ImportError: - print(f"[TTG DEBUG] Step 2: PIL not available, falling back to solid color") - # Fallback to solid color if PIL is not available - pixel_pattern = [props.background_color[0], props.background_color[1], props.background_color[2], 1.0] + print(f"DEBUG: Texture generation completed successfully") + except ImportError as e: + pass + # PIL not available - should not happen with proper Blender manifest dependencies + print(f"ERROR: PIL dependency not available despite manifest declaration: {e}") + print(f"ERROR: Check if Blender properly installed addon dependencies from manifest") + # Create a visible yellow error pattern to indicate dependency issue + error_pattern = [1.0, 1.0, 0.0, 1.0] # Yellow color to indicate dependency problem total_pixels = width * height - blender_img.pixels[:] = pixel_pattern * total_pixels + blender_img.pixels[:] = error_pattern * total_pixels + print(f"DEBUG: Yellow error texture created (dependency issue)") + + except ImportError as e: + pass + # PIL installation or import failed - create visible error pattern + print(f"ERROR: PIL import error: {e}") + # Create a visible red error pattern instead of black + error_pattern = [1.0, 0.0, 0.0, 1.0] # Red color to indicate PIL failure + total_pixels = width * height + blender_img.pixels[:] = error_pattern * total_pixels + print(f"DEBUG: Red error texture created (PIL import error)") + except Exception as e: + pass + print(f"ERROR: Texture generation failed: {e}") + import traceback + traceback.print_exc() + # Create a visible error pattern instead of black + error_pattern = [1.0, 0.0, 1.0, 1.0] # Magenta color for general errors + total_pixels = width * height + blender_img.pixels[:] = error_pattern * total_pixels + print(f"DEBUG: Magenta error texture created (general error)") - print(f"[TTG DEBUG] Step 3: Packing image...") blender_img.pack() - print(f"[TTG DEBUG] Step 3: Image packing completed") # Generate normal map if enabled normal_map_img = None if props.generate_normal_map: - print(f"[TTG DEBUG] Step 4: Normal map generation enabled, starting...") - from ..core.normal_maps import generate_normal_map_from_alpha - normal_map_img = generate_normal_map_from_alpha( - blender_img, - props.normal_map_strength, - props.normal_map_blur_radius, - props.normal_map_invert - ) - print(f"[TTG DEBUG] Step 4: Normal map generation completed") + pass + try: + pass + from ..core.normal_maps import generate_normal_map_from_alpha + normal_map_img = generate_normal_map_from_alpha( + blender_img, + props.normal_map_strength, + props.normal_map_blur_radius, + props.normal_map_invert + ) + except ImportError: + pass + normal_map_img = None else: - print(f"[TTG DEBUG] Step 4: Normal map generation disabled, skipping") + pass + print("Normal map generation disabled, skipping") - print(f"[TTG DEBUG] All steps completed successfully") return blender_img, normal_map_img except Exception as e: - print(f"[TTG ERROR] Failed to generate texture image: {e}") + pass import traceback traceback.print_exc() return None, None def generate_preview(props, preview_width=512, preview_height=512): + pass """ Generate a preview version of the texture at lower resolution. @@ -272,20 +317,20 @@ def generate_preview(props, preview_width=512, preview_height=512): Blender image object or None on error """ try: - print(f"[TTG DEBUG] Generating preview at {preview_width}x{preview_height}px") + pass # Generate at preview resolution result = generate_texture_image(props, preview_width, preview_height) if result and result[0]: + pass preview_img = result[0] - print(f"[TTG DEBUG] Preview generation completed successfully") return preview_img else: - print(f"[TTG ERROR] Preview generation failed") + pass return None except Exception as e: - print(f"[TTG ERROR] Failed to generate preview: {e}") + pass import traceback traceback.print_exc() return None diff --git a/src/core/normal_maps.py b/src/core/normal_maps.py index 646e7a7..25ec0d6 100644 --- a/src/core/normal_maps.py +++ b/src/core/normal_maps.py @@ -1,25 +1,35 @@ def generate_normal_map_from_alpha(img, strength=1.0, blur_radius=1.0, invert=False): + pass """Generate a normal map from an image's alpha channel using height-based algorithm""" try: - from PIL import Image, ImageFilter + pass + try: + pass + from PIL import Image, ImageFilter + except ImportError: + pass + raise ImportError( + "PIL (Pillow) is required for normal map generation. " + "Please install it using: pip install Pillow in Blender's Python environment, " + "or use Blender's bundled Python if available." + ) import numpy as np - print(f"[Normal Map] Starting generation with strength={strength}, blur={blur_radius}, invert={invert}") # Extract alpha channel as height map if img.mode != 'RGBA': + pass img = img.convert('RGBA') # Get alpha channel alpha_channel = img.split()[3] # Alpha is the 4th channel width, height = alpha_channel.size - print(f"[Normal Map] Processing {width}x{height} alpha channel") # Apply blur if specified if blur_radius > 0: + pass alpha_channel = alpha_channel.filter(ImageFilter.GaussianBlur(radius=blur_radius)) - print(f"[Normal Map] Applied blur with radius {blur_radius}") # Convert to numpy array for gradient calculations height_map = np.array(alpha_channel, dtype=np.float32) / 255.0 @@ -41,14 +51,15 @@ def generate_normal_map_from_alpha(img, strength=1.0, blur_radius=1.0, invert=Fa grad_y = np.zeros_like(height_map) for i in range(height): + pass for j in range(width): + pass # Extract 3x3 neighborhood neighborhood = padded_height[i:i+3, j:j+3] # Apply Sobel operators grad_x[i, j] = np.sum(neighborhood * sobel_x) grad_y[i, j] = np.sum(neighborhood * sobel_y) - print(f"[Normal Map] Calculated gradients") # Convert gradients to normal vectors # Normal map RGB values are calculated as: @@ -62,9 +73,9 @@ def generate_normal_map_from_alpha(img, strength=1.0, blur_radius=1.0, invert=Fa # Apply invert if specified if invert: + pass grad_x = -grad_x grad_y = -grad_y - print(f"[Normal Map] Applied inversion") # Calculate normal map channels # Red channel: X gradient mapped to 0-1 @@ -79,24 +90,24 @@ def generate_normal_map_from_alpha(img, strength=1.0, blur_radius=1.0, invert=Fa grad_z = np.sqrt(np.maximum(0, 1.0 - grad_magnitude_sq)) normal_b = (grad_z * 255).astype(np.uint8) - print(f"[Normal Map] Calculated normal vectors") # Create the normal map image normal_map = Image.new('RGB', (width, height)) # Combine channels for y in range(height): + pass for x in range(width): + pass r = int(normal_r[y, x]) g = int(normal_g[y, x]) b = int(normal_b[y, x]) normal_map.putpixel((x, y), (r, g, b)) - print(f"[Normal Map] Normal map generation completed successfully") return normal_map except Exception as e: - print(f"[Normal Map] Error generating normal map: {e}") + pass import traceback traceback.print_exc() return None \ No newline at end of file diff --git a/src/core/text_fitting.py b/src/core/text_fitting.py index b35b92a..f66f984 100644 --- a/src/core/text_fitting.py +++ b/src/core/text_fitting.py @@ -1,13 +1,15 @@ def calculate_available_text_area(width, height, overlays): + pass """Calculate available area for text placement, avoiding overlay positions""" try: - print(f"[Text Fitting] Calculating available area for {width}x{height} with {len(overlays)} overlays") + pass # Create a boolean mask for available areas (True = available, False = blocked) available_mask = [[True for _ in range(width)] for _ in range(height)] # Mark overlay positions as unavailable for overlay in overlays: + pass overlay_x = int(overlay.get('x', 0)) overlay_y = int(overlay.get('y', 0)) overlay_width = int(overlay.get('width', 0)) @@ -22,8 +24,11 @@ def calculate_available_text_area(width, height, overlays): end_y = min(height, overlay_y + overlay_height + padding) for y in range(start_y, end_y): + pass for x in range(start_x, end_x): + pass if 0 <= y < height and 0 <= x < width: + pass available_mask[y][x] = False # Find largest available rectangular area @@ -34,11 +39,15 @@ def calculate_available_text_area(width, height, overlays): heights = [0] * width for row in range(height): + pass # Update heights array for col in range(width): + pass if available_mask[row][col]: + pass heights[col] += 1 else: + pass heights[col] = 0 # Find largest rectangle in histogram @@ -46,10 +55,10 @@ def calculate_available_text_area(width, height, overlays): area = rect[2] * rect[3] # width * height if area > max_area: + pass max_area = area best_rect = (rect[0], row - rect[3] + 1, rect[2], rect[3]) - print(f"[Text Fitting] Found best rectangle: x={best_rect[0]}, y={best_rect[1]}, w={best_rect[2]}, h={best_rect[3]}, area={max_area}") return { 'x': best_rect[0], @@ -60,24 +69,28 @@ def calculate_available_text_area(width, height, overlays): } except Exception as e: - print(f"[Text Fitting] Error calculating available text area: {e}") + pass import traceback traceback.print_exc() return {'x': 0, 'y': 0, 'width': width, 'height': height, 'area': width * height} def largest_rectangle_in_histogram(heights): + pass """Find the largest rectangle in a histogram using stack-based algorithm""" stack = [] max_area = 0 best_rect = (0, 0, 0, 0) # x, y, width, height for i, h in enumerate(heights): + pass start = i while stack and stack[-1][1] > h: + pass idx, height = stack.pop() area = height * (i - idx) if area > max_area: + pass max_area = area best_rect = (idx, 0, i - idx, height) start = idx @@ -86,20 +99,23 @@ def largest_rectangle_in_histogram(heights): # Process remaining heights in stack while stack: + pass idx, height = stack.pop() area = height * (len(heights) - idx) if area > max_area: + pass max_area = area best_rect = (idx, 0, len(heights) - idx, height) return best_rect def calculate_text_position(text, font, available_area, alignment='center'): + pass """Calculate optimal text position within available area""" try: + pass from PIL import ImageFont, ImageDraw, Image - print(f"[Text Fitting] Calculating text position for alignment: {alignment}") # Create temporary image to measure text temp_img = Image.new('RGBA', (1, 1)) @@ -115,10 +131,10 @@ def calculate_text_position(text, font, available_area, alignment='center'): area_width = available_area['width'] area_height = available_area['height'] - print(f"[Text Fitting] Text size: {text_width}x{text_height}, Available area: {area_width}x{area_height}") # Calculate position based on alignment if alignment == 'center': + pass x = area_x + (area_width - text_width) // 2 y = area_y + (area_height - text_height) // 2 elif alignment == 'top-left': @@ -146,6 +162,7 @@ def calculate_text_position(text, font, available_area, alignment='center'): x = area_x + area_width - text_width y = area_y + area_height - text_height else: + pass # Default to center x = area_x + (area_width - text_width) // 2 y = area_y + (area_height - text_height) // 2 @@ -154,7 +171,6 @@ def calculate_text_position(text, font, available_area, alignment='center'): x = max(area_x, min(x, area_x + area_width - text_width)) y = max(area_y, min(y, area_y + area_height - text_height)) - print(f"[Text Fitting] Calculated position: ({x}, {y})") return { 'x': x, @@ -164,7 +180,7 @@ def calculate_text_position(text, font, available_area, alignment='center'): } except Exception as e: - print(f"[Text Fitting] Error calculating text position: {e}") + pass import traceback traceback.print_exc() return { @@ -175,11 +191,12 @@ def calculate_text_position(text, font, available_area, alignment='center'): } def find_optimal_font_size(text, font_path, max_width, max_height, min_size=8, max_size=200): + pass """Find the largest font size that fits within the given constraints""" try: + pass from PIL import ImageFont, ImageDraw, Image - print(f"[Text Fitting] Finding optimal font size for constraints: {max_width}x{max_height}") # Binary search for optimal font size low, high = min_size, max_size @@ -189,39 +206,43 @@ def find_optimal_font_size(text, font_path, max_width, max_height, min_size=8, m draw = ImageDraw.Draw(temp_img) while low <= high: + pass mid = (low + high) // 2 try: + pass font = ImageFont.truetype(font_path, mid) bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] if text_width <= max_width and text_height <= max_height: + pass best_size = mid low = mid + 1 else: + pass high = mid - 1 except Exception as font_error: - print(f"[Text Fitting] Font size {mid} failed: {font_error}") + pass high = mid - 1 - print(f"[Text Fitting] Optimal font size: {best_size}") return best_size except Exception as e: - print(f"[Text Fitting] Error finding optimal font size: {e}") + pass import traceback traceback.print_exc() return min_size def calculate_text_scaling(text, font, target_width, target_height): + pass """Calculate scaling factors to fit text within target dimensions""" try: + pass from PIL import ImageDraw, Image - print(f"[Text Fitting] Calculating scaling for target: {target_width}x{target_height}") # Measure current text size temp_img = Image.new('RGBA', (1, 1)) @@ -231,6 +252,7 @@ def calculate_text_scaling(text, font, target_width, target_height): current_height = bbox[3] - bbox[1] if current_width == 0 or current_height == 0: + pass return 1.0, 1.0, 1.0 # Calculate scaling factors @@ -240,74 +262,79 @@ def calculate_text_scaling(text, font, target_width, target_height): # Use uniform scaling (smaller factor to ensure fit) uniform_scale = min(width_scale, height_scale) - print(f"[Text Fitting] Current size: {current_width}x{current_height}") - print(f"[Text Fitting] Scale factors: width={width_scale:.2f}, height={height_scale:.2f}, uniform={uniform_scale:.2f}") return width_scale, height_scale, uniform_scale except Exception as e: - print(f"[Text Fitting] Error calculating text scaling: {e}") + pass import traceback traceback.print_exc() return 1.0, 1.0, 1.0 def check_text_overlap(text_positions): + pass """Check for overlapping text positions and resolve conflicts""" try: - print(f"[Text Fitting] Checking overlap for {len(text_positions)} text positions") + pass overlapping = [] for i, pos1 in enumerate(text_positions): + pass for j, pos2 in enumerate(text_positions[i+1:], i+1): + pass # Check if rectangles overlap x1, y1, w1, h1 = pos1['x'], pos1['y'], pos1['width'], pos1['height'] x2, y2, w2, h2 = pos2['x'], pos2['y'], pos2['width'], pos2['height'] # Check overlap conditions if not (x1 + w1 <= x2 or x2 + w2 <= x1 or y1 + h1 <= y2 or y2 + h2 <= y1): + pass overlapping.append((i, j)) - print(f"[Text Fitting] Overlap detected between positions {i} and {j}") return overlapping except Exception as e: - print(f"[Text Fitting] Error checking text overlap: {e}") + pass import traceback traceback.print_exc() return [] def resolve_text_conflicts(text_positions, canvas_width, canvas_height): + pass """Resolve overlapping text positions by repositioning""" try: - print(f"[Text Fitting] Resolving text conflicts for {len(text_positions)} positions") + pass resolved_positions = text_positions.copy() overlaps = check_text_overlap(resolved_positions) # Simple conflict resolution: move overlapping text for i, j in overlaps: + pass pos1 = resolved_positions[i] pos2 = resolved_positions[j] # Move the second text down or to the right new_y = pos1['y'] + pos1['height'] + 10 if new_y + pos2['height'] <= canvas_height: + pass resolved_positions[j]['y'] = new_y else: + pass # Try moving to the right new_x = pos1['x'] + pos1['width'] + 10 if new_x + pos2['width'] <= canvas_width: + pass resolved_positions[j]['x'] = new_x else: + pass # If can't fit, reduce to smaller area - print(f"[Text Fitting] Could not resolve conflict for position {j}") - print(f"[Text Fitting] Resolved {len(overlaps)} conflicts") return resolved_positions except Exception as e: - print(f"[Text Fitting] Error resolving text conflicts: {e}") + pass import traceback traceback.print_exc() return text_positions \ No newline at end of file diff --git a/src/core/text_processor.py b/src/core/text_processor.py index cab9b9e..2824558 100644 --- a/src/core/text_processor.py +++ b/src/core/text_processor.py @@ -1,9 +1,10 @@ def process_multiline_text(text, font, max_width, max_height): + pass """Process multiline text with automatic wrapping and fitting""" try: + pass from PIL import ImageFont, ImageDraw, Image - print(f"[Text Processor] Processing multiline text with constraints: {max_width}x{max_height}") # Split text into lines lines = text.split('\n') @@ -16,7 +17,9 @@ def process_multiline_text(text, font, max_width, max_height): total_height = 0 for line in lines: + pass if not line.strip(): + pass # Empty line bbox = draw.textbbox((0, 0), "A", font=font) line_height = bbox[3] - bbox[1] @@ -28,12 +31,13 @@ def process_multiline_text(text, font, max_width, max_height): wrapped_lines = wrap_text_to_width(line, font, max_width) for wrapped_line in wrapped_lines: + pass bbox = draw.textbbox((0, 0), wrapped_line, font=font) line_height = bbox[3] - bbox[1] # Check if adding this line exceeds max height if total_height + line_height > max_height and processed_lines: - print(f"[Text Processor] Height limit reached at {total_height + line_height} > {max_height}") + pass break processed_lines.append(wrapped_line) @@ -41,9 +45,9 @@ def process_multiline_text(text, font, max_width, max_height): # Break if height limit reached if total_height >= max_height: + pass break - print(f"[Text Processor] Processed {len(processed_lines)} lines, total height: {total_height}") return { 'lines': processed_lines, @@ -52,17 +56,20 @@ def process_multiline_text(text, font, max_width, max_height): } except Exception as e: - print(f"[Text Processor] Error processing multiline text: {e}") + pass import traceback traceback.print_exc() return {'lines': [text], 'total_height': 0, 'line_count': 1} def wrap_text_to_width(text, font, max_width): + pass """Wrap text to fit within specified width""" try: + pass from PIL import ImageDraw, Image if not text.strip(): + pass return [""] # Create temporary image for measurements @@ -74,6 +81,7 @@ def wrap_text_to_width(text, font, max_width): text_width = bbox[2] - bbox[0] if text_width <= max_width: + pass return [text] # Split into words and wrap @@ -82,55 +90,68 @@ def wrap_text_to_width(text, font, max_width): current_line = "" for word in words: + pass test_line = current_line + (" " if current_line else "") + word bbox = draw.textbbox((0, 0), test_line, font=font) test_width = bbox[2] - bbox[0] if test_width <= max_width: + pass current_line = test_line else: + pass if current_line: + pass lines.append(current_line) current_line = word else: + pass # Single word is too long, force wrap lines.append(word) current_line = "" if current_line: + pass lines.append(current_line) return lines except Exception as e: - print(f"[Text Processor] Error wrapping text: {e}") + pass return [text] def render_text_with_stroke(draw, position, text, font, fill_color, stroke_color=None, stroke_width=0): + pass """Render text with optional stroke/outline""" try: + pass x, y = position if stroke_color and stroke_width > 0: + pass # Render stroke by drawing text at offset positions for dx in range(-stroke_width, stroke_width + 1): + pass for dy in range(-stroke_width, stroke_width + 1): + pass if dx != 0 or dy != 0: + pass draw.text((x + dx, y + dy), text, font=font, fill=stroke_color) # Render main text draw.text((x, y), text, font=font, fill=fill_color) - print(f"[Text Processor] Rendered text with stroke: width={stroke_width}") except Exception as e: - print(f"[Text Processor] Error rendering text with stroke: {e}") + pass import traceback traceback.print_exc() def calculate_text_metrics(text, font): + pass """Calculate comprehensive text metrics""" try: + pass from PIL import ImageDraw, Image # Create temporary image for measurements @@ -151,31 +172,34 @@ def calculate_text_metrics(text, font): # Get additional font metrics if available try: + pass metrics['ascent'] = font.getmetrics()[0] metrics['descent'] = font.getmetrics()[1] except: + pass metrics['ascent'] = metrics['height'] metrics['descent'] = 0 - print(f"[Text Processor] Text metrics: {metrics}") return metrics except Exception as e: - print(f"[Text Processor] Error calculating text metrics: {e}") + pass import traceback traceback.print_exc() return {'width': 0, 'height': 0} def apply_text_effects(img, text_position, text_size, effects): + pass """Apply various text effects like shadow, glow, etc.""" try: + pass from PIL import Image, ImageFilter, ImageEnhance - print(f"[Text Processor] Applying text effects: {list(effects.keys())}") result_img = img.copy() if 'shadow' in effects: + pass shadow_config = effects['shadow'] offset_x = shadow_config.get('offset_x', 2) offset_y = shadow_config.get('offset_y', 2) @@ -187,13 +211,14 @@ def apply_text_effects(img, text_position, text_size, effects): # Draw shadow text (this would need the actual text rendering logic) if blur_radius > 0: + pass shadow_img = shadow_img.filter(ImageFilter.GaussianBlur(radius=blur_radius)) # Composite shadow result_img = Image.alpha_composite(result_img, shadow_img) - print(f"[Text Processor] Applied shadow effect") if 'glow' in effects: + pass glow_config = effects['glow'] glow_color = glow_config.get('color', (255, 255, 255, 128)) glow_radius = glow_config.get('radius', 3) @@ -203,33 +228,34 @@ def apply_text_effects(img, text_position, text_size, effects): # Apply glow rendering logic here result_img = Image.alpha_composite(result_img, glow_img) - print(f"[Text Processor] Applied glow effect") if 'gradient' in effects: + pass gradient_config = effects['gradient'] start_color = gradient_config.get('start', (255, 255, 255)) end_color = gradient_config.get('end', (0, 0, 0)) direction = gradient_config.get('direction', 'horizontal') # Apply gradient to text - print(f"[Text Processor] Applied gradient effect") return result_img except Exception as e: - print(f"[Text Processor] Error applying text effects: {e}") + pass import traceback traceback.print_exc() return img def optimize_text_layout(text_blocks, canvas_width, canvas_height): + pass """Optimize layout of multiple text blocks to minimize overlap""" try: - print(f"[Text Processor] Optimizing layout for {len(text_blocks)} text blocks") + pass optimized_blocks = [] for i, block in enumerate(text_blocks): + pass x = block.get('x', 0) y = block.get('y', 0) width = block.get('width', 0) @@ -238,6 +264,7 @@ def optimize_text_layout(text_blocks, canvas_width, canvas_height): # Check for overlaps with previous blocks overlap_found = False for prev_block in optimized_blocks: + pass if rectangles_overlap( (x, y, width, height), (prev_block['x'], prev_block['y'], prev_block['width'], prev_block['height']) @@ -246,27 +273,27 @@ def optimize_text_layout(text_blocks, canvas_width, canvas_height): break if overlap_found: + pass # Find new position new_x, new_y = find_non_overlapping_position( width, height, optimized_blocks, canvas_width, canvas_height ) x, y = new_x, new_y - print(f"[Text Processor] Moved block {i} to avoid overlap: ({x}, {y})") optimized_block = block.copy() optimized_block.update({'x': x, 'y': y}) optimized_blocks.append(optimized_block) - print(f"[Text Processor] Layout optimization complete") return optimized_blocks except Exception as e: - print(f"[Text Processor] Error optimizing text layout: {e}") + pass import traceback traceback.print_exc() return text_blocks def rectangles_overlap(rect1, rect2): + pass """Check if two rectangles overlap""" x1, y1, w1, h1 = rect1 x2, y2, w2, h2 = rect2 @@ -274,32 +301,40 @@ def rectangles_overlap(rect1, rect2): return not (x1 + w1 <= x2 or x2 + w2 <= x1 or y1 + h1 <= y2 or y2 + h2 <= y1) def find_non_overlapping_position(width, height, existing_blocks, canvas_width, canvas_height): + pass """Find a position that doesn't overlap with existing blocks""" # Try positions from top-left, moving right then down for y in range(0, canvas_height - height, 20): + pass for x in range(0, canvas_width - width, 20): + pass rect = (x, y, width, height) overlap = False for block in existing_blocks: + pass if rectangles_overlap(rect, (block['x'], block['y'], block['width'], block['height'])): + pass overlap = True break if not overlap: + pass return x, y # If no position found, return original or default return 0, 0 def validate_text_rendering(img, text_positions): + pass """Validate that text rendering was successful""" try: - print(f"[Text Processor] Validating text rendering for {len(text_positions)} positions") + pass validation_results = [] for i, pos in enumerate(text_positions): + pass result = { 'position_index': i, 'x': pos.get('x', 0), @@ -312,31 +347,34 @@ def validate_text_rendering(img, text_positions): # Check bounds if pos.get('x', 0) < 0 or pos.get('y', 0) < 0: + pass result['valid'] = False result['issues'].append('Negative position') if pos.get('x', 0) + pos.get('width', 0) > img.width: + pass result['valid'] = False result['issues'].append('Exceeds image width') if pos.get('y', 0) + pos.get('height', 0) > img.height: + pass result['valid'] = False result['issues'].append('Exceeds image height') # Check for zero dimensions if pos.get('width', 0) == 0 or pos.get('height', 0) == 0: + pass result['valid'] = False result['issues'].append('Zero dimensions') validation_results.append(result) valid_count = sum(1 for r in validation_results if r['valid']) - print(f"[Text Processor] Validation complete: {valid_count}/{len(text_positions)} positions valid") return validation_results except Exception as e: - print(f"[Text Processor] Error validating text rendering: {e}") + pass import traceback traceback.print_exc() return [] \ No newline at end of file diff --git a/src/operators/__init__.py b/src/operators/__init__.py index a3e9c11..e0cc52b 100644 --- a/src/operators/__init__.py +++ b/src/operators/__init__.py @@ -4,67 +4,88 @@ Contains all Blender operators for text texture generation and management. """ from .generation_ops import * -from .preset_ops import * -from .utility_ops import * __all__ = [] # Import all operator classes and add to __all__ from .generation_ops import ( TEXT_TEXTURE_OT_generate, + TEXT_TEXTURE_OT_preview, TEXT_TEXTURE_OT_generate_shader, ) -from .preset_ops import ( - TEXT_TEXTURE_OT_save_preset, - TEXT_TEXTURE_OT_save_preset_enter, - TEXT_TEXTURE_OT_load_preset, - TEXT_TEXTURE_OT_delete_preset, - TEXT_TEXTURE_OT_refresh_presets, - TEXT_TEXTURE_OT_export_presets, - TEXT_TEXTURE_OT_import_presets, - TEXT_TEXTURE_OT_show_migration_report, -) +# Conditional imports for modules that might not exist in free version +try: + pass + from .preset_ops import * + from .preset_ops import ( + TEXT_TEXTURE_OT_save_preset, + TEXT_TEXTURE_OT_save_preset_enter, + TEXT_TEXTURE_OT_load_preset, + TEXT_TEXTURE_OT_delete_preset, + TEXT_TEXTURE_OT_refresh_presets, + TEXT_TEXTURE_OT_export_presets, + TEXT_TEXTURE_OT_import_presets, + TEXT_TEXTURE_OT_show_migration_report, + ) + PRESET_OPERATORS = [ + 'TEXT_TEXTURE_OT_save_preset', + 'TEXT_TEXTURE_OT_save_preset_enter', + 'TEXT_TEXTURE_OT_load_preset', + 'TEXT_TEXTURE_OT_delete_preset', + 'TEXT_TEXTURE_OT_refresh_presets', + 'TEXT_TEXTURE_OT_export_presets', + 'TEXT_TEXTURE_OT_import_presets', + 'TEXT_TEXTURE_OT_show_migration_report', + ] +except ImportError: + pass + # preset_ops module not available (free version) + PRESET_OPERATORS = [] -from .utility_ops import ( - TEXT_TEXTURE_OT_refresh_preview, - TEXT_TEXTURE_OT_view_preview_zoom, - TEXT_TEXTURE_OT_open_panel, - TEXT_TEXTURE_OT_refresh_fonts, - TEXT_TEXTURE_OT_add_overlay, - TEXT_TEXTURE_OT_remove_overlay, - TEXT_TEXTURE_OT_set_anchor_point, - TEXT_TEXTURE_OT_sync_margins, - TEXT_TEXTURE_OT_duplicate_overlay, - TEXT_TEXTURE_OT_delete_overlay, - TEXT_TEXTURE_OT_move_overlay, -) +try: + pass + from .utility_ops import * + from .utility_ops import ( + TEXT_TEXTURE_OT_refresh_preview, + TEXT_TEXTURE_OT_view_preview_zoom, + TEXT_TEXTURE_OT_open_panel, + TEXT_TEXTURE_OT_refresh_fonts, + TEXT_TEXTURE_OT_add_overlay, + TEXT_TEXTURE_OT_remove_overlay, + TEXT_TEXTURE_OT_set_anchor_point, + TEXT_TEXTURE_OT_sync_margins, + TEXT_TEXTURE_OT_duplicate_overlay, + TEXT_TEXTURE_OT_delete_overlay, + TEXT_TEXTURE_OT_move_overlay, + ) + UTILITY_OPERATORS = [ + 'TEXT_TEXTURE_OT_refresh_preview', + 'TEXT_TEXTURE_OT_view_preview_zoom', + 'TEXT_TEXTURE_OT_open_panel', + 'TEXT_TEXTURE_OT_refresh_fonts', + 'TEXT_TEXTURE_OT_add_overlay', + 'TEXT_TEXTURE_OT_remove_overlay', + 'TEXT_TEXTURE_OT_set_anchor_point', + 'TEXT_TEXTURE_OT_sync_margins', + 'TEXT_TEXTURE_OT_duplicate_overlay', + 'TEXT_TEXTURE_OT_delete_overlay', + 'TEXT_TEXTURE_OT_move_overlay', + ] +except ImportError: + pass + # utility_ops module not available (free version) + UTILITY_OPERATORS = [] __all__.extend([ # Generation operators 'TEXT_TEXTURE_OT_generate', + 'TEXT_TEXTURE_OT_preview', 'TEXT_TEXTURE_OT_generate_shader', - - # Preset operators - 'TEXT_TEXTURE_OT_save_preset', - 'TEXT_TEXTURE_OT_save_preset_enter', - 'TEXT_TEXTURE_OT_load_preset', - 'TEXT_TEXTURE_OT_delete_preset', - 'TEXT_TEXTURE_OT_refresh_presets', - 'TEXT_TEXTURE_OT_export_presets', - 'TEXT_TEXTURE_OT_import_presets', - 'TEXT_TEXTURE_OT_show_migration_report', - - # Utility operators - 'TEXT_TEXTURE_OT_refresh_preview', - 'TEXT_TEXTURE_OT_view_preview_zoom', - 'TEXT_TEXTURE_OT_open_panel', - 'TEXT_TEXTURE_OT_refresh_fonts', - 'TEXT_TEXTURE_OT_add_overlay', - 'TEXT_TEXTURE_OT_remove_overlay', - 'TEXT_TEXTURE_OT_set_anchor_point', - 'TEXT_TEXTURE_OT_sync_margins', - 'TEXT_TEXTURE_OT_duplicate_overlay', - 'TEXT_TEXTURE_OT_delete_overlay', - 'TEXT_TEXTURE_OT_move_overlay', -]) \ No newline at end of file +]) + +# Add preset operators if available +__all__.extend(PRESET_OPERATORS) + +# Add utility operators if available +__all__.extend(UTILITY_OPERATORS) \ No newline at end of file diff --git a/src/operators/__pycache__/__init__.cpython-311.pyc b/src/operators/__pycache__/__init__.cpython-311.pyc index 2070c31..0e6542a 100644 Binary files a/src/operators/__pycache__/__init__.cpython-311.pyc and b/src/operators/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/operators/__pycache__/generation_ops.cpython-311.pyc b/src/operators/__pycache__/generation_ops.cpython-311.pyc index 07c8c27..8bfd308 100644 Binary files a/src/operators/__pycache__/generation_ops.cpython-311.pyc and b/src/operators/__pycache__/generation_ops.cpython-311.pyc differ diff --git a/src/operators/__pycache__/preset_ops.cpython-311.pyc b/src/operators/__pycache__/preset_ops.cpython-311.pyc index 4c49c97..a810a4d 100644 Binary files a/src/operators/__pycache__/preset_ops.cpython-311.pyc and b/src/operators/__pycache__/preset_ops.cpython-311.pyc differ diff --git a/src/operators/__pycache__/utility_ops.cpython-311.pyc b/src/operators/__pycache__/utility_ops.cpython-311.pyc index 7764075..e7071c5 100644 Binary files a/src/operators/__pycache__/utility_ops.cpython-311.pyc and b/src/operators/__pycache__/utility_ops.cpython-311.pyc differ diff --git a/src/operators/generation_ops.py b/src/operators/generation_ops.py index 1b34dec..568e229 100644 --- a/src/operators/generation_ops.py +++ b/src/operators/generation_ops.py @@ -13,6 +13,7 @@ from ..core.generation_engine import generate_texture_image, generate_preview class TEXT_TEXTURE_OT_generate(Operator): + pass """Generate final texture and pack it""" bl_idname = "text_texture.generate" bl_label = "Generate Text Texture" @@ -20,7 +21,9 @@ class TEXT_TEXTURE_OT_generate(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass try: + pass props = context.scene.text_texture_props # IMPORTANT: Generate at EXACT user-specified dimensions @@ -32,18 +35,22 @@ class TEXT_TEXTURE_OT_generate(Operator): # Generate at FULL resolution specified by user result = generate_texture_image(props, final_width, final_height) if not result: + pass self.report({'ERROR'}, "Failed to generate texture image") return {'CANCELLED'} # Extract diffuse and normal map images if isinstance(result, tuple): + pass img, normal_map_img = result else: + pass # Backward compatibility img = result normal_map_img = None if not img: + pass self.report({'ERROR'}, "Failed to generate diffuse image") return {'CANCELLED'} @@ -55,11 +62,13 @@ class TEXT_TEXTURE_OT_generate(Operator): sanitized_text = re.sub(r'[\s_-]+', '_', sanitized_text).strip('_') # Fallback if text becomes empty if not sanitized_text: + pass sanitized_text = "Texture" img_name = f"TextTexture_{sanitized_text}" # Remove existing diffuse texture if img_name in bpy.data.images: + pass bpy.data.images.remove(bpy.data.images[img_name]) # Create new diffuse image with USER SPECIFIED dimensions @@ -73,10 +82,12 @@ class TEXT_TEXTURE_OT_generate(Operator): # Create normal map texture if enabled and generated normal_map_blender_img = None if normal_map_img and props.generate_normal_map: + pass normal_map_name = f"TextTexture_Normal_{sanitized_text}" # Remove existing normal map texture if normal_map_name in bpy.data.images: + pass bpy.data.images.remove(bpy.data.images[normal_map_name]) # Create new normal map image @@ -87,16 +98,19 @@ class TEXT_TEXTURE_OT_generate(Operator): # Copy RGB channels and set alpha to 1.0 temp_pixels = list(normal_map_img.pixels[:]) for i in range(3, len(temp_pixels), 4): + pass temp_pixels[i] = 1.0 # Set alpha to 1.0 for normal maps normal_map_blender_img.pixels[:] = temp_pixels normal_map_blender_img.pack() # PACK NORMAL MAP - print(f"[Normal Map] Created and packed normal map texture: {normal_map_name}") # Add to shader if context.space_data and context.space_data.type == 'NODE_EDITOR': + pass if context.space_data.tree_type == 'ShaderNodeTree': + pass material = context.space_data.id if material and material.use_nodes: + pass nodes = material.node_tree.nodes # Add diffuse texture node @@ -108,25 +122,93 @@ class TEXT_TEXTURE_OT_generate(Operator): # Add normal map node if normal map was created if normal_map_blender_img: + pass normal_node = nodes.new(type='ShaderNodeTexImage') normal_node.image = normal_map_blender_img normal_node.location = (0, -300) normal_node.label = "Normal Map" - print(f"[Normal Map] Added normal map node to shader editor") # Provide comprehensive feedback if normal_map_blender_img: + pass self.report({'INFO'}, f"Generated and packed: {img_name} + {normal_map_blender_img.name} ({final_width}x{final_height}px)") else: + pass self.report({'INFO'}, f"Generated and packed: {img_name} ({final_width}x{final_height}px)") return {'FINISHED'} except Exception as e: + pass self.report({'ERROR'}, f"Failed: {str(e)}") return {'CANCELLED'} +class TEXT_TEXTURE_OT_preview(Operator): + pass + """Generate basic preview for free version users""" + bl_idname = "text_texture.preview" + bl_label = "Generate Preview" + bl_description = "Generate a basic preview of your text texture" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + pass + try: + pass + # Import the preview function + from ..core.generation_engine import generate_preview + + # Get properties and generate preview with correct parameters + props = context.scene.text_texture_props + preview_size = props.preview_size if hasattr(props, 'preview_size') else 512 + result = generate_preview(props, preview_size, preview_size) + + if result: + pass + self.report({'INFO'}, f"Preview generated at {preview_size}px") + + # Open the preview image in the image editor + # Find or create an image editor area + for area in context.screen.areas: + pass + if area.type == 'IMAGE_EDITOR': + pass + for space in area.spaces: + pass + if space.type == 'IMAGE_EDITOR': + pass + space.image = result + area.tag_redraw() + break + break + else: + pass + # If no image editor found, try to open in UV editor or create viewer + try: + pass + # Try to show in any available viewer + bpy.ops.image.view_all('INVOKE_DEFAULT') + except: + pass + # Fallback message + self.report({'INFO'}, f"Preview '{result.name}' created - open Image Editor to view") + + return {'FINISHED'} + else: + pass + self.report({'ERROR'}, "Failed to generate preview - check console for details") + return {'CANCELLED'} + + except Exception as e: + pass + self.report({'ERROR'}, f"Preview failed: {str(e)}") + import traceback + traceback.print_exc() + return {'CANCELLED'} + + class TEXT_TEXTURE_OT_generate_shader(Operator): + pass """Generate complete shader with text texture""" bl_idname = "text_texture.generate_shader" bl_label = "Generate Shader" @@ -134,41 +216,39 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass try: - print(f"[TTG SHADER DEBUG] === SHADER GENERATION STARTED ===") + pass props = context.scene.text_texture_props # Generate texture first final_width = props.texture_width final_height = props.texture_height - print(f"[TTG SHADER DEBUG] Step 1: Starting texture generation at {final_width}x{final_height}px") result = generate_texture_image(props, final_width, final_height) - print(f"[TTG SHADER DEBUG] Step 2: Texture generation returned: {type(result)}") if not result: + pass self.report({'ERROR'}, "Failed to generate texture image") return {'CANCELLED'} - print(f"[TTG SHADER DEBUG] Step 3: Processing result...") # Extract diffuse and normal map images if isinstance(result, tuple): + pass img, normal_map_img = result - print(f"[TTG SHADER DEBUG] Step 3a: Got tuple result - img: {type(img)}, normal: {type(normal_map_img)}") else: + pass # Backward compatibility img = result normal_map_img = None - print(f"[TTG SHADER DEBUG] Step 3b: Got single result - img: {type(img)}") if not img: + pass self.report({'ERROR'}, "Failed to generate diffuse image") return {'CANCELLED'} - print(f"[TTG SHADER DEBUG] Step 4: Image validation passed, proceeding with shader creation...") - print(f"[TTG SHADER DEBUG] Step 5: Creating diffuse texture...") # Create diffuse texture name with proper sanitization import re # Remove non-ASCII and non-alphanumeric characters, keep spaces temporarily @@ -177,74 +257,66 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): sanitized_text = re.sub(r'[\s_-]+', '_', sanitized_text).strip('_') # Fallback if text becomes empty if not sanitized_text: + pass sanitized_text = "Texture" img_name = f"TextTexture_{sanitized_text}" - print(f"[TTG SHADER DEBUG] Step 5a: Sanitized texture name: {img_name}") # Remove existing diffuse texture if it exists if img_name in bpy.data.images: - print(f"[TTG SHADER DEBUG] Step 5b: Removing existing texture") + pass bpy.data.images.remove(bpy.data.images[img_name]) - print(f"[TTG SHADER DEBUG] Step 5c: Creating new Blender image...") # Create new diffuse texture blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True) - print(f"[TTG SHADER DEBUG] Step 5d: New image created, copying pixel data...") # Directly copy pixel data from generated image to Blender image # The generated image is already in the correct format, just copy it blender_img.pixels[:] = img.pixels[:] - print(f"[TTG SHADER DEBUG] Step 5e: Pixel data copied, packing image...") blender_img.pack() - print(f"[TTG SHADER DEBUG] Step 5f: Image packed successfully") - print(f"[TTG SHADER DEBUG] Step 6: Processing normal maps...") # Create normal map texture if enabled and generated normal_map_blender_img = None if normal_map_img and props.generate_normal_map: - print(f"[TTG SHADER DEBUG] Step 6a: Normal map enabled and generated, processing...") + pass # Use the same sanitized text for consistency normal_map_name = f"TextTexture_Normal_{sanitized_text}" - print(f"[TTG SHADER DEBUG] Step 6b: Sanitized normal map name: {normal_map_name}") # Remove existing normal map texture if normal_map_name in bpy.data.images: - print(f"[TTG SHADER DEBUG] Step 6c: Removing existing normal map") + pass bpy.data.images.remove(bpy.data.images[normal_map_name]) - print(f"[TTG SHADER DEBUG] Step 6d: Creating normal map image...") # Create new normal map image normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False) normal_map_blender_img.colorspace_settings.name = 'Non-Color' # Important for normal maps - print(f"[TTG SHADER DEBUG] Step 6e: Normal map image created, copying pixels...") # Directly copy normal map pixel data # Copy RGB channels and set alpha to 1.0 temp_pixels = list(normal_map_img.pixels[:]) for i in range(3, len(temp_pixels), 4): + pass temp_pixels[i] = 1.0 # Set alpha to 1.0 for normal maps normal_map_blender_img.pixels[:] = temp_pixels - print(f"[TTG SHADER DEBUG] Step 6f: Normal map pixels copied, packing...") normal_map_blender_img.pack() - print(f"[TTG SHADER DEBUG] Step 6g: Normal map packed successfully") else: - print(f"[TTG SHADER DEBUG] Step 6: Normal map generation skipped") + pass - print(f"[TTG SHADER DEBUG] Step 7: Creating material...") # Create or get material using custom name or fallback if props.custom_material_name.strip(): + pass material_name = props.custom_material_name.strip() - print(f"[TTG SHADER DEBUG] Step 7a: Using custom material name: {material_name}") else: + pass # Use the same sanitized text for consistency material_name = f"TextTexture_Mat_{sanitized_text}" - print(f"[TTG SHADER DEBUG] Step 7b: Using sanitized material name: {material_name}") if material_name in bpy.data.materials: + pass mat = bpy.data.materials[material_name] # Clear existing nodes mat.node_tree.nodes.clear() else: + pass mat = bpy.data.materials.new(name=material_name) mat.use_nodes = True mat.node_tree.nodes.clear() @@ -259,6 +331,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Shader type based on settings if props.shader_type == 'PRINCIPLED': + pass shader_node = nodes.new(type='ShaderNodeBsdfPrincipled') shader_node.location = (200, 0) @@ -269,6 +342,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Connect texture to shader if props.shader_connection == 'BASE_COLOR': + pass links.new(tex_node.outputs['Color'], shader_node.inputs['Base Color']) elif props.shader_connection == 'EMISSION': links.new(tex_node.outputs['Color'], shader_node.inputs['Emission Color']) @@ -281,11 +355,13 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Connect alpha if enabled if props.use_alpha: + pass links.new(tex_node.outputs['Alpha'], shader_node.inputs['Alpha']) mat.blend_method = 'BLEND' # Add normal map nodes if normal map was generated if normal_map_blender_img: + pass # Create normal map texture node normal_tex_node = nodes.new(type='ShaderNodeTexImage') normal_tex_node.image = normal_map_blender_img @@ -302,7 +378,6 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Connect normal map node to shader links.new(normal_map_node.outputs['Normal'], shader_node.inputs['Normal']) - print(f"[Normal Map] Added normal map nodes to Principled BSDF shader") # Handle shadow/reflection disabling current_shader = shader_node @@ -310,6 +385,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Disable shadows if enabled if props.disable_shadows: + pass # Create light path node light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 150) @@ -334,8 +410,10 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Disable reflections if enabled if props.disable_reflections: + pass # Create or reuse light path node if not props.disable_shadows: + pass light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 200) @@ -359,8 +437,10 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Disable backfacing if enabled if props.disable_backfacing: + pass # Create or reuse light path node if not props.disable_shadows and not props.disable_reflections: + pass light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 250) @@ -413,6 +493,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Disable shadows if enabled if props.disable_shadows: + pass # Create light path node light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 150) @@ -437,8 +518,10 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Disable reflections if enabled if props.disable_reflections: + pass # Create or reuse light path node if not props.disable_shadows: + pass light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 200) @@ -495,6 +578,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Disable shadows if enabled if props.disable_shadows: + pass # Create light path node light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 200) @@ -519,8 +603,10 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Disable reflections if enabled if props.disable_reflections: + pass # Create or reuse light path node if not props.disable_shadows: + pass light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 250) @@ -544,8 +630,10 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Disable backfacing if enabled if props.disable_backfacing: + pass # Create or reuse light path node if not props.disable_shadows and not props.disable_reflections: + pass light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 300) @@ -581,29 +669,38 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Assign material to active object if enabled and possible if props.auto_assign_material and context.active_object and context.active_object.type in ['MESH', 'CURVE']: + pass obj = context.active_object # Check if material slot exists, reuse if found, otherwise create new one slot_found = False for i, slot in enumerate(obj.material_slots): + pass if slot.material and slot.material.name == material_name: + pass # Material already exists in a slot, update it obj.material_slots[i].material = mat slot_found = True break if not slot_found: + pass # Create new material slot if not found obj.data.materials.append(mat) self.report({'INFO'}, f"Generated shader '{material_name}' and assigned to {obj.name}") else: + pass self.report({'INFO'}, f"Generated shader '{material_name}' (assign manually to object)") # Switch to Shader Editor if available and set material for area in context.screen.areas: + pass if area.type == 'NODE_EDITOR': + pass for space in area.spaces: + pass if space.type == 'NODE_EDITOR': + pass space.shader_type = 'OBJECT' space.id = mat area.tag_redraw() @@ -612,6 +709,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): return {'FINISHED'} except Exception as e: + pass self.report({'ERROR'}, f"Shader generation failed: {str(e)}") import traceback traceback.print_exc() @@ -620,5 +718,6 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Export all operator classes for wildcard imports __all__ = [ 'TEXT_TEXTURE_OT_generate', + 'TEXT_TEXTURE_OT_preview', 'TEXT_TEXTURE_OT_generate_shader', ] \ No newline at end of file diff --git a/src/operators/preset_ops.py b/src/operators/preset_ops.py index fef9e55..0f9b77d 100644 --- a/src/operators/preset_ops.py +++ b/src/operators/preset_ops.py @@ -15,6 +15,7 @@ from ..presets.storage_system import ( from ..utils import bl_info class TEXT_TEXTURE_OT_save_preset(Operator): + pass """Save current settings as preset""" bl_idname = "text_texture.save_preset" bl_label = "Save Preset" @@ -34,16 +35,19 @@ class TEXT_TEXTURE_OT_save_preset(Operator): ) def execute(self, context): + pass props = context.scene.text_texture_props # Use provided preset name or fallback to input field preset_name = self.preset_name.strip() if self.preset_name else props.new_preset_name.strip() if not preset_name: + pass self.report({'ERROR'}, "Please enter a preset name") return {'CANCELLED'} # When overwriting from button, update the input field for consistency if self.preset_name and self.preset_name.strip(): + pass props.new_preset_name = preset_name # Validate preset name characters (avoid filesystem issues) @@ -51,23 +55,27 @@ class TEXT_TEXTURE_OT_save_preset(Operator): # Allow Unicode letters, numbers, spaces, hyphens, underscores, and common accented characters # This pattern supports international characters like ö, ü, ñ, etc. if not re.match(r'^[\w\s\-]+$', preset_name, re.UNICODE): + pass self.report({'ERROR'}, "Preset name contains invalid characters for filesystem compatibility") return {'CANCELLED'} # Additional check for filesystem-unsafe characters invalid_chars = ['<', '>', ':', '"', '|', '?', '*', '/', '\\'] if any(char in preset_name for char in invalid_chars): + pass self.report({'ERROR'}, f"Preset name cannot contain: {' '.join(invalid_chars)}") 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: + pass # Suggest alternative names base_name = preset_name counter = 1 suggested_name = f"{base_name}_{counter}" while os.path.exists(os.path.join(get_preset_path(), f"{suggested_name}.json")): + pass counter += 1 suggested_name = f"{base_name}_{counter}" @@ -75,16 +83,6 @@ class TEXT_TEXTURE_OT_save_preset(Operator): return {'CANCELLED'} # DETAILED LOGGING: Current shader properties before saving - print(f"[PRESET SAVE DEBUG] ==================== SAVING PRESET: '{preset_name}' ====================") - print(f"[PRESET SAVE DEBUG] Current shader properties from props:") - print(f"[PRESET SAVE DEBUG] disable_shadows: {props.disable_shadows}") - print(f"[PRESET SAVE DEBUG] disable_reflections: {props.disable_reflections}") - print(f"[PRESET SAVE DEBUG] disable_backfacing: {props.disable_backfacing}") - print(f"[PRESET SAVE DEBUG] shader_type: {props.shader_type}") - print(f"[PRESET SAVE DEBUG] shader_connection: {props.shader_connection}") - print(f"[PRESET SAVE DEBUG] use_alpha: {props.use_alpha}") - print(f"[PRESET SAVE DEBUG] emission_strength: {props.emission_strength}") - print(f"[PRESET SAVE DEBUG] auto_assign_material: {props.auto_assign_material}") preset_data = { 'text': props.text, @@ -142,13 +140,10 @@ class TEXT_TEXTURE_OT_save_preset(Operator): } # DETAILED LOGGING: Verify shader properties in preset_data - print(f"[PRESET SAVE DEBUG] Shader properties in preset_data dictionary:") - print(f"[PRESET SAVE DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") - print(f"[PRESET SAVE DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") - print(f"[PRESET SAVE DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") # Save overlay data for overlay in props.image_overlays: + pass overlay_data = { 'name': overlay.name, 'image_path': overlay.image_path, @@ -162,25 +157,31 @@ class TEXT_TEXTURE_OT_save_preset(Operator): preset_data['image_overlays'].append(overlay_data) try: + pass # Save based on checkbox setting - single destination only success = save_preset_unified(preset_data, preset_name, props.save_with_blend_file) if not success: + pass self.report({'ERROR'}, f"Failed to save preset '{preset_name}'") return {'CANCELLED'} # Check if preset already exists in the collection and update it existing_preset = None for preset in props.presets: + pass if preset.name == preset_name: + pass existing_preset = preset break if not existing_preset: + pass preset = props.presets.add() preset.name = preset_name preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE' else: + pass # Update source based on where it was saved existing_preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE' @@ -188,38 +189,47 @@ class TEXT_TEXTURE_OT_save_preset(Operator): location = "with .blend file" if props.save_with_blend_file else "to persistent storage" if self.overwrite: + pass self.report({'INFO'}, f"✅ Preset '{preset_name}' updated {location}") else: + pass self.report({'INFO'}, f"✅ Preset '{preset_name}' saved {location}") except (OSError, IOError, json.JSONEncodeError) as e: + pass self.report({'ERROR'}, f"Failed to save preset: {str(e)}") return {'CANCELLED'} except Exception as e: + pass self.report({'ERROR'}, f"Unexpected error saving preset: {str(e)}") return {'CANCELLED'} return {'FINISHED'} def invoke(self, context, event): + pass # 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": + pass preset_file = os.path.join(get_preset_path(), f"{preset_name}.json") if os.path.exists(preset_file): + pass return context.window_manager.invoke_confirm(self, event) return self.execute(context) def draw(self, context): + pass 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): + pass """Save preset when Enter key is pressed in name field""" bl_idname = "text_texture.save_preset_enter" bl_label = "Save Preset (Enter)" @@ -227,10 +237,12 @@ class TEXT_TEXTURE_OT_save_preset_enter(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass # Delegate to the main save preset operator return bpy.ops.text_texture.save_preset('INVOKE_DEFAULT') class TEXT_TEXTURE_OT_load_preset(Operator): + pass """Load preset""" bl_idname = "text_texture.load_preset" bl_label = "Load Preset" @@ -240,33 +252,29 @@ class TEXT_TEXTURE_OT_load_preset(Operator): preset_name: StringProperty() def execute(self, context): + pass props = context.scene.text_texture_props # DEFENSIVE PROGRAMMING: Ensure presets are available before loading if not ensure_presets_available(): + pass self.report({'ERROR'}, "Failed to ensure presets are loaded - cannot load preset") return {'CANCELLED'} # DETAILED LOGGING: Current shader properties before loading - print(f"[PRESET LOAD DEBUG] ==================== LOADING PRESET: '{self.preset_name}' ====================") - print(f"[PRESET LOAD DEBUG] Current shader properties BEFORE loading:") - print(f"[PRESET LOAD DEBUG] disable_shadows: {props.disable_shadows}") - print(f"[PRESET LOAD DEBUG] disable_reflections: {props.disable_reflections}") - print(f"[PRESET LOAD DEBUG] disable_backfacing: {props.disable_backfacing}") - print(f"[PRESET LOAD DEBUG] shader_type: {props.shader_type}") - print(f"[PRESET LOAD DEBUG] shader_connection: {props.shader_connection}") try: + pass # Get unified preset list and find the one to load all_presets = get_unified_preset_list() if self.preset_name not in all_presets: + pass self.report({'ERROR'}, f"Preset '{self.preset_name}' not found") return {'CANCELLED'} preset_data, source = all_presets[self.preset_name] - print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from {source}") props.is_updating = True @@ -290,56 +298,36 @@ class TEXT_TEXTURE_OT_load_preset(Operator): props.prepend_append_layout = preset_data.get('prepend_append_layout', props.prepend_append_layout) # DETAILED LOGGING: Shader property loading with before/after values - print(f"[PRESET LOAD DEBUG] Loading shader properties...") old_shader_type = props.shader_type props.shader_type = preset_data.get('shader_type', props.shader_type) - print(f"[PRESET LOAD DEBUG] shader_type: {old_shader_type} -> {props.shader_type}") old_shader_connection = props.shader_connection props.shader_connection = preset_data.get('shader_connection', props.shader_connection) - print(f"[PRESET LOAD DEBUG] shader_connection: {old_shader_connection} -> {props.shader_connection}") old_use_alpha = props.use_alpha props.use_alpha = preset_data.get('use_alpha', props.use_alpha) - print(f"[PRESET LOAD DEBUG] use_alpha: {old_use_alpha} -> {props.use_alpha}") old_emission_strength = props.emission_strength props.emission_strength = preset_data.get('emission_strength', props.emission_strength) - print(f"[PRESET LOAD DEBUG] emission_strength: {old_emission_strength} -> {props.emission_strength}") old_auto_assign = props.auto_assign_material props.auto_assign_material = preset_data.get('auto_assign_material', props.auto_assign_material) - print(f"[PRESET LOAD DEBUG] auto_assign_material: {old_auto_assign} -> {props.auto_assign_material}") # CRITICAL SHADER PROPERTIES - Most detailed logging - print(f"[PRESET LOAD DEBUG] ==================== CRITICAL SHADER PROPERTIES ====================") old_disable_shadows = props.disable_shadows new_disable_shadows = preset_data.get('disable_shadows', props.disable_shadows) props.disable_shadows = new_disable_shadows - print(f"[PRESET LOAD DEBUG] disable_shadows:") - print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_shadows} (type: {type(old_disable_shadows)})") - print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_shadows', 'NOT FOUND')} (type: {type(preset_data.get('disable_shadows', 'N/A'))})") - print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_shadows} (type: {type(props.disable_shadows)})") old_disable_reflections = props.disable_reflections new_disable_reflections = preset_data.get('disable_reflections', props.disable_reflections) props.disable_reflections = new_disable_reflections - print(f"[PRESET LOAD DEBUG] disable_reflections:") - print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_reflections} (type: {type(old_disable_reflections)})") - print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_reflections', 'NOT FOUND')} (type: {type(preset_data.get('disable_reflections', 'N/A'))})") - print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_reflections} (type: {type(props.disable_reflections)})") old_disable_backfacing = props.disable_backfacing new_disable_backfacing = preset_data.get('disable_backfacing', props.disable_backfacing) props.disable_backfacing = new_disable_backfacing - print(f"[PRESET LOAD DEBUG] disable_backfacing:") - print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_backfacing} (type: {type(old_disable_backfacing)})") - print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_backfacing', 'NOT FOUND')} (type: {type(preset_data.get('disable_backfacing', 'N/A'))})") - print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_backfacing} (type: {type(props.disable_backfacing)})") - print(f"[PRESET LOAD DEBUG] ================================================================") props.generate_normal_map = preset_data.get('generate_normal_map', props.generate_normal_map) props.normal_map_strength = preset_data.get('normal_map_strength', props.normal_map_strength) @@ -369,6 +357,7 @@ class TEXT_TEXTURE_OT_load_preset(Operator): props.shadow_color = preset_data.get('shadow_color', props.shadow_color) # Handle new shadow_spread property with backward compatibility if hasattr(props, 'shadow_spread'): + pass props.shadow_spread = preset_data.get('shadow_spread', 1.0) props.enable_glow = preset_data.get('enable_glow', props.enable_glow) props.glow_blur = preset_data.get('glow_blur', props.glow_blur) @@ -389,6 +378,7 @@ class TEXT_TEXTURE_OT_load_preset(Operator): props.image_overlays.clear() overlay_data_list = preset_data.get('image_overlays', []) for overlay_data in overlay_data_list: + pass overlay = props.image_overlays.add() overlay.name = overlay_data.get('name', 'Overlay') overlay.image_path = overlay_data.get('image_path', '') @@ -402,12 +392,6 @@ class TEXT_TEXTURE_OT_load_preset(Operator): props.is_updating = False # FINAL VERIFICATION: Check what the properties actually are after loading - print(f"[PRESET LOAD DEBUG] ==================== FINAL VERIFICATION ====================") - print(f"[PRESET LOAD DEBUG] Properties AFTER loading preset '{self.preset_name}':") - print(f"[PRESET LOAD DEBUG] disable_shadows: {props.disable_shadows} (type: {type(props.disable_shadows)})") - print(f"[PRESET LOAD DEBUG] disable_reflections: {props.disable_reflections} (type: {type(props.disable_reflections)})") - print(f"[PRESET LOAD DEBUG] disable_backfacing: {props.disable_backfacing} (type: {type(props.disable_backfacing)})") - print(f"[PRESET LOAD DEBUG] ================================================================") # Always generate preview after loading preset # Import preview generation function @@ -422,14 +406,17 @@ class TEXT_TEXTURE_OT_load_preset(Operator): self.report({'INFO'}, f"✅ Loaded preset '{self.preset_name}' {source_info}") except (OSError, IOError) as e: + pass props.is_updating = False self.report({'ERROR'}, f"Failed to read preset file: {str(e)}") return {'CANCELLED'} except json.JSONDecodeError as e: + pass props.is_updating = False self.report({'ERROR'}, f"Invalid preset file format: {str(e)}") return {'CANCELLED'} except Exception as e: + pass props.is_updating = False self.report({'ERROR'}, f"Unexpected error loading preset: {str(e)}") return {'CANCELLED'} @@ -437,6 +424,7 @@ class TEXT_TEXTURE_OT_load_preset(Operator): return {'FINISHED'} class TEXT_TEXTURE_OT_delete_preset(Operator): + pass """Delete preset""" bl_idname = "text_texture.delete_preset" bl_label = "Delete Preset" @@ -446,55 +434,68 @@ class TEXT_TEXTURE_OT_delete_preset(Operator): preset_name: StringProperty() def execute(self, context): + pass props = context.scene.text_texture_props try: + pass # Delete from both blend file and local storage blend_deleted = delete_blend_file_preset(self.preset_name) preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") local_deleted = False if os.path.exists(preset_file): + pass os.remove(preset_file) local_deleted = True if not blend_deleted and not local_deleted: + pass self.report({'WARNING'}, f"Preset '{self.preset_name}' not found in blend file or local storage") # Remove from UI list for i, preset in enumerate(props.presets): + pass if preset.name == self.preset_name: + pass props.presets.remove(i) break delete_info = [] if blend_deleted: + pass delete_info.append("blend file") if local_deleted: + pass delete_info.append("local storage") location_text = " and ".join(delete_info) if delete_info else "nowhere (not found)" self.report({'INFO'}, f"🗑️ Deleted preset '{self.preset_name}' from {location_text}") except (OSError, IOError) as e: + pass self.report({'ERROR'}, f"Failed to delete preset file: {str(e)}") return {'CANCELLED'} except Exception as e: + pass self.report({'ERROR'}, f"Unexpected error deleting preset: {str(e)}") return {'CANCELLED'} return {'FINISHED'} def invoke(self, context, event): + pass # Add confirmation dialog for better UX return context.window_manager.invoke_confirm(self, event) def draw(self, context): + pass 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_refresh_presets(Operator): + pass """Refresh presets from blend file and local storage""" bl_idname = "text_texture.refresh_presets" bl_label = "Refresh Presets" @@ -502,11 +503,14 @@ class TEXT_TEXTURE_OT_refresh_presets(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass try: + pass # Use the reliable synchronous refresh system success = refresh_presets_sync() if success: + pass props = context.scene.text_texture_props total_presets = len(props.presets) @@ -517,21 +521,25 @@ class TEXT_TEXTURE_OT_refresh_presets(Operator): self.report({'INFO'}, f"✅ Refreshed {total_presets} presets ({blend_count} blend file, {persistent_count} persistent, {local_count} local)") else: + pass self.report({'WARNING'}, "Preset refresh completed with issues - check console for details") # Force UI redraw for area in context.screen.areas: + pass area.tag_redraw() return {'FINISHED'} except Exception as e: + pass self.report({'ERROR'}, f"Failed to refresh presets: {str(e)}") import traceback traceback.print_exc() return {'CANCELLED'} class TEXT_TEXTURE_OT_export_presets(Operator): + pass """Export all presets to a file for backup""" bl_idname = "text_texture.export_presets" bl_label = "Export Presets" @@ -546,7 +554,9 @@ class TEXT_TEXTURE_OT_export_presets(Operator): ) def execute(self, context): + pass try: + pass props = context.scene.text_texture_props # Collect all presets from all sources @@ -555,6 +565,7 @@ class TEXT_TEXTURE_OT_export_presets(Operator): # Get blend file presets blend_presets = get_blend_file_presets() for name, data in blend_presets.items(): + pass all_presets[name] = { "data": data, "source": "BLEND_FILE" @@ -563,11 +574,15 @@ class TEXT_TEXTURE_OT_export_presets(Operator): # Get persistent presets persistent_dir = get_persistent_preset_path() if os.path.exists(persistent_dir): + pass for filename in os.listdir(persistent_dir): + pass if filename.endswith('.json'): + pass preset_name = os.path.splitext(filename)[0] if preset_name not in all_presets: # Don't override blend file presets try: + pass with open(os.path.join(persistent_dir, filename), 'r') as f: preset_data = json.load(f) all_presets[preset_name] = { @@ -575,16 +590,20 @@ class TEXT_TEXTURE_OT_export_presets(Operator): "source": "PERSISTENT_FILE" } except (json.JSONDecodeError, OSError) as e: - print(f"[TTG] Error reading persistent preset {preset_name}: {e}") + pass # Get legacy presets legacy_dir = get_preset_path() if os.path.exists(legacy_dir) and legacy_dir != persistent_dir: + pass for filename in os.listdir(legacy_dir): + pass if filename.endswith('.json'): + pass preset_name = os.path.splitext(filename)[0] if preset_name not in all_presets: # Don't override higher priority presets try: + pass with open(os.path.join(legacy_dir, filename), 'r') as f: preset_data = json.load(f) all_presets[preset_name] = { @@ -592,9 +611,10 @@ class TEXT_TEXTURE_OT_export_presets(Operator): "source": "LOCAL_FILE" } except (json.JSONDecodeError, OSError) as e: - print(f"[TTG] Error reading legacy preset {preset_name}: {e}") + pass if not all_presets: + pass self.report({'WARNING'}, "No presets found to export") return {'CANCELLED'} @@ -614,10 +634,12 @@ class TEXT_TEXTURE_OT_export_presets(Operator): return {'FINISHED'} except Exception as e: + pass self.report({'ERROR'}, f"Failed to export presets: {str(e)}") return {'CANCELLED'} def invoke(self, context, event): + pass # Set default filename with timestamp import datetime timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") @@ -627,6 +649,7 @@ class TEXT_TEXTURE_OT_export_presets(Operator): return {'RUNNING_MODAL'} class TEXT_TEXTURE_OT_import_presets(Operator): + pass """Import presets from a backup file""" bl_idname = "text_texture.import_presets" bl_label = "Import Presets" @@ -657,8 +680,11 @@ class TEXT_TEXTURE_OT_import_presets(Operator): ) def execute(self, context): + pass try: + pass if not os.path.exists(self.filepath): + pass self.report({'ERROR'}, "Backup file not found") return {'CANCELLED'} @@ -670,11 +696,13 @@ class TEXT_TEXTURE_OT_import_presets(Operator): # Validate format if "presets" not in import_data: + pass self.report({'ERROR'}, "Invalid backup file format") return {'CANCELLED'} imported_presets = import_data["presets"] if not imported_presets: + pass self.report({'WARNING'}, "No presets found in backup file") return {'CANCELLED'} @@ -685,8 +713,11 @@ class TEXT_TEXTURE_OT_import_presets(Operator): # Also check persistent storage persistent_dir = get_persistent_preset_path() if os.path.exists(persistent_dir): + pass for filename in os.listdir(persistent_dir): + pass if filename.endswith('.json'): + pass existing_preset_names.add(os.path.splitext(filename)[0]) imported_count = 0 @@ -694,29 +725,37 @@ class TEXT_TEXTURE_OT_import_presets(Operator): errors = [] for preset_name, preset_info in imported_presets.items(): + pass preset_data = preset_info.get("data", {}) # Handle conflicts final_name = preset_name if preset_name in existing_preset_names: + pass if self.import_mode == 'SKIP': + pass skipped_count += 1 continue elif self.import_mode == 'RENAME': counter = 1 base_name = preset_name while final_name in existing_preset_names: + pass final_name = f"{base_name}_imported_{counter}" counter += 1 # OVERWRITE mode uses original name try: + pass # Import to blend file or persistent storage based on setting if self.import_to_blend: + pass success = save_blend_file_preset(final_name, preset_data) if not success: + pass raise Exception("Failed to save to blend file") else: + pass # Save to persistent storage persistent_file = os.path.join(get_persistent_preset_path(), f"{final_name}.json") with open(persistent_file, 'w') as f: @@ -727,50 +766,63 @@ class TEXT_TEXTURE_OT_import_presets(Operator): # Add to UI list if not already present existing_preset = None for preset in props.presets: + pass if preset.name == final_name: + pass existing_preset = preset break if not existing_preset: + pass new_preset = props.presets.add() new_preset.name = final_name new_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE' else: + pass existing_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE' except Exception as e: + pass errors.append(f"{preset_name}: {str(e)}") # Report results if imported_count > 0: + pass storage_location = "blend file" if self.import_to_blend else "persistent storage" self.report({'INFO'}, f"✅ Imported {imported_count} presets to {storage_location}") if skipped_count > 0: + pass self.report({'INFO'}, f"⏭️ Skipped {skipped_count} existing presets") else: + pass self.report({'WARNING'}, "No presets were imported") if errors: + pass self.report({'WARNING'}, f"Errors importing {len(errors)} presets - check console") for error in errors[:5]: # Show first 5 errors - print(f"[TTG] Import error: {error}") + print(f"Import error: {error}") return {'FINISHED'} except Exception as e: + pass self.report({'ERROR'}, f"Failed to import presets: {str(e)}") return {'CANCELLED'} def invoke(self, context, event): + pass context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} def draw(self, context): + pass layout = self.layout layout.prop(self, "import_mode") layout.prop(self, "import_to_blend") class TEXT_TEXTURE_OT_show_migration_report(Operator): + pass """Show migration report with backup and upgrade information""" bl_idname = "text_texture.show_migration_report" bl_label = "Show Migration Report" @@ -778,12 +830,15 @@ class TEXT_TEXTURE_OT_show_migration_report(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass return {'FINISHED'} def invoke(self, context, event): + pass return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context): + pass layout = self.layout layout.label(text="Migration Report", icon='INFO') layout.separator() diff --git a/src/operators/utility_ops.py b/src/operators/utility_ops.py index 47c2acb..4a8270e 100644 --- a/src/operators/utility_ops.py +++ b/src/operators/utility_ops.py @@ -10,53 +10,52 @@ import os # Import functions from parent module try: - print("[TTG DEBUG] IMPORT TRACE: Attempting to import from ui.preview...") - print("[TTG DEBUG] IMPORT TRACE: Looking for generate_preview in ui.preview module") + pass # First, let's see what's actually available in ui.preview from .. import ui - print(f"[TTG DEBUG] IMPORT TRACE: ui.preview module contents: {dir(ui.preview)}") # Now try the actual import that's failing from ..core.generation_engine import generate_preview - print("[TTG DEBUG] IMPORT TRACE: SUCCESS - generate_preview imported from ui.preview") from ..utils.system import get_system_fonts, get_font_enum_items from ..presets.storage_system import refresh_presets_sync, ensure_presets_available from ..utils.constants import sync_margin_values except ImportError as e: - print(f"[TTG DEBUG] IMPORT ERROR: Failed to import from ui.preview: {e}") - print("[TTG DEBUG] IMPORT TRACE: This confirms generate_preview is NOT in ui.preview") + pass # Let's check if it's available in core.generation_engine try: - print("[TTG DEBUG] IMPORT TRACE: Attempting fallback import from core.generation_engine...") + pass from ..core.generation_engine import generate_preview - print("[TTG DEBUG] IMPORT TRACE: SUCCESS - generate_preview found in core.generation_engine!") from ..utils.system import get_system_fonts, get_font_enum_items from ..presets.storage_system import refresh_presets_sync, ensure_presets_available from ..utils.constants import sync_margin_values - print("[TTG DEBUG] IMPORT TRACE: All imports successful with corrected path") except ImportError as fallback_error: - print(f"[TTG DEBUG] IMPORT ERROR: Fallback import also failed: {fallback_error}") - print(f"[TTG DEBUG] IMPORT ERROR: Original error was: {e}") + pass # Fallback imports for development/testing - print("[TTG DEBUG] IMPORT TRACE: Using fallback placeholder functions") def generate_preview(context): + pass print("Fallback generate_preview called") return None def get_system_fonts(): + pass return {} def get_font_enum_items(): + pass return [("default", "Default Font", "Use system default font", 0)] def refresh_presets_sync(): + pass return True def ensure_presets_available(): + pass return True def sync_margin_values(props, changed_margin): pass + pass class TEXT_TEXTURE_OT_refresh_preview(Operator): + pass """Force refresh the preview""" bl_idname = "text_texture.refresh_preview" bl_label = "Refresh Preview" @@ -64,6 +63,7 @@ class TEXT_TEXTURE_OT_refresh_preview(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass props = context.scene.text_texture_props # Clear existing preview first @@ -73,18 +73,22 @@ class TEXT_TEXTURE_OT_refresh_preview(Operator): result = generate_preview(context) if result: + pass self.report({'INFO'}, f"Preview refreshed at {props.preview_size}px") else: + pass self.report({'ERROR'}, "Failed to generate preview - check console for details") # Force UI update for area in context.screen.areas: + pass area.tag_redraw() return {'FINISHED'} class TEXT_TEXTURE_OT_view_preview_zoom(Operator): + pass """Open preview in new Image Editor window with zoom support""" bl_idname = "text_texture.view_preview_zoom" bl_label = "Enable Zoom" @@ -92,55 +96,66 @@ class TEXT_TEXTURE_OT_view_preview_zoom(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass 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: + pass self.report({'ERROR'}, "Failed to generate preview") return {'CANCELLED'} if not props.preview_image: + pass 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: + pass # 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(): + pass # Find the newest window (should be the one we just opened) newest_window = None for window in bpy.context.window_manager.windows: + pass if window.screen.name.startswith("temp"): + pass newest_window = window break if not newest_window: + pass # Fallback: use any non-main window for window in bpy.context.window_manager.windows: + pass if window != bpy.context.window: + pass newest_window = window break if newest_window: + pass # Set the area type to Image Editor for area in newest_window.screen.areas: + pass if area.type == 'PREFERENCES': + pass area.type = 'IMAGE_EDITOR' - print("[ZOOM] Changed area to Image Editor") # Set the preview image for space in area.spaces: + pass if space.type == 'IMAGE_EDITOR': + pass space.image = props.preview_image - print(f"[ZOOM] Set preview image: {props.preview_image.name}") # Set up proper viewing override = { @@ -152,18 +167,17 @@ class TEXT_TEXTURE_OT_view_preview_zoom(Operator): with bpy.context.temp_override(**override): try: + pass 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}") + pass 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") + pass self.report({'ERROR'}, "Failed to setup new window") return None # Stop timer @@ -172,14 +186,15 @@ class TEXT_TEXTURE_OT_view_preview_zoom(Operator): bpy.app.timers.register(setup_image_window, first_interval=0.1) except Exception as e: - print(f"[ZOOM] Error creating new window: {e}") + pass # 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: + pass if area.type in ['NODE_EDITOR', 'VIEW_3D', 'PROPERTIES']: - print(f"[ZOOM] Fallback: Splitting {area.type} to create Image Editor") + pass # Split horizontally to create a new area with context.temp_override(area=area): @@ -187,14 +202,17 @@ class TEXT_TEXTURE_OT_view_preview_zoom(Operator): # Find the newly created area for new_area in context.screen.areas: + pass if new_area != area and new_area.y != area.y: + pass new_area.type = 'IMAGE_EDITOR' # Set the image in the Image Editor for space in new_area.spaces: + pass if space.type == 'IMAGE_EDITOR': + pass space.image = props.preview_image - print("[ZOOM] Fallback: Image set in split Image Editor") break # Tag for redraw and fit to view @@ -205,10 +223,10 @@ class TEXT_TEXTURE_OT_view_preview_zoom(Operator): with context.temp_override(**override): try: + pass 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}") + pass self.report({'INFO'}, "✅ ZOOM ENABLED! Use mouse wheel to zoom, middle-mouse to pan") return {'FINISHED'} @@ -221,6 +239,7 @@ class TEXT_TEXTURE_OT_view_preview_zoom(Operator): class TEXT_TEXTURE_OT_open_panel(Operator): + pass """Open Text Texture Generator panel""" bl_idname = "text_texture.open_panel" bl_label = "Open Text Texture Panel" @@ -228,11 +247,16 @@ class TEXT_TEXTURE_OT_open_panel(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass # Ensure the sidebar is visible for area in context.screen.areas: + pass if area.type == 'VIEW_3D': + pass for space in area.spaces: + pass if space.type == 'VIEW_3D': + pass space.show_region_ui = True # Switch to Tool tab area.tag_redraw() @@ -240,7 +264,9 @@ class TEXT_TEXTURE_OT_open_panel(Operator): return {'FINISHED'} elif area.type == 'NODE_EDITOR': for space in area.spaces: + pass if space.type == 'NODE_EDITOR': + pass space.show_region_ui = True area.tag_redraw() self.report({'INFO'}, "Text Texture panel opened in Shader Editor sidebar") @@ -251,6 +277,7 @@ class TEXT_TEXTURE_OT_open_panel(Operator): class TEXT_TEXTURE_OT_refresh_fonts(Operator): + pass """Refresh fonts""" bl_idname = "text_texture.refresh_fonts" bl_label = "Refresh Fonts" @@ -258,7 +285,9 @@ class TEXT_TEXTURE_OT_refresh_fonts(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass if hasattr(get_font_enum_items, 'cached_fonts'): + pass del get_font_enum_items.cached_fonts get_font_enum_items.cached_fonts = get_system_fonts() self.report({'INFO'}, f"Found {len(get_font_enum_items.cached_fonts)} fonts") @@ -266,6 +295,7 @@ class TEXT_TEXTURE_OT_refresh_fonts(Operator): class TEXT_TEXTURE_OT_refresh_presets(Operator): + pass """Refresh presets from blend file and local storage""" bl_idname = "text_texture.refresh_presets" bl_label = "Refresh Presets" @@ -273,11 +303,14 @@ class TEXT_TEXTURE_OT_refresh_presets(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass try: + pass # Use the reliable synchronous refresh system success = refresh_presets_sync() if success: + pass props = context.scene.text_texture_props total_presets = len(props.presets) @@ -288,15 +321,18 @@ class TEXT_TEXTURE_OT_refresh_presets(Operator): self.report({'INFO'}, f"✅ Refreshed {total_presets} presets ({blend_count} blend file, {persistent_count} persistent, {local_count} local)") else: + pass self.report({'WARNING'}, "Preset refresh completed with issues - check console for details") # Force UI redraw for area in context.screen.areas: + pass area.tag_redraw() return {'FINISHED'} except Exception as e: + pass self.report({'ERROR'}, f"Failed to refresh presets: {str(e)}") import traceback traceback.print_exc() @@ -304,6 +340,7 @@ class TEXT_TEXTURE_OT_refresh_presets(Operator): class TEXT_TEXTURE_OT_add_overlay(Operator): + pass """Add new image overlay""" bl_idname = "text_texture.add_overlay" bl_label = "Add Overlay" @@ -311,6 +348,7 @@ class TEXT_TEXTURE_OT_add_overlay(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass props = context.scene.text_texture_props overlay = props.image_overlays.add() @@ -319,6 +357,7 @@ class TEXT_TEXTURE_OT_add_overlay(Operator): # Force UI redraw for area in context.screen.areas: + pass area.tag_redraw() self.report({'INFO'}, f"Added overlay: {overlay.name}") @@ -326,6 +365,7 @@ class TEXT_TEXTURE_OT_add_overlay(Operator): class TEXT_TEXTURE_OT_remove_overlay(Operator): + pass """Remove active image overlay""" bl_idname = "text_texture.remove_overlay" bl_label = "Remove Overlay" @@ -333,28 +373,34 @@ class TEXT_TEXTURE_OT_remove_overlay(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + pass props = context.scene.text_texture_props if props.image_overlays and props.active_overlay_index < len(props.image_overlays): + pass 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: + pass props.active_overlay_index = len(props.image_overlays) - 1 # Force UI redraw for area in context.screen.areas: + pass area.tag_redraw() self.report({'INFO'}, f"Removed overlay: {overlay_name}") else: + pass self.report({'WARNING'}, "No overlay to remove") return {'FINISHED'} class TEXT_TEXTURE_OT_set_anchor_point(Operator): + pass """Set anchor point for text positioning""" bl_idname = "text_texture.set_anchor_point" bl_label = "Set Anchor Point" @@ -364,12 +410,14 @@ class TEXT_TEXTURE_OT_set_anchor_point(Operator): anchor_point: StringProperty() def execute(self, context): + pass props = context.scene.text_texture_props props.anchor_point = self.anchor_point return {'FINISHED'} class TEXT_TEXTURE_OT_sync_margins(Operator): + pass """Sync margin values when linked""" bl_idname = "text_texture.sync_margins" bl_label = "Sync Margins" @@ -379,12 +427,14 @@ class TEXT_TEXTURE_OT_sync_margins(Operator): margin_type: StringProperty() def execute(self, context): + pass props = context.scene.text_texture_props sync_margin_values(props, self.margin_type) return {'FINISHED'} class TEXT_TEXTURE_OT_duplicate_overlay(Operator): + pass """Duplicate image overlay""" bl_idname = "text_texture.duplicate_overlay" bl_label = "Duplicate Overlay" @@ -394,19 +444,14 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator): overlay_index: IntProperty() def execute(self, context): + pass props = context.scene.text_texture_props if 0 <= self.overlay_index < len(props.image_overlays): + pass source_overlay = props.image_overlays[self.overlay_index] # Debug logging - print(f"[DUPLICATE DEBUG] Duplicating overlay '{source_overlay.name}'") - print(f"[DUPLICATE DEBUG] Source properties:") - print(f"[DUPLICATE DEBUG] image_path: '{source_overlay.image_path}'") - print(f"[DUPLICATE DEBUG] enabled: {source_overlay.enabled}") - print(f"[DUPLICATE DEBUG] positioning_mode: '{source_overlay.positioning_mode}'") - print(f"[DUPLICATE DEBUG] scale: {source_overlay.scale}") - print(f"[DUPLICATE DEBUG] z_index: {source_overlay.z_index}") new_overlay = props.image_overlays.add() @@ -421,10 +466,11 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator): # For PREPEND/APPEND overlays, increment z_index to ensure duplicate appears above original # This prevents the duplicate from being rendered behind the original overlay if source_overlay.positioning_mode in ['PREPEND', 'APPEND']: + pass # Increment z_index to ensure proper layering order for duplicated overlay new_overlay.z_index = min(source_overlay.z_index + 1, 10) # Cap at max z_index value - print(f"[DUPLICATE DEBUG] Fixed z_index for {source_overlay.positioning_mode} overlay: {source_overlay.z_index} -> {new_overlay.z_index}") else: + pass # For ABSOLUTE overlays, keep same z_index (different positions so no overlap issue) new_overlay.z_index = source_overlay.z_index new_overlay.positioning_mode = source_overlay.positioning_mode @@ -433,55 +479,47 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator): new_overlay.enabled = source_overlay.enabled # Verify the copy was successful - print(f"[DUPLICATE DEBUG] New overlay properties:") - print(f"[DUPLICATE DEBUG] name: '{new_overlay.name}'") - print(f"[DUPLICATE DEBUG] image_path: '{new_overlay.image_path}'") - print(f"[DUPLICATE DEBUG] enabled: {new_overlay.enabled}") - print(f"[DUPLICATE DEBUG] positioning_mode: '{new_overlay.positioning_mode}'") - print(f"[DUPLICATE DEBUG] scale: {new_overlay.scale}") - print(f"[DUPLICATE DEBUG] z_index: {new_overlay.z_index}") # Move to position after source new_index = len(props.image_overlays) - 1 target_index = min(self.overlay_index + 1, new_index) if new_index != target_index: + pass props.image_overlays.move(new_index, target_index) - print(f"[DUPLICATE DEBUG] Moved overlay from index {new_index} to {target_index}") props.active_overlay_index = target_index # Clear any cached images to force reload - print(f"[DUPLICATE DEBUG] Clearing image cache and forcing preview update...") # Set the updating flag to prevent recursive updates props.is_updating = True try: + pass # Force immediate preview regeneration - print(f"[DUPLICATE DEBUG] Generating new preview...") preview_result = generate_preview(context) if preview_result: - print(f"[DUPLICATE DEBUG] Preview generated successfully") + pass else: - print(f"[DUPLICATE DEBUG] Preview generation failed") + pass # Force UI redraw for area in context.screen.areas: + pass area.tag_redraw() except Exception as e: - print(f"[DUPLICATE DEBUG] Error updating preview: {e}") + pass import traceback traceback.print_exc() finally: props.is_updating = False - print(f"[DUPLICATE DEBUG] Total overlays now: {len(props.image_overlays)}") - print(f"[DUPLICATE DEBUG] Duplication operation completed") self.report({'INFO'}, f"✅ Duplicated overlay: {source_overlay.name}") else: + pass self.report({'ERROR'}, "Invalid overlay index") return {'CANCELLED'} @@ -489,6 +527,7 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator): class TEXT_TEXTURE_OT_delete_overlay(Operator): + pass """Delete image overlay""" bl_idname = "text_texture.delete_overlay" bl_label = "Delete Overlay" @@ -498,38 +537,39 @@ class TEXT_TEXTURE_OT_delete_overlay(Operator): overlay_index: IntProperty() def execute(self, context): + pass props = context.scene.text_texture_props - print(f"[DELETE DEBUG] Deleting overlay at index {self.overlay_index}") if 0 <= self.overlay_index < len(props.image_overlays): + pass overlay_name = props.image_overlays[self.overlay_index].name - print(f"[DELETE DEBUG] Deleting overlay: '{overlay_name}'") props.image_overlays.remove(self.overlay_index) # Update active index if props.active_overlay_index >= len(props.image_overlays) and props.image_overlays: + pass props.active_overlay_index = len(props.image_overlays) - 1 elif not props.image_overlays: props.active_overlay_index = 0 - print(f"[DELETE DEBUG] Overlay deleted successfully. Total overlays now: {len(props.image_overlays)}") # Force preview update after deletion try: + pass generate_preview(context) - print(f"[DELETE DEBUG] Preview updated after deletion") except Exception as e: - print(f"[DELETE DEBUG] Error updating preview: {e}") + pass # Force UI redraw for area in context.screen.areas: + pass area.tag_redraw() self.report({'INFO'}, f"Deleted overlay: {overlay_name}") else: - print(f"[DELETE DEBUG] Invalid overlay index: {self.overlay_index}") + pass self.report({'ERROR'}, "Invalid overlay index") return {'CANCELLED'} @@ -537,6 +577,7 @@ class TEXT_TEXTURE_OT_delete_overlay(Operator): class TEXT_TEXTURE_OT_move_overlay(Operator): + pass """Move image overlay up or down in the list""" bl_idname = "text_texture.move_overlay" bl_label = "Move Overlay" @@ -552,39 +593,41 @@ class TEXT_TEXTURE_OT_move_overlay(Operator): ) def execute(self, context): + pass props = context.scene.text_texture_props - print(f"[MOVE DEBUG] Moving overlay at index {self.overlay_index} direction: {self.direction}") if 0 <= self.overlay_index < len(props.image_overlays): + pass current_index = self.overlay_index new_index = current_index if self.direction == 'UP' and current_index > 0: + pass new_index = current_index - 1 elif self.direction == 'DOWN' and current_index < len(props.image_overlays) - 1: new_index = current_index + 1 if new_index != current_index: + pass overlay_name = props.image_overlays[current_index].name props.image_overlays.move(current_index, new_index) props.active_overlay_index = new_index - print(f"[MOVE DEBUG] Successfully moved overlay '{overlay_name}' from {current_index} to {new_index}") # Force preview update after moving (order affects rendering) try: + pass generate_preview(context) - print(f"[MOVE DEBUG] Preview updated after move operation") except Exception as e: - print(f"[MOVE DEBUG] Error updating preview: {e}") + pass self.report({'INFO'}, f"Moved overlay {self.direction.lower()}") else: - print(f"[MOVE DEBUG] No movement needed - overlay is already at edge") + pass self.report({'INFO'}, f"Overlay is already at {self.direction.lower()} edge") else: - print(f"[MOVE DEBUG] Invalid overlay index: {self.overlay_index}") + pass self.report({'ERROR'}, "Invalid overlay index") return {'CANCELLED'} diff --git a/src/presets/__pycache__/__init__.cpython-311.pyc b/src/presets/__pycache__/__init__.cpython-311.pyc index 0276c6f..e9006ad 100644 Binary files a/src/presets/__pycache__/__init__.cpython-311.pyc and b/src/presets/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/presets/__pycache__/migration.cpython-311.pyc b/src/presets/__pycache__/migration.cpython-311.pyc index 4bfb94c..b965d0a 100644 Binary files a/src/presets/__pycache__/migration.cpython-311.pyc and b/src/presets/__pycache__/migration.cpython-311.pyc differ diff --git a/src/presets/__pycache__/storage_system.cpython-311.pyc b/src/presets/__pycache__/storage_system.cpython-311.pyc index 748d6e5..410ba1c 100644 Binary files a/src/presets/__pycache__/storage_system.cpython-311.pyc and b/src/presets/__pycache__/storage_system.cpython-311.pyc differ diff --git a/src/presets/migration.py b/src/presets/migration.py index dd08a70..74e13fd 100644 --- a/src/presets/migration.py +++ b/src/presets/migration.py @@ -6,26 +6,28 @@ import shutil from datetime import datetime def migrate_presets_if_needed(): + pass """ Migrate presets from older versions if needed. This function checks if migration is required and performs it automatically. Returns: dict with 'migrated_count' key indicating number of presets migrated. """ - print("[TTG MIGRATION DEBUG] Starting migrate_presets_if_needed()") preferences = bpy.context.preferences.addons.get("Text_Texture_Generator_Pro") if not preferences: - print("[TTG MIGRATION DEBUG] No preferences found - returning proper dict format") + pass return {'migrated_count': 0} addon_version = preferences.preferences.get("addon_version", "0.0.0") current_version = "1.0.0" # This should match the addon's current version if addon_version != current_version: + pass print(f"Text Texture Generator: Migrating presets from version {addon_version} to {current_version}") # Backup existing presets before migration backup_success = backup_presets() if not backup_success: + pass print("Text Texture Generator: Warning - Failed to backup presets before migration") # Perform migration steps based on version differences @@ -33,6 +35,7 @@ def migrate_presets_if_needed(): migrated_count = 0 try: + pass # Version-specific migration logic would go here # For now, we'll just update the version number preferences.preferences["addon_version"] = current_version @@ -42,27 +45,30 @@ def migrate_presets_if_needed(): migrated_count = 1 if migration_success: + pass print(f"Text Texture Generator: Successfully migrated presets to version {current_version}") else: + pass print("Text Texture Generator: Migration completed with warnings") except Exception as e: + pass print(f"Text Texture Generator: Error during preset migration: {str(e)}") migration_success = False migrated_count = 0 - print(f"[TTG MIGRATION DEBUG] Migration completed, returning proper dict format: migrated_count={migrated_count}") return {'migrated_count': migrated_count} - print("[TTG MIGRATION DEBUG] No migration needed, returning proper dict format: migrated_count=0") return {'migrated_count': 0} # No migration needed def backup_presets(): + pass """ Create a backup of current presets before migration. Returns True if backup was successful, False otherwise. """ try: + pass # Get the addon directory addon_dir = os.path.dirname(__file__) parent_dir = os.path.dirname(addon_dir) @@ -76,28 +82,35 @@ def backup_presets(): # Check for blend file presets if bpy.data.filepath: + pass blend_file_dir = os.path.dirname(bpy.data.filepath) preset_file = os.path.join(blend_file_dir, ".text_texture_presets.json") if os.path.exists(preset_file): + pass preset_files.append(preset_file) # Check for persistent presets preferences = bpy.context.preferences.addons.get("Text_Texture_Generator_Pro") if preferences and hasattr(preferences.preferences, 'persistent_presets_path'): + pass persistent_path = preferences.preferences.persistent_presets_path if persistent_path and os.path.exists(persistent_path): + pass preset_files.append(persistent_path) # Check for legacy presets in addon directory legacy_preset_file = os.path.join(parent_dir, "text_texture_presets.json") if os.path.exists(legacy_preset_file): + pass preset_files.append(legacy_preset_file) # Create backup if there are files to backup if preset_files: + pass os.makedirs(backup_dir, exist_ok=True) for preset_file in preset_files: + pass filename = os.path.basename(preset_file) backup_path = os.path.join(backup_dir, filename) shutil.copy2(preset_file, backup_path) @@ -117,9 +130,11 @@ def backup_presets(): print(f"Text Texture Generator: Created preset backup in {backup_dir}") return True else: + pass print("Text Texture Generator: No preset files found to backup") return True # Not an error if there are no presets to backup except Exception as e: + pass print(f"Text Texture Generator: Error creating preset backup: {str(e)}") return False \ No newline at end of file diff --git a/src/presets/storage_system.py b/src/presets/storage_system.py index a705733..42a1bf4 100644 --- a/src/presets/storage_system.py +++ b/src/presets/storage_system.py @@ -34,6 +34,7 @@ import platform # ============================================================================ def get_preset_path(): + pass """Get path for storing presets (legacy local storage) LEGACY SYSTEM: This storage location is vulnerable to addon updates. @@ -46,36 +47,43 @@ def get_preset_path(): addon_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) presets_dir = os.path.join(addon_dir, "presets") if not os.path.exists(presets_dir): + pass try: + pass os.makedirs(presets_dir) except OSError as e: + pass print(f"Error creating presets directory: {e}") return presets_dir def get_persistent_preset_path(): + pass """Get persistent path for storing presets (survives addon updates)""" import bpy # Get Blender's user config directory config_dir = bpy.utils.user_resource('CONFIG') if not config_dir: + pass # Fallback to addon directory if config path unavailable - print("[TTG] Warning: Could not get user config directory, falling back to addon directory") return get_preset_path() # Create addon-specific subdirectory persistent_dir = os.path.join(config_dir, "text_texture_generator", "presets") try: + pass if not os.path.exists(persistent_dir): + pass os.makedirs(persistent_dir, exist_ok=True) return persistent_dir except OSError as e: - print(f"[TTG] Error creating persistent presets directory: {e}") + pass # Fallback to addon directory return get_preset_path() def get_addon_version(): + pass """Get current addon version for migration tracking""" # Import bl_info from the main addon file import sys @@ -86,192 +94,179 @@ def get_addon_version(): init_file = os.path.join(main_module_path, "__init__.py") try: + pass spec = importlib.util.spec_from_file_location("main_addon", init_file) main_addon = importlib.util.module_from_spec(spec) spec.loader.exec_module(main_addon) return main_addon.bl_info["version"] except Exception as e: - print(f"[TTG] Error getting addon version: {e}") + pass return (2, 3, 2) # Fallback version def get_version_file_path(): + pass """Get path to version tracking file""" persistent_dir = os.path.dirname(get_persistent_preset_path()) return os.path.join(persistent_dir, "addon_version.json") def get_stored_version(): + pass """Get previously stored addon version""" version_file = get_version_file_path() try: + pass if os.path.exists(version_file): + pass with open(version_file, 'r') as f: data = json.load(f) return tuple(data.get('version', (0, 0, 0))) except (json.JSONDecodeError, OSError) as e: - print(f"[TTG] Error reading version file: {e}") + pass + print(f"Error reading version file: {e}") return (0, 0, 0) def save_current_version(): + pass """Save current addon version to persistent storage""" version_file = get_version_file_path() try: + pass version_data = { 'version': get_addon_version(), 'timestamp': time.time() } with open(version_file, 'w') as f: json.dump(version_data, f, indent=2) - print(f"[TTG] Saved addon version {get_addon_version()} to persistent storage") except (OSError, json.JSONEncodeError) as e: - print(f"[TTG] Error saving version file: {e}") + pass + print(f"Error saving version file: {e}") def get_blend_file_presets(): + pass """Get presets stored in the current blend file""" try: + pass # Get custom properties from the scene import bpy scene = bpy.context.scene # DETAILED LOGGING: Function entry - print(f"[GET_BLEND_FILE_PRESETS DEBUG] ==================== LOADING FROM BLEND FILE ==================") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Function called to get presets from blend file") # Store presets in scene custom properties if "text_texture_presets" not in scene: - print(f"[GET_BLEND_FILE_PRESETS DEBUG] No 'text_texture_presets' found in scene, creating empty") + pass scene["text_texture_presets"] = "{}" else: - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Found 'text_texture_presets' in scene") + pass + print("Found 'text_texture_presets' in scene") preset_data = scene.get("text_texture_presets", "{}") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data type: {type(preset_data)}") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data length: {len(str(preset_data))} characters") if isinstance(preset_data, str): - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is string, attempting JSON parse...") + pass try: + pass import json parsed_presets = json.loads(preset_data) - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Successfully parsed JSON") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets type: {type(parsed_presets)}") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets count: {len(parsed_presets) if isinstance(parsed_presets, dict) else 'N/A'}") if isinstance(parsed_presets, dict): - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset names: {list(parsed_presets.keys())}") + pass + print(f"Parsed presets count: {len(parsed_presets)}") # DETAILED VERIFICATION: Check shader properties in each preset for preset_name, preset_content in parsed_presets.items(): - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset '{preset_name}' analysis:") + pass if isinstance(preset_content, dict): - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Keys: {list(preset_content.keys())}") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_shadows: {preset_content.get('disable_shadows', 'NOT FOUND')}") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_reflections: {preset_content.get('disable_reflections', 'NOT FOUND')}") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_backfacing: {preset_content.get('disable_backfacing', 'NOT FOUND')}") + pass + print(f"Preset '{preset_name}' is valid") else: - print(f"[GET_BLEND_FILE_PRESETS DEBUG] ERROR: Preset content is not a dict: {type(preset_content)}") + pass + print(f"Preset '{preset_name}' is not a dict") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Returning parsed presets") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================") return parsed_presets except json.JSONDecodeError as json_err: - print(f"[GET_BLEND_FILE_PRESETS DEBUG] JSON decode error: {json_err}") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw data causing error: {preset_data[:200]}...") # First 200 chars + pass print("Error parsing blend file presets, resetting to empty") scene["text_texture_presets"] = "{}" - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Reset to empty, returning empty dict") return {} else: - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is not string, returning empty dict") - print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================") + pass return {} except Exception as e: - print(f"[GET_BLEND_FILE_PRESETS DEBUG] EXCEPTION occurred: {e}") + pass import traceback - print(f"[GET_BLEND_FILE_PRESETS DEBUG] Full traceback: {traceback.format_exc()}") print(f"Error getting blend file presets: {e}") return {} def save_blend_file_preset(preset_name, preset_data): + pass """Save a preset to the current blend file""" try: + pass import bpy import json scene = bpy.context.scene # DETAILED LOGGING: Function entry - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ==================== SAVING TO BLEND FILE ===================") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Function called with preset_name: '{preset_name}'") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data type: {type(preset_data)}") if isinstance(preset_data, dict): - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data keys: {list(preset_data.keys())}") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Shader properties in preset_data:") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") + pass + print(f"Preset data keys: {list(preset_data.keys())}") else: - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: preset_data is not a dictionary!") + pass + print("ERROR: preset_data is not a dictionary!") # Get existing presets - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Getting existing presets from blend file...") presets = get_blend_file_presets() - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing presets count: {len(presets)}") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing preset names: {list(presets.keys())}") # Add or update the preset - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Adding/updating preset '{preset_name}'...") presets[preset_name] = preset_data - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Presets now contains {len(presets)} items") # VERIFY: Check that shader properties are in the preset we're about to save if preset_name in presets and isinstance(presets[preset_name], dict): + pass saved_preset = presets[preset_name] - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] VERIFICATION - Shader properties in final preset:") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {saved_preset.get('disable_shadows', 'NOT FOUND')}") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {saved_preset.get('disable_reflections', 'NOT FOUND')}") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {saved_preset.get('disable_backfacing', 'NOT FOUND')}") + print(f"Verified preset saved successfully") # Save back to scene custom properties - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Converting presets to JSON...") json_data = json.dumps(presets, indent=2) - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] JSON data length: {len(json_data)} characters") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Saving to scene custom properties...") scene["text_texture_presets"] = json_data - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Successfully saved to scene['text_texture_presets']") # FINAL VERIFICATION: Read back from scene to confirm save - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] FINAL VERIFICATION - Reading back from scene...") readback_data = scene.get("text_texture_presets", "{}") if readback_data: + pass try: + pass readback_presets = json.loads(readback_data) if preset_name in readback_presets: + pass readback_preset = readback_presets[preset_name] - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] READBACK VERIFICATION - Shader properties:") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {readback_preset.get('disable_shadows', 'NOT FOUND')}") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {readback_preset.get('disable_reflections', 'NOT FOUND')}") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {readback_preset.get('disable_backfacing', 'NOT FOUND')}") + print(f"Readback verification successful") else: - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Preset '{preset_name}' not found in readback data!") + pass + print(f"ERROR: Preset '{preset_name}' not found in readback data!") except json.JSONDecodeError as json_err: - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Could not parse readback JSON: {json_err}") + pass + print(f"ERROR: Could not parse readback JSON: {json_err}") else: - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: No data in scene['text_texture_presets'] after save!") + pass + print(f"ERROR: No data in scene['text_texture_presets'] after save!") print(f"Saved preset '{preset_name}' to blend file") - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ================================================================") return True except Exception as e: - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] EXCEPTION occurred: {e}") + pass import traceback - print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Full traceback: {traceback.format_exc()}") print(f"Error saving preset to blend file: {e}") return False def delete_blend_file_preset(preset_name): + pass """Delete a preset from the current blend file""" try: + pass import bpy import json scene = bpy.context.scene @@ -281,51 +276,57 @@ def delete_blend_file_preset(preset_name): # Remove the preset if it exists if preset_name in presets: + pass del presets[preset_name] scene["text_texture_presets"] = json.dumps(presets, indent=2) print(f"Deleted preset '{preset_name}' from blend file") return True else: + pass print(f"Preset '{preset_name}' not found in blend file") return False except Exception as e: + pass print(f"Error deleting preset from blend file: {e}") return False def migrate_from_legacy_system(): + pass """ One-time migration from old three-tier system to simplified two-tier system. Moves LOCAL_FILE presets to PERSISTENT_FILE and creates backup. """ try: + pass legacy_dir = get_preset_path() persistent_dir = get_persistent_preset_path() # Check if legacy directory exists and is different from persistent if not os.path.exists(legacy_dir) or legacy_dir == persistent_dir: - print("[TTG] No separate legacy presets directory found, migration not needed") + pass return True # Check for migration marker file to avoid duplicate migrations migration_marker = os.path.join(persistent_dir, ".migration_complete") if os.path.exists(migration_marker): - print("[TTG] Migration already completed previously") + pass return True # Find legacy preset files legacy_files = [] for filename in os.listdir(legacy_dir): + pass if filename.endswith('.json'): + pass legacy_files.append(filename) if not legacy_files: - print("[TTG] No legacy presets to migrate") + pass # Create marker even if no files to migrate with open(migration_marker, 'w') as f: f.write("Migration completed - no legacy presets found") return True - print(f"[TTG] Found {len(legacy_files)} legacy presets to migrate") # Create backup directory in persistent location backup_dir = os.path.join(persistent_dir, "legacy_backup") @@ -334,8 +335,11 @@ def migrate_from_legacy_system(): # Load existing persistent presets to avoid conflicts existing_persistent = set() if os.path.exists(persistent_dir): + pass for filename in os.listdir(persistent_dir): + pass if filename.endswith('.json'): + pass existing_persistent.add(filename) migrated_count = 0 @@ -343,27 +347,32 @@ def migrate_from_legacy_system(): # Migrate each legacy preset for filename in legacy_files: + pass preset_name = os.path.splitext(filename)[0] legacy_file = os.path.join(legacy_dir, filename) persistent_file = os.path.join(persistent_dir, filename) backup_file = os.path.join(backup_dir, filename) try: + pass # Create backup copy first import shutil shutil.copy2(legacy_file, backup_file) # If preset doesn't exist in persistent storage, migrate it if filename not in existing_persistent: + pass shutil.copy2(legacy_file, persistent_file) migrated_count += 1 - print(f"[TTG] Migrated preset '{preset_name}' to persistent storage") else: + pass skipped_count += 1 - print(f"[TTG] Skipped preset '{preset_name}' (already exists in persistent storage)") except Exception as e: - print(f"[TTG] Error migrating preset '{preset_name}': {e}") + pass + print(f"Error migrating preset '{preset_name}': {e}") + + print(f"Migration completed: {migrated_count} migrated, {skipped_count} skipped") # Create migration completion marker with open(migration_marker, 'w') as f: @@ -376,15 +385,14 @@ def migrate_from_legacy_system(): import json json.dump(migration_info, f, indent=2) - print(f"[TTG] ✅ Migration completed: {migrated_count} migrated, {skipped_count} skipped") - print(f"[TTG] Backup created at: {backup_dir}") return True except Exception as e: - print(f"[TTG] ❌ Error during migration: {e}") + pass return False def get_unified_preset_list(): + pass """ Get unified preset list with two-tier loading and priority rules. Returns dict with preset names as keys and (preset_data, source) as values. @@ -395,64 +403,81 @@ def get_unified_preset_list(): all_presets = {} try: + pass # Load persistent presets first (lower priority) persistent_dir = get_persistent_preset_path() if os.path.exists(persistent_dir): + pass for filename in os.listdir(persistent_dir): + pass if filename.endswith('.json') and not filename.startswith('.'): + pass preset_name = os.path.splitext(filename)[0] try: + pass with open(os.path.join(persistent_dir, filename), 'r') as f: preset_data = json.load(f) all_presets[preset_name] = (preset_data, 'PERSISTENT_FILE') except (json.JSONDecodeError, OSError) as e: - print(f"[TTG] Error reading persistent preset {preset_name}: {e}") + pass + print(f"Error reading persistent preset {preset_name}: {e}") # Load blend file presets and override any conflicts (higher priority) blend_presets = get_blend_file_presets() for name, data in blend_presets.items(): + pass all_presets[name] = (data, 'BLEND_FILE') except Exception as e: - print(f"[TTG] Error in get_unified_preset_list: {e}") + pass + print(f"Error in get_unified_preset_list: {e}") return all_presets def save_preset_unified(preset_data, name, save_to_blend_file=False): + pass """ Save a preset to either persistent file or blend file based on user choice. Default behavior saves to persistent file. """ if save_to_blend_file: + pass return save_blend_file_preset(name, preset_data) else: + pass # Save to persistent file try: + pass persistent_dir = get_persistent_preset_path() persistent_file = os.path.join(persistent_dir, f"{name}.json") with open(persistent_file, 'w') as f: json.dump(preset_data, f, indent=2) - print(f"[TTG] Saved preset '{name}' to persistent storage") return True except Exception as e: - print(f"[TTG] Error saving preset to persistent storage: {e}") + pass + print(f"Error saving preset to persistent storage: {e}") return False def initialize_presets(): + pass """Initialize the simplified preset system and run migration if needed.""" try: + pass import bpy if not hasattr(bpy.context, 'scene') or not bpy.context.scene: + pass return props = bpy.context.scene.text_texture_props if not props: + pass return - print(f"[TTG] Starting preset initialization...") + + print("Starting preset initialization...") # Run one-time migration from old system migrate_from_legacy_system() @@ -465,6 +490,7 @@ def initialize_presets(): # Add all presets to UI list for preset_name, (preset_data, source) in all_presets.items(): + pass preset = props.presets.add() preset.name = preset_name preset.source = source @@ -473,40 +499,49 @@ def initialize_presets(): blend_count = len([p for p in props.presets if p.source == 'BLEND_FILE']) persistent_count = len([p for p in props.presets if p.source == 'PERSISTENT_FILE']) - print(f"[TTG] ✅ Initialized {len(props.presets)} presets ({blend_count} blend file, {persistent_count} persistent)") + + print(f"Initialized {len(props.presets)} presets ({blend_count} blend file, {persistent_count} persistent)") except Exception as e: - print(f"[TTG] Critical error in initialize_presets: {e}") + pass + print(f"Critical error in initialize_presets: {e}") import traceback traceback.print_exc() def refresh_presets(): + pass """Refresh the preset list by rescanning files (legacy function)""" import bpy refresh_presets_sync() # Force UI update try: + pass if bpy.context.area: + pass bpy.context.area.tag_redraw() except: pass + pass def refresh_presets_sync(): + pass """Centralized, synchronous preset refresh - guarantees completion""" try: + pass import bpy - print("[TTG] 🔄 Synchronizing presets...") + + print("Synchronizing presets...") # Ensure we have valid context if not hasattr(bpy.context, 'scene') or not bpy.context.scene: - print("[TTG] ⚠️ No valid scene context for preset refresh") + pass return False props = bpy.context.scene.text_texture_props if not props: - print("[TTG] ⚠️ No text_texture_props found") + pass return False # Run the simplified initialization @@ -515,37 +550,45 @@ def refresh_presets_sync(): # Validate that presets are actually loaded preset_count = len(props.presets) if preset_count == 0: - print("[TTG] ⚠️ Warning: No presets found after refresh") + pass + print("Warning: No presets found after refresh") else: - print(f"[TTG] ✅ Preset sync complete - {preset_count} presets available") + pass + print(f"Preset sync complete - {preset_count} presets available") return True except Exception as e: - print(f"[TTG] ❌ Error in refresh_presets_sync: {e}") + pass import traceback traceback.print_exc() return False def ensure_presets_available(): + pass """Defensive programming - ensure presets are loaded before UI operations""" try: + pass import bpy if not hasattr(bpy.context, 'scene') or not bpy.context.scene: + pass return False props = bpy.context.scene.text_texture_props if not props: + pass return False # If no presets loaded, try to load them if len(props.presets) == 0: - print("[TTG] 🛡️ No presets available - attempting to load...") + pass + print("No presets available - attempting to load...") return refresh_presets_sync() return True except Exception as e: - print(f"[TTG] Error in ensure_presets_available: {e}") + pass + print(f"Error in ensure_presets_available: {e}") return False \ No newline at end of file diff --git a/src/properties/__pycache__/__init__.cpython-311.pyc b/src/properties/__pycache__/__init__.cpython-311.pyc index c5cb4ea..8426375 100644 Binary files a/src/properties/__pycache__/__init__.cpython-311.pyc and b/src/properties/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/properties/__pycache__/property_groups.cpython-311.pyc b/src/properties/__pycache__/property_groups.cpython-311.pyc index f7b56d2..12cba6f 100644 Binary files a/src/properties/__pycache__/property_groups.cpython-311.pyc and b/src/properties/__pycache__/property_groups.cpython-311.pyc differ diff --git a/src/properties/property_groups.py b/src/properties/property_groups.py index 0cc2d05..2f37140 100644 --- a/src/properties/property_groups.py +++ b/src/properties/property_groups.py @@ -18,55 +18,65 @@ import os # Import update functions from the main module (will need to be adjusted during integration) def update_live_preview(self, context): + pass """Update preview when properties change""" if not hasattr(context, 'scene'): + pass return props = context.scene.text_texture_props - print(f"DEBUG: update_live_preview called, is_updating: {props.is_updating}") # Always update preview unless already updating if not props.is_updating: - print("DEBUG: Triggering preview generation from property update") + pass # Use a timer to debounce rapid property changes for better performance if hasattr(update_live_preview, '_timer'): + pass bpy.app.timers.unregister(update_live_preview._timer) def delayed_preview(): + pass # Import generate_preview function (will need to be adjusted during integration) try: + pass from . import generate_preview generate_preview(context) except ImportError: + pass # Fallback during development - print("DEBUG: generate_preview not available during extraction") return None # Stop timer update_live_preview._timer = delayed_preview bpy.app.timers.register(delayed_preview, first_interval=0.1) def get_font_enum_items(self, context): + pass """Dynamic enum items for font selection""" items = [("default", "Default Font", "Use system default font", 0)] if not hasattr(get_font_enum_items, 'cached_fonts'): + pass # Import get_system_fonts function (will need to be adjusted during integration) try: + pass from ..utils.system import get_system_fonts get_font_enum_items.cached_fonts = get_system_fonts() except ImportError: + pass # Fallback during development get_font_enum_items.cached_fonts = {} fonts = get_font_enum_items.cached_fonts for i, (font_name, font_path) in enumerate(sorted(fonts.items())[:50]): + pass items.append((font_path, font_name, f"Font: {font_name}", i + 1)) return items class TEXT_TEXTURE_ImageOverlay(PropertyGroup): + pass """Single image overlay item""" name: StringProperty( name="Image Name", @@ -168,6 +178,7 @@ class TEXT_TEXTURE_ImageOverlay(PropertyGroup): ) class TEXT_TEXTURE_Preset(PropertyGroup): + pass """Single preset item""" name: StringProperty( name="Preset Name", @@ -186,6 +197,7 @@ class TEXT_TEXTURE_Preset(PropertyGroup): ) class TEXT_TEXTURE_Properties(PropertyGroup): + pass """Properties for text texture generation""" is_updating: BoolProperty(default=False) @@ -238,6 +250,12 @@ class TEXT_TEXTURE_Properties(PropertyGroup): default=False ) + expand_version_info: BoolProperty( + name="Version Information", + description="Show/hide Version Information section", + default=False + ) + text: StringProperty( name="Text", description="Text to render as texture", diff --git a/src/ui/__init__.py b/src/ui/__init__.py index b16a4cf..7a9e079 100644 --- a/src/ui/__init__.py +++ b/src/ui/__init__.py @@ -8,14 +8,17 @@ from .panels import ( # Menu functions for Blender integration def menu_func_node_editor(self, context): + pass """Add menu item to Node Editor's Add menu""" self.layout.operator("text_texture.generate", text="Text Texture", icon='IMAGE_DATA') def menu_func_view3d(self, context): + pass """Add menu item to 3D View's Add menu""" self.layout.operator("text_texture.generate", text="Text Texture", icon='IMAGE_DATA') def menu_func_image(self, context): + pass """Add menu item to Image Editor menu""" self.layout.operator("text_texture.generate", text="Text Texture", icon='IMAGE_DATA') diff --git a/src/ui/__pycache__/__init__.cpython-311.pyc b/src/ui/__pycache__/__init__.cpython-311.pyc index 9107426..9a0a997 100644 Binary files a/src/ui/__pycache__/__init__.cpython-311.pyc and b/src/ui/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/ui/__pycache__/panels.cpython-311.pyc b/src/ui/__pycache__/panels.cpython-311.pyc index 841efdd..d61d935 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/__pycache__/preview.cpython-311.pyc b/src/ui/__pycache__/preview.cpython-311.pyc index 346f708..66c6a85 100644 Binary files a/src/ui/__pycache__/preview.cpython-311.pyc and b/src/ui/__pycache__/preview.cpython-311.pyc differ diff --git a/src/ui/panels.py b/src/ui/panels.py index c32736d..5f036e2 100644 --- a/src/ui/panels.py +++ b/src/ui/panels.py @@ -6,13 +6,70 @@ Contains all UI panel classes for different editor contexts. import bpy from bpy.types import Panel from ..core.generation_engine import generate_preview +from ..utils.constants import ( + is_free_version, get_version_badge_text, get_version_badge_icon, + get_upgrade_hint_text, should_show_feature_lock, + get_feature_availability_text, get_version_info_details, + has_image_overlays, has_basic_utilities, has_presets, + has_normal_maps, has_batch_processing, has_advanced_utilities, + has_advanced_styling +) # ============================================================================ # SHARED UI COMPONENTS # ============================================================================ +def is_operator_available(operator_id): + """Check if an operator is available/registered in Blender""" + try: + import bpy + # Try to get operator class - if it doesn't exist, this will fail + return hasattr(bpy.ops, operator_id.split('.')[0]) and hasattr(getattr(bpy.ops, operator_id.split('.')[0]), operator_id.split('.')[1]) + except: + return False + +def create_premium_lock_function(feature_name=None): + """Create a standard premium lock function for a specific feature + + Args: + feature_name: Optional feature name for context-specific hints + + Returns: + A function that can be used as fallback_content_func in safe_operator_call + """ + def show_premium_lock(fallback_layout): + fallback_layout.enabled = False + fallback_layout.label(text="🔒 Pro Feature", icon='LOCKED') + return show_premium_lock + +def safe_operator_call(layout, operator_id, fallback_content_func=None, **kwargs): + """Safely call an operator, showing premium content if operator doesn't exist + + Args: + layout: Blender UI layout object + operator_id: String ID of operator (e.g., 'text_texture.add_overlay') + fallback_content_func: Function to call if operator doesn't exist + **kwargs: Arguments to pass to layout.operator() + + Returns: + Operator reference if successful, None if operator doesn't exist + """ + if is_operator_available(operator_id): + return layout.operator(operator_id, **kwargs) + else: + # Operator doesn't exist - show premium lock content instead + if fallback_content_func: + fallback_content_func(layout) + else: + # Default fallback - show generic premium lock + lock_row = layout.row() + lock_row.enabled = False + lock_row.label(text="🔒 Pro Feature", icon='LOCKED') + return None + def draw_collapsible_header(layout, prop_name, text, icon='TRIA_DOWN'): + pass """Draw a collapsible section header with expand/collapse arrow""" props = bpy.context.scene.text_texture_props expanded = getattr(props, prop_name) @@ -27,33 +84,159 @@ def draw_collapsible_header(layout, prop_name, text, icon='TRIA_DOWN'): return box if expanded else None +def draw_version_badge(layout): + pass + """Draw version badge in panel header""" + badge_row = layout.row(align=True) + badge_row.scale_x = 0.7 + badge_row.scale_y = 0.8 + badge_row.alignment = 'RIGHT' + + # Simple label-based badge (more reliable than operator) + badge_label = badge_row.label(text=get_version_badge_text()) + if is_free_version(): + pass + badge_row.label(text="", icon='GIFT') + else: + pass + badge_row.label(text="", icon='STAR') + +def draw_feature_lock_indicator(layout, feature_name, show_hint=True): + """Draw feature lock indicator for premium features""" + if should_show_feature_lock(feature_name): + # Create a box for better visual separation + lock_box = layout.box() + + # Main lock indicator with proper vertical centering using row alignment + lock_row = lock_box.row(align=True) + lock_row.scale_y = 1.2 # Slightly larger for better visual balance + lock_row.alignment = 'CENTER' + + # Use a single row with proper alignment instead of split for better centering + # Add icon with proper spacing + icon_col = lock_row.column(align=True) + icon_col.scale_x = 0.8 + icon_col.alignment = 'CENTER' + icon_col.label(text="", icon='LOCKED') + + # Add text with proper alignment + text_col = lock_row.column(align=True) + text_col.alignment = 'CENTER' + text_col.label(text="🔒 Pro Feature") + + if show_hint: + hint_row = lock_box.row() + hint_row.scale_y = 0.8 + hint_row.alignment = 'CENTER' + hint_row.label(text=get_upgrade_hint_text(feature_name)) + +def draw_version_info_section(layout, props): + pass + """Draw version information display section""" + info_box = layout.box() + info_box.label(text="Version Information", icon='INFO') + + # Version details + version_info = get_version_info_details() + + col = info_box.column(align=True) + col.scale_y = 0.8 + + # Basic version info + col.label(text=f"Version: {version_info['version_type']}") + col.label(text=get_feature_availability_text()) + + # Current limits + 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") + limits_col.label(text=f"• Active features: {version_info['features_enabled']}") + + # Feature availability status + col.separator() + availability_col = col.column(align=True) + availability_col.label(text="Feature Status:") + + if has_presets(): + pass + availability_col.label(text="• Presets: ✓ Available", icon='CHECKMARK') + else: + pass + availability_col.label(text="• Presets: 🔒 Pro Only", icon='LOCKED') + + if has_image_overlays(): + pass + availability_col.label(text="• Image Overlays: ✓ Available", icon='CHECKMARK') + else: + pass + availability_col.label(text="• Image Overlays: 🔒 Pro Only", icon='LOCKED') + + if has_basic_utilities(): + pass + availability_col.label(text="• Basic Tools: ✓ Available", icon='CHECKMARK') + else: + pass + availability_col.label(text="• Basic Tools: 🔒 Pro Only", icon='LOCKED') + + if has_advanced_utilities(): + pass + availability_col.label(text="• Advanced Tools: ✓ Available", icon='CHECKMARK') + else: + pass + availability_col.label(text="• Advanced Tools: 🔒 Pro Only", icon='LOCKED') + + # Upgrade hint for free version + if version_info['upgrade_available']: + pass + col.separator() + upgrade_box = info_box.box() + upgrade_box.scale_y = 0.9 + upgrade_row = upgrade_box.row() + upgrade_row.alignment = 'CENTER' + upgrade_row.label(text="💡 Upgrade to Pro for all features", icon='STAR') + def draw_text_input_section(layout, props): + pass """Draw the main text input field""" layout.prop(props, "text", text="") def draw_font_dropdown_section(layout, props): + pass """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): + pass """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): + pass """Draw image overlays controls with collapsible individual overlays""" - # Add overlay button + # Check if image overlays are available + if not has_image_overlays(): + pass + draw_feature_lock_indicator(layout, 'image_overlays') + return + + # Add overlay button - use safe operator call row = layout.row() - row.operator("text_texture.add_overlay", text="Add Image Overlay", icon='ADD') + safe_operator_call(row, "text_texture.add_overlay", + lambda layout: draw_feature_lock_indicator(layout, 'image_overlays'), + text="Add Image Overlay", icon='ADD') # Individual overlay controls if props.image_overlays: + pass layout.separator() for i, overlay in enumerate(props.image_overlays): + pass # Create collapsible box for each overlay box = layout.box() @@ -69,24 +252,33 @@ def draw_image_overlays_section(layout, props): name_row.prop(overlay, "name", text="") # Action buttons - # Duplicate button - dup_op = header_row.operator("text_texture.duplicate_overlay", text="", icon='DUPLICATE') - dup_op.overlay_index = i + # Duplicate button - use safe operator call + premium_lock = create_premium_lock_function() + dup_op = safe_operator_call(header_row, "text_texture.duplicate_overlay", premium_lock, + text="", icon='DUPLICATE') + if dup_op: + dup_op.overlay_index = i - # Delete button - del_op = header_row.operator("text_texture.delete_overlay", text="", icon='TRASH') - del_op.overlay_index = i + # Delete button - use safe operator call + del_op = safe_operator_call(header_row, "text_texture.delete_overlay", premium_lock, + text="", icon='TRASH') + if del_op: + del_op.overlay_index = i - # Move buttons for reordering + # Move buttons for reordering - use safe operator calls if i > 0: - move_up_op = header_row.operator("text_texture.move_overlay", text="", icon='TRIA_UP') - move_up_op.overlay_index = i - move_up_op.direction = 'UP' + move_up_op = safe_operator_call(header_row, "text_texture.move_overlay", premium_lock, + text="", icon='TRIA_UP') + if move_up_op: + move_up_op.overlay_index = i + move_up_op.direction = 'UP' if i < len(props.image_overlays) - 1: - move_down_op = header_row.operator("text_texture.move_overlay", text="", icon='TRIA_DOWN') - move_down_op.overlay_index = i - move_down_op.direction = 'DOWN' + move_down_op = safe_operator_call(header_row, "text_texture.move_overlay", premium_lock, + text="", icon='TRIA_DOWN') + if move_down_op: + move_down_op.overlay_index = i + move_down_op.direction = 'DOWN' # Show details section with enable/disable state details_col = box.column() @@ -94,6 +286,7 @@ def draw_image_overlays_section(layout, props): # Status indicator if not overlay.enabled: + pass status_row = details_col.row() status_row.label(text="⚠️ Overlay Disabled", icon='INFO') details_col.separator() @@ -105,6 +298,7 @@ def draw_image_overlays_section(layout, props): details_col.prop(overlay, "positioning_mode", text="Position Mode") if overlay.positioning_mode == 'ABSOLUTE': + pass # Position controls for absolute mode pos_row = details_col.row(align=True) pos_row.prop(overlay, "x_position", text="X Position") @@ -121,6 +315,7 @@ def draw_image_overlays_section(layout, props): trans_row.prop(overlay, "z_index", text="Z-Level") def draw_positioning_section(layout, props): + pass """Draw positioning and alignment controls""" from ..utils.system import get_anchor_point_matrix @@ -142,30 +337,40 @@ def draw_positioning_section(layout, props): row = layout.row(align=True) row.prop(props, "prepend_text", text="Prepend") if props.prepend_text.strip(): + pass row.prop(props, "prepend_margin", text="", slider=True) row = layout.row(align=True) row.prop(props, "append_text", text="Append") if props.append_text.strip(): + pass row.prop(props, "append_margin", text="", slider=True) layout.separator() layout.prop(props, "position_mode", text="Mode") if props.position_mode == 'PRESET': + pass layout.separator() layout.label(text="Anchor Point:", icon='ANCHOR_TOP') anchor_matrix = get_anchor_point_matrix() for row_idx, row in enumerate(anchor_matrix): + pass grid_row = layout.row(align=True) grid_row.scale_y = 1.2 for col_idx, anchor in enumerate(row): + pass if anchor == props.anchor_point: - op = grid_row.operator("text_texture.set_anchor_point", text="●", depress=True) + premium_lock = create_premium_lock_function() + op = safe_operator_call(grid_row, "text_texture.set_anchor_point", premium_lock, + text="●", depress=True) else: - op = grid_row.operator("text_texture.set_anchor_point", text="○") - op.anchor_point = anchor + premium_lock = create_premium_lock_function() + op = safe_operator_call(grid_row, "text_texture.set_anchor_point", premium_lock, + text="○") + if op: + op.anchor_point = anchor # Margins layout.separator() @@ -176,6 +381,7 @@ def draw_positioning_section(layout, props): margins_row = layout.row(align=True) margins_row.prop(props, "margin_x", text="X") if not props.margins_linked: + pass margins_row.prop(props, "margin_y", text="Y") # Offsets @@ -196,6 +402,7 @@ def draw_positioning_section(layout, props): layout.prop(props, "constrain_to_canvas") def draw_font_settings_section(layout, props): + pass """Draw extended font settings""" # Font size moved here from top level layout.prop(props, "font_size") @@ -211,6 +418,7 @@ def draw_font_settings_section(layout, props): # Custom font controls moved here from top level layout.prop(props, "use_custom_font") if props.use_custom_font: + pass layout.prop(props, "custom_font_path", text="") layout.separator() @@ -219,6 +427,7 @@ def draw_font_settings_section(layout, props): row.prop(props, "text_align") def draw_colors_section(layout, props): + pass """Draw color controls for text and background""" layout.label(text="Text Color:", icon='COLOR') layout.prop(props, "text_color", text="") @@ -227,72 +436,110 @@ def draw_colors_section(layout, props): layout.label(text="Background Color:", icon='IMAGE_PLANE') layout.prop(props, "background_transparent") if not props.background_transparent: + pass layout.prop(props, "background_color", text="") def draw_preview_options_section(layout, props): + pass """Draw preview options (preview-specific settings only)""" layout.prop(props, "preview_size", text="Preview Size") layout.separator() layout.label(text="Preview Background Display:", icon='SCENE') layout.prop(props, "preview_bg_type", text="Background") if props.preview_bg_type == 'color': + pass layout.prop(props, "preview_bg_color", text="Preview Display 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): + pass """Draw unified presets save/load controls""" - # Save preset section + # Check if presets are available, show lock if not + if not has_presets(): + pass + draw_feature_lock_indicator(layout, 'presets') + return + + # Save preset section - use safe operator calls 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') + + premium_lock = create_premium_lock_function() + safe_operator_call(save_row, "text_texture.save_preset", premium_lock, + text="Save", icon='FILE_NEW') # Save with blend file checkbox layout.prop(props, "save_with_blend_file") layout.separator() - # Import/Export and refresh buttons + # Import/Export and refresh buttons - use safe operator calls backup_row = layout.row(align=True) - backup_row.operator("text_texture.export_presets", text="Export Backup", icon='EXPORT') - backup_row.operator("text_texture.import_presets", text="Import Backup", icon='IMPORT') + + export_lock = create_premium_lock_function() + import_lock = create_premium_lock_function() + + safe_operator_call(backup_row, "text_texture.export_presets", export_lock, + text="Export Backup", icon='EXPORT') + safe_operator_call(backup_row, "text_texture.import_presets", import_lock, + text="Import Backup", icon='IMPORT') utility_row = layout.row(align=True) - utility_row.operator("text_texture.refresh_presets", text="Refresh", icon='FILE_REFRESH') - utility_row.operator("text_texture.show_migration_report", text="Migration Report", icon='INFO') + + refresh_lock = create_premium_lock_function() + migration_lock = create_premium_lock_function() + + safe_operator_call(utility_row, "text_texture.refresh_presets", refresh_lock, + text="Refresh", icon='FILE_REFRESH') + safe_operator_call(utility_row, "text_texture.show_migration_report", migration_lock, + text="Migration Report", icon='INFO') layout.separator() # Load presets section - unified list if props.presets: + pass layout.label(text=f"📁 Available Presets ({len(props.presets)}):", icon='PRESET') # Sort presets alphabetically for consistent display sorted_presets = sorted(props.presets, key=lambda p: p.name.lower()) for preset in sorted_presets: + pass row = layout.row(align=True) row.scale_y = 0.9 # Storage indicator icon if preset.source == 'BLEND_FILE': + pass icon = '📄' # Blend file indicator else: + pass icon = '🏠' # Persistent storage indicator - # Load button with indicator - load_btn = row.operator("text_texture.load_preset", text=f"{icon} {preset.name}") - load_btn.preset_name = preset.name + # Load button with indicator - use safe operator call + load_lock = create_premium_lock_function() + load_btn = safe_operator_call(row, "text_texture.load_preset", load_lock, + text=f"{icon} {preset.name}") + if load_btn: + load_btn.preset_name = preset.name - # Quick overwrite button - overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH') - overwrite_btn.overwrite = True - overwrite_btn.preset_name = preset.name + # Quick overwrite button - use safe operator call + overwrite_lock = create_premium_lock_function() + overwrite_btn = safe_operator_call(row, "text_texture.save_preset", overwrite_lock, + text="", icon='FILE_REFRESH') + if overwrite_btn: + overwrite_btn.overwrite = True + overwrite_btn.preset_name = preset.name - # Delete button - delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') - delete_btn.preset_name = preset.name + # Delete button - use safe operator call + delete_lock = create_premium_lock_function() + delete_btn = safe_operator_call(row, "text_texture.delete_preset", delete_lock, + text="", icon='TRASH') + if delete_btn: + delete_btn.preset_name = preset.name # Legend for storage indicators layout.separator() @@ -303,6 +550,7 @@ def draw_presets_section(layout, props): legend_box.label(text="📄 Blend file (travels with .blend file)") else: + pass layout.label(text="📂 No presets saved yet", icon='INFO') layout.label(text="💡 Save your first preset above!", icon='LIGHT_DATA') layout.separator() @@ -311,7 +559,14 @@ def draw_presets_section(layout, props): info_box.label(text="Check the box to save with your .blend file!") def draw_shader_section(layout, props): + pass """Draw shader generation controls""" + # Check if shader generation is available + from ..utils.constants import has_shader_generation + if not has_shader_generation(): + draw_feature_lock_indicator(layout, 'shader_generation') + return + # Create Shader Button (as requested) row = layout.row() row.scale_y = 1.5 @@ -325,8 +580,10 @@ def draw_shader_section(layout, props): # Show relevant options based on shader type if props.shader_type == 'PRINCIPLED': + pass col.prop(props, "shader_connection", text="Connect") if props.shader_connection == 'EMISSION': + pass col.prop(props, "emission_strength", text="Strength") elif props.shader_type == 'EMISSION': col.prop(props, "emission_strength", text="Strength") @@ -340,28 +597,42 @@ def draw_shader_section(layout, props): # Clean status info if props.auto_assign_material: + pass layout.separator() info_row = layout.row() info_row.scale_y = 0.8 try: + pass context = bpy.context if context and context.active_object and context.active_object.type in ['MESH', 'CURVE']: + pass info_row.label(text=f"→ {context.active_object.name}", icon='CHECKMARK') else: + pass info_row.label(text="Select mesh/curve object", icon='INFO') except: + pass info_row.label(text="Select mesh/curve object", icon='INFO') def draw_shader_options_only(layout, props): + pass """Draw only shader options without the Create button - for collapsible section""" + # Check if shader generation is available + from ..utils.constants import has_shader_generation + if not has_shader_generation(): + draw_feature_lock_indicator(layout, 'shader_generation') + return + # Clean grid layout for shader options grid = layout.grid_flow(row_major=True, columns=2, align=True) grid.prop(props, "shader_type", text="Type") if props.shader_type == 'PRINCIPLED': + pass grid.prop(props, "shader_connection", text="Connect") if props.shader_connection == 'EMISSION': + pass grid.prop(props, "emission_strength", text="Strength") elif props.shader_type == 'EMISSION': grid.prop(props, "emission_strength", text="Strength") @@ -379,12 +650,14 @@ def draw_shader_options_only(layout, props): row.prop(props, "disable_backfacing", toggle=True) def draw_normal_maps_section(layout, props): + pass """Draw normal map generation controls""" # Main toggle for normal map generation layout.prop(props, "generate_normal_map", text="Generate Normal Map", icon='TEXTURE') # Show controls only if normal map generation is enabled if props.generate_normal_map: + pass layout.separator() # Normal map settings in a clean layout @@ -405,12 +678,14 @@ def draw_normal_maps_section(layout, props): info_box.label(text="• Invert changes raised/recessed effect") def draw_text_fitting_section(layout, props): + pass """Draw text fitting controls""" # Main toggle for text fitting layout.prop(props, "enable_text_fitting", text="Enable Text Fitting", icon='FULLSCREEN_ENTER') # Show controls only if text fitting is enabled if props.enable_text_fitting: + pass layout.separator() # Fitting mode selection @@ -437,6 +712,7 @@ def draw_text_fitting_section(layout, props): info_box.label(text="💡 Text fitting automatically sizes text", icon='INFO') if props.text_fitting_mode == 'FIT_WIDTH': + pass info_box.label(text="• Fits text width to canvas width") elif props.text_fitting_mode == 'FIT_HEIGHT': info_box.label(text="• Fits text height to canvas height") @@ -448,12 +724,14 @@ def draw_text_fitting_section(layout, props): info_box.label(text="• Respects min/max font size limits") def draw_multiline_section(layout, props): + pass """Draw multiline text controls""" # Main toggle for multiline text layout.prop(props, "enable_multiline", text="Enable Multiline", icon='TEXT') # Show controls only if multiline is enabled if props.enable_multiline: + pass layout.separator() # Line height and alignment settings @@ -466,6 +744,7 @@ def draw_multiline_section(layout, props): # Auto wrap settings layout.prop(props, "auto_wrap", text="Auto Wrap") if props.auto_wrap: + pass layout.prop(props, "wrap_width", text="Wrap Width (0 = texture width)") layout.separator() @@ -486,12 +765,20 @@ def draw_multiline_section(layout, props): info_box.label(text="• Line height controls spacing") def draw_stroke_section(layout, props): + pass """Draw stroke and outline controls""" + # Check if stroke effects are available + from ..utils.constants import has_stroke_effects + if not has_stroke_effects(): + draw_feature_lock_indicator(layout, 'stroke_effects') + return + # Main toggle for stroke layout.prop(props, "enable_stroke", text="Enable Stroke", icon='MOD_OUTLINE') # Show controls only if stroke is enabled if props.enable_stroke: + pass layout.separator() # Stroke width and color @@ -511,6 +798,7 @@ def draw_stroke_section(layout, props): info_box.label(text="💡 Stroke adds outline to text:", icon='INFO') if props.stroke_position == 'OUTER': + pass info_box.label(text="• Outer: Expands text bounds outward") elif props.stroke_position == 'INNER': info_box.label(text="• Inner: Shrinks text, stroke inside") @@ -521,11 +809,19 @@ def draw_stroke_section(layout, props): info_box.label(text="• Works with multiline text") def draw_shadow_glow_section(layout, props): + pass """Draw shadow and glow effects controls""" + # Check if shadow/glow effects are available + from ..utils.constants import has_shadow_glow_effects + if not has_shadow_glow_effects(): + draw_feature_lock_indicator(layout, 'shadow_glow_effects') + return + # Drop shadow section layout.prop(props, "enable_shadow", text="Enable Drop Shadow", icon='SHADERFX') if props.enable_shadow: + pass layout.separator() # Shadow offset controls in a row @@ -545,6 +841,7 @@ def draw_shadow_glow_section(layout, props): layout.prop(props, "enable_glow", text="Enable Glow", icon='LIGHT_SUN') if props.enable_glow: + pass layout.separator() # Glow properties @@ -554,26 +851,37 @@ def draw_shadow_glow_section(layout, props): # Info box with usage tips if props.enable_shadow or props.enable_glow: + pass layout.separator() info_box = layout.box() info_box.scale_y = 0.8 info_box.label(text="💡 Shadow & Glow effects:", icon='INFO') if props.enable_shadow: + pass info_box.label(text="• Drop shadow: offset + blur for depth") if props.enable_glow: + pass info_box.label(text="• Glow: blur without offset for aura effect") info_box.label(text="• Higher blur = softer effect") info_box.label(text="• Alpha controls effect intensity") def draw_blur_section(layout, props): + pass """Draw blur effects controls""" + # Check if blur effects are available + from ..utils.constants import has_blur_effects + if not has_blur_effects(): + draw_feature_lock_indicator(layout, 'blur_effects') + return + # Main toggle for blur layout.prop(props, "enable_blur", text="Enable Blur", icon='MOD_SMOOTH') # Show controls only if blur is enabled if props.enable_blur: + pass layout.separator() # Blur radius setting @@ -595,6 +903,7 @@ def draw_blur_section(layout, props): # ============================================================================ class TEXT_TEXTURE_PT_panel(Panel): + pass """Main Panel in Shader Editor""" bl_label = "Text Texture Generator" bl_idname = "TEXT_TEXTURE_PT_panel" @@ -604,29 +913,29 @@ class TEXT_TEXTURE_PT_panel(Panel): @classmethod def poll(cls, context): + pass # 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}") + pass else: - print("[TTG DEBUG] No tree_type attribute") + pass # More permissive poll condition to ensure panel shows up if context.space_data.type == 'NODE_EDITOR': + pass if hasattr(context.space_data, 'tree_type'): + pass result = context.space_data.tree_type == 'ShaderNodeTree' - print(f"[TTG DEBUG] Poll result: {result}") return result else: + pass # 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): + pass layout = self.layout props = context.scene.text_texture_props @@ -642,83 +951,205 @@ class TEXT_TEXTURE_PT_panel(Panel): layout.separator(factor=0.5) draw_generate_button(layout) - # CREATE SHADER BUTTON AT TOP LEVEL - AS REQUESTED + # CREATE SHADER BUTTON - WITH PROPER RESTRICTION layout.separator(factor=0.5) create_shader_row = layout.row() create_shader_row.scale_y = 1.5 - create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL') - # Preview Button at top level + # Check if shader generation is available + from ..utils.constants import has_shader_generation + if has_shader_generation(): + create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL') + else: + # Show locked shader button for free version + lock_row = create_shader_row.row() + lock_row.enabled = False + lock_row.operator("text_texture.generate_shader", text="🔒 Create Shader (Pro Only)", icon='LOCKED') + + # Preview Button - ALWAYS available (core functionality for all users) layout.separator(factor=0.5) preview_row = layout.row() preview_row.scale_y = 1.5 - preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + preview_row.operator("text_texture.preview", text="Preview Texture", icon='HIDE_OFF') layout.separator(factor=1.2) - # COLLAPSIBLE SECTIONS + # COLLAPSIBLE SECTIONS - FREE FEATURES FIRST + + # === FREE FEATURES === # Multiline Section (collapsed by default) multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT') if multiline_section: + pass draw_multiline_section(multiline_section, props) - # Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON - shader_section = draw_collapsible_header(layout, "expand_shader", "Shader Options", 'SETTINGS') - if shader_section: - draw_shader_options_only(shader_section, props) - - # Image Overlays Section (collapsed by default) - overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') - if overlay_section: - 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: + pass draw_font_settings_section(font_section, props) + # Positioning & Layout Section (collapsed by default) - FREE FEATURE moved to free group + position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER') + if position_section: + draw_positioning_section(position_section, props) + # Text Fitting Section (collapsed by default) text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER') if text_fitting_section: + pass draw_text_fitting_section(text_fitting_section, props) - # Stroke Section (collapsed by default) - stroke_section = draw_collapsible_header(layout, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE') - if stroke_section: - draw_stroke_section(stroke_section, props) - - # Shadow & Glow Section (collapsed by default) - shadow_glow_section = draw_collapsible_header(layout, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX') - if shadow_glow_section: - draw_shadow_glow_section(shadow_glow_section, props) - - # Blur Section (collapsed by default) - blur_section = draw_collapsible_header(layout, "expand_blur", "Blur Effects", 'MOD_SMOOTH') - if blur_section: - draw_blur_section(blur_section, props) - - # Normal Maps Section (collapsed by default) - normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE') - if normal_maps_section: - draw_normal_maps_section(normal_maps_section, props) - # Colors Section (collapsed by default) colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR') if colors_section: + pass draw_colors_section(colors_section, props) - # Presets Section (collapsed by default) - presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET') - if presets_section: - draw_presets_section(presets_section, props) + # === PREMIUM FEATURES === + + # Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON + shader_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_shader_generation + if not has_shader_generation(): + shader_header_row.enabled = False + # Don't allow expansion for locked features + shader_expanded = None + # Show lock indicator instead of expandable header + lock_header = shader_header_row.row() + lock_header.label(text="Shader Options", icon='SETTINGS') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + shader_expanded = draw_collapsible_header(shader_header_row, "expand_shader", "Shader Options", 'SETTINGS') + + # Only show content if available and expanded + if shader_expanded: + draw_shader_options_only(shader_expanded, props) + + # Image Overlays Section (collapsed by default) - Show with premium indicator + overlay_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_image_overlays + if not has_image_overlays(): + overlay_header_row.enabled = False + # Don't allow expansion for locked features + overlay_expanded = None + # Show lock indicator instead of expandable header + lock_header = overlay_header_row.row() + lock_header.label(text="Image Overlays", icon='IMAGE_DATA') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + overlay_expanded = draw_collapsible_header(overlay_header_row, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') + + # Only show content if available and expanded + if overlay_expanded: + draw_image_overlays_section(overlay_expanded, props) + + + # Stroke Section (collapsed by default) - Show with premium indicator + stroke_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_stroke_effects + if not has_stroke_effects(): + stroke_header_row.enabled = False + # Don't allow expansion for locked features + stroke_expanded = None + # Show lock indicator instead of expandable header + lock_header = stroke_header_row.row() + lock_header.label(text="Stroke & Outlines", icon='MOD_OUTLINE') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + stroke_expanded = draw_collapsible_header(stroke_header_row, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE') + + # Only show content if available and expanded + if stroke_expanded: + draw_stroke_section(stroke_expanded, props) + + # Shadow & Glow Section (collapsed by default) - Show with premium indicator + shadow_glow_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_shadow_glow_effects + if not has_shadow_glow_effects(): + shadow_glow_header_row.enabled = False + # Don't allow expansion for locked features + shadow_glow_expanded = None + # Show lock indicator instead of expandable header + lock_header = shadow_glow_header_row.row() + lock_header.label(text="Shadow & Glow Effects", icon='SHADERFX') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + shadow_glow_expanded = draw_collapsible_header(shadow_glow_header_row, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX') + + # Only show content if available and expanded + if shadow_glow_expanded: + draw_shadow_glow_section(shadow_glow_expanded, props) + + # Blur Section (collapsed by default) - Show with premium indicator + blur_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_blur_effects + if not has_blur_effects(): + blur_header_row.enabled = False + # Don't allow expansion for locked features + blur_expanded = None + # Show lock indicator instead of expandable header + lock_header = blur_header_row.row() + lock_header.label(text="Blur Effects", icon='MOD_SMOOTH') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + blur_expanded = draw_collapsible_header(blur_header_row, "expand_blur", "Blur Effects", 'MOD_SMOOTH') + + # Only show content if available and expanded + if blur_expanded: + draw_blur_section(blur_expanded, props) + + # Normal Maps Section (collapsed by default) - Show with premium indicator + normal_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + if not has_normal_maps(): + normal_header_row.enabled = False + # Don't allow expansion for locked features + normal_expanded = None + # Show lock indicator instead of expandable header + lock_header = normal_header_row.row() + lock_header.label(text="Normal Maps", icon='TEXTURE') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + normal_expanded = draw_collapsible_header(normal_header_row, "expand_normal_maps", "Normal Maps", 'TEXTURE') + + # Only show content if available and expanded + if normal_expanded: + draw_normal_maps_section(normal_expanded, props) + + # Presets Section (collapsed by default) - Show with premium indicator + presets_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + if not has_presets(): + presets_header_row.enabled = False + # Don't allow expansion for locked features + presets_expanded = None + # Show lock indicator instead of expandable header + lock_header = presets_header_row.row() + lock_header.label(text="Presets", icon='PRESET') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + presets_expanded = draw_collapsible_header(presets_header_row, "expand_presets", "Presets", 'PRESET') + + # Only show content if available and expanded + if presets_expanded: + draw_presets_section(presets_expanded, props) class TEXT_TEXTURE_PT_panel_3d(Panel): + pass """3D View Panel - Access from sidebar (N key)""" bl_label = "Text Texture Generator" bl_idname = "TEXT_TEXTURE_PT_panel_3d" @@ -728,6 +1159,7 @@ class TEXT_TEXTURE_PT_panel_3d(Panel): bl_options = {'DEFAULT_CLOSED'} def draw(self, context): + pass layout = self.layout props = context.scene.text_texture_props @@ -743,87 +1175,205 @@ class TEXT_TEXTURE_PT_panel_3d(Panel): layout.separator(factor=0.5) draw_generate_button(layout) - # CREATE SHADER BUTTON AT TOP LEVEL - AS REQUESTED + # CREATE SHADER BUTTON - WITH PROPER RESTRICTION layout.separator(factor=0.5) create_shader_row = layout.row() create_shader_row.scale_y = 1.5 - create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL') - # Preview Button at top level + # Check if shader generation is available + from ..utils.constants import has_shader_generation + if has_shader_generation(): + create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL') + else: + # Show locked shader button for free version + lock_row = create_shader_row.row() + lock_row.enabled = False + lock_row.operator("text_texture.generate_shader", text="🔒 Create Shader (Pro Only)", icon='LOCKED') + + # Preview Button - ALWAYS available (core functionality for all users) layout.separator(factor=0.5) preview_row = layout.row() preview_row.scale_y = 1.5 - preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + preview_row.operator("text_texture.preview", text="Preview Texture", icon='HIDE_OFF') layout.separator(factor=1.2) - # COLLAPSIBLE SECTIONS + # COLLAPSIBLE SECTIONS - FREE FEATURES FIRST + + # === FREE FEATURES === # Multiline Section (collapsed by default) multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT') if multiline_section: + pass draw_multiline_section(multiline_section, props) - # Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON - shader_section = draw_collapsible_header(layout, "expand_shader", "Shader Options", 'SETTINGS') - if shader_section: - draw_shader_options_only(shader_section, props) - - # Image Overlays Section (collapsed by default) - overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') - if overlay_section: - 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: + pass draw_font_settings_section(font_section, props) + # Positioning & Layout Section (collapsed by default) - FREE FEATURE moved to free group + position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER') + if position_section: + draw_positioning_section(position_section, props) + # Text Fitting Section (collapsed by default) text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER') if text_fitting_section: + pass draw_text_fitting_section(text_fitting_section, props) - # Stroke Section (collapsed by default) - stroke_section = draw_collapsible_header(layout, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE') - if stroke_section: - draw_stroke_section(stroke_section, props) - - # Shadow & Glow Section (collapsed by default) - shadow_glow_section = draw_collapsible_header(layout, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX') - if shadow_glow_section: - draw_shadow_glow_section(shadow_glow_section, props) - - # Tiling Section removed - feature was removed from addon - - # Blur Section (collapsed by default) - blur_section = draw_collapsible_header(layout, "expand_blur", "Blur Effects", 'MOD_SMOOTH') - if blur_section: - draw_blur_section(blur_section, props) - - # Batch Processing Section removed - feature was removed from addon - - # Normal Maps Section (collapsed by default) - normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE') - if normal_maps_section: - draw_normal_maps_section(normal_maps_section, props) - # Colors Section (collapsed by default) colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR') if colors_section: + pass draw_colors_section(colors_section, props) - # Presets Section (collapsed by default) - presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET') - if presets_section: - draw_presets_section(presets_section, props) + # === PREMIUM FEATURES === + + # Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON + shader_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_shader_generation + if not has_shader_generation(): + shader_header_row.enabled = False + # Don't allow expansion for locked features + shader_expanded = None + # Show lock indicator instead of expandable header + lock_header = shader_header_row.row() + lock_header.label(text="Shader Options", icon='SETTINGS') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + shader_expanded = draw_collapsible_header(shader_header_row, "expand_shader", "Shader Options", 'SETTINGS') + + # Only show content if available and expanded + if shader_expanded: + draw_shader_options_only(shader_expanded, props) + + # Image Overlays Section (collapsed by default) - Show with premium indicator + overlay_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_image_overlays + if not has_image_overlays(): + overlay_header_row.enabled = False + # Don't allow expansion for locked features + overlay_expanded = None + # Show lock indicator instead of expandable header + lock_header = overlay_header_row.row() + lock_header.label(text="Image Overlays", icon='IMAGE_DATA') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + overlay_expanded = draw_collapsible_header(overlay_header_row, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') + + # Only show content if available and expanded + if overlay_expanded: + draw_image_overlays_section(overlay_expanded, props) + + + # Stroke Section (collapsed by default) - Show with premium indicator + stroke_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_stroke_effects + if not has_stroke_effects(): + stroke_header_row.enabled = False + # Don't allow expansion for locked features + stroke_expanded = None + # Show lock indicator instead of expandable header + lock_header = stroke_header_row.row() + lock_header.label(text="Stroke & Outlines", icon='MOD_OUTLINE') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + stroke_expanded = draw_collapsible_header(stroke_header_row, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE') + + # Only show content if available and expanded + if stroke_expanded: + draw_stroke_section(stroke_expanded, props) + + # Shadow & Glow Section (collapsed by default) - Show with premium indicator + shadow_glow_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_shadow_glow_effects + if not has_shadow_glow_effects(): + shadow_glow_header_row.enabled = False + # Don't allow expansion for locked features + shadow_glow_expanded = None + # Show lock indicator instead of expandable header + lock_header = shadow_glow_header_row.row() + lock_header.label(text="Shadow & Glow Effects", icon='SHADERFX') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + shadow_glow_expanded = draw_collapsible_header(shadow_glow_header_row, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX') + + # Only show content if available and expanded + if shadow_glow_expanded: + draw_shadow_glow_section(shadow_glow_expanded, props) + + # Blur Section (collapsed by default) - Show with premium indicator + blur_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_blur_effects + if not has_blur_effects(): + blur_header_row.enabled = False + # Don't allow expansion for locked features + blur_expanded = None + # Show lock indicator instead of expandable header + lock_header = blur_header_row.row() + lock_header.label(text="Blur Effects", icon='MOD_SMOOTH') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + blur_expanded = draw_collapsible_header(blur_header_row, "expand_blur", "Blur Effects", 'MOD_SMOOTH') + + # Only show content if available and expanded + if blur_expanded: + draw_blur_section(blur_expanded, props) + + # Normal Maps Section (collapsed by default) - Show with premium indicator + normal_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + if not has_normal_maps(): + normal_header_row.enabled = False + # Don't allow expansion for locked features + normal_expanded = None + # Show lock indicator instead of expandable header + lock_header = normal_header_row.row() + lock_header.label(text="Normal Maps", icon='TEXTURE') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + normal_expanded = draw_collapsible_header(normal_header_row, "expand_normal_maps", "Normal Maps", 'TEXTURE') + + # Only show content if available and expanded + if normal_expanded: + draw_normal_maps_section(normal_expanded, props) + + # Presets Section (collapsed by default) - Show with premium indicator + presets_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + if not has_presets(): + presets_header_row.enabled = False + # Don't allow expansion for locked features + presets_expanded = None + # Show lock indicator instead of expandable header + lock_header = presets_header_row.row() + lock_header.label(text="Presets", icon='PRESET') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + presets_expanded = draw_collapsible_header(presets_header_row, "expand_presets", "Presets", 'PRESET') + + # Only show content if available and expanded + if presets_expanded: + draw_presets_section(presets_expanded, props) class TEXT_TEXTURE_PT_panel_properties(Panel): + pass """Properties Panel""" bl_label = "Text Texture Generator" bl_idname = "TEXT_TEXTURE_PT_panel_properties" @@ -833,6 +1383,7 @@ class TEXT_TEXTURE_PT_panel_properties(Panel): bl_category = "Tool" def draw(self, context): + pass layout = self.layout props = context.scene.text_texture_props @@ -848,71 +1399,179 @@ class TEXT_TEXTURE_PT_panel_properties(Panel): layout.separator(factor=0.5) draw_generate_button(layout) - # Preview Button at top level + # CREATE SHADER BUTTON - WITH PROPER RESTRICTION + layout.separator(factor=0.5) + create_shader_row = layout.row() + create_shader_row.scale_y = 1.5 + + # Check if shader generation is available + from ..utils.constants import has_shader_generation + if has_shader_generation(): + create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL') + else: + # Show locked shader button for free version + lock_row = create_shader_row.row() + lock_row.enabled = False + lock_row.operator("text_texture.generate_shader", text="🔒 Create Shader (Pro Only)", icon='LOCKED') + + # Preview Button - ALWAYS available (core functionality for all users) layout.separator(factor=0.5) preview_row = layout.row() preview_row.scale_y = 1.5 - preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + preview_row.operator("text_texture.preview", text="Preview Texture", icon='HIDE_OFF') layout.separator(factor=1.2) - # COLLAPSIBLE SECTIONS + # COLLAPSIBLE SECTIONS - FREE FEATURES FIRST + + # === FREE FEATURES === # Multiline Section (collapsed by default) multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT') if multiline_section: + pass draw_multiline_section(multiline_section, props) - # 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: + pass draw_font_settings_section(font_section, props) + # Positioning & Layout Section (collapsed by default) - FREE FEATURE moved to free group + position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER') + if position_section: + draw_positioning_section(position_section, props) + # Text Fitting Section (collapsed by default) text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER') if text_fitting_section: + pass draw_text_fitting_section(text_fitting_section, props) - # Stroke Section (collapsed by default) - stroke_section = draw_collapsible_header(layout, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE') - if stroke_section: - draw_stroke_section(stroke_section, props) - - # Shadow & Glow Section (collapsed by default) - shadow_glow_section = draw_collapsible_header(layout, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX') - if shadow_glow_section: - draw_shadow_glow_section(shadow_glow_section, props) - - # Tiling Section removed - feature was removed from addon - - # Blur Section (collapsed by default) - blur_section = draw_collapsible_header(layout, "expand_blur", "Blur Effects", 'MOD_SMOOTH') - if blur_section: - draw_blur_section(blur_section, props) - - # Batch Processing Section removed - feature was removed from addon - - # Normal Maps Section (collapsed by default) - normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE') - if normal_maps_section: - draw_normal_maps_section(normal_maps_section, props) - # Colors Section (collapsed by default) colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR') if colors_section: + pass draw_colors_section(colors_section, props) - # Presets Section (collapsed by default) - presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET') - if presets_section: - draw_presets_section(presets_section, props) \ No newline at end of file + # === PREMIUM FEATURES === + + # Image Overlays Section (collapsed by default) - Show with premium indicator + overlay_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_image_overlays + if not has_image_overlays(): + overlay_header_row.enabled = False + # Don't allow expansion for locked features + overlay_expanded = None + # Show lock indicator instead of expandable header + lock_header = overlay_header_row.row() + lock_header.label(text="Image Overlays", icon='IMAGE_DATA') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + overlay_expanded = draw_collapsible_header(overlay_header_row, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') + + # Only show content if available and expanded + if overlay_expanded: + draw_image_overlays_section(overlay_expanded, props) + + + # Stroke Section (collapsed by default) - Show with premium indicator + stroke_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_stroke_effects + if not has_stroke_effects(): + stroke_header_row.enabled = False + # Don't allow expansion for locked features + stroke_expanded = None + # Show lock indicator instead of expandable header + lock_header = stroke_header_row.row() + lock_header.label(text="Stroke & Outlines", icon='MOD_OUTLINE') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + stroke_expanded = draw_collapsible_header(stroke_header_row, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE') + + # Only show content if available and expanded + if stroke_expanded: + draw_stroke_section(stroke_expanded, props) + + # Shadow & Glow Section (collapsed by default) - Show with premium indicator + shadow_glow_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_shadow_glow_effects + if not has_shadow_glow_effects(): + shadow_glow_header_row.enabled = False + # Don't allow expansion for locked features + shadow_glow_expanded = None + # Show lock indicator instead of expandable header + lock_header = shadow_glow_header_row.row() + lock_header.label(text="Shadow & Glow Effects", icon='SHADERFX') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + shadow_glow_expanded = draw_collapsible_header(shadow_glow_header_row, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX') + + # Only show content if available and expanded + if shadow_glow_expanded: + draw_shadow_glow_section(shadow_glow_expanded, props) + + # Blur Section (collapsed by default) - Show with premium indicator + blur_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + from ..utils.constants import has_blur_effects + if not has_blur_effects(): + blur_header_row.enabled = False + # Don't allow expansion for locked features + blur_expanded = None + # Show lock indicator instead of expandable header + lock_header = blur_header_row.row() + lock_header.label(text="Blur Effects", icon='MOD_SMOOTH') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + blur_expanded = draw_collapsible_header(blur_header_row, "expand_blur", "Blur Effects", 'MOD_SMOOTH') + + # Only show content if available and expanded + if blur_expanded: + draw_blur_section(blur_expanded, props) + + # Normal Maps Section (collapsed by default) - Show with premium indicator + normal_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + if not has_normal_maps(): + normal_header_row.enabled = False + # Don't allow expansion for locked features + normal_expanded = None + # Show lock indicator instead of expandable header + lock_header = normal_header_row.row() + lock_header.label(text="Normal Maps", icon='TEXTURE') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + normal_expanded = draw_collapsible_header(normal_header_row, "expand_normal_maps", "Normal Maps", 'TEXTURE') + + # Only show content if available and expanded + if normal_expanded: + draw_normal_maps_section(normal_expanded, props) + + # Presets Section (collapsed by default) - Show with premium indicator + presets_header_row = layout.row(align=True) + + # Gray out header if locked and prevent expansion + if not has_presets(): + presets_header_row.enabled = False + # Don't allow expansion for locked features + presets_expanded = None + # Show lock indicator instead of expandable header + lock_header = presets_header_row.row() + lock_header.label(text="Presets", icon='PRESET') + lock_header.label(text="🔒 PRO", icon='LOCKED') + else: + presets_expanded = draw_collapsible_header(presets_header_row, "expand_presets", "Presets", 'PRESET') + + # Only show content if available and expanded + if presets_expanded: + draw_presets_section(presets_expanded, props) \ No newline at end of file diff --git a/src/ui/preview.py b/src/ui/preview.py index 8bb0f36..697869e 100644 --- a/src/ui/preview.py +++ b/src/ui/preview.py @@ -2,14 +2,43 @@ import bpy import os import platform -from PIL import Image, ImageDraw, ImageFont + +# Global flag to track PIL availability +PIL_AVAILABLE = False +Image = None +ImageDraw = None +ImageFont = None + +try: + pass + from PIL import Image, ImageDraw, ImageFont + PIL_AVAILABLE = True +except ImportError: + pass + PIL_AVAILABLE = False + # Create placeholder classes to prevent import errors + class _MockPIL: + pass + def __getattr__(self, name): + pass + raise ImportError( + "PIL (Pillow) is required for preview functionality. " + "Please install it using: pip install Pillow in Blender's Python environment, " + "or use Blender's bundled Python if available." + ) + + Image = _MockPIL() + ImageDraw = _MockPIL() + ImageFont = _MockPIL() def test_font_mixed_case_support(font_path): + pass """ Test if a font properly supports mixed case rendering. Returns True if the font supports mixed case, False if it only renders uppercase. """ try: + pass # Test string with mixed case test_text = "AaBbCc" @@ -20,8 +49,10 @@ def test_font_mixed_case_support(font_path): # Try to load the font try: + pass font = ImageFont.truetype(font_path, 24) except (OSError, IOError): + pass print(f"Text Texture Generator: Could not load font for testing: {font_path}") return False @@ -40,42 +71,54 @@ def test_font_mixed_case_support(font_path): # Compare the two renderings - if they're identical, the font only supports uppercase if pixels == pixels_upper: + pass print(f"Text Texture Generator: Font only supports uppercase rendering: {os.path.basename(font_path)}") return False else: + pass print(f"Text Texture Generator: Font supports mixed case rendering: {os.path.basename(font_path)}") return True except Exception as e: + pass print(f"Text Texture Generator: Error testing font mixed case support: {str(e)}") return False def get_validated_font(font_path, fallback_fonts=None): + pass """ Get a validated font that supports mixed case rendering. Returns the original font path if valid, otherwise returns a fallback font. """ if not font_path or not os.path.exists(font_path): + pass print(f"Text Texture Generator: Font file not found: {font_path}") return get_system_fallback_fonts()[0] if get_system_fallback_fonts() else None # Test if the font supports mixed case if test_font_mixed_case_support(font_path): + pass return font_path else: + pass print(f"Text Texture Generator: Font does not support mixed case, using fallback: {font_path}") # Try fallback fonts if provided if fallback_fonts: + pass for fallback_font in fallback_fonts: + pass if os.path.exists(fallback_font) and test_font_mixed_case_support(fallback_font): + pass print(f"Text Texture Generator: Using fallback font: {fallback_font}") return fallback_font # Use system fallback fonts system_fallbacks = get_system_fallback_fonts() for fallback_font in system_fallbacks: + pass if test_font_mixed_case_support(fallback_font): + pass print(f"Text Texture Generator: Using system fallback font: {fallback_font}") return fallback_font @@ -84,6 +127,7 @@ def get_validated_font(font_path, fallback_fonts=None): return font_path def get_system_fallback_fonts(): + pass """ Get a list of system fallback fonts based on the operating system. Returns a list of font paths that are likely to be available. @@ -92,6 +136,7 @@ def get_system_fallback_fonts(): fallback_fonts = [] if system == "Windows": + pass windows_fonts = [ "C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/calibri.ttf", @@ -133,17 +178,25 @@ def get_system_fallback_fonts(): ] for font_dir in common_directories: + pass if os.path.exists(font_dir): + pass for root, dirs, files in os.walk(font_dir): + pass for file in files: + pass if file.lower().endswith(('.ttf', '.otf')): + pass font_path = os.path.join(root, file) if font_path not in fallback_fonts: + pass fallback_fonts.append(font_path) # Limit the number of fallback fonts to prevent excessive scanning if len(fallback_fonts) >= 20: + pass break if len(fallback_fonts) >= 20: + pass break print(f"Text Texture Generator: Found {len(fallback_fonts)} system fallback fonts") diff --git a/src/utils/__pycache__/__init__.cpython-311.pyc b/src/utils/__pycache__/__init__.cpython-311.pyc index 0fc3815..35c05c1 100644 Binary files a/src/utils/__pycache__/__init__.cpython-311.pyc and b/src/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/utils/__pycache__/constants.cpython-311.pyc b/src/utils/__pycache__/constants.cpython-311.pyc index e9653ea..0412da5 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/__pycache__/feature_flags.cpython-311.pyc b/src/utils/__pycache__/feature_flags.cpython-311.pyc deleted file mode 100644 index 227a232..0000000 Binary files a/src/utils/__pycache__/feature_flags.cpython-311.pyc and /dev/null differ diff --git a/src/utils/__pycache__/system.cpython-311.pyc b/src/utils/__pycache__/system.cpython-311.pyc index 3e7eef9..4039c11 100644 Binary files a/src/utils/__pycache__/system.cpython-311.pyc and b/src/utils/__pycache__/system.cpython-311.pyc differ diff --git a/src/utils/constants.py b/src/utils/constants.py index 4383f93..41d72bc 100644 --- a/src/utils/constants.py +++ b/src/utils/constants.py @@ -2,26 +2,39 @@ Constants and metadata for the Text Texture Generator addon. """ +# Version information +VERSION_TYPE = "free" # "free" or "full" + bl_info = { - "name": "Text Texture Generator", + "name": "Text Texture Generator" + (" Free" if VERSION_TYPE == "free" else " Full"), "author": "Marc Mintel ", "version": (1, 0, 0), "blender": (4, 0, 0), "location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab", - "description": "Generate image textures from text with accurate dimensions and instant live preview", + "description": "Generate image textures from text with accurate dimensions and instant live preview" + (" - Free Version (Limited Features)" if VERSION_TYPE == "free" else " - Full Version"), "category": "Material", } -# Version information -VERSION_TYPE = "full" # "free" or "full" +# Feature configuration based on version type +ENABLED_FEATURES = ["basic", "preview"] if VERSION_TYPE == "free" else [ + "basic", "preview", "presets", "normal_maps", "batch_processing", + "image_overlays", "basic_utilities", "advanced_utilities", "advanced_styling", + "stroke_effects", "blur_effects", "shadow_glow_effects", "shader_generation" +] # Version-specific limits (applied at build time through file exclusions) def get_version_limits(): """Get version-specific limits.""" - return { - "max_texture_size": 4096, - "max_concurrent_operations": 4 - } + if VERSION_TYPE == "free": + return { + "max_texture_size": 1024, + "max_concurrent_operations": 1 + } + else: + return { + "max_texture_size": 4096, + "max_concurrent_operations": 4 + } def is_free_version(): """Check if this is the free version.""" @@ -31,37 +44,116 @@ def is_full_version(): """Check if this is the full version.""" return VERSION_TYPE == "full" -def sync_margin_values(props, margin_type='all'): - """Synchronize margin values across properties +# UI Helper Functions for Version Display +def get_version_badge_text(): + """Get version badge text for UI display.""" + return "FREE" if is_free_version() else "PRO" + +def get_version_badge_icon(): + """Get appropriate icon for version badge.""" + return 'GIFT' if is_free_version() else 'STAR' + +def get_upgrade_hint_text(feature_name): + """Get contextual upgrade hint text for specific features.""" + if not is_free_version(): + return "" + + return f"Upgrade to Pro version to access {feature_name}" + +# Feature availability functions +def has_feature(feature_name): + """Check if a specific feature is available.""" + return feature_name in ENABLED_FEATURES + +def has_image_overlays(): + """Check if image overlay features are available""" + return has_feature("image_overlays") + +def has_basic_utilities(): + """Check if basic utility operations are available""" + return has_feature("basic_utilities") + +def has_presets(): + """Check if preset features are available""" + return has_feature("presets") + +def has_normal_maps(): + """Check if normal map generation is available""" + return has_feature("normal_maps") + +def has_batch_processing(): + """Check if batch processing is available""" + return has_feature("batch_processing") + +def has_advanced_utilities(): + """Check if advanced utility operations are available""" + return has_feature("advanced_utilities") + +def has_advanced_styling(): + """Check if advanced styling options are available""" + return has_feature("advanced_styling") + +def has_stroke_effects(): + """Check if stroke effects are available""" + return has_feature("stroke_effects") + +def has_blur_effects(): + """Check if blur effects are available""" + return has_feature("blur_effects") + +def has_shadow_glow_effects(): + """Check if shadow and glow effects are available""" + return has_feature("shadow_glow_effects") + +def has_shader_generation(): + """Check if shader generation is available""" + return has_feature("shader_generation") + +def get_max_resolution(): + """Get maximum texture resolution based on version""" + limits = get_version_limits() + return limits.get("max_texture_size", 1024) + +def should_show_feature_lock(feature_name): + """Check if feature lock indicator should be shown.""" + # Map feature names to their availability functions + feature_checks = { + 'presets': has_presets, + 'normal_maps': has_normal_maps, + 'batch_processing': has_batch_processing, + 'advanced_utilities': has_advanced_utilities, + 'advanced_styling': has_advanced_styling, + 'image_overlays': has_image_overlays, + 'basic_utilities': has_basic_utilities, + 'stroke_effects': has_stroke_effects, + 'blur_effects': has_blur_effects, + 'shadow_glow_effects': has_shadow_glow_effects, + 'shader_generation': has_shader_generation + } - Args: - props: Properties object containing margin values - margin_type: Type of margin sync ('all', 'horizontal', 'vertical') - """ - if not props: - return - - try: - # Get base margin value - base_margin = getattr(props, 'text_fitting_margin', 10.0) - - if margin_type == 'all': - # Sync all margins to base margin - if hasattr(props, 'prepend_margin'): - props.prepend_margin = base_margin - if hasattr(props, 'append_margin'): - props.append_margin = base_margin - - elif margin_type == 'horizontal': - # Sync horizontal margins - if hasattr(props, 'prepend_margin') and hasattr(props, 'append_margin'): - avg_margin = (props.prepend_margin + props.append_margin) / 2 - props.prepend_margin = avg_margin - props.append_margin = avg_margin - - elif margin_type == 'vertical': - # Sync vertical margins (if any exist in future) - pass - - except Exception as e: - print(f"Warning: Failed to sync margin values: {e}") \ No newline at end of file + if is_full_version(): + return False + + # For free version, show lock if feature is not available + if feature_name in feature_checks: + return not feature_checks[feature_name]() + + return True # Show lock for unknown features + +def get_feature_availability_text(): + """Get text describing current feature availability.""" + if is_free_version(): + return "Free version - Basic features available" + return "Pro version - All features available" + +def get_version_info_details(): + """Get detailed version information for display.""" + limits = get_version_limits() + info = { + 'version_type': VERSION_TYPE.upper(), + 'max_texture_size': limits.get('max_texture_size', 'Unlimited'), + '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 diff --git a/src/utils/feature_flags.py.backup b/src/utils/feature_flags.py.backup deleted file mode 100644 index 7f1db9c..0000000 --- a/src/utils/feature_flags.py.backup +++ /dev/null @@ -1,150 +0,0 @@ -""" -Feature flag system for Text Texture Generator. -Provides runtime checking of enabled features based on build configuration. -""" - -import json -import os -from pathlib import Path -from typing import List, Dict, Any, Optional - -# Import the build-time injected constants -try: - from .constants import ENABLED_FEATURES, VERSION_TYPE, get_version_limits -except ImportError: - # Fallback for development/testing - ENABLED_FEATURES = ["basic", "preview", "advanced_styling", "normal_maps", "presets"] - VERSION_TYPE = "full" - def get_version_limits(): - return {"max_texture_size": 4096, "max_concurrent_operations": 4} - -class FeatureManager: - """Manages feature flags and version-specific limitations.""" - - def __init__(self): - self._enabled_features = set(ENABLED_FEATURES) - self._version_type = VERSION_TYPE - self._limits = get_version_limits() - self._features_config = self._load_features_config() - - def _load_features_config(self) -> Optional[Dict]: - """Load the features configuration file if available.""" - try: - # Try to load from build directory (development) - config_path = Path(__file__).parent.parent.parent / "build" / "features.json" - if config_path.exists(): - with open(config_path, 'r') as f: - return json.load(f) - except Exception: - pass - return None - - def is_feature_enabled(self, feature_name: str) -> bool: - """Check if a feature is enabled in the current build.""" - return feature_name in self._enabled_features - - def get_enabled_features(self) -> List[str]: - """Get list of all enabled features.""" - return list(self._enabled_features) - - def get_version_type(self) -> str: - """Get the version type (free or full).""" - return self._version_type - - def get_limit(self, limit_name: str, default: Any = None) -> Any: - """Get a version-specific limit.""" - return self._limits.get(limit_name, default) - - def get_max_texture_size(self) -> int: - """Get the maximum texture size for this version.""" - return self.get_limit("max_texture_size", 1024) - - def get_max_concurrent_operations(self) -> int: - """Get the maximum concurrent operations for this version.""" - return self.get_limit("max_concurrent_operations", 1) - - def require_feature(self, feature_name: str) -> bool: - """Check if feature is enabled, raise exception if not.""" - if not self.is_feature_enabled(feature_name): - raise FeatureNotAvailableError( - f"Feature '{feature_name}' is not available in {self._version_type} version" - ) - return True - - def get_feature_info(self, feature_name: str) -> Optional[Dict]: - """Get detailed information about a feature.""" - if not self._features_config: - return None - - return self._features_config.get("features", {}).get(feature_name) - - def is_component_enabled(self, component_path: str) -> bool: - """Check if a specific component is enabled based on feature flags.""" - if not self._features_config: - return True # Assume enabled if no config - - features = self._features_config.get("features", {}) - for feature_name, feature_config in features.items(): - if self.is_feature_enabled(feature_name): - components = feature_config.get("components", []) - if component_path in components: - return True - - return False - -class FeatureNotAvailableError(Exception): - """Raised when trying to use a feature that's not available in the current version.""" - pass - -# Global feature manager instance -feature_manager = FeatureManager() - -# Convenience functions -def is_feature_enabled(feature_name: str) -> bool: - """Check if a feature is enabled.""" - return feature_manager.is_feature_enabled(feature_name) - -def require_feature(feature_name: str) -> bool: - """Require a feature to be enabled.""" - return feature_manager.require_feature(feature_name) - -def get_version_type() -> str: - """Get the current version type.""" - return feature_manager.get_version_type() - -def get_max_texture_size() -> int: - """Get maximum texture size for this version.""" - return feature_manager.get_max_texture_size() - -def get_max_concurrent_operations() -> int: - """Get maximum concurrent operations for this version.""" - return feature_manager.get_max_concurrent_operations() - -# Decorators for feature-gated functionality -def feature_required(feature_name: str): - """Decorator to require a feature for a function or method.""" - def decorator(func): - def wrapper(*args, **kwargs): - require_feature(feature_name) - return func(*args, **kwargs) - wrapper.__name__ = func.__name__ - wrapper.__doc__ = func.__doc__ - return wrapper - return decorator - -def feature_gated(feature_name: str, fallback=None): - """Decorator to conditionally execute a function based on feature availability.""" - def decorator(func): - def wrapper(*args, **kwargs): - if is_feature_enabled(feature_name): - return func(*args, **kwargs) - elif fallback is not None: - return fallback(*args, **kwargs) if callable(fallback) else fallback - else: - raise FeatureNotAvailableError( - f"Feature '{feature_name}' is not available in {get_version_type()} version" - ) - wrapper.__name__ = func.__name__ - wrapper.__doc__ = func.__doc__ - return wrapper - return decorator \ No newline at end of file diff --git a/src/utils/system.py b/src/utils/system.py index c7bebf3..cbe1d82 100644 --- a/src/utils/system.py +++ b/src/utils/system.py @@ -8,16 +8,21 @@ import platform import os def install_pillow(): + pass """Install Pillow if not available""" try: + pass import PIL except ImportError: + pass print("Installing Pillow...") subprocess.check_call([sys.executable, "-m", "pip", "install", "pillow>=10.0.0"]) def get_system_fonts(): + pass """Get detailed list of available system fonts with paths""" try: + pass from PIL import ImageFont import glob @@ -25,6 +30,7 @@ def get_system_fonts(): system = platform.system() if system == "Windows": + pass font_dirs = [ "C:/Windows/Fonts/", os.path.expanduser("~/AppData/Local/Microsoft/Windows/Fonts/") @@ -47,35 +53,46 @@ def get_system_fonts(): extensions = ["*.ttf", "*.ttc", "*.otf"] for font_dir in font_dirs: + pass if os.path.exists(font_dir): + pass for ext in extensions: + pass for font_path in glob.glob(os.path.join(font_dir, "**", ext), recursive=True): + pass try: + pass font_name = os.path.splitext(os.path.basename(font_path))[0] font_name = font_name.replace("-", " ").replace("_", " ") fonts[font_name] = font_path except Exception: + pass continue return fonts except ImportError: + pass return {} def get_font_enum_items(self, context): + pass """Dynamic enum items for font selection""" items = [("default", "Default Font", "Use system default font", 0)] if not hasattr(get_font_enum_items, 'cached_fonts'): + pass get_font_enum_items.cached_fonts = get_system_fonts() fonts = get_font_enum_items.cached_fonts for i, (font_name, font_path) in enumerate(sorted(fonts.items())[:50]): + pass items.append((font_path, font_name, f"Font: {font_name}", i + 1)) return items def get_anchor_point_matrix(): + pass """Return a 3x3 matrix of anchor point identifiers for UI positioning grid""" return [ ['TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT'], diff --git a/tests/__pycache__/pytest_plugins.cpython-311.pyc b/tests/__pycache__/pytest_plugins.cpython-311.pyc deleted file mode 100644 index 88451b2..0000000 Binary files a/tests/__pycache__/pytest_plugins.cpython-311.pyc and /dev/null differ diff --git a/tests/integration/__pycache__/__init__.cpython-311.pyc b/tests/integration/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 5176657..0000000 Binary files a/tests/integration/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/tests/integration/__pycache__/test_text_generation_pipeline.cpython-311-pytest-8.3.5.pyc b/tests/integration/__pycache__/test_text_generation_pipeline.cpython-311-pytest-8.3.5.pyc deleted file mode 100644 index 2072785..0000000 Binary files a/tests/integration/__pycache__/test_text_generation_pipeline.cpython-311-pytest-8.3.5.pyc and /dev/null differ diff --git a/tests/unit/__pycache__/test_import_error_handling.cpython-311-pytest-8.3.5.pyc b/tests/unit/__pycache__/test_import_error_handling.cpython-311-pytest-8.3.5.pyc new file mode 100644 index 0000000..9644e29 Binary files /dev/null and b/tests/unit/__pycache__/test_import_error_handling.cpython-311-pytest-8.3.5.pyc differ diff --git a/tests/unit/__pycache__/test_text_fitting.cpython-311-pytest-8.3.5.pyc b/tests/unit/__pycache__/test_text_fitting.cpython-311-pytest-8.3.5.pyc deleted file mode 100644 index 3a4f0ce..0000000 Binary files a/tests/unit/__pycache__/test_text_fitting.cpython-311-pytest-8.3.5.pyc and /dev/null differ diff --git a/tests/unit/__pycache__/test_version_differentiation.cpython-311-pytest-8.3.5.pyc b/tests/unit/__pycache__/test_version_differentiation.cpython-311-pytest-8.3.5.pyc new file mode 100644 index 0000000..d56a5d6 Binary files /dev/null and b/tests/unit/__pycache__/test_version_differentiation.cpython-311-pytest-8.3.5.pyc differ diff --git a/tests/unit/test_import_error_handling.py b/tests/unit/test_import_error_handling.py new file mode 100644 index 0000000..7173031 --- /dev/null +++ b/tests/unit/test_import_error_handling.py @@ -0,0 +1,256 @@ +""" +Comprehensive tests for import error handling in the text texture generator addon. +Tests conditional imports, PIL handling, mock operators, and version-specific features. +""" + +import sys +import pytest +import tempfile +import zipfile +import os +from unittest.mock import patch, MagicMock, Mock +import importlib + + +class TestConditionalImports: + """Test that modules handle missing imports gracefully.""" + + def test_operators_init_handles_missing_imports(self): + """Test that operators/__init__.py sets up empty lists when modules missing.""" + # Test operators module can import and has necessary attributes + try: + from src.operators import PRESET_OPERATORS, UTILITY_OPERATORS + # Should have lists (empty or populated) + assert isinstance(PRESET_OPERATORS, list) + assert isinstance(UTILITY_OPERATORS, list) + except ImportError as e: + pytest.fail(f"operators/__init__.py should import successfully: {e}") + + def test_main_init_has_conditional_imports(self): + """Test that main __init__.py has the conditional import structure.""" + # Read the main __init__.py to verify it has conditional imports + from pathlib import Path + init_file = Path("src/__init__.py") + with open(init_file, 'r') as f: + content = f.read() + + # Should have try-catch blocks for conditional imports + assert 'try:' in content + assert 'except ImportError' in content + assert 'MockOperator' in content + + def test_core_init_has_conditional_imports(self): + """Test that core/__init__.py has conditional import structure.""" + # Test core module can import + try: + import src.core + # Should have the normal maps function available (real or mock) + assert hasattr(src.core, 'generate_normal_map_from_alpha') + assert callable(src.core.generate_normal_map_from_alpha) + # Should have the availability flag + assert hasattr(src.core, 'NORMAL_MAPS_AVAILABLE') + assert isinstance(src.core.NORMAL_MAPS_AVAILABLE, bool) + except ImportError as e: + pytest.fail(f"core/__init__.py should import successfully: {e}") + + +class TestPILImportHandling: + """Test that PIL imports are handled gracefully when PIL is not available.""" + + def test_normal_maps_pil_handling(self): + """Test that normal_maps.py has PIL handling structure.""" + # Simply test that normal_maps can be imported + try: + from src.core import normal_maps + # Should have some indication of PIL handling + assert hasattr(normal_maps, 'PIL_AVAILABLE') or hasattr(normal_maps, 'generate_normal_map_from_alpha') + except ImportError as e: + pytest.fail(f"normal_maps.py should handle PIL gracefully: {e}") + + def test_ui_preview_handles_missing_pil(self): + """Test that ui/preview.py handles missing PIL gracefully.""" + try: + from src.ui import preview + # Should import successfully with PIL handling + assert hasattr(preview, 'PIL_AVAILABLE') + assert isinstance(preview.PIL_AVAILABLE, bool) + except ImportError as e: + pytest.fail(f"ui/preview.py should handle missing PIL gracefully: {e}") + + def test_generation_engine_imports_successfully(self): + """Test that generation_engine.py imports successfully.""" + try: + from src.core import generation_engine + # Should import successfully and have main functions + assert hasattr(generation_engine, 'generate_texture_image') + except ImportError as e: + pytest.fail(f"generation_engine.py should import successfully: {e}") + + +class TestMockOperators: + """Test that mock operators work correctly in free version.""" + + def test_mock_operator_exists_in_main_init(self): + """Test that MockOperator is defined in main __init__.py.""" + # Read the main __init__.py to verify MockOperator is defined + from pathlib import Path + init_file = Path("src/__init__.py") + with open(init_file, 'r') as f: + content = f.read() + + # Should have MockOperator class definition + assert 'class MockOperator' in content + assert 'bl_idname' in content + assert 'bl_label' in content + assert 'execute' in content + + def test_mock_operators_structure_in_main_init(self): + """Test that mock operators are properly structured in main __init__.py.""" + # Read the main __init__.py to verify conditional operator assignments + from pathlib import Path + init_file = Path("src/__init__.py") + with open(init_file, 'r') as f: + content = f.read() + + # Should have conditional operator assignments + operator_names = [ + 'TEXT_TEXTURE_OT_save_preset', + 'TEXT_TEXTURE_OT_load_preset', + 'TEXT_TEXTURE_OT_delete_preset' + ] + + for op_name in operator_names: + assert op_name in content, f"Should reference {op_name} in __init__.py" + + +class TestVersionSpecificConstants: + """Test that version-specific constants are set correctly.""" + + def test_constants_has_enabled_features(self): + """Test that constants.py includes ENABLED_FEATURES.""" + from src.utils import constants + + assert hasattr(constants, 'ENABLED_FEATURES') + assert isinstance(constants.ENABLED_FEATURES, list) + assert len(constants.ENABLED_FEATURES) > 0 + + def test_constants_has_version_type(self): + """Test that constants.py includes VERSION_TYPE.""" + from src.utils import constants + + assert hasattr(constants, 'VERSION_TYPE') + assert constants.VERSION_TYPE in ['free', 'full'] + + def test_version_helper_functions(self): + """Test that version helper functions work correctly.""" + from src.utils import constants + + assert hasattr(constants, 'is_free_version') + assert hasattr(constants, 'is_full_version') + assert hasattr(constants, 'get_version_limits') + + # Functions should return appropriate types + assert isinstance(constants.is_free_version(), bool) + assert isinstance(constants.is_full_version(), bool) + assert isinstance(constants.get_version_limits(), dict) + + +class TestPackageIntegrity: + """Test that built packages have correct structure and no import issues.""" + + def test_free_package_excludes_correct_files(self): + """Test that free version package excludes the right files.""" + from pathlib import Path + free_package = Path("dist/text_texture_generator_v1.0.0_free.zip") + if not free_package.exists(): + pytest.skip("Free package not found - run build first") + + with zipfile.ZipFile(free_package, 'r') as zip_file: + file_list = zip_file.namelist() + + # Files that should be excluded from free version + excluded_files = [ + 'operators/preset_ops.py', + 'operators/utility_ops.py', + 'core/normal_maps.py' + ] + + for excluded_file in excluded_files: + matching_files = [f for f in file_list if excluded_file in f] + assert len(matching_files) == 0, f"Free version should not contain {excluded_file}" + + def test_full_package_includes_all_files(self): + """Test that full version package includes all files.""" + from pathlib import Path + full_package = Path("dist/text_texture_generator_v1.0.0.zip") + if not full_package.exists(): + pytest.skip("Full package not found - run build first") + + with zipfile.ZipFile(full_package, 'r') as zip_file: + file_list = zip_file.namelist() + + # Files that should be included in full version + required_files = [ + 'operators/preset_ops.py', + 'operators/utility_ops.py', + 'core/normal_maps.py' + ] + + for required_file in required_files: + matching_files = [f for f in file_list if required_file in f] + assert len(matching_files) > 0, f"Full version should contain {required_file}" + + def test_constants_contains_enabled_features_in_packages(self): + """Test that both packages have constants.py with ENABLED_FEATURES.""" + from pathlib import Path + + packages = [ + ("dist/text_texture_generator_v1.0.0_free.zip", "Free"), + ("dist/text_texture_generator_v1.0.0.zip", "Full") + ] + + for package_path, package_type in packages: + package = Path(package_path) + if not package.exists(): + continue # Skip if package doesn't exist + + with zipfile.ZipFile(package, 'r') as zip_file: + file_list = zip_file.namelist() + constants_files = [f for f in file_list if 'utils/constants.py' in f] + assert len(constants_files) > 0, f"{package_type} package should contain constants.py" + + # Read constants file and check for ENABLED_FEATURES + constants_file = constants_files[0] + constants_content = zip_file.read(constants_file).decode('utf-8') + assert 'ENABLED_FEATURES' in constants_content, f"{package_type} package constants.py should contain ENABLED_FEATURES" + + +class TestBuildSystemRobustness: + """Test that the build system handles edge cases properly.""" + + def test_build_system_handles_missing_git_tags(self): + """Test that build system works even without git tags.""" + from build.sync_version import get_latest_git_tag + + # Should return a version or None (both are acceptable) + version = get_latest_git_tag() + # Either a valid version string or None is acceptable + if version is not None: + assert isinstance(version, str) + assert len(version) > 0 + + def test_template_system_has_all_required_variables(self): + """Test that template system includes all required variables.""" + from build.sync_version import create_template_variables, load_config + + config = load_config() + variables = create_template_variables(config, "1.0.0", "full") + + required_vars = [ + 'VERSION', + 'VERSION_TYPE', + 'ENABLED_FEATURES' + ] + + for var in required_vars: + assert var in variables, f"Template variables should include {var}" \ No newline at end of file diff --git a/tests/unit/test_version_differentiation.py b/tests/unit/test_version_differentiation.py new file mode 100644 index 0000000..a56c035 --- /dev/null +++ b/tests/unit/test_version_differentiation.py @@ -0,0 +1,258 @@ +""" +Comprehensive tests for version differentiation in the Blender text texture generator addon. + +Tests the quadruple-layer detection system: +1. Template-generated constants (VERSION_TYPE, is_free_version(), ENABLED_FEATURES) +2. Runtime import detection flags (PRESETS_AVAILABLE, UTILITY_OPERATORS_AVAILABLE) +3. Version limits (get_version_limits() returns correct values) +4. UI helper functions (version display and feature gating) +""" + +import pytest +import sys +from unittest.mock import Mock, patch, MagicMock +from types import ModuleType + + +class TestVersionDetection: + """Test suite for version detection methods.""" + + def test_version_type_constant(self): + """Test VERSION_TYPE constant is properly set.""" + from src.utils.constants import VERSION_TYPE + assert VERSION_TYPE in ["free", "full"], f"VERSION_TYPE should be 'free' or 'full', got '{VERSION_TYPE}'" + + def test_is_free_version_function(self): + """Test is_free_version() function returns correct boolean.""" + from src.utils.constants import is_free_version, VERSION_TYPE + + result = is_free_version() + assert isinstance(result, bool), "is_free_version() should return a boolean" + + # Result should match VERSION_TYPE + expected = VERSION_TYPE == "free" + assert result == expected, f"is_free_version() returned {result}, expected {expected} based on VERSION_TYPE='{VERSION_TYPE}'" + + def test_is_full_version_function(self): + """Test is_full_version() function returns correct boolean.""" + from src.utils.constants import is_full_version, VERSION_TYPE + + result = is_full_version() + assert isinstance(result, bool), "is_full_version() should return a boolean" + + # Result should match VERSION_TYPE + expected = VERSION_TYPE == "full" + assert result == expected, f"is_full_version() returned {result}, expected {expected} based on VERSION_TYPE='{VERSION_TYPE}'" + + def test_enabled_features_list(self): + """Test ENABLED_FEATURES contains appropriate features for version.""" + from src.utils.constants import ENABLED_FEATURES, VERSION_TYPE + + assert isinstance(ENABLED_FEATURES, list), "ENABLED_FEATURES should be a list" + assert len(ENABLED_FEATURES) > 0, "ENABLED_FEATURES should not be empty" + + # Basic features should always be present + assert "basic" in ENABLED_FEATURES, "Basic features should always be enabled" + assert "preview" in ENABLED_FEATURES, "Preview features should always be enabled" + + if VERSION_TYPE == "free": + # Free version should have limited features - only basic and preview + expected_free_features = ["basic", "preview"] + for feature in ENABLED_FEATURES: + assert feature in expected_free_features, f"Free version should only have basic features, found '{feature}'" + else: + # Full version should have additional features beyond basic and preview + assert len(ENABLED_FEATURES) > 2, "Full version should have more than just basic and preview features" + + def test_get_version_limits_function(self): + """Test get_version_limits() returns correct technical limits.""" + from src.utils.constants import get_version_limits, VERSION_TYPE + + limits = get_version_limits() + assert isinstance(limits, dict), "get_version_limits() should return a dictionary" + + # Check required keys + required_keys = ["max_texture_size", "max_concurrent_operations"] + for key in required_keys: + assert key in limits, f"Missing required key '{key}' in version limits" + + # 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']}" + + +class TestRuntimeImportDetection: + """Test suite for runtime import detection flags.""" + + def test_presets_available_flag(self): + """Test PRESETS_AVAILABLE flag is properly set based on import success.""" + from src import PRESETS_AVAILABLE + assert isinstance(PRESETS_AVAILABLE, bool), "PRESETS_AVAILABLE should be a boolean" + + def test_utility_operators_available_flag(self): + """Test UTILITY_OPERATORS_AVAILABLE flag is properly set based on import success.""" + from src import UTILITY_OPERATORS_AVAILABLE + assert isinstance(UTILITY_OPERATORS_AVAILABLE, bool), "UTILITY_OPERATORS_AVAILABLE should be a boolean" + + +class TestFeatureAvailability: + """Test suite for feature availability and gating.""" + + def test_feature_gating_based_on_version(self): + """Test that features are properly gated based on version type.""" + from src.utils.constants import is_free_version, ENABLED_FEATURES + + if is_free_version(): + # Free version should only have basic and preview features + expected_features = ["basic", "preview"] + assert set(ENABLED_FEATURES) == set(expected_features), f"Free version should only have {expected_features}" + else: + # Full version should have more features + assert len(ENABLED_FEATURES) > 2, "Full version should have more than just basic features" + assert "basic" in ENABLED_FEATURES, "Full version should include basic features" + assert "preview" in ENABLED_FEATURES, "Full version should include preview features" + + def test_runtime_import_detection_flags_exist(self): + """Test that runtime import detection flags exist.""" + try: + from src import PRESETS_AVAILABLE, UTILITY_OPERATORS_AVAILABLE + assert isinstance(PRESETS_AVAILABLE, bool), "PRESETS_AVAILABLE should be a boolean" + assert isinstance(UTILITY_OPERATORS_AVAILABLE, bool), "UTILITY_OPERATORS_AVAILABLE should be a boolean" + except ImportError as e: + pytest.fail(f"Runtime detection flags should be available: {e}") + + +class TestUIComponents: + """Test suite for UI component version display.""" + + def test_get_version_badge_text(self): + """Test get_version_badge_text() returns appropriate badge text.""" + from src.utils.constants import get_version_badge_text, is_free_version + + badge_text = get_version_badge_text() + assert isinstance(badge_text, str), "get_version_badge_text() should return a string" + assert len(badge_text) > 0, "Badge text should not be empty" + + if is_free_version(): + assert "FREE" in badge_text.upper(), "Free version badge should contain 'FREE'" + else: + assert "PRO" in badge_text.upper() or "FULL" in badge_text.upper(), "Full version badge should contain 'PRO' or 'FULL'" + + def test_should_show_feature_lock(self): + """Test should_show_feature_lock() returns correct values for features.""" + from src.utils.constants import should_show_feature_lock, is_free_version + + # Test premium features from the actual implementation + premium_features = ["presets", "utility_operators", "image_overlays", "batch_processing", "advanced_positioning"] + for feature in premium_features: + result = should_show_feature_lock(feature) + assert isinstance(result, bool), f"should_show_feature_lock('{feature}') should return boolean" + + if is_free_version(): + assert result == True, f"Feature lock should be shown for '{feature}' in free version" + else: + assert result == False, f"Feature lock should not be shown for '{feature}' in full version" + + def test_get_upgrade_hint_text(self): + """Test get_upgrade_hint_text() returns appropriate upgrade messages.""" + from src.utils.constants import get_upgrade_hint_text, is_free_version + + # The function requires a feature_name parameter + feature_name = "Test Feature" + hint_text = get_upgrade_hint_text(feature_name) + assert isinstance(hint_text, str), "get_upgrade_hint_text() should return a string" + + if is_free_version(): + assert len(hint_text) > 0, "Free version should have upgrade hint text" + assert feature_name in hint_text, "Upgrade hint should mention the feature name" + assert "Pro" in hint_text, "Upgrade hint should mention Pro version" + else: + assert len(hint_text) == 0, "Full version should have empty upgrade hint text" + + def test_get_version_info_details(self): + """Test get_version_info_details() returns comprehensive version information.""" + from src.utils.constants import get_version_info_details, is_free_version, get_version_limits + + info_details = get_version_info_details() + assert isinstance(info_details, dict), "get_version_info_details() should return a dictionary" + assert len(info_details) > 0, "Version info details should not be empty" + + +class TestIntegrationScenarios: + """Test suite for cross-system integration validation.""" + + def test_template_and_runtime_consistency(self): + """Test that template-generated constants and runtime flags are consistent.""" + from src.utils.constants import is_free_version, ENABLED_FEATURES + from src import PRESETS_AVAILABLE, UTILITY_OPERATORS_AVAILABLE + + # In a properly built system, runtime availability should align with template configuration + if is_free_version(): + # Free version should have limited features in template + expected_features = ["basic", "preview"] + assert set(ENABLED_FEATURES) == set(expected_features), f"Template should only enable basic features in free version, got {ENABLED_FEATURES}" + else: + # Full version should have more features enabled in template + assert len(ENABLED_FEATURES) > 2, "Template should enable more features in full version" + assert "basic" in ENABLED_FEATURES, "Template should enable basic features in full version" + assert "preview" in ENABLED_FEATURES, "Template should enable preview features in full version" + + def test_mock_operators_dont_crash(self): + """Test that mock operators are properly implemented and don't crash.""" + # This test ensures that when premium features are missing, + # the system gracefully handles it with mock implementations + + # Test that we can import the main module without crashes + try: + import src + assert True, "Main module import should not crash" + except Exception as e: + pytest.fail(f"Main module import crashed: {e}") + + # Test that runtime flags are properly set + assert hasattr(src, 'PRESETS_AVAILABLE'), "Runtime should set PRESETS_AVAILABLE flag" + assert hasattr(src, 'UTILITY_OPERATORS_AVAILABLE'), "Runtime should set UTILITY_OPERATORS_AVAILABLE flag" + + +class TestVersionDifferentiationScenarios: + """Test specific free vs full version difference scenarios.""" + + def test_free_version_complete_scenario(self): + """Test complete free version scenario with all restrictions.""" + from src.utils.constants import is_free_version, get_version_limits, ENABLED_FEATURES + + if is_free_version(): + # Feature restrictions - free version should only have basic features + expected_features = ["basic", "preview"] + assert set(ENABLED_FEATURES) == set(expected_features), f"Free version should only have {expected_features}" + + # Technical limitations + limits = get_version_limits() + assert limits["max_texture_size"] == 1024, "Should have 1024px texture limit" + assert limits["max_concurrent_operations"] == 1, "Should have 1 concurrent operation limit" + + def test_full_version_complete_scenario(self): + """Test complete full version scenario with all features enabled.""" + from src.utils.constants import is_free_version, is_full_version, get_version_limits, ENABLED_FEATURES + + if not is_free_version(): + # Version detection + assert is_full_version() == True, "Should detect full version" + + # Full version should have more features than free + assert len(ENABLED_FEATURES) > 2, "Full version should have more than just basic features" + assert "basic" in ENABLED_FEATURES, "Full version should include basic features" + assert "preview" in ENABLED_FEATURES, "Full version should include preview features" + + # Generous technical limits + limits = get_version_limits() + assert limits["max_texture_size"] == 4096, "Should have 4096px texture limit" + assert limits["max_concurrent_operations"] == 4, "Should have 4 concurrent operations limit" \ No newline at end of file