feat: Update version to 0.13.0 in project files and adjust dependencies
This commit is contained in:
@@ -113,17 +113,16 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
cls.upscale = importlib.import_module("anyimage.operators.upscale")
|
||||
cls.image_interaction = importlib.import_module("anyimage.common.viewport")
|
||||
cls.edit_mask = importlib.import_module(
|
||||
"anyimage.operators.image_tool.edit_mask"
|
||||
"anyimage.operators.crop_tool.selection"
|
||||
)
|
||||
cls.image_selection = importlib.import_module("anyimage.common.selection")
|
||||
cls.image_box = importlib.import_module("anyimage.operators.image_tool.box")
|
||||
cls.image_lasso = importlib.import_module("anyimage.operators.image_tool.lasso")
|
||||
cls.image_line = importlib.import_module("anyimage.operators.image_tool.line")
|
||||
cls.image_box = importlib.import_module("anyimage.operators.crop_tool.box")
|
||||
cls.image_lasso = importlib.import_module("anyimage.operators.crop_tool.lasso")
|
||||
cls.image_polyline = importlib.import_module(
|
||||
"anyimage.operators.image_tool.polyline"
|
||||
"anyimage.operators.crop_tool.polyline"
|
||||
)
|
||||
cls.image_perspective = importlib.import_module(
|
||||
"anyimage.operators.perspective_tool.edit_perspective"
|
||||
"anyimage.operators.crop_tool.perspective"
|
||||
)
|
||||
cls.cutout_geometry = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool.geometry"
|
||||
@@ -1304,6 +1303,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
64,
|
||||
selection_mask.values,
|
||||
selection_mask.bounds,
|
||||
inward_axis="NEGATIVE_Z",
|
||||
color_image="Color",
|
||||
)
|
||||
|
||||
@@ -1852,17 +1852,16 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_image_edit_tools_and_operators_are_registered(self):
|
||||
operator = self.edit_mask.EditImageMask
|
||||
operator = self.edit_mask.CropImageSelection
|
||||
self.assertIn(operator, self.anyimage.CLASSES)
|
||||
self.assertIn(self.edit_mask.RefineImageSelection, self.anyimage.CLASSES)
|
||||
self.assertIn(self.edit_mask.ActivateImageTool, self.anyimage.CLASSES)
|
||||
self.assertIn(self.edit_mask.RefineCropSelection, self.anyimage.CLASSES)
|
||||
self.assertIn(self.edit_mask.ActivateCropTool, self.anyimage.CLASSES)
|
||||
self.assertNotIn("selection_path_json", operator.__annotations__)
|
||||
tool_cases = (
|
||||
(self.image_box.ImageBoxTool, "BOX", "ops.sculpt.border_mask"),
|
||||
(self.image_lasso.ImageLassoTool, "LASSO", "ops.sculpt.lasso_mask"),
|
||||
(self.image_line.ImageLineTool, "LINE", "ops.sculpt.line_mask"),
|
||||
(self.image_box.CropBoxTool, "BOX", "ops.sculpt.border_mask"),
|
||||
(self.image_lasso.CropLassoTool, "LASSO", "ops.sculpt.lasso_mask"),
|
||||
(
|
||||
self.image_polyline.ImagePolylineTool,
|
||||
self.image_polyline.CropPolylineTool,
|
||||
"POLYLINE",
|
||||
"ops.sculpt.polyline_mask",
|
||||
),
|
||||
@@ -1873,22 +1872,38 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.assertEqual(tool.bl_icon, icon)
|
||||
self.assertEqual(tool.bl_keymap[0][2]["properties"], [("gesture", gesture)])
|
||||
self.assertNotIn("bl_cursor", tool.__dict__)
|
||||
self.assertIn("draw_settings", self.image_box.ImageBoxTool.__dict__)
|
||||
self.assertIn("draw_settings", self.image_lasso.ImageLassoTool.__dict__)
|
||||
self.assertIn("draw_settings", self.image_box.CropBoxTool.__dict__)
|
||||
self.assertIn("draw_settings", self.image_lasso.CropLassoTool.__dict__)
|
||||
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=object()))
|
||||
drawn_settings = []
|
||||
layout = SimpleNamespace(
|
||||
prop=lambda _settings, name: drawn_settings.append(name)
|
||||
)
|
||||
with patch.object(self.image_box, "draw_selection_settings") as draw:
|
||||
self.image_box.ImageBoxTool.draw_settings(context, layout, None)
|
||||
draw.assert_called_once_with(context, layout, "image_box_refine_selection")
|
||||
with patch.object(self.image_lasso, "draw_selection_settings") as draw:
|
||||
self.image_lasso.ImageLassoTool.draw_settings(context, layout, None)
|
||||
draw.assert_called_once_with(context, layout, "image_lasso_refine_selection")
|
||||
with patch.object(self.image_box, "draw_crop_settings") as draw:
|
||||
self.image_box.CropBoxTool.draw_settings(context, layout, None)
|
||||
draw.assert_called_once_with(
|
||||
context,
|
||||
layout,
|
||||
refine_selection="crop_box_refine_selection",
|
||||
invert="crop_box_invert",
|
||||
keep_original="crop_box_keep_original",
|
||||
)
|
||||
with patch.object(self.image_lasso, "draw_crop_settings") as draw:
|
||||
self.image_lasso.CropLassoTool.draw_settings(context, layout, None)
|
||||
draw.assert_called_once_with(
|
||||
context,
|
||||
layout,
|
||||
refine_selection="crop_lasso_refine_selection",
|
||||
invert="crop_lasso_invert",
|
||||
keep_original="crop_lasso_keep_original",
|
||||
)
|
||||
cutout_tool = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool"
|
||||
).CutoutTool
|
||||
self.assertEqual(
|
||||
cutout_tool.bl_icon,
|
||||
"ops.mesh.primitive_sphere_add_gizmo",
|
||||
)
|
||||
cutout_module = sys.modules[cutout_tool.__module__]
|
||||
self.assertNotIn(
|
||||
"selection_path_json",
|
||||
@@ -1922,12 +1937,10 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
"cutout_refine_selection",
|
||||
"cutout_generate_normal",
|
||||
"cutout_mesh_detail",
|
||||
"cutout_inward_axis",
|
||||
],
|
||||
)
|
||||
self.assertNotIn("draw_settings", self.image_line.ImageLineTool.__dict__)
|
||||
self.assertNotIn(
|
||||
"draw_settings", self.image_polyline.ImagePolylineTool.__dict__
|
||||
)
|
||||
self.assertIn("draw_settings", self.image_polyline.CropPolylineTool.__dict__)
|
||||
self.assertEqual(operator.__annotations__["gesture"]["default"], "BOX")
|
||||
self.assertIn("invert", operator.__annotations__)
|
||||
self.assertNotIn("continuous", operator.__annotations__)
|
||||
@@ -1935,8 +1948,8 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
settings = self.anyimage.properties.AnyImageSettings.__annotations__
|
||||
for name in (
|
||||
"cutout_refine_selection",
|
||||
"image_box_refine_selection",
|
||||
"image_lasso_refine_selection",
|
||||
"crop_box_refine_selection",
|
||||
"crop_lasso_refine_selection",
|
||||
):
|
||||
self.assertFalse(settings[name]["default"])
|
||||
self.assertNotIn("refine_selection", settings)
|
||||
@@ -1952,6 +1965,17 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
self.assertFalse(settings["cutout_generate_normal"]["default"])
|
||||
self.assertEqual(settings["cutout_inward_axis"]["default"], "NEGATIVE_Z")
|
||||
for name in (
|
||||
"crop_box_invert",
|
||||
"crop_lasso_invert",
|
||||
"crop_polyline_invert",
|
||||
"crop_box_keep_original",
|
||||
"crop_lasso_keep_original",
|
||||
"crop_polyline_keep_original",
|
||||
"crop_perspective_keep_original",
|
||||
):
|
||||
self.assertFalse(settings[name]["default"])
|
||||
self.assertEqual(
|
||||
settings["cutout_refine_selection"]["name"], "Refine Selection"
|
||||
)
|
||||
@@ -1999,14 +2023,27 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
setup = ("operator", "anyimage.setup_ai_environment", True)
|
||||
with patch.object(self.ai, "ai_status", return_value=missing):
|
||||
for tool, setting_name in (
|
||||
(self.image_box.ImageBoxTool, "image_box_refine_selection"),
|
||||
(self.image_lasso.ImageLassoTool, "image_lasso_refine_selection"),
|
||||
(self.image_box.CropBoxTool, "crop_box_refine_selection"),
|
||||
(self.image_lasso.CropLassoTool, "crop_lasso_refine_selection"),
|
||||
):
|
||||
layout = Layout()
|
||||
tool.draw_settings(context, layout, None)
|
||||
self.assertEqual(
|
||||
layout.events,
|
||||
[setup, ("prop", setting_name, False)],
|
||||
[
|
||||
setup,
|
||||
("prop", setting_name, False),
|
||||
(
|
||||
"prop",
|
||||
setting_name.replace("refine_selection", "invert"),
|
||||
True,
|
||||
),
|
||||
(
|
||||
"prop",
|
||||
setting_name.replace("refine_selection", "keep_original"),
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
cutout_tool = importlib.import_module(
|
||||
@@ -2021,18 +2058,25 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
("prop", "cutout_refine_selection", False),
|
||||
("prop", "cutout_generate_normal", False),
|
||||
("prop", "cutout_mesh_detail", True),
|
||||
("prop", "cutout_inward_axis", True),
|
||||
],
|
||||
)
|
||||
|
||||
layout = Layout()
|
||||
operator = SimpleNamespace(gesture="BOX")
|
||||
self.image_interaction.draw_selection_operator_settings(layout, operator)
|
||||
operator = SimpleNamespace(
|
||||
gesture="BOX",
|
||||
refine_selection_settings={"BOX": "crop_box_refine_selection"},
|
||||
invert_settings={"BOX": "crop_box_invert"},
|
||||
keep_original_settings={"BOX": "crop_box_keep_original"},
|
||||
)
|
||||
self.image_interaction.draw_crop_operator_settings(layout, operator)
|
||||
self.assertEqual(
|
||||
layout.events,
|
||||
[
|
||||
setup,
|
||||
("prop", "refine_selection", False),
|
||||
("prop", "invert", True),
|
||||
("prop", "keep_original", True),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -2046,34 +2090,57 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
def test_refine_selection_is_independent_for_each_area_tool(self):
|
||||
settings = SimpleNamespace(
|
||||
cutout_refine_selection=True,
|
||||
image_box_refine_selection=False,
|
||||
image_lasso_refine_selection=True,
|
||||
crop_box_refine_selection=False,
|
||||
crop_lasso_refine_selection=True,
|
||||
crop_box_invert=False,
|
||||
crop_lasso_invert=True,
|
||||
crop_polyline_invert=False,
|
||||
crop_box_keep_original=True,
|
||||
crop_lasso_keep_original=False,
|
||||
crop_polyline_keep_original=True,
|
||||
)
|
||||
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=settings))
|
||||
with patch.object(self.image_interaction, "ai_ready", return_value=True):
|
||||
for gesture in ("LINE", "POLYLINE"):
|
||||
operator = SimpleNamespace(
|
||||
gesture=gesture,
|
||||
refine_selection=True,
|
||||
)
|
||||
self.image_interaction.apply_selection_settings(operator, context)
|
||||
self.assertFalse(operator.refine_selection)
|
||||
|
||||
box = SimpleNamespace(
|
||||
gesture="BOX",
|
||||
refine_selection=True,
|
||||
refine_selection_settings={"BOX": "image_box_refine_selection"},
|
||||
invert=True,
|
||||
keep_original=False,
|
||||
refine_selection_settings={"BOX": "crop_box_refine_selection"},
|
||||
invert_settings={"BOX": "crop_box_invert"},
|
||||
keep_original_settings={"BOX": "crop_box_keep_original"},
|
||||
)
|
||||
self.image_interaction.apply_selection_settings(box, context)
|
||||
self.image_interaction.apply_crop_settings(box, context)
|
||||
self.assertFalse(box.refine_selection)
|
||||
self.assertFalse(box.invert)
|
||||
self.assertTrue(box.keep_original)
|
||||
|
||||
lasso = SimpleNamespace(
|
||||
gesture="LASSO",
|
||||
refine_selection=False,
|
||||
refine_selection_settings={"LASSO": "image_lasso_refine_selection"},
|
||||
invert=False,
|
||||
keep_original=True,
|
||||
refine_selection_settings={"LASSO": "crop_lasso_refine_selection"},
|
||||
invert_settings={"LASSO": "crop_lasso_invert"},
|
||||
keep_original_settings={"LASSO": "crop_lasso_keep_original"},
|
||||
)
|
||||
self.image_interaction.apply_selection_settings(lasso, context)
|
||||
self.image_interaction.apply_crop_settings(lasso, context)
|
||||
self.assertTrue(lasso.refine_selection)
|
||||
self.assertTrue(lasso.invert)
|
||||
self.assertFalse(lasso.keep_original)
|
||||
|
||||
polyline = SimpleNamespace(
|
||||
gesture="POLYLINE",
|
||||
refine_selection=True,
|
||||
invert=True,
|
||||
keep_original=False,
|
||||
invert_settings={"POLYLINE": "crop_polyline_invert"},
|
||||
keep_original_settings={"POLYLINE": "crop_polyline_keep_original"},
|
||||
)
|
||||
self.image_interaction.apply_crop_settings(polyline, context)
|
||||
self.assertFalse(polyline.refine_selection)
|
||||
self.assertFalse(polyline.invert)
|
||||
self.assertTrue(polyline.keep_original)
|
||||
|
||||
cutout = SimpleNamespace(
|
||||
gesture="LASSO",
|
||||
@@ -2081,9 +2148,38 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
refine_selection=False,
|
||||
refine_selection_settings={"LASSO": "cutout_refine_selection"},
|
||||
)
|
||||
self.image_interaction.apply_selection_settings(cutout, context)
|
||||
self.image_interaction.apply_crop_settings(cutout, context)
|
||||
self.assertTrue(cutout.refine_selection)
|
||||
|
||||
def test_crop_f_does_not_toggle_invert_or_appear_in_status(self):
|
||||
operator = SimpleNamespace(
|
||||
gesture="BOX",
|
||||
active_tool_id="",
|
||||
invert=True,
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
workspace=SimpleNamespace(status_text_set=Mock())
|
||||
)
|
||||
event = SimpleNamespace(type="F", value="PRESS")
|
||||
|
||||
with patch.object(
|
||||
self.image_interaction,
|
||||
"active_view3d_tool_id",
|
||||
return_value=None,
|
||||
):
|
||||
result = self.image_interaction.CropSelectionGesture.modal(
|
||||
operator,
|
||||
context,
|
||||
event,
|
||||
)
|
||||
self.image_interaction.CropSelectionGesture._set_status(operator, context)
|
||||
|
||||
self.assertEqual(result, {"PASS_THROUGH"})
|
||||
self.assertTrue(operator.invert)
|
||||
status = context.workspace.status_text_set.call_args.args[0]
|
||||
self.assertIn("Invert On", status)
|
||||
self.assertNotIn("F", status)
|
||||
|
||||
def test_image_edit_first_click_selects_another_image_without_editing(self):
|
||||
selected = []
|
||||
|
||||
@@ -2421,7 +2517,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.assertAlmostEqual(ratio, 2.0)
|
||||
|
||||
def test_image_perspective_ignores_an_already_removed_preview(self):
|
||||
operator = self.image_perspective.EditImagePerspective()
|
||||
operator = self.image_perspective.CropImagePerspective()
|
||||
operator._preview_image = object()
|
||||
|
||||
def remove(*_args, **_options):
|
||||
@@ -2442,8 +2538,8 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.assertEqual(bounds, (8.0, 18.0, 32.0, 42.0))
|
||||
|
||||
def test_image_perspective_tool_registration_and_properties(self):
|
||||
operator = self.image_perspective.EditImagePerspective
|
||||
tool = self.image_perspective.ImagePerspectiveTool
|
||||
operator = self.image_perspective.CropImagePerspective
|
||||
tool = self.image_perspective.CropPerspectiveTool
|
||||
settings = self.anyimage.properties.AnyImageSettings.__annotations__
|
||||
|
||||
self.assertIn(operator, self.anyimage.CLASSES)
|
||||
@@ -2451,7 +2547,8 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.assertEqual(tool.bl_operator, operator.bl_idname)
|
||||
self.assertEqual(tool.bl_icon, "ops.sculpt.line_project")
|
||||
self.assertNotIn("bl_cursor", tool.__dict__)
|
||||
self.assertNotIn("draw_settings", tool.__dict__)
|
||||
self.assertIn("draw_settings", tool.__dict__)
|
||||
self.assertIn("keep_original", operator.__annotations__)
|
||||
self.assertNotIn("image_perspective_aspect_mode", settings)
|
||||
self.assertNotIn("image_perspective_aspect_ratio", settings)
|
||||
self.assertNotIn("aspect_mode", operator.__annotations__)
|
||||
@@ -2500,7 +2597,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
return_value=polygon,
|
||||
),
|
||||
):
|
||||
result = self.image_interaction.ImageSelectionGesture._complete(operator, context)
|
||||
result = self.image_interaction.CropSelectionGesture._complete(operator, context)
|
||||
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
self.assertEqual(submitted[0][0], context)
|
||||
@@ -2544,7 +2641,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
) as rasterize,
|
||||
patch.object(self.edit_mask, "_edit_image_object") as edit,
|
||||
):
|
||||
result = self.edit_mask.EditImageMask._submit_selection_path(
|
||||
result = self.edit_mask.CropImageSelection._submit_selection_path(
|
||||
operator,
|
||||
SimpleNamespace(),
|
||||
selection_path,
|
||||
@@ -2553,7 +2650,13 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
read_rgba.assert_called_once_with(source.data)
|
||||
rasterize.assert_called_once()
|
||||
edit.assert_called_once_with(source, source_rgba, selection_mask)
|
||||
edit.assert_called_once_with(
|
||||
unittest.mock.ANY,
|
||||
source,
|
||||
source_rgba,
|
||||
selection_mask,
|
||||
keep_original=False,
|
||||
)
|
||||
|
||||
def test_image_edit_applies_the_local_selection_mask(self):
|
||||
import numpy as np
|
||||
@@ -2597,7 +2700,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
)
|
||||
refine = Mock(return_value={"FINISHED"})
|
||||
ops = SimpleNamespace(
|
||||
anyimage=SimpleNamespace(refine_image_selection=refine)
|
||||
anyimage=SimpleNamespace(refine_crop_selection=refine)
|
||||
)
|
||||
|
||||
with (
|
||||
@@ -2624,7 +2727,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
) as prepare_input,
|
||||
patch.object(self.edit_mask.bpy, "ops", ops, create=True),
|
||||
):
|
||||
result = self.edit_mask.EditImageMask._submit_selection_path(
|
||||
result = self.edit_mask.CropImageSelection._submit_selection_path(
|
||||
operator,
|
||||
SimpleNamespace(),
|
||||
selection_path,
|
||||
@@ -2655,7 +2758,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
"selection_image": Path(__file__),
|
||||
}
|
||||
result = SimpleNamespace(file=lambda key: files[key])
|
||||
operator = self.edit_mask.RefineImageSelection()
|
||||
operator = self.edit_mask.RefineCropSelection()
|
||||
operator.source_object_name = "Source"
|
||||
operator.selection_bounds_json = "[10,20,18,26]"
|
||||
ops = SimpleNamespace(
|
||||
@@ -2673,7 +2776,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
"load_packed_rgba_image",
|
||||
return_value=refined,
|
||||
) as load_result,
|
||||
patch.object(self.edit_mask, "replace_empty_image") as replace,
|
||||
patch.object(self.edit_mask, "place_empty_image_result") as place,
|
||||
patch.object(self.edit_mask, "image_rgba") as read_rgba,
|
||||
patch.object(
|
||||
self.edit_mask,
|
||||
@@ -2683,16 +2786,18 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
):
|
||||
message = operator.response(None, result)
|
||||
|
||||
self.assertEqual(message, "Image Selection refined")
|
||||
self.assertEqual(message, "Crop Selection refined")
|
||||
load_result.assert_called_once_with(
|
||||
Path(__file__),
|
||||
source_image,
|
||||
"Poster",
|
||||
)
|
||||
replace.assert_called_once_with(
|
||||
place.assert_called_once_with(
|
||||
None,
|
||||
source,
|
||||
refined,
|
||||
placement_bounds=(12, 21, 15, 24),
|
||||
keep_original=False,
|
||||
)
|
||||
read_rgba.assert_not_called()
|
||||
create_pixels.assert_not_called()
|
||||
@@ -2832,8 +2937,8 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
patch.object(self.image_interaction, "draw_polygon_fill") as draw_fill,
|
||||
patch.object(self.image_interaction, "draw_dashed_line") as draw_outline,
|
||||
):
|
||||
self.image_interaction.ImageSelectionGesture._draw_selection(operator)
|
||||
self.image_interaction.ImageSelectionGesture._draw_selection(operator)
|
||||
self.image_interaction.CropSelectionGesture._draw_selection(operator)
|
||||
self.image_interaction.CropSelectionGesture._draw_selection(operator)
|
||||
|
||||
build_fill.assert_called_once_with(polygon)
|
||||
build_outline.assert_called_once_with((*polygon, polygon[0]))
|
||||
@@ -2867,7 +2972,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
try:
|
||||
result = self.edit_mask.ActivateImageTool().execute(None)
|
||||
result = self.edit_mask.ActivateCropTool().execute(None)
|
||||
finally:
|
||||
if previous_ops is None:
|
||||
del self.fake_bpy.ops
|
||||
@@ -2877,25 +2982,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[{"name": "anyimage.image_box", "space_type": "VIEW_3D"}],
|
||||
)
|
||||
|
||||
def test_image_line_snaps_and_keeps_the_previewed_half_plane(self):
|
||||
import math
|
||||
|
||||
endpoint = self.image_interaction.snapped_line_endpoint((0, 0), (10, 1), True)
|
||||
self.assertAlmostEqual(math.degrees(math.atan2(endpoint[1], endpoint[0])), 5.0)
|
||||
preview = self.image_interaction.line_half_plane_polygon((0, 5), (10, 5), (10, 10))
|
||||
image = self.image_interaction.image_line_half_plane_polygon(
|
||||
((0, 5), (10, 5)), (10, 10)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_selection.selection_path_bounds(preview),
|
||||
(0, 5, 10, 10),
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_selection.selection_path_bounds(image),
|
||||
(0, 0, 10, 5),
|
||||
[{"name": "anyimage.crop_box", "space_type": "VIEW_3D"}],
|
||||
)
|
||||
|
||||
def test_image_operators_stay_clickable_until_server_is_busy(self):
|
||||
@@ -3043,47 +3130,36 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.registered_tools,
|
||||
[
|
||||
(
|
||||
self.image_box.ImageBoxTool,
|
||||
self.image_box.CropBoxTool,
|
||||
{
|
||||
"group": True,
|
||||
"separator": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
self.image_lasso.ImageLassoTool,
|
||||
self.image_lasso.CropLassoTool,
|
||||
{
|
||||
"after": {self.image_box.ImageBoxTool.bl_idname},
|
||||
"after": {self.image_box.CropBoxTool.bl_idname},
|
||||
"separator": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
self.image_line.ImageLineTool,
|
||||
self.image_polyline.CropPolylineTool,
|
||||
{
|
||||
"after": {self.image_lasso.ImageLassoTool.bl_idname},
|
||||
"after": {self.image_lasso.CropLassoTool.bl_idname},
|
||||
"separator": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
self.image_polyline.ImagePolylineTool,
|
||||
{
|
||||
"after": {self.image_line.ImageLineTool.bl_idname},
|
||||
"separator": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
self.image_perspective.ImagePerspectiveTool,
|
||||
self.image_perspective.CropPerspectiveTool,
|
||||
{
|
||||
"after": {self.image_polyline.CropPolylineTool.bl_idname},
|
||||
"separator": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
self.anyimage.CutoutTool,
|
||||
{
|
||||
"after": {
|
||||
self.image_perspective.ImagePerspectiveTool.bl_idname
|
||||
},
|
||||
"separator": True,
|
||||
},
|
||||
{},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -7,7 +7,8 @@ SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common.selection import SelectionPath
|
||||
from anyimage.common.viewport import ImageSelectionGesture
|
||||
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
|
||||
|
||||
@@ -31,10 +32,16 @@ class WindowManager:
|
||||
draw(menu, None)
|
||||
|
||||
|
||||
def cutout_context(window_manager, mesh_detail="LOW", generate_normal=False):
|
||||
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,
|
||||
@@ -75,11 +82,17 @@ def test_cutout_pie_uses_two_local_shapes_without_ai(monkeypatch):
|
||||
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)
|
||||
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 = []
|
||||
@@ -122,6 +135,7 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
|
||||
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():
|
||||
@@ -155,7 +169,7 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
|
||||
)
|
||||
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_selection_settings", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout_main, "apply_crop_settings", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout_main, "serialize_matrix", lambda _matrix: "matrix")
|
||||
monkeypatch.setattr(
|
||||
cutout_main,
|
||||
@@ -184,7 +198,7 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
|
||||
operator, context, SimpleNamespace(value="DOUBLE_CLICK")
|
||||
)
|
||||
|
||||
assert issubclass(cutout_main.SelectCutoutSelection, ImageSelectionGesture)
|
||||
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 == (
|
||||
@@ -200,3 +214,36 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
|
||||
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]
|
||||
|
||||
@@ -60,6 +60,7 @@ def _create_cutout_shape(
|
||||
depth_image=None,
|
||||
depth_metadata=None,
|
||||
selection_values=None,
|
||||
inward_axis=cutout_object.CutoutInwardAxis.POSITIVE_X,
|
||||
):
|
||||
if bounds is None:
|
||||
bounds = (0, 0, *tuple(int(value) for value in source.data.size))
|
||||
@@ -90,6 +91,7 @@ def _create_cutout_shape(
|
||||
base_shape,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
inward_axis=inward_axis,
|
||||
color_image=color_image,
|
||||
)
|
||||
|
||||
@@ -123,6 +125,101 @@ def _depth_metadata(intrinsics, image_size, reference_depth):
|
||||
}
|
||||
|
||||
|
||||
def test_keep_original_places_and_activates_result_copy():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Crop Source", width=8, height=4, alpha=True)
|
||||
result_image = bpy.data.images.new("Crop Result", width=4, height=2, alpha=True)
|
||||
source = bpy.data.objects.new("Crop Source", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = source_image
|
||||
source.empty_display_size = 3.0
|
||||
source.empty_image_offset = (0.2, -0.1)
|
||||
source.matrix_world = Matrix.Translation((1.0, 2.0, 3.0))
|
||||
bpy.context.collection.objects.link(source)
|
||||
source.select_set(True)
|
||||
bpy.context.view_layer.objects.active = source
|
||||
|
||||
placed = image_data.place_empty_image_result(
|
||||
bpy.context,
|
||||
source,
|
||||
result_image,
|
||||
placement_bounds=(2, 1, 6, 3),
|
||||
keep_original=True,
|
||||
)
|
||||
|
||||
assert placed is not source
|
||||
assert source.data == source_image
|
||||
assert placed.data == result_image
|
||||
assert placed.users_collection[:] == source.users_collection[:]
|
||||
assert np.allclose(placed.matrix_world, source.matrix_world)
|
||||
assert not source.select_get()
|
||||
assert placed.select_get()
|
||||
assert bpy.context.view_layer.objects.active == placed
|
||||
|
||||
|
||||
def test_object_normal_image_follows_cutout_inward_axis():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new(
|
||||
"Object Normal",
|
||||
width=1,
|
||||
height=1,
|
||||
alpha=True,
|
||||
float_buffer=True,
|
||||
)
|
||||
decoded = np.asarray((0.2, -0.4, 0.8), dtype=np.float32)
|
||||
encoded = decoded * 0.5 + 0.5
|
||||
image.pixels.foreach_set((*encoded, 0.3))
|
||||
|
||||
cutout_object.orient_object_normal_image(
|
||||
image,
|
||||
cutout_object.CutoutInwardAxis.NEGATIVE_Z,
|
||||
)
|
||||
|
||||
actual = np.empty(4, dtype=np.float32)
|
||||
image.pixels.foreach_get(actual)
|
||||
expected = np.asarray((0.4, 0.8, -0.2), dtype=np.float32) * 0.5 + 0.5
|
||||
assert np.allclose(actual[:3], expected, atol=1e-6)
|
||||
assert np.isclose(actual[3], 0.3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", ("SURFACE", "BALLOON"))
|
||||
def test_cutout_axes_produce_the_same_world_geometry(shape):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Axis Source", width=32, height=16, alpha=True)
|
||||
source = bpy.data.objects.new("Axis Source", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
source.empty_display_size = 2.0
|
||||
source.matrix_world = (
|
||||
Matrix.Translation((2.0, -3.0, 1.0))
|
||||
@ Matrix.Rotation(0.35, 4, "Y")
|
||||
@ Matrix.Diagonal((1.5, 0.75, 2.0, 1.0))
|
||||
)
|
||||
bpy.context.collection.objects.link(source)
|
||||
|
||||
positive_x = _create_cutout_shape(
|
||||
source,
|
||||
shape,
|
||||
8,
|
||||
inward_axis=cutout_object.CutoutInwardAxis.POSITIVE_X,
|
||||
)
|
||||
negative_z = _create_cutout_shape(
|
||||
source,
|
||||
shape,
|
||||
8,
|
||||
inward_axis=cutout_object.CutoutInwardAxis.NEGATIVE_Z,
|
||||
)
|
||||
|
||||
def world_positions(obj):
|
||||
local = _evaluated_positions(obj)
|
||||
return np.asarray([tuple(obj.matrix_world @ Vector(point)) for point in local])
|
||||
|
||||
expected = np.asarray(sorted(map(tuple, world_positions(positive_x))))
|
||||
actual = np.asarray(sorted(map(tuple, world_positions(negative_z))))
|
||||
assert np.allclose(actual, expected, atol=2e-6)
|
||||
assert np.allclose(negative_z.matrix_world, source.matrix_world)
|
||||
|
||||
|
||||
def test_depth_result_image_loads_server_vector_exr_and_keeps_data_name(tmp_path):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Source.png", width=2, height=2)
|
||||
@@ -202,6 +299,7 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
assert _inputs(cutout) == [
|
||||
"Geometry",
|
||||
"Shape",
|
||||
"Inward Axis",
|
||||
"Thickness",
|
||||
"Depth Scale",
|
||||
"Double Sided",
|
||||
@@ -1024,6 +1122,11 @@ def _add_cutout_modifier(obj, shape):
|
||||
object_data.modifier_input_identifier(group, "Shape"),
|
||||
cutout_shape.CUTOUT_SHAPE_INDICES[shape],
|
||||
)
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(group, "Inward Axis"),
|
||||
1,
|
||||
)
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(group, "Cleanup"),
|
||||
|
||||
@@ -77,14 +77,12 @@ class ExtensionPackagingTest(unittest.TestCase):
|
||||
"operators/convert_to_plane/object.py",
|
||||
"operators/convert_to_plane/conversion.py",
|
||||
"operators/convert_to_plane/mesh_plane.py",
|
||||
"operators/image_tool/__init__.py",
|
||||
"operators/image_tool/edit_mask.py",
|
||||
"operators/image_tool/box.py",
|
||||
"operators/image_tool/lasso.py",
|
||||
"operators/image_tool/line.py",
|
||||
"operators/image_tool/polyline.py",
|
||||
"operators/perspective_tool/__init__.py",
|
||||
"operators/perspective_tool/edit_perspective.py",
|
||||
"operators/crop_tool/__init__.py",
|
||||
"operators/crop_tool/selection.py",
|
||||
"operators/crop_tool/box.py",
|
||||
"operators/crop_tool/lasso.py",
|
||||
"operators/crop_tool/polyline.py",
|
||||
"operators/crop_tool/perspective.py",
|
||||
"operators/ai_setup.py",
|
||||
"operators/remove_background.py",
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user