import json import sys from pathlib import Path from types import 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.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", alpha_threshold=0.9, fine_outline=True, ): settings = SimpleNamespace( cutout_mesh_detail=mesh_detail, cutout_generate_normal=generate_normal, cutout_inward_axis=inward_axis, cutout_alpha_threshold=alpha_threshold, cutout_fine_outline=fine_outline, ) return SimpleNamespace( window_manager=window_manager, scene=SimpleNamespace(anyimage_settings=settings), ) def test_cutout_menu_captures_fine_outline_before_later_redraw(monkeypatch): window_manager = WindowManager() context = cutout_context(window_manager) monkeypatch.setattr(interaction, "ai_ready", lambda: False) def delayed_popup(_event, draw, **_options): context.scene.anyimage_settings.cutout_fine_outline = False draw(SimpleNamespace(layout=SimpleNamespace(menu_pie=lambda: window_manager.pie)), None) window_manager.popup_menu_pie = delayed_popup interaction.open_shape_pie(context, object(), cutout_source(), SelectionPath(points=((0, 0), (10, 0), (0, 10)))) assert all(call[2].fine_outline is True for call in window_manager.pie.calls) 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))), ) assert [call[2].shape for call in window_manager.pie.calls] == [ "FLAT", "SOLID", ] assert [call[1]["text"] for call in window_manager.pie.calls] == [ "Flat", "Solid", ] assert all( call[0] == "anyimage.cutout_selection_to_shape" 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].point_spacing == 32 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", alpha_threshold=0.1, fine_outline=False, ) 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)), ), ) assert [call[2].shape for call in window_manager.pie.calls] == [ "FLAT", "SOLID", "DEPTH_BALLOON", "DEPTH_SOLID", ] assert all(call[2].generate_normal for call in window_manager.pie.calls) assert all(call[2].alpha_threshold == 0.1 for call in window_manager.pie.calls) assert all(call[2].fine_outline is False for call in window_manager.pie.calls) assert len(serialized) == 1 assert set(json.loads(serialized[0])) == {"points"} 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).points == ((0.0, 0.0), (10.0, 0.0), (0.0, 10.0)) for call in window_manager.pie.calls ) assert all(call[2].point_spacing == 16 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_SOLID", False) == "OBJECT" assert cutout_main.normal_mode_for_shape("DEPTH_BALLOON", False) == "TANGENT" assert cutout_main.normal_mode_for_shape("FLAT", True) == "TANGENT" assert cutout_main.normal_mode_for_shape("SOLID", True) == "TANGENT" assert cutout_main.normal_mode_for_shape("DEPTH_BALLOON", True) == "TANGENT" assert cutout_main.normal_mode_for_shape("DEPTH_SOLID", True) == "OBJECT" 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_lasso_submission_passes_only_the_selected_path(monkeypatch): source = cutout_source() window_manager = WindowManager() context = cutout_context(window_manager) monkeypatch.setattr(cutout_main, "require_image_empty", lambda _name: source) monkeypatch.setattr(interaction, "ai_ready", lambda: False) path = SelectionPath(points=((0, 0), (10, 0), (0, 10))) operator = SimpleNamespace(source_object_name=source.name, _pie_event=object()) result = cutout_main.SelectCutoutSelection._submit_selection_path(operator, context, path) assert result == {"FINISHED"} assert SelectionPath.from_json(window_manager.pie.calls[0][2].selection_path_json) == path def test_cutout_tool_only_binds_lasso_drag_and_native_selection(): keymap = cutout_main.CutoutTool.bl_keymap assert keymap[0][1]["value"] == "CLICK_DRAG" assert [entry[0] for entry in keymap[1:]] == ["view3d.select", "view3d.select"] assert all(entry[1]["value"] != "DOUBLE_CLICK" for entry in keymap) assert "full_image" not in cutout_main.SelectCutoutSelection.__annotations__ assert keymap[1][2] == {"properties": [("deselect_all", False)]} assert keymap[2][2] == {"properties": [("toggle", True)]} def test_cutout_f_has_no_option_shortcut(monkeypatch): operator = SimpleNamespace( gesture="LASSO", active_tool_id="anyimage.cutout_lasso", ) 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"} 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]