feat: Update version to 0.13.0 in project files and adjust dependencies

This commit is contained in:
2026-09-02 19:50:55 +08:00
parent 8d279e9ab3
commit a6041cbe03
47 changed files with 1204 additions and 587 deletions

View File

@@ -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,
},
{},
),
],
)

View File

@@ -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]

View File

@@ -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"),

View File

@@ -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",
):