font
This commit is contained in:
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,6 +5,7 @@ Core functionality for generating texture images with text overlays.
|
||||
|
||||
import bpy
|
||||
from ..utils.constants import has_text_fitting
|
||||
from ..utils.system import find_default_truetype_font
|
||||
|
||||
def generate_texture_image(props, width, height):
|
||||
pass
|
||||
@@ -187,24 +188,48 @@ def generate_texture_image(props, width, height):
|
||||
layout_mode = "vertical"
|
||||
|
||||
|
||||
# Determine candidate font paths based on user configuration
|
||||
candidate_paths = []
|
||||
seen_candidates = set()
|
||||
for candidate in (
|
||||
getattr(props, 'custom_font_path', '') if getattr(props, 'use_custom_font', False) else '',
|
||||
getattr(props, 'font_path', '') if getattr(props, 'font_path', 'default') != "default" else '',
|
||||
find_default_truetype_font()
|
||||
):
|
||||
if candidate and candidate not in seen_candidates:
|
||||
candidate_paths.append(candidate)
|
||||
seen_candidates.add(candidate)
|
||||
|
||||
failed_candidates = set()
|
||||
active_font_path = None
|
||||
|
||||
def load_font_for_size(size):
|
||||
pass
|
||||
nonlocal active_font_path
|
||||
for path in candidate_paths:
|
||||
pass
|
||||
if path in failed_candidates:
|
||||
pass
|
||||
continue
|
||||
try:
|
||||
pass
|
||||
font_obj = ImageFont.truetype(path, size)
|
||||
active_font_path = path
|
||||
return font_obj
|
||||
except (OSError, IOError, AttributeError, TypeError) as font_error:
|
||||
pass
|
||||
failed_candidates.add(path)
|
||||
continue
|
||||
active_font_path = None
|
||||
return ImageFont.load_default()
|
||||
|
||||
# Determine font size (with text fitting if enabled)
|
||||
font_size = props.font_size
|
||||
if props.enable_text_fitting and has_text_fitting():
|
||||
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
|
||||
min_size = props.min_font_size
|
||||
max_size = props.max_font_size
|
||||
target_width = width * (1.0 - props.text_fitting_margin / 100.0)
|
||||
@@ -212,14 +237,8 @@ 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()
|
||||
test_font = load_font_for_size(test_size)
|
||||
|
||||
try:
|
||||
bbox = draw.textbbox((0, 0), full_text, font=test_font)
|
||||
@@ -235,29 +254,13 @@ def generate_texture_image(props, width, height):
|
||||
else:
|
||||
pass
|
||||
break
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
break
|
||||
|
||||
font_size = optimal_size
|
||||
except Exception as 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)
|
||||
elif props.font_path != "default":
|
||||
font = ImageFont.truetype(props.font_path, font_size)
|
||||
else:
|
||||
pass
|
||||
font = ImageFont.load_default()
|
||||
except (OSError, IOError) as e:
|
||||
pass
|
||||
font = ImageFont.load_default()
|
||||
font = load_font_for_size(font_size)
|
||||
|
||||
# Calculate text position and draw
|
||||
if layout_mode == "vertical" and props.prepend_append_layout == 'VERTICAL':
|
||||
|
||||
Binary file not shown.
@@ -16,40 +16,15 @@ from bpy.props import (
|
||||
)
|
||||
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
|
||||
"""Placeholder property update hook.
|
||||
|
||||
Live preview support was removed; keep hook to maintain compatibility
|
||||
with existing property update assignments while doing nothing.
|
||||
"""
|
||||
return
|
||||
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
|
||||
# Always update preview unless already updating
|
||||
if not props.is_updating:
|
||||
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
|
||||
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"""
|
||||
|
||||
Binary file not shown.
31
src/ui/font_helpers.py
Normal file
31
src/ui/font_helpers.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Helper utilities for font sizing and text fitting presentation logic.
|
||||
"""
|
||||
|
||||
from ..utils.constants import has_text_fitting
|
||||
|
||||
FONT_FITTING_MODE_LABELS = {
|
||||
'FIT_WIDTH': "Fit Width",
|
||||
'FIT_HEIGHT': "Fit Height",
|
||||
'FIT_BOTH': "Fit Width & Height",
|
||||
'FIT_CONTAIN': "Fit Contain",
|
||||
}
|
||||
|
||||
|
||||
def font_size_ui_state(props):
|
||||
"""
|
||||
Determine whether the manual font size control should be enabled and what
|
||||
status message to show based on the current text fitting configuration.
|
||||
|
||||
Returns:
|
||||
tuple(bool, str): (is_manual_enabled, status_message)
|
||||
"""
|
||||
fitting_available = bool(has_text_fitting())
|
||||
fitting_enabled = fitting_available and bool(getattr(props, "enable_text_fitting", False))
|
||||
|
||||
if not fitting_enabled:
|
||||
return True, ""
|
||||
|
||||
mode = getattr(props, "text_fitting_mode", "")
|
||||
mode_label = FONT_FITTING_MODE_LABELS.get(mode, mode.replace("_", " ").title() if mode else "Auto")
|
||||
return False, f"Auto-sized via Text Fitting ({mode_label})"
|
||||
137
src/ui/panels.py
137
src/ui/panels.py
@@ -14,6 +14,7 @@ from ..utils.constants import (
|
||||
has_normal_maps, has_batch_processing, has_advanced_utilities,
|
||||
has_advanced_styling, has_text_fitting
|
||||
)
|
||||
from .font_helpers import font_size_ui_state
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -403,28 +404,63 @@ def draw_positioning_section(layout, props):
|
||||
|
||||
def draw_font_settings_section(layout, props):
|
||||
pass
|
||||
"""Draw extended font settings"""
|
||||
# Font size moved here from top level
|
||||
layout.prop(props, "font_size")
|
||||
"""Draw unified font sizing and fitting controls."""
|
||||
content = layout.column(align=True)
|
||||
content.use_property_split = True
|
||||
content.use_property_decorate = False
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Font Selection:")
|
||||
# Default font dropdown moved here from top level
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "font_path", text="")
|
||||
row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH')
|
||||
sizing_box = content.box()
|
||||
sizing_box.use_property_split = True
|
||||
sizing_box.use_property_decorate = False
|
||||
sizing_box.label(text="Size & Text Fitting", icon='FULLSCREEN_ENTER')
|
||||
|
||||
layout.separator()
|
||||
# Custom font controls moved here from top level
|
||||
layout.prop(props, "use_custom_font")
|
||||
manual_enabled, status_message = font_size_ui_state(props)
|
||||
manual_row = sizing_box.row(align=True)
|
||||
manual_row.enabled = manual_enabled
|
||||
manual_row.prop(props, "font_size", text="Manual Size")
|
||||
|
||||
if status_message:
|
||||
info_row = sizing_box.row()
|
||||
info_row.label(text=status_message, icon='INFO')
|
||||
|
||||
if has_text_fitting():
|
||||
toggle_row = sizing_box.row(align=True)
|
||||
toggle_row.prop(props, "enable_text_fitting", text="Enable Text Fitting", toggle=True)
|
||||
|
||||
if props.enable_text_fitting:
|
||||
sizing_box.separator(factor=0.5)
|
||||
|
||||
mode_row = sizing_box.row(align=True)
|
||||
mode_row.prop(props, "text_fitting_mode", text="Mode")
|
||||
|
||||
margin_row = sizing_box.row(align=True)
|
||||
margin_row.prop(props, "text_fitting_margin", text="Margin", slider=True)
|
||||
|
||||
limit_row = sizing_box.row(align=True)
|
||||
limit_row.prop(props, "min_font_size", text="Min Size")
|
||||
limit_row.prop(props, "max_font_size", text="Max Size")
|
||||
else:
|
||||
sizing_box.separator(factor=0.5)
|
||||
draw_feature_lock_indicator(sizing_box, 'text_fitting', show_hint=False)
|
||||
|
||||
content.separator(factor=0.8)
|
||||
|
||||
selection_box = content.box()
|
||||
selection_box.use_property_split = True
|
||||
selection_box.use_property_decorate = False
|
||||
selection_box.label(text="Font Selection", icon='FONT_DATA')
|
||||
|
||||
font_row = selection_box.row(align=True)
|
||||
font_row.prop(props, "font_path", text="")
|
||||
font_row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH')
|
||||
|
||||
selection_box.prop(props, "use_custom_font")
|
||||
if props.use_custom_font:
|
||||
pass
|
||||
layout.prop(props, "custom_font_path", text="")
|
||||
selection_box.prop(props, "custom_font_path", text="")
|
||||
|
||||
layout.separator()
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "padding")
|
||||
row.prop(props, "text_align")
|
||||
selection_box.separator(factor=0.5)
|
||||
selection_box.prop(props, "text_align", text="Alignment")
|
||||
|
||||
def draw_colors_section(layout, props):
|
||||
pass
|
||||
@@ -654,55 +690,6 @@ def draw_normal_maps_section(layout, props):
|
||||
info_box.label(text="• Blur smooths sharp edges")
|
||||
info_box.label(text="• Invert changes raised/recessed effect")
|
||||
|
||||
def draw_text_fitting_section(layout, props):
|
||||
"""Draw text fitting controls"""
|
||||
# Check if text fitting is available
|
||||
if not has_text_fitting():
|
||||
draw_feature_lock_indicator(layout, 'text_fitting')
|
||||
return
|
||||
# Main toggle for text fitting
|
||||
layout.prop(props, "enable_text_fitting", text="Enable Text Fitting", icon='FULLSCREEN_ENTER')
|
||||
|
||||
# Show controls only if text fitting is enabled
|
||||
if props.enable_text_fitting:
|
||||
pass
|
||||
layout.separator()
|
||||
|
||||
# Fitting mode selection
|
||||
layout.prop(props, "text_fitting_mode", text="Fitting Mode")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Margin setting
|
||||
layout.prop(props, "text_fitting_margin", text="Margin", slider=True)
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Font size limits
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Font Size Limits:", icon='RESTRICT_SELECT_ON')
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "min_font_size", text="Min")
|
||||
row.prop(props, "max_font_size", text="Max")
|
||||
|
||||
# Info box with usage tips
|
||||
layout.separator()
|
||||
info_box = layout.box()
|
||||
info_box.scale_y = 0.8
|
||||
info_box.label(text="💡 Text fitting automatically sizes text", icon='INFO')
|
||||
|
||||
if props.text_fitting_mode == 'FIT_WIDTH':
|
||||
pass
|
||||
info_box.label(text="• Fits text width to canvas width")
|
||||
elif props.text_fitting_mode == 'FIT_HEIGHT':
|
||||
info_box.label(text="• Fits text height to canvas height")
|
||||
elif props.text_fitting_mode == 'FIT_BOTH':
|
||||
info_box.label(text="• Fits both width and height (may distort)")
|
||||
elif props.text_fitting_mode == 'FIT_CONTAIN':
|
||||
info_box.label(text="• Fits entirely within canvas (preserves ratio)")
|
||||
|
||||
info_box.label(text="• Respects min/max font size limits")
|
||||
|
||||
def draw_stroke_section(layout, props):
|
||||
pass
|
||||
"""Draw stroke and outline controls"""
|
||||
@@ -916,12 +903,6 @@ class TEXT_TEXTURE_PT_panel(Panel):
|
||||
if position_section:
|
||||
draw_positioning_section(position_section, props)
|
||||
|
||||
# Text Fitting Section (collapsed by default)
|
||||
text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER')
|
||||
if text_fitting_section:
|
||||
pass
|
||||
draw_text_fitting_section(text_fitting_section, props)
|
||||
|
||||
# Colors Section (collapsed by default)
|
||||
colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
|
||||
if colors_section:
|
||||
@@ -1134,12 +1115,6 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
|
||||
if position_section:
|
||||
draw_positioning_section(position_section, props)
|
||||
|
||||
# Text Fitting Section (collapsed by default)
|
||||
text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER')
|
||||
if text_fitting_section:
|
||||
pass
|
||||
draw_text_fitting_section(text_fitting_section, props)
|
||||
|
||||
# Colors Section (collapsed by default)
|
||||
colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
|
||||
if colors_section:
|
||||
@@ -1352,12 +1327,6 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
|
||||
if position_section:
|
||||
draw_positioning_section(position_section, props)
|
||||
|
||||
# Text Fitting Section (collapsed by default)
|
||||
text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER')
|
||||
if text_fitting_section:
|
||||
pass
|
||||
draw_text_fitting_section(text_fitting_section, props)
|
||||
|
||||
# Colors Section (collapsed by default)
|
||||
colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
|
||||
if colors_section:
|
||||
|
||||
@@ -18,13 +18,14 @@ from .constants import (
|
||||
is_free_version,
|
||||
is_full_version
|
||||
)
|
||||
from .system import install_pillow, get_system_fonts, get_font_enum_items
|
||||
from .system import install_pillow, get_system_fonts, get_font_enum_items, find_default_truetype_font
|
||||
|
||||
__all__ = [
|
||||
'bl_info',
|
||||
'install_pillow',
|
||||
'get_system_fonts',
|
||||
'get_font_enum_items',
|
||||
'find_default_truetype_font',
|
||||
'has_image_overlays',
|
||||
'has_basic_utilities',
|
||||
'has_presets',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,6 +7,8 @@ import sys
|
||||
import platform
|
||||
import os
|
||||
|
||||
_DEFAULT_FONT_CACHE = None
|
||||
|
||||
def install_pillow():
|
||||
pass
|
||||
"""Install Pillow if not available"""
|
||||
@@ -99,3 +101,99 @@ def get_anchor_point_matrix():
|
||||
['MIDDLE_LEFT', 'MIDDLE_CENTER', 'MIDDLE_RIGHT'],
|
||||
['BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT']
|
||||
]
|
||||
|
||||
def find_default_truetype_font():
|
||||
pass
|
||||
"""Return a system font path that can act as scalable default."""
|
||||
global _DEFAULT_FONT_CACHE
|
||||
if _DEFAULT_FONT_CACHE is not None:
|
||||
pass
|
||||
return _DEFAULT_FONT_CACHE
|
||||
|
||||
system = platform.system().lower()
|
||||
preferred_fonts = []
|
||||
if system == "windows":
|
||||
preferred_fonts = [
|
||||
"C:/Windows/Fonts/arial.ttf",
|
||||
"C:/Windows/Fonts/segoeui.ttf",
|
||||
"C:/Windows/Fonts/calibri.ttf",
|
||||
"C:/Windows/Fonts/tahoma.ttf",
|
||||
]
|
||||
elif system == "darwin":
|
||||
preferred_fonts = [
|
||||
"/System/Library/Fonts/SFNS.ttf",
|
||||
"/System/Library/Fonts/SFNSDisplay.ttf",
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
"/Library/Fonts/Arial.ttf",
|
||||
"/Library/Fonts/HelveticaNeue.ttc",
|
||||
]
|
||||
else:
|
||||
preferred_fonts = [
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
|
||||
]
|
||||
|
||||
def _font_supports_basic_latin(font_path):
|
||||
pass
|
||||
try:
|
||||
from PIL import ImageFont
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, 48)
|
||||
except (OSError, IOError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
try:
|
||||
bbox_upper = font.getbbox("H")
|
||||
bbox_lower = font.getbbox("e")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if not bbox_upper or not bbox_lower:
|
||||
return False
|
||||
|
||||
try:
|
||||
upper_width = float(bbox_upper[2] - bbox_upper[0])
|
||||
upper_height = float(bbox_upper[3] - bbox_upper[1])
|
||||
lower_width = float(bbox_lower[2] - bbox_lower[0])
|
||||
lower_height = float(bbox_lower[3] - bbox_lower[1])
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
if min(upper_width, upper_height, lower_width, lower_height) <= 0:
|
||||
return False
|
||||
|
||||
if lower_height > upper_height * 1.3:
|
||||
return False
|
||||
|
||||
if lower_width > upper_width * 1.5:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for candidate in preferred_fonts:
|
||||
if candidate and os.path.exists(candidate) and _font_supports_basic_latin(candidate):
|
||||
_DEFAULT_FONT_CACHE = candidate
|
||||
return _DEFAULT_FONT_CACHE
|
||||
|
||||
fonts = get_system_fonts()
|
||||
for _, font_path in sorted(fonts.items()):
|
||||
if os.path.exists(font_path) and _font_supports_basic_latin(font_path):
|
||||
_DEFAULT_FONT_CACHE = font_path
|
||||
return _DEFAULT_FONT_CACHE
|
||||
|
||||
try:
|
||||
from ..ui.preview import get_system_fallback_fonts
|
||||
fallback_fonts = get_system_fallback_fonts()
|
||||
for font_path in fallback_fonts:
|
||||
if os.path.exists(font_path) and _font_supports_basic_latin(font_path):
|
||||
_DEFAULT_FONT_CACHE = font_path
|
||||
return _DEFAULT_FONT_CACHE
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_DEFAULT_FONT_CACHE = None
|
||||
return _DEFAULT_FONT_CACHE
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -48,6 +48,7 @@ def setup_pil_mocks(mock_pil_image_module, mock_pil_imagedraw_module,
|
||||
# Setup ImageFont mock based on font_type
|
||||
if font_type == 'default':
|
||||
mock_font = MagicMock()
|
||||
mock_pil_imagefont_module.truetype.return_value = mock_font
|
||||
mock_pil_imagefont_module.load_default.return_value = mock_font
|
||||
elif font_type == 'custom':
|
||||
mock_font = MagicMock()
|
||||
@@ -196,6 +197,46 @@ class TestGenerateTextureImage:
|
||||
assert call_args[0][0] == "RGBA"
|
||||
assert call_args[0][2] == (0, 0, 0, 0)
|
||||
|
||||
def test_default_font_uses_scalable_size(self):
|
||||
"""Ensure default font configuration respects requested font size."""
|
||||
with patch('PIL.Image') as mock_pil_image_module, \
|
||||
patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
|
||||
patch('PIL.ImageFont') as mock_pil_imagefont_module, \
|
||||
patch('src.core.generation_engine.bpy') as mock_bpy, \
|
||||
patch('src.core.generation_engine.find_default_truetype_font', return_value="/fonts/fallback.ttf", create=True):
|
||||
|
||||
setup_pil_mocks(
|
||||
mock_pil_image_module,
|
||||
mock_pil_imagedraw_module,
|
||||
mock_pil_imagefont_module,
|
||||
DEFAULT_WIDTH,
|
||||
DEFAULT_HEIGHT,
|
||||
font_type='default'
|
||||
)
|
||||
setup_bpy_mocks(mock_bpy, DEFAULT_WIDTH, DEFAULT_HEIGHT)
|
||||
|
||||
fallback_font = MagicMock()
|
||||
|
||||
def truetype_side_effect(font_path_arg, size_arg):
|
||||
if font_path_arg is None:
|
||||
raise TypeError("file must be a path, not None")
|
||||
return fallback_font
|
||||
|
||||
mock_pil_imagefont_module.truetype.side_effect = truetype_side_effect
|
||||
mock_pil_imagefont_module.load_default.return_value = MagicMock()
|
||||
|
||||
props = create_mock_props(
|
||||
text="Size Probe",
|
||||
font_path='default',
|
||||
use_custom_font=False,
|
||||
font_size=42
|
||||
)
|
||||
|
||||
generate_texture_image(props, DEFAULT_WIDTH, DEFAULT_HEIGHT)
|
||||
|
||||
mock_pil_imagefont_module.truetype.assert_called_with("/fonts/fallback.ttf", 42)
|
||||
mock_pil_imagefont_module.load_default.assert_not_called()
|
||||
|
||||
def test_generate_colored_background(self):
|
||||
"""Test generation with custom background color."""
|
||||
with patch('PIL.Image') as mock_pil_image_module, \
|
||||
|
||||
89
tests/unit/properties/test_property_group_updates.py
Normal file
89
tests/unit/properties/test_property_group_updates.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Tests for property group update hooks."""
|
||||
|
||||
from types import ModuleType, SimpleNamespace
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def property_groups_module():
|
||||
"""Import property_groups with minimal bpy stubs."""
|
||||
module_name = "src.properties.property_groups"
|
||||
original_modules = {
|
||||
name: sys.modules.get(name)
|
||||
for name in (module_name, "bpy", "bpy.props", "bpy.types", "src", "src.properties")
|
||||
}
|
||||
|
||||
class TypesStub(SimpleNamespace):
|
||||
def __getattr__(self, name):
|
||||
value = type(name, (), {})
|
||||
setattr(self, name, value)
|
||||
return value
|
||||
|
||||
bpy_stub = SimpleNamespace(
|
||||
props=SimpleNamespace(
|
||||
StringProperty=lambda **kwargs: None,
|
||||
IntProperty=lambda **kwargs: None,
|
||||
FloatVectorProperty=lambda **kwargs: None,
|
||||
FloatProperty=lambda **kwargs: None,
|
||||
BoolProperty=lambda **kwargs: None,
|
||||
EnumProperty=lambda **kwargs: None,
|
||||
CollectionProperty=lambda **kwargs: None,
|
||||
PointerProperty=lambda **kwargs: None,
|
||||
),
|
||||
types=TypesStub(PropertyGroup=object),
|
||||
)
|
||||
|
||||
sys.modules["bpy"] = bpy_stub
|
||||
sys.modules["bpy.props"] = bpy_stub.props
|
||||
sys.modules["bpy.types"] = bpy_stub.types
|
||||
|
||||
src_module = sys.modules.get("src")
|
||||
if src_module is None:
|
||||
src_module = ModuleType("src")
|
||||
src_module.__path__ = [str(pathlib.Path(__file__).resolve().parents[3] / "src")]
|
||||
sys.modules["src"] = src_module
|
||||
|
||||
props_package = sys.modules.get("src.properties")
|
||||
if props_package is None:
|
||||
props_package = ModuleType("src.properties")
|
||||
props_package.__path__ = [str(pathlib.Path(__file__).resolve().parents[3] / "src" / "properties")]
|
||||
sys.modules["src.properties"] = props_package
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
module_name,
|
||||
pathlib.Path(__file__).resolve().parents[3] / "src" / "properties" / "property_groups.py",
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
try:
|
||||
yield module
|
||||
finally:
|
||||
for name, value in original_modules.items():
|
||||
if value is None:
|
||||
sys.modules.pop(name, None)
|
||||
else:
|
||||
sys.modules[name] = value
|
||||
|
||||
|
||||
def test_update_live_preview_no_scene_is_noop(property_groups_module):
|
||||
"""Function should safely return when context has no scene."""
|
||||
result = property_groups_module.update_live_preview(SimpleNamespace(), SimpleNamespace())
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_update_live_preview_with_scene_noop(property_groups_module):
|
||||
"""Even with props present, function should not modify state."""
|
||||
props = SimpleNamespace(is_updating=False)
|
||||
context = SimpleNamespace(scene=SimpleNamespace(text_texture_props=props))
|
||||
|
||||
result = property_groups_module.update_live_preview(SimpleNamespace(), context)
|
||||
|
||||
assert result is None
|
||||
assert props.is_updating is False
|
||||
45
tests/unit/ui/test_font_ui_helpers.py
Normal file
45
tests/unit/ui/test_font_ui_helpers.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Unit tests for font sizing UI helpers."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def font_helpers_module():
|
||||
"""Import the font helper utilities."""
|
||||
return importlib.import_module("src.ui.font_helpers")
|
||||
|
||||
|
||||
def test_font_size_ui_state_manual_when_fitting_disabled(monkeypatch, font_helpers_module):
|
||||
"""Manual font size should remain enabled when fitting is off."""
|
||||
monkeypatch.setattr(font_helpers_module, "has_text_fitting", lambda: True)
|
||||
props = SimpleNamespace(enable_text_fitting=False, text_fitting_mode="FIT_CONTAIN")
|
||||
|
||||
enabled, message = font_helpers_module.font_size_ui_state(props)
|
||||
|
||||
assert enabled is True
|
||||
assert message == ""
|
||||
|
||||
|
||||
def test_font_size_ui_state_auto_when_fitting_enabled(monkeypatch, font_helpers_module):
|
||||
"""Font size control shows auto-sizing message when fitting is active."""
|
||||
monkeypatch.setattr(font_helpers_module, "has_text_fitting", lambda: True)
|
||||
props = SimpleNamespace(enable_text_fitting=True, text_fitting_mode="FIT_WIDTH")
|
||||
|
||||
enabled, message = font_helpers_module.font_size_ui_state(props)
|
||||
|
||||
assert enabled is False
|
||||
assert "Fit Width" in message
|
||||
|
||||
|
||||
def test_font_size_ui_state_without_text_fitting_feature(monkeypatch, font_helpers_module):
|
||||
"""Without text fitting support, manual font sizing stays enabled."""
|
||||
monkeypatch.setattr(font_helpers_module, "has_text_fitting", lambda: False)
|
||||
props = SimpleNamespace(enable_text_fitting=True, text_fitting_mode="FIT_WIDTH")
|
||||
|
||||
enabled, message = font_helpers_module.font_size_ui_state(props)
|
||||
|
||||
assert enabled is True
|
||||
assert message == ""
|
||||
77
tests/unit/utils/test_system_fonts.py
Normal file
77
tests/unit/utils/test_system_fonts.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Tests for system font utilities."""
|
||||
|
||||
from types import ModuleType
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def system_module():
|
||||
"""Load the system utilities module without importing the addon __init__."""
|
||||
module_name = "src.utils.system"
|
||||
original = sys.modules.pop(module_name, None)
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
module_name,
|
||||
pathlib.Path(__file__).resolve().parents[3] / "src" / "utils" / "system.py",
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
try:
|
||||
yield module
|
||||
finally:
|
||||
if original is None:
|
||||
sys.modules.pop(module_name, None)
|
||||
else:
|
||||
sys.modules[module_name] = original
|
||||
|
||||
|
||||
def test_find_default_truetype_font_skips_fonts_without_latin(monkeypatch, system_module):
|
||||
"""Ensure selection skips fonts that produce unusable glyph metrics."""
|
||||
try:
|
||||
from PIL import ImageFont
|
||||
except ImportError:
|
||||
pytest.skip("Pillow not available in test environment")
|
||||
|
||||
def fake_system():
|
||||
return "Darwin"
|
||||
|
||||
monkeypatch.setattr(system_module.platform, "system", fake_system)
|
||||
|
||||
def fake_exists(path):
|
||||
return path in {"/bad/font.ttf", "/good/font.ttf"}
|
||||
|
||||
monkeypatch.setattr(system_module.os.path, "exists", fake_exists)
|
||||
|
||||
monkeypatch.setattr(
|
||||
system_module,
|
||||
"get_system_fonts",
|
||||
lambda: {"Bad": "/bad/font.ttf", "Good": "/good/font.ttf"},
|
||||
)
|
||||
|
||||
class FontStub:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
def getbbox(self, text):
|
||||
if self.path == "/bad/font.ttf":
|
||||
if text == "H":
|
||||
return (0, 10, 40, 50)
|
||||
return (0, 0, 100, 200)
|
||||
if text == "H":
|
||||
return (0, 12, 36, 52)
|
||||
return (0, 20, 30, 48)
|
||||
|
||||
def fake_truetype(path, size):
|
||||
return FontStub(path)
|
||||
|
||||
monkeypatch.setattr(ImageFont, "truetype", fake_truetype)
|
||||
system_module._DEFAULT_FONT_CACHE = None
|
||||
|
||||
selected = system_module.find_default_truetype_font()
|
||||
assert selected == "/good/font.ttf"
|
||||
Reference in New Issue
Block a user