update
This commit is contained in:
Binary file not shown.
@@ -4,6 +4,7 @@ import pytest
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
|
||||
from src.core.generation_engine import generate_texture_image, generate_preview
|
||||
from src.utils.overlay_assets import RasterizationResult
|
||||
# Test configuration constants
|
||||
DEFAULT_WIDTH = 512
|
||||
DEFAULT_HEIGHT = 512
|
||||
@@ -507,6 +508,58 @@ class TestGenerateTextureImage:
|
||||
assert center_pixel[2] == pytest.approx(0.0, abs=1e-3)
|
||||
assert center_pixel[3] == pytest.approx(1.0, rel=1e-3)
|
||||
|
||||
def test_svg_overlay_is_rasterized_and_applied(self, tmp_path):
|
||||
"""SVG overlays should be rasterized before being composited."""
|
||||
Image = pytest.importorskip("PIL.Image")
|
||||
with patch('src.core.generation_engine.bpy') as mock_bpy, \
|
||||
patch('src.core.generation_engine.ensure_svg_raster') as mock_rasterize:
|
||||
width = height = 64
|
||||
svg_path = tmp_path / "overlay.svg"
|
||||
svg_path.write_text('<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>')
|
||||
|
||||
raster_path = tmp_path / "overlay_raster.png"
|
||||
overlay_source = Image.new("RGBA", (16, 16), (0, 255, 0, 255))
|
||||
overlay_source.save(raster_path)
|
||||
|
||||
mock_rasterize.return_value = RasterizationResult(path=str(raster_path), applied_scale=1.0)
|
||||
|
||||
mock_blender_image = MagicMock()
|
||||
mock_blender_image.pixels = [0.0] * (width * height * 4)
|
||||
mock_bpy.data.images.new.return_value = mock_blender_image
|
||||
mock_bpy.data.images.__contains__.return_value = False
|
||||
|
||||
props = create_mock_props(
|
||||
text="",
|
||||
background_transparent=False,
|
||||
background_color=(1.0, 1.0, 1.0, 1.0)
|
||||
)
|
||||
|
||||
overlay = MagicMock()
|
||||
overlay.enabled = True
|
||||
overlay.image_path = str(svg_path)
|
||||
overlay.positioning_mode = 'ABSOLUTE'
|
||||
overlay.x_position = 0.5
|
||||
overlay.y_position = 0.5
|
||||
overlay.scale = 1.0
|
||||
overlay.rotation = 0.0
|
||||
overlay.z_index = 0
|
||||
overlay.text_spacing = 0.0
|
||||
overlay.image_spacing = 0.0
|
||||
props.image_overlays = [overlay]
|
||||
|
||||
result, normal_map = generate_texture_image(props, width, height)
|
||||
|
||||
assert result is mock_blender_image
|
||||
assert normal_map is None
|
||||
mock_rasterize.assert_called_once_with(str(svg_path), overlay.scale)
|
||||
|
||||
center_index = ((height // 2) * width + (width // 2)) * 4
|
||||
center_pixel = mock_blender_image.pixels[center_index:center_index + 4]
|
||||
assert center_pixel[0] == pytest.approx(0.0, abs=1e-3)
|
||||
assert center_pixel[1] == pytest.approx(1.0, rel=1e-3)
|
||||
assert center_pixel[2] == pytest.approx(0.0, abs=1e-3)
|
||||
assert center_pixel[3] == pytest.approx(1.0, rel=1e-3)
|
||||
|
||||
def test_skips_overlays_without_valid_path(self, tmp_path):
|
||||
"""Overlays without a valid image path should be ignored."""
|
||||
pytest.importorskip("PIL.Image")
|
||||
|
||||
193
tests/unit/ui/test_panel_layout_overhaul.py
Normal file
193
tests/unit/ui/test_panel_layout_overhaul.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Tests for the overhauled Positioning & Layout UI."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
panels = importlib.import_module("src.ui.panels")
|
||||
|
||||
|
||||
class FakeLayout:
|
||||
"""Minimal substitute for Blender layout that records UI calls."""
|
||||
|
||||
def __init__(self, kind="layout"):
|
||||
self.kind = kind
|
||||
self.calls = []
|
||||
self.children = []
|
||||
self.enabled = True
|
||||
self.alignment = None
|
||||
self.scale_x = 1.0
|
||||
self.scale_y = 1.0
|
||||
self.use_property_split = False
|
||||
self.use_property_decorate = True
|
||||
|
||||
def _child(self, kind, kwargs):
|
||||
child = FakeLayout(kind)
|
||||
self.calls.append((kind, kwargs, child))
|
||||
self.children.append(child)
|
||||
return child
|
||||
|
||||
def label(self, text="", **kwargs):
|
||||
self.calls.append(("label", {"text": text, **kwargs}))
|
||||
return None
|
||||
|
||||
def prop(self, props, prop_name, **kwargs):
|
||||
# Access attribute to mirror Blender's behaviour of raising AttributeError
|
||||
getattr(props, prop_name)
|
||||
data = {"name": prop_name}
|
||||
data.update(kwargs)
|
||||
self.calls.append(("prop", data))
|
||||
return None
|
||||
|
||||
def separator(self, **kwargs):
|
||||
self.calls.append(("separator", kwargs))
|
||||
return None
|
||||
|
||||
def row(self, **kwargs):
|
||||
return self._child("row", kwargs)
|
||||
|
||||
def column(self, **kwargs):
|
||||
return self._child("column", kwargs)
|
||||
|
||||
def box(self):
|
||||
return self._child("box", {})
|
||||
|
||||
def operator(self, operator_id, **kwargs):
|
||||
self.calls.append(("operator", {"id": operator_id, **kwargs}))
|
||||
return SimpleNamespace()
|
||||
|
||||
|
||||
def collect_calls(layout):
|
||||
"""Flatten recorded calls from layout tree."""
|
||||
flattened = []
|
||||
for entry in layout.calls:
|
||||
if len(entry) == 3:
|
||||
kind, kwargs, child = entry
|
||||
flattened.append((kind, kwargs))
|
||||
flattened.extend(collect_calls(child))
|
||||
else:
|
||||
flattened.append(entry)
|
||||
return flattened
|
||||
|
||||
|
||||
def make_default_props(**overrides):
|
||||
"""Helper to build a props namespace with defaults for positioning tests."""
|
||||
defaults = dict(
|
||||
text="Main",
|
||||
texture_width=1024,
|
||||
texture_height=1024,
|
||||
prepend_text="",
|
||||
append_text="",
|
||||
prepend_margin=10.0,
|
||||
append_margin=10.0,
|
||||
prepend_append_layout="HORIZONTAL",
|
||||
position_mode="PRESET",
|
||||
anchor_point="MIDDLE_CENTER",
|
||||
margins_linked=False,
|
||||
margin_x=0.0,
|
||||
margin_y=0.0,
|
||||
offset_x=0,
|
||||
offset_y=0,
|
||||
manual_position_unit="PERCENTAGE",
|
||||
manual_position_x=50.0,
|
||||
manual_position_y=50.0,
|
||||
constrain_to_canvas=True,
|
||||
composition_active_tab="TEXT",
|
||||
image_overlays=[],
|
||||
active_overlay_index=0,
|
||||
)
|
||||
defaults.update(overrides)
|
||||
return SimpleNamespace(**defaults)
|
||||
|
||||
|
||||
def test_text_input_section_exposes_dimensions():
|
||||
"""Dimensions should appear alongside the main text control."""
|
||||
layout = FakeLayout()
|
||||
props = make_default_props()
|
||||
|
||||
panels.draw_text_input_section(layout, props)
|
||||
|
||||
calls = collect_calls(layout)
|
||||
prop_names = [data["name"] for kind, data in calls if kind == "prop"]
|
||||
assert "texture_width" in prop_names
|
||||
assert "texture_height" in prop_names
|
||||
|
||||
|
||||
def test_positioning_section_drops_additional_text(monkeypatch):
|
||||
"""Positioning panel should no longer render prepend/append controls."""
|
||||
layout = FakeLayout()
|
||||
props = make_default_props()
|
||||
|
||||
monkeypatch.setattr("src.utils.system.get_anchor_point_matrix", lambda: [["CENTER"]])
|
||||
monkeypatch.setattr(panels, "safe_operator_call", lambda *args, **kwargs: SimpleNamespace())
|
||||
|
||||
panels.draw_positioning_section(layout, props)
|
||||
|
||||
calls = collect_calls(layout)
|
||||
labels = [data["text"] for kind, data in calls if kind == "label"]
|
||||
prop_names = [data["name"] for kind, data in calls if kind == "prop"]
|
||||
|
||||
assert "Additional Text:" not in labels
|
||||
assert "prepend_text" not in prop_names
|
||||
assert "append_text" not in prop_names
|
||||
|
||||
|
||||
def test_composition_section_shows_text_controls(monkeypatch):
|
||||
"""Composition panel should surface text controls when text tab is active."""
|
||||
layout = FakeLayout()
|
||||
props = make_default_props(
|
||||
composition_active_tab="TEXT",
|
||||
prepend_text="Intro",
|
||||
append_text="Outro",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(panels, "safe_operator_call", lambda *args, **kwargs: SimpleNamespace())
|
||||
monkeypatch.setattr(panels, "has_image_overlays", lambda: True)
|
||||
|
||||
panels.draw_composition_section(layout, props)
|
||||
|
||||
calls = collect_calls(layout)
|
||||
prop_names = [data["name"] for kind, data in calls if kind == "prop"]
|
||||
labels = [data["text"] for kind, data in calls if kind == "label"]
|
||||
|
||||
assert "composition_active_tab" in prop_names
|
||||
assert "prepend_text" in prop_names
|
||||
assert "append_text" in prop_names
|
||||
assert any("Text" in text for text in labels)
|
||||
|
||||
|
||||
def test_composition_section_shows_overlay_controls(monkeypatch):
|
||||
"""Composition panel should surface overlay controls when image tab is active."""
|
||||
layout = FakeLayout()
|
||||
overlay = SimpleNamespace(
|
||||
enabled=True,
|
||||
name="Logo",
|
||||
image_path="/tmp/logo.png",
|
||||
positioning_mode="ABSOLUTE",
|
||||
x_position=0.5,
|
||||
y_position=0.5,
|
||||
text_spacing=10.0,
|
||||
image_spacing=5.0,
|
||||
scale=1.0,
|
||||
rotation=0.0,
|
||||
z_index=1,
|
||||
)
|
||||
props = make_default_props(
|
||||
composition_active_tab="IMAGE",
|
||||
image_overlays=[overlay],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(panels, "safe_operator_call", lambda *args, **kwargs: SimpleNamespace())
|
||||
monkeypatch.setattr(panels, "has_image_overlays", lambda: True)
|
||||
|
||||
panels.draw_composition_section(layout, props)
|
||||
|
||||
calls = collect_calls(layout)
|
||||
prop_names = [data["name"] for kind, data in calls if kind == "prop"]
|
||||
labels = [data["text"] for kind, data in calls if kind == "label"]
|
||||
|
||||
assert "composition_active_tab" in prop_names
|
||||
assert "positioning_mode" in prop_names
|
||||
assert any("Image" in text for text in labels)
|
||||
Reference in New Issue
Block a user