This commit is contained in:
2025-09-17 19:43:11 +02:00
parent 7c041e2a26
commit 0e9a655ea1
58 changed files with 2821 additions and 892 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -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
}
}
}

View File

@@ -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": []
}
}

View File

@@ -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'])
}

View File

@@ -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."""
@@ -30,3 +43,117 @@ def is_free_version():
def is_full_version():
"""Check if this is the full version."""
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

View File

@@ -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'}")

Binary file not shown.

Binary file not shown.

View File

@@ -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 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}")
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():
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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 []

View File

@@ -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',
])
# Add preset operators if available
__all__.extend(PRESET_OPERATORS)
# Add utility operators if available
__all__.extend(UTILITY_OPERATORS)

View File

@@ -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',
]

View File

@@ -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()

View File

@@ -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'}

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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')

File diff suppressed because it is too large Load Diff

View File

@@ -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")

View File

@@ -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 <marc@mintel.me>",
"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"
Args:
props: Properties object containing margin values
margin_type: Type of margin sync ('all', 'horizontal', 'vertical')
"""
if not props:
return
def get_version_badge_icon():
"""Get appropriate icon for version badge."""
return 'GIFT' if is_free_version() else 'STAR'
try:
# Get base margin value
base_margin = getattr(props, 'text_fitting_margin', 10.0)
def get_upgrade_hint_text(feature_name):
"""Get contextual upgrade hint text for specific features."""
if not is_free_version():
return ""
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
return f"Upgrade to Pro version to access {feature_name}"
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
# Feature availability functions
def has_feature(feature_name):
"""Check if a specific feature is available."""
return feature_name in ENABLED_FEATURES
elif margin_type == 'vertical':
# Sync vertical margins (if any exist in future)
pass
def has_image_overlays():
"""Check if image overlay features are available"""
return has_feature("image_overlays")
except Exception as e:
print(f"Warning: Failed to sync margin values: {e}")
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

View File

@@ -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

View File

@@ -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'],

View File

@@ -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}"

View File

@@ -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"