Add comprehensive tests for cutout and mesh functionalities
- Introduced `test_cutout.py` to validate cutout pie menu behavior, image lasso options, and mesh detail calculations. - Added `test_mesh.py` to ensure correct tangent normal rasterization, plane source topology, and depth surface evaluations. - Created `test_mesh_blender.py` to verify material node groups, tangent baking, and local cutout building from Blender images. - Implemented tests for handling alpha regions and ensuring geometry nodes maintain expected behavior.
This commit is contained in:
136
tests/test_cutout.py
Normal file
136
tests/test_cutout.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import MethodType, SimpleNamespace
|
||||
|
||||
import bpy
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.operators import cutout, mesh
|
||||
from anyimage.operators.image_edit.mask import ImageMaskGesture
|
||||
|
||||
|
||||
class Pie:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def operator(self, identifier, **options):
|
||||
operator = SimpleNamespace()
|
||||
self.calls.append((identifier, options, operator))
|
||||
return operator
|
||||
|
||||
|
||||
class WindowManager:
|
||||
def __init__(self):
|
||||
self.pie = Pie()
|
||||
|
||||
def popup_menu_pie(self, _event, draw, **_options):
|
||||
menu = SimpleNamespace(layout=SimpleNamespace(menu_pie=lambda: self.pie))
|
||||
draw(menu, None)
|
||||
|
||||
|
||||
def cutout_context(window_manager, pixel_size=32, normal_analysis=False):
|
||||
settings = SimpleNamespace(
|
||||
cutout_pixel_size=pixel_size,
|
||||
cutout_normal_analysis=normal_analysis,
|
||||
)
|
||||
return SimpleNamespace(
|
||||
window_manager=window_manager,
|
||||
scene=SimpleNamespace(anyimage_settings=settings),
|
||||
)
|
||||
|
||||
|
||||
def cutout_source(name="Source", size=(2048, 1024)):
|
||||
return SimpleNamespace(name=name, data=SimpleNamespace(size=size))
|
||||
|
||||
|
||||
def test_cutout_pie_uses_four_semantic_shapes_in_cardinal_order():
|
||||
window_manager = WindowManager()
|
||||
context = cutout_context(window_manager)
|
||||
source = cutout_source()
|
||||
|
||||
cutout._open_shape_pie(context, object(), source, ((0, 0), (10, 0), (0, 10)))
|
||||
|
||||
assert [call[2].shape for call in window_manager.pie.calls] == [
|
||||
mesh.CUTOUT,
|
||||
mesh.BALLOON,
|
||||
mesh.DEPTH_BALLOON,
|
||||
mesh.DEPTH_SURFACE,
|
||||
]
|
||||
assert all(
|
||||
call[0] == "anyimage.convert_cutout_shape" for call in window_manager.pie.calls
|
||||
)
|
||||
assert not any(call[2].refine_selection for call in window_manager.pie.calls)
|
||||
assert not any(call[2].normal_analysis for call in window_manager.pie.calls)
|
||||
assert not any(call[2].invert for call in window_manager.pie.calls)
|
||||
assert all(call[2].mesh_detail == 64 for call in window_manager.pie.calls)
|
||||
|
||||
|
||||
def test_cutout_pie_passes_image_lasso_options_to_every_shape():
|
||||
window_manager = WindowManager()
|
||||
context = cutout_context(
|
||||
window_manager, pixel_size=16, normal_analysis=True
|
||||
)
|
||||
source = cutout_source(size=(4096, 2048))
|
||||
|
||||
cutout._open_shape_pie(
|
||||
context,
|
||||
object(),
|
||||
source,
|
||||
((0, 0), (10, 0), (0, 10)),
|
||||
refine_selection=True,
|
||||
invert=True,
|
||||
)
|
||||
|
||||
assert all(call[2].refine_selection for call in window_manager.pie.calls)
|
||||
assert all(call[2].normal_analysis for call in window_manager.pie.calls)
|
||||
assert all(call[2].invert for call in window_manager.pie.calls)
|
||||
assert all(call[2].mesh_detail == 256 for call in window_manager.pie.calls)
|
||||
|
||||
|
||||
def test_cutout_mesh_detail_tracks_the_source_long_edge_and_clamps():
|
||||
assert cutout.cutout_mesh_detail((4096, 2048), 32) == 128
|
||||
assert cutout.cutout_mesh_detail((16, 8), 4) == 4
|
||||
assert cutout.cutout_mesh_detail((65536, 32768), 4) == 1024
|
||||
|
||||
|
||||
def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypatch):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Source", width=320, height=180)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
bpy.context.collection.objects.link(source)
|
||||
window_manager = WindowManager()
|
||||
context = cutout_context(window_manager)
|
||||
|
||||
monkeypatch.setattr(
|
||||
cutout, "resolve_image_edit_click", lambda *_args: (source, True)
|
||||
)
|
||||
monkeypatch.setattr(cutout, "video_total_frames", lambda _image: 1)
|
||||
monkeypatch.setattr(cutout, "apply_image_mask_settings", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout, "selected_image_rgba", lambda *_args: object())
|
||||
|
||||
operator = SimpleNamespace(
|
||||
full_image=True,
|
||||
refine_selection=False,
|
||||
report=lambda *_args: None,
|
||||
)
|
||||
operator.execute = MethodType(cutout.SelectCutoutRegion.execute, operator)
|
||||
result = cutout.SelectCutoutRegion.invoke(
|
||||
operator, context, SimpleNamespace(value="DOUBLE_CLICK")
|
||||
)
|
||||
|
||||
assert issubclass(cutout.SelectCutoutRegion, ImageMaskGesture)
|
||||
assert result == {"FINISHED"}
|
||||
selection = json.loads(window_manager.pie.calls[0][2].selection_data)
|
||||
assert selection == [[0, 0], [320, 0], [320, 180], [0, 180]]
|
||||
assert window_manager.pie.calls[0][2].mesh_detail == 10
|
||||
assert not hasattr(cutout, "pending_selection")
|
||||
|
||||
|
||||
def test_cutout_tool_prioritizes_double_click_before_lasso_press():
|
||||
assert cutout.CutoutTool.bl_keymap[0][1]["value"] == "DOUBLE_CLICK"
|
||||
assert cutout.CutoutTool.bl_keymap[1][1]["value"] == "PRESS"
|
||||
Reference in New Issue
Block a user