Files
AnyImage/tests/test_cutout_interaction.py

250 lines
8.1 KiB
Python

import json
import sys
from pathlib import Path
from types import MethodType, SimpleNamespace
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common.selection import SelectionPath
from anyimage.common import viewport
from anyimage.common.viewport import CropSelectionGesture
from anyimage.operators.cutout_tool import interaction
from anyimage.operators.cutout_tool import main as cutout_main
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,
mesh_detail="LOW",
generate_normal=False,
inward_axis="NEGATIVE_Z",
):
settings = SimpleNamespace(
cutout_mesh_detail=mesh_detail,
cutout_generate_normal=generate_normal,
cutout_inward_axis=inward_axis,
)
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_two_local_shapes_without_ai(monkeypatch):
window_manager = WindowManager()
context = cutout_context(window_manager, generate_normal=True)
source = cutout_source()
monkeypatch.setattr(interaction, "ai_ready", lambda: False)
interaction.open_shape_pie(
context,
object(),
source,
SelectionPath(points=((0, 0), (10, 0), (0, 10))),
refine_selection=True,
)
assert [call[2].shape for call in window_manager.pie.calls] == [
"SURFACE",
"BALLOON",
]
assert [call[1]["text"] for call in window_manager.pie.calls] == [
"Surface",
"Balloon",
]
assert all(
call[0] == "anyimage.cutout_selection_to_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].generate_normal for call in window_manager.pie.calls)
assert all(call[2].mesh_resolution == 64 for call in window_manager.pie.calls)
assert all(call[2].inward_axis == "NEGATIVE_Z" for call in window_manager.pie.calls)
def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
window_manager = WindowManager()
context = cutout_context(
window_manager,
mesh_detail="MEDIUM",
generate_normal=True,
inward_axis="POSITIVE_X",
)
source = cutout_source(size=(4096, 2048))
monkeypatch.setattr(interaction, "ai_ready", lambda: True)
serialized = []
original_to_json = SelectionPath.to_json
def record_to_json(selection_path):
value = original_to_json(selection_path)
serialized.append(value)
return value
monkeypatch.setattr(SelectionPath, "to_json", record_to_json)
interaction.open_shape_pie(
context,
object(),
source,
SelectionPath(
points=((0, 0), (10, 0), (0, 10)),
invert=True,
),
refine_selection=True,
)
assert [call[2].shape for call in window_manager.pie.calls] == [
"SURFACE",
"BALLOON",
"DEPTH_BALLOON",
"DEPTH_SURFACE",
]
assert all(call[2].refine_selection for call in window_manager.pie.calls)
assert all(call[2].generate_normal for call in window_manager.pie.calls)
assert len(serialized) == 1
assert set(json.loads(serialized[0])) == {"points", "invert"}
assert all(
call[2].selection_path_json == serialized[0]
for call in window_manager.pie.calls
)
assert all(
SelectionPath.from_json(call[2].selection_path_json).invert
for call in window_manager.pie.calls
)
assert all(call[2].mesh_resolution == 256 for call in window_manager.pie.calls)
assert all(call[2].inward_axis == "POSITIVE_X" for call in window_manager.pie.calls)
def test_cutout_normal_space_is_derived_from_shape():
assert cutout_main.normal_mode_for_shape("DEPTH_SURFACE", False) == "NONE"
assert cutout_main.normal_mode_for_shape("SURFACE", True) == "TANGENT"
assert cutout_main.normal_mode_for_shape("BALLOON", True) == "TANGENT"
assert cutout_main.normal_mode_for_shape("DEPTH_BALLOON", True) == "TANGENT"
assert cutout_main.normal_mode_for_shape("DEPTH_SURFACE", True) == "OBJECT"
def test_cutout_mesh_detail_tracks_the_source_long_edge_and_clamps():
assert interaction.cutout_mesh_resolution((4096, 2048), 32) == 128
assert interaction.cutout_mesh_resolution((16, 8), 4) == 4
assert interaction.cutout_mesh_resolution((65536, 32768), 4) == 1024
def test_cutout_mesh_detail_maps_to_denser_sampling():
assert [
interaction.cutout_mesh_detail_spacing(detail)
for detail in ("LOW", "MEDIUM", "HIGH", "ULTRA")
] == [32, 16, 8, 4]
def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypatch):
source = cutout_source(size=(65536, 32768))
window_manager = WindowManager()
context = cutout_context(window_manager)
monkeypatch.setattr(
cutout_main, "resolve_image_edit_click", lambda *_args: (source, True)
)
monkeypatch.setattr(cutout_main, "video_total_frames", lambda _image: 1)
monkeypatch.setattr(cutout_main, "require_image_empty", lambda _name: source)
monkeypatch.setattr(cutout_main, "apply_crop_settings", lambda *_args: None)
monkeypatch.setattr(cutout_main, "serialize_matrix", lambda _matrix: "matrix")
monkeypatch.setattr(
cutout_main,
"image_rgba",
lambda *_args: (_ for _ in ()).throw(AssertionError("unexpected pixel read")),
)
monkeypatch.setattr(
cutout_main,
"rasterize_selection_path",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
AssertionError("unexpected Selection rasterization")
),
)
operator = SimpleNamespace(
full_image=True,
refine_selection=False,
report=lambda *_args: None,
)
source.matrix_world = object()
operator._submit_selection_path = MethodType(
cutout_main.SelectCutoutSelection._submit_selection_path,
operator,
)
result = cutout_main.SelectCutoutSelection.invoke(
operator, context, SimpleNamespace(value="DOUBLE_CLICK")
)
assert issubclass(cutout_main.SelectCutoutSelection, CropSelectionGesture)
assert result == {"FINISHED"}
selection_path = SelectionPath.from_json(window_manager.pie.calls[0][2].selection_path_json)
assert selection_path.points == (
(0, 0),
(65536, 0),
(65536, 32768),
(0, 32768),
)
assert window_manager.pie.calls[0][2].mesh_resolution == 1024
assert not hasattr(cutout_main, "pending_selection")
def test_cutout_tool_prioritizes_double_click_before_lasso_press():
assert cutout_main.CutoutTool.bl_keymap[0][1]["value"] == "DOUBLE_CLICK"
assert cutout_main.CutoutTool.bl_keymap[1][1]["value"] == "PRESS"
def test_cutout_f_does_not_toggle_invert(monkeypatch):
operator = SimpleNamespace(
gesture="LASSO",
active_tool_id="anyimage.cutout_lasso",
invert=False,
)
monkeypatch.setattr(viewport, "active_view3d_tool_id", lambda _context: None)
result = cutout_main.SelectCutoutSelection.modal(
operator,
SimpleNamespace(),
SimpleNamespace(type="F", value="PRESS"),
)
assert result == {"PASS_THROUGH"}
assert operator.invert is False
def test_cutout_status_has_no_letter_shortcut():
status = []
operator = SimpleNamespace()
context = SimpleNamespace(
workspace=SimpleNamespace(status_text_set=status.append)
)
cutout_main.SelectCutoutSelection._set_status(operator, context)
assert status == [
"Cutout Lasso: drag and release for shapes; RMB or Esc cancels"
]
assert "F" not in status[0]