wip
This commit is contained in:
Binary file not shown.
BIN
build/__pycache__/sync_version.cpython-311.pyc
Normal file
BIN
build/__pycache__/sync_version.cpython-311.pyc
Normal file
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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'}")
|
||||
|
||||
|
||||
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
Binary file not shown.
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
Binary file not shown.
256
src/__init__.py
256
src/__init__.py
@@ -16,14 +16,41 @@ from .utils import bl_info
|
||||
# Import utility functions
|
||||
from .utils import install_pillow, get_system_fonts, get_font_enum_items
|
||||
|
||||
# Import preset system functions
|
||||
from .presets import (
|
||||
initialize_presets,
|
||||
refresh_presets,
|
||||
refresh_presets_sync,
|
||||
ensure_presets_available,
|
||||
migrate_presets_if_needed
|
||||
)
|
||||
# Import preset system functions (conditionally for free version)
|
||||
try:
|
||||
pass
|
||||
from .presets import (
|
||||
initialize_presets,
|
||||
refresh_presets,
|
||||
refresh_presets_sync,
|
||||
ensure_presets_available,
|
||||
migrate_presets_if_needed
|
||||
)
|
||||
PRESETS_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
# Free version - no presets functionality
|
||||
PRESETS_AVAILABLE = False
|
||||
def initialize_presets():
|
||||
pass
|
||||
"""Mock function for free version"""
|
||||
pass
|
||||
def refresh_presets():
|
||||
pass
|
||||
"""Mock function for free version"""
|
||||
pass
|
||||
def refresh_presets_sync():
|
||||
pass
|
||||
"""Mock function for free version"""
|
||||
return True
|
||||
def ensure_presets_available():
|
||||
pass
|
||||
"""Mock function for free version"""
|
||||
return True
|
||||
def migrate_presets_if_needed():
|
||||
pass
|
||||
"""Mock function for free version"""
|
||||
return {'migrated_count': 0}
|
||||
|
||||
# Import core generation functions
|
||||
from .core import generate_texture_image, generate_preview
|
||||
@@ -35,31 +62,90 @@ from .properties import (
|
||||
TEXT_TEXTURE_Properties
|
||||
)
|
||||
|
||||
# Import operators
|
||||
# Import operators - generation operators are always available
|
||||
from .operators import (
|
||||
TEXT_TEXTURE_OT_generate,
|
||||
TEXT_TEXTURE_OT_preview,
|
||||
TEXT_TEXTURE_OT_generate_shader,
|
||||
TEXT_TEXTURE_OT_refresh_preview,
|
||||
TEXT_TEXTURE_OT_view_preview_zoom,
|
||||
TEXT_TEXTURE_OT_save_preset,
|
||||
TEXT_TEXTURE_OT_save_preset_enter,
|
||||
TEXT_TEXTURE_OT_load_preset,
|
||||
TEXT_TEXTURE_OT_delete_preset,
|
||||
TEXT_TEXTURE_OT_refresh_presets,
|
||||
TEXT_TEXTURE_OT_add_overlay,
|
||||
TEXT_TEXTURE_OT_remove_overlay,
|
||||
TEXT_TEXTURE_OT_duplicate_overlay,
|
||||
TEXT_TEXTURE_OT_delete_overlay,
|
||||
TEXT_TEXTURE_OT_move_overlay,
|
||||
TEXT_TEXTURE_OT_open_panel,
|
||||
TEXT_TEXTURE_OT_refresh_fonts,
|
||||
TEXT_TEXTURE_OT_set_anchor_point,
|
||||
TEXT_TEXTURE_OT_sync_margins,
|
||||
TEXT_TEXTURE_OT_export_presets,
|
||||
TEXT_TEXTURE_OT_import_presets,
|
||||
TEXT_TEXTURE_OT_show_migration_report
|
||||
)
|
||||
|
||||
# Try to import utility operators (might not be available in free version)
|
||||
try:
|
||||
pass
|
||||
from .operators import (
|
||||
TEXT_TEXTURE_OT_refresh_preview,
|
||||
TEXT_TEXTURE_OT_view_preview_zoom,
|
||||
TEXT_TEXTURE_OT_add_overlay,
|
||||
TEXT_TEXTURE_OT_remove_overlay,
|
||||
TEXT_TEXTURE_OT_duplicate_overlay,
|
||||
TEXT_TEXTURE_OT_delete_overlay,
|
||||
TEXT_TEXTURE_OT_move_overlay,
|
||||
TEXT_TEXTURE_OT_open_panel,
|
||||
TEXT_TEXTURE_OT_refresh_fonts,
|
||||
TEXT_TEXTURE_OT_set_anchor_point,
|
||||
TEXT_TEXTURE_OT_sync_margins,
|
||||
)
|
||||
UTILITY_OPERATORS_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
# Free version - create mock operators for utility functionality
|
||||
UTILITY_OPERATORS_AVAILABLE = False
|
||||
|
||||
class MockOperator:
|
||||
pass
|
||||
bl_idname = ""
|
||||
bl_label = ""
|
||||
bl_description = ""
|
||||
def execute(self, context):
|
||||
pass
|
||||
return {'CANCELLED'}
|
||||
|
||||
TEXT_TEXTURE_OT_refresh_preview = MockOperator
|
||||
TEXT_TEXTURE_OT_view_preview_zoom = MockOperator
|
||||
TEXT_TEXTURE_OT_add_overlay = MockOperator
|
||||
TEXT_TEXTURE_OT_remove_overlay = MockOperator
|
||||
TEXT_TEXTURE_OT_duplicate_overlay = MockOperator
|
||||
TEXT_TEXTURE_OT_delete_overlay = MockOperator
|
||||
TEXT_TEXTURE_OT_move_overlay = MockOperator
|
||||
TEXT_TEXTURE_OT_open_panel = MockOperator
|
||||
TEXT_TEXTURE_OT_refresh_fonts = MockOperator
|
||||
TEXT_TEXTURE_OT_set_anchor_point = MockOperator
|
||||
TEXT_TEXTURE_OT_sync_margins = MockOperator
|
||||
|
||||
# Conditionally import preset-related operators
|
||||
if PRESETS_AVAILABLE:
|
||||
pass
|
||||
from .operators import (
|
||||
TEXT_TEXTURE_OT_save_preset,
|
||||
TEXT_TEXTURE_OT_save_preset_enter,
|
||||
TEXT_TEXTURE_OT_load_preset,
|
||||
TEXT_TEXTURE_OT_delete_preset,
|
||||
TEXT_TEXTURE_OT_refresh_presets,
|
||||
TEXT_TEXTURE_OT_export_presets,
|
||||
TEXT_TEXTURE_OT_import_presets,
|
||||
TEXT_TEXTURE_OT_show_migration_report
|
||||
)
|
||||
else:
|
||||
pass
|
||||
# Create mock classes for free version
|
||||
class MockOperator:
|
||||
pass
|
||||
bl_idname = ""
|
||||
bl_label = ""
|
||||
bl_description = ""
|
||||
def execute(self, context):
|
||||
pass
|
||||
return {'CANCELLED'}
|
||||
|
||||
TEXT_TEXTURE_OT_save_preset = MockOperator
|
||||
TEXT_TEXTURE_OT_save_preset_enter = MockOperator
|
||||
TEXT_TEXTURE_OT_load_preset = MockOperator
|
||||
TEXT_TEXTURE_OT_delete_preset = MockOperator
|
||||
TEXT_TEXTURE_OT_refresh_presets = MockOperator
|
||||
TEXT_TEXTURE_OT_export_presets = MockOperator
|
||||
TEXT_TEXTURE_OT_import_presets = MockOperator
|
||||
TEXT_TEXTURE_OT_show_migration_report = MockOperator
|
||||
|
||||
# Import UI panels and functions
|
||||
from .ui import (
|
||||
TEXT_TEXTURE_PT_panel,
|
||||
@@ -74,11 +160,15 @@ from .ui import (
|
||||
# REGISTRATION
|
||||
# ============================================================================
|
||||
|
||||
# Global list to track successfully registered classes
|
||||
_registered_classes = []
|
||||
|
||||
classes = (
|
||||
TEXT_TEXTURE_ImageOverlay,
|
||||
TEXT_TEXTURE_Preset,
|
||||
TEXT_TEXTURE_Properties,
|
||||
TEXT_TEXTURE_OT_generate,
|
||||
TEXT_TEXTURE_OT_preview,
|
||||
TEXT_TEXTURE_OT_generate_shader,
|
||||
TEXT_TEXTURE_OT_refresh_preview,
|
||||
TEXT_TEXTURE_OT_view_preview_zoom,
|
||||
@@ -104,30 +194,57 @@ classes = (
|
||||
TEXT_TEXTURE_PT_panel_properties,
|
||||
)
|
||||
|
||||
def 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()
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 []
|
||||
@@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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',
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
@@ -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'}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
1009
src/ui/panels.py
1009
src/ui/panels.py
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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'],
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
256
tests/unit/test_import_error_handling.py
Normal file
256
tests/unit/test_import_error_handling.py
Normal 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}"
|
||||
258
tests/unit/test_version_differentiation.py
Normal file
258
tests/unit/test_version_differentiation.py
Normal 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"
|
||||
Reference in New Issue
Block a user