feat: Enhance AI integration for Cutout, Image Box, and Image Lasso tools with unified settings and improved shape selection

This commit is contained in:
2026-08-31 16:10:08 +08:00
parent e7aeeb21d4
commit 14d63001d2
15 changed files with 473 additions and 89 deletions

View File

@@ -1433,13 +1433,23 @@ class BlenderAddonTest(unittest.TestCase):
).CutoutTool
cutout_module = sys.modules[cutout_tool.__module__]
drawn_settings.clear()
with patch.object(
cutout_module,
"draw_region_settings",
side_effect=lambda _context, _layout, name: drawn_settings.append(name),
) as draw:
with (
patch.object(
cutout_module,
"draw_ai_setup",
return_value={"ready": True},
) as setup,
patch.object(
cutout_module,
"draw_ai_property",
side_effect=lambda _layout, _settings, name, _status: (
drawn_settings.append(name)
),
) as draw,
):
cutout_tool.draw_settings(context, layout, None)
draw.assert_called_once_with(context, layout, "cutout_refine_region")
setup.assert_called_once_with(layout)
self.assertEqual(draw.call_count, 2)
self.assertEqual(
drawn_settings,
[
@@ -1481,6 +1491,83 @@ class BlenderAddonTest(unittest.TestCase):
self.assertNotIn("image_extract_padding", settings)
self.assertNotIn("image_extract_remove_background", settings)
def test_image_area_tool_settings_gate_ai_options(self):
class Row:
def __init__(self, events):
self.events = events
self.enabled = True
def prop(self, _owner, name):
self.events.append(("prop", name, self.enabled))
class Layout:
def __init__(self):
self.events = []
def operator(self, identifier, **_options):
self.events.append(("operator", identifier, True))
def row(self):
return Row(self.events)
def prop(self, _owner, name):
self.events.append(("prop", name, True))
context = SimpleNamespace(
scene=SimpleNamespace(anyimage_settings=SimpleNamespace())
)
missing = {
"environment_ready": False,
"missing_models": (),
"ready": False,
}
setup = ("operator", "anyimage.setup_ai_environment", True)
with patch.object(self.image_interaction, "ai_status", return_value=missing):
for tool, setting_name in (
(self.image_box.ImageBoxTool, "image_box_refine_region"),
(self.image_lasso.ImageLassoTool, "image_lasso_refine_region"),
):
layout = Layout()
tool.draw_settings(context, layout, None)
self.assertEqual(
layout.events,
[setup, ("prop", setting_name, False)],
)
cutout_tool = importlib.import_module(
"anyimage.operators.cutout_tool"
).CutoutTool
layout = Layout()
cutout_tool.draw_settings(context, layout, None)
self.assertEqual(
layout.events,
[
setup,
("prop", "cutout_refine_region", False),
("prop", "cutout_point_spacing", True),
("prop", "cutout_generate_normal", False),
],
)
layout = Layout()
operator = SimpleNamespace(gesture="BOX")
self.image_interaction.draw_region_operator_settings(layout, operator)
self.assertEqual(
layout.events,
[
setup,
("prop", "refine_region", False),
("prop", "invert", True),
],
)
ready = {"environment_ready": True, "missing_models": (), "ready": True}
with patch.object(self.image_interaction, "ai_status", return_value=ready):
layout = Layout()
cutout_tool.draw_settings(context, layout, None)
self.assertNotIn(setup, layout.events)
self.assertTrue(all(event[2] for event in layout.events))
def test_refine_region_is_independent_for_each_area_tool(self):
settings = SimpleNamespace(
cutout_refine_region=True,
@@ -1636,6 +1723,57 @@ class BlenderAddonTest(unittest.TestCase):
atol=1e-6,
)
def test_image_perspective_preview_uses_srgb_without_changing_pixels(self):
written = []
preview = SimpleNamespace(
colorspace_settings=SimpleNamespace(name="Non-Color"),
alpha_mode=None,
pixels=SimpleNamespace(foreach_set=written.extend),
update=lambda: None,
)
previous_data = self.fake_bpy.data
self.fake_bpy.data = SimpleNamespace(
images=SimpleNamespace(new=lambda *_args, **_options: preview)
)
pixels = (0.1, 0.2, 0.3, 0.4)
source = SimpleNamespace(
name="Source.png",
colorspace_settings=SimpleNamespace(name="Non-Color"),
)
try:
result = self.image_perspective.create_perspective_preview_image(
source, pixels
)
finally:
self.fake_bpy.data = previous_data
self.assertIs(result, preview)
self.assertEqual(preview.colorspace_settings.name, "sRGB")
self.assertEqual(preview.alpha_mode, "STRAIGHT")
self.assertEqual(written, list(pixels))
def test_image_perspective_preview_skips_cursor_at_last_clicked_point(self):
points = ((10.0, 10.0), (80.0, 10.0), (70.0, 60.0))
stationary = self.image_perspective.interactive_preview_polygon(
points, points[-1]
)
moving = self.image_perspective.interactive_preview_polygon(
points, (20.0, 70.0)
)
self.assertEqual(len(stationary), 3)
self.assertEqual(len(moving), 4)
def test_image_perspective_draws_scene_linear_texture_as_srgb_in_blender_5(self):
draw_options = self.image_perspective.preview_texture_draw_options
self.assertEqual(draw_options((4, 5, 3)), {})
self.assertEqual(
draw_options((5, 0, 0)),
{"is_scene_linear_with_rec709_srgb_target": True},
)
def test_image_perspective_tightly_crops_alpha(self):
import numpy as np