feat: Enhance AI integration for Cutout, Image Box, and Image Lasso tools with unified settings and improved shape selection
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -574,6 +574,24 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
finally:
|
||||
self.actions.bpy.data = original_data
|
||||
|
||||
def test_names_new_packed_image_clipboard(self):
|
||||
image = FakeLoadedImage()
|
||||
images = FakeLoadedImages(image)
|
||||
original_data = self.actions.bpy.data
|
||||
self.actions.bpy.data = SimpleNamespace(images=images)
|
||||
try:
|
||||
loaded_image, reused = self.actions.acquire_packed_image(
|
||||
b"image-data",
|
||||
".png",
|
||||
)
|
||||
finally:
|
||||
self.actions.bpy.data = original_data
|
||||
|
||||
self.assertIs(loaded_image, image)
|
||||
self.assertFalse(reused)
|
||||
self.assertEqual(image.name, "Clipboard")
|
||||
self.assertTrue(image.packed)
|
||||
|
||||
def test_sets_and_reuses_stencil_brush_texture(self):
|
||||
textures = FakeTextures()
|
||||
original_data = self.actions.bpy.data
|
||||
@@ -865,6 +883,28 @@ class FakeImage:
|
||||
return self.properties.get(property_name, default)
|
||||
|
||||
|
||||
class FakeLoadedImage(dict):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = None
|
||||
self.packed = False
|
||||
|
||||
def pack(self):
|
||||
self.packed = True
|
||||
|
||||
|
||||
class FakeLoadedImages(list):
|
||||
def __init__(self, loaded_image):
|
||||
super().__init__()
|
||||
self.loaded_image = loaded_image
|
||||
|
||||
def load(self, _path, check_existing):
|
||||
return self.loaded_image
|
||||
|
||||
def remove(self, image):
|
||||
super().remove(image)
|
||||
|
||||
|
||||
class FakeTexture(dict):
|
||||
def __init__(self, name, texture_type):
|
||||
super().__init__()
|
||||
|
||||
@@ -47,29 +47,27 @@ 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():
|
||||
def test_cutout_pie_uses_two_local_shapes_without_ai(monkeypatch):
|
||||
window_manager = WindowManager()
|
||||
context = cutout_context(window_manager)
|
||||
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,
|
||||
RegionPath(points=((0, 0), (10, 0), (0, 10))),
|
||||
refine_region=True,
|
||||
)
|
||||
|
||||
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"
|
||||
@@ -80,10 +78,11 @@ def test_cutout_pie_uses_four_semantic_shapes_in_cardinal_order():
|
||||
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():
|
||||
def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
|
||||
window_manager = WindowManager()
|
||||
context = cutout_context(window_manager, point_spacing=16, generate_normal=True)
|
||||
source = cutout_source(size=(4096, 2048))
|
||||
monkeypatch.setattr(interaction, "ai_ready", lambda: True)
|
||||
|
||||
interaction.open_shape_pie(
|
||||
context,
|
||||
@@ -96,6 +95,12 @@ def test_cutout_pie_passes_image_lasso_options_to_every_shape():
|
||||
refine_region=True,
|
||||
)
|
||||
|
||||
assert [call[2].shape for call in window_manager.pie.calls] == [
|
||||
"SURFACE",
|
||||
"BALLOON",
|
||||
"DEPTH_BALLOON",
|
||||
"DEPTH_SURFACE",
|
||||
]
|
||||
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(
|
||||
|
||||
@@ -91,6 +91,49 @@ class ImageDataTest(unittest.TestCase):
|
||||
PROJECT_ROOT / "src" / "anyimage" / "assets" / "O_AnyImage.blend",
|
||||
)
|
||||
|
||||
def test_runtime_modifier_enum_input_uses_string_identifier(self):
|
||||
enum_items = (
|
||||
SimpleNamespace(identifier="SURFACE"),
|
||||
SimpleNamespace(identifier="BALLOON"),
|
||||
)
|
||||
value_property = SimpleNamespace(type="ENUM", enum_items=enum_items)
|
||||
input_slot = SimpleNamespace(
|
||||
value=None,
|
||||
bl_rna=SimpleNamespace(properties={"value": value_property}),
|
||||
)
|
||||
modifier = SimpleNamespace(
|
||||
properties=SimpleNamespace(
|
||||
inputs=SimpleNamespace(Shape=input_slot),
|
||||
)
|
||||
)
|
||||
|
||||
self.image_depth.set_modifier_input(modifier, "Shape", 1)
|
||||
|
||||
self.assertEqual(input_slot.value, "BALLOON")
|
||||
|
||||
def test_runtime_modifier_non_enum_and_legacy_inputs_keep_values(self):
|
||||
value_property = SimpleNamespace(type="FLOAT")
|
||||
input_slot = SimpleNamespace(
|
||||
value=None,
|
||||
bl_rna=SimpleNamespace(properties={"value": value_property}),
|
||||
)
|
||||
runtime_modifier = SimpleNamespace(
|
||||
properties=SimpleNamespace(
|
||||
inputs=SimpleNamespace(Thickness=input_slot),
|
||||
)
|
||||
)
|
||||
|
||||
self.image_depth.set_modifier_input(runtime_modifier, "Thickness", 2.0)
|
||||
|
||||
self.assertEqual(input_slot.value, 2.0)
|
||||
|
||||
class LegacyModifier(dict):
|
||||
properties = None
|
||||
|
||||
legacy_modifier = LegacyModifier()
|
||||
self.image_depth.set_modifier_input(legacy_modifier, "Shape", 1)
|
||||
self.assertEqual(legacy_modifier["Shape"], 1)
|
||||
|
||||
def test_material_color_space_matches_scene_view_transform(self):
|
||||
mappings = {
|
||||
"ACES 1.3": "ACES 1.3 sRGB",
|
||||
|
||||
Reference in New Issue
Block a user