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.common.image_interaction import ImageRegionGesture from anyimage.operators.common.image_region import RegionPath from anyimage.operators.cutout_tool import interaction from anyimage.operators.cutout_tool import main as cutout 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, point_spacing=32, generate_normal=False): settings = SimpleNamespace( cutout_point_spacing=point_spacing, cutout_generate_normal=generate_normal, ) 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() interaction.open_shape_pie( context, object(), source, RegionPath(points=((0, 0), (10, 0), (0, 10))), ) assert [call[2].shape for call in window_manager.pie.calls] == [ "SURFACE", "BALLOON", "DEPTH_BALLOON", "DEPTH_SURFACE", ] assert [call[1]["text"] for call in window_manager.pie.calls] == [ "Surface", "Balloon", "Depth Balloon", "Depth Surface", ] assert all( call[0] == "anyimage.cutout_region_to_shape" for call in window_manager.pie.calls ) assert not any(call[2].refine_region 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) def test_cutout_pie_passes_image_lasso_options_to_every_shape(): window_manager = WindowManager() context = cutout_context(window_manager, point_spacing=16, generate_normal=True) source = cutout_source(size=(4096, 2048)) interaction.open_shape_pie( context, object(), source, RegionPath( points=((0, 0), (10, 0), (0, 10)), invert=True, ), refine_region=True, ) assert all(call[2].refine_region for call in window_manager.pie.calls) assert all(call[2].generate_normal for call in window_manager.pie.calls) assert all( RegionPath.from_json(call[2].region_path_json).invert for call in window_manager.pie.calls ) assert all(call[2].mesh_resolution == 256 for call in window_manager.pie.calls) def test_cutout_normal_space_is_derived_from_shape(): assert cutout.normal_mode_for_shape("DEPTH_SURFACE", False) == "NONE" assert cutout.normal_mode_for_shape("SURFACE", True) == "TANGENT" assert cutout.normal_mode_for_shape("BALLOON", True) == "TANGENT" assert cutout.normal_mode_for_shape("DEPTH_BALLOON", True) == "TANGENT" assert cutout.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_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_region_settings", lambda *_args: None) monkeypatch.setattr(cutout, "validate_cutout_region", lambda *_args: None) operator = SimpleNamespace( full_image=True, refine_region=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, ImageRegionGesture) assert result == {"FINISHED"} region_path = RegionPath.from_json(window_manager.pie.calls[0][2].region_path_json) assert region_path.points == ((0, 0), (320, 0), (320, 180), (0, 180)) assert window_manager.pie.calls[0][2].mesh_resolution == 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"