- Implement tests for image layer functionality including alpha curve and depth layer behavior. - Create tests for image plane evaluation and modifier input handling. - Add tests to verify the saved asset inventory and its persistence. - Introduce tests for the documentation command to ensure correct project version deployment. - Validate the extension manifest structure and required permissions. - Develop tests for the packaging process, ensuring correct platform handling and dependency management. - Enhance image depth plane group functionality with new split threshold input and surface processing.
1090 lines
37 KiB
Python
1090 lines
37 KiB
Python
import sys
|
|
from types import ModuleType, SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
from tests.support.blender import BlenderTestCase
|
|
|
|
|
|
class MaskToolTest(BlenderTestCase):
|
|
def test_selection_mask_keeps_nonzero_winding_overlap(self):
|
|
path = (
|
|
(1, 1),
|
|
(7, 1),
|
|
(7, 7),
|
|
(1, 7),
|
|
(1, 1),
|
|
(4, 1),
|
|
(10, 1),
|
|
(10, 7),
|
|
(4, 7),
|
|
(4, 1),
|
|
(1, 1),
|
|
)
|
|
|
|
selection = self.image_selection.rasterize_selection_path(
|
|
(12, 9),
|
|
self.image_selection.SelectionPath(points=path),
|
|
).full_values((12, 9))
|
|
|
|
self.assertTrue(selection[3, 5])
|
|
|
|
triangles = self.image_interaction.preview_fill_geometry(path)[0]
|
|
preview_area = sum(
|
|
abs(
|
|
self.image_interaction._triangle_cross(
|
|
triangles[index],
|
|
triangles[index + 1],
|
|
triangles[index + 2],
|
|
)
|
|
)
|
|
* 0.5
|
|
for index in range(0, len(triangles), 3)
|
|
)
|
|
|
|
self.assertAlmostEqual(preview_area, 54.0)
|
|
|
|
|
|
def test_mask_freezes_scene_settings_on_invoke(self):
|
|
settings = SimpleNamespace(
|
|
mask_gesture="BRUSH",
|
|
mask_mode="ADD",
|
|
mask_radius=64,
|
|
)
|
|
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=settings))
|
|
operator = SimpleNamespace(gesture="LASSO", mode="SET", radius=25)
|
|
|
|
self.edit_mask.EditImageAlpha._read_gesture_settings(operator, context)
|
|
|
|
self.assertEqual(operator.gesture, "BRUSH")
|
|
self.assertEqual(operator.mode, "ADD")
|
|
self.assertEqual(operator.radius, 64)
|
|
|
|
|
|
def test_mask_has_no_letter_option_shortcut(self):
|
|
operator = SimpleNamespace(
|
|
gesture="LASSO",
|
|
mode="SUBTRACT",
|
|
radius=25,
|
|
active_tool_id="anyimage.mask",
|
|
)
|
|
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.ImageGesture.modal(
|
|
operator,
|
|
context,
|
|
event,
|
|
)
|
|
self.edit_mask.EditImageAlpha._set_status(operator, context)
|
|
|
|
self.assertEqual(result, {"PASS_THROUGH"})
|
|
status = context.workspace.status_text_set.call_args.args[0]
|
|
self.assertIn("Subtract", status)
|
|
self.assertNotIn("F", status)
|
|
|
|
|
|
def test_mask_polyline_status_explains_point_controls(self):
|
|
context = SimpleNamespace(
|
|
workspace=SimpleNamespace(status_text_set=Mock())
|
|
)
|
|
operator = SimpleNamespace(gesture="POLYLINE", mode="ADD", radius=25)
|
|
|
|
self.edit_mask.EditImageAlpha._set_status(operator, context)
|
|
|
|
status = context.workspace.status_text_set.call_args.args[0]
|
|
self.assertIn("click start, double-click, or Enter", status)
|
|
self.assertIn("Backspace removes", status)
|
|
self.assertIn("Add", status)
|
|
|
|
|
|
def test_mask_and_rectify_start_after_an_empty_pick(self):
|
|
source = SimpleNamespace(
|
|
name="Source",
|
|
data=SimpleNamespace(size=(8, 6)),
|
|
matrix_world="matrix",
|
|
)
|
|
area = SimpleNamespace(as_pointer=lambda: 1, tag_redraw=Mock())
|
|
region = SimpleNamespace(as_pointer=lambda: 2, width=100, height=80)
|
|
window_manager = SimpleNamespace(
|
|
modal_handler_add=Mock(),
|
|
event_timer_add=Mock(return_value="timer"),
|
|
)
|
|
context = SimpleNamespace(
|
|
area=area,
|
|
region=region,
|
|
window=object(),
|
|
window_manager=window_manager,
|
|
workspace=SimpleNamespace(status_text_set=Mock()),
|
|
scene=SimpleNamespace(
|
|
anyimage_settings=SimpleNamespace(
|
|
mask_gesture="LASSO",
|
|
mask_mode="SUBTRACT",
|
|
mask_radius=25,
|
|
)
|
|
),
|
|
)
|
|
event = SimpleNamespace(
|
|
mouse_region_x=12,
|
|
mouse_region_y=18,
|
|
value="PRESS",
|
|
)
|
|
previous_space = getattr(self.fake_bpy.types, "SpaceView3D", None)
|
|
self.fake_bpy.types.SpaceView3D = SimpleNamespace(
|
|
draw_handler_add=Mock(return_value="handler")
|
|
)
|
|
try:
|
|
with (
|
|
patch.object(
|
|
self.image_interaction,
|
|
"resolve_image_edit_click",
|
|
return_value=(source, True),
|
|
),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"video_total_frames",
|
|
return_value=1,
|
|
),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"serialize_matrix",
|
|
return_value="serialized",
|
|
),
|
|
):
|
|
mask = self.edit_mask.EditImageAlpha()
|
|
mask.report = Mock()
|
|
self.assertEqual(mask.invoke(context, event), {"RUNNING_MODAL"})
|
|
self.assertEqual(mask._path, [(12.0, 18.0)])
|
|
|
|
with (
|
|
patch.object(
|
|
self.rectify,
|
|
"resolve_image_edit_click",
|
|
return_value=(source, True),
|
|
),
|
|
patch.object(
|
|
self.rectify,
|
|
"video_total_frames",
|
|
return_value=1,
|
|
),
|
|
patch.object(
|
|
self.rectify,
|
|
"serialize_matrix",
|
|
return_value="serialized",
|
|
),
|
|
):
|
|
rectify = self.rectify.RectifyImagePerspective()
|
|
rectify.report = Mock()
|
|
self.assertEqual(rectify.invoke(context, event), {"RUNNING_MODAL"})
|
|
self.assertEqual(rectify._points, [(12.0, 18.0)])
|
|
finally:
|
|
if previous_space is None:
|
|
del self.fake_bpy.types.SpaceView3D
|
|
else:
|
|
self.fake_bpy.types.SpaceView3D = previous_space
|
|
|
|
|
|
def test_mask_and_rectify_cancel_without_an_active_image(self):
|
|
context = SimpleNamespace(
|
|
scene=SimpleNamespace(
|
|
anyimage_settings=SimpleNamespace(
|
|
mask_gesture="LASSO",
|
|
mask_mode="SUBTRACT",
|
|
mask_radius=25,
|
|
)
|
|
)
|
|
)
|
|
event = SimpleNamespace()
|
|
|
|
with patch.object(
|
|
self.image_interaction,
|
|
"resolve_image_edit_click",
|
|
return_value=(None, False),
|
|
):
|
|
mask = self.edit_mask.EditImageAlpha()
|
|
mask.report = Mock()
|
|
self.assertEqual(mask.invoke(context, event), {"CANCELLED"})
|
|
with patch.object(
|
|
self.rectify,
|
|
"resolve_image_edit_click",
|
|
return_value=(None, False),
|
|
):
|
|
rectify = self.rectify.RectifyImagePerspective()
|
|
rectify.report = Mock()
|
|
self.assertEqual(rectify.invoke(context, event), {"CANCELLED"})
|
|
|
|
|
|
def test_image_mask_fill_triangulates_a_concave_polygon_without_overlap(self):
|
|
polygon = (
|
|
(0, 0),
|
|
(4, 0),
|
|
(4, 4),
|
|
(3, 4),
|
|
(3, 1),
|
|
(1, 1),
|
|
(1, 4),
|
|
(0, 4),
|
|
)
|
|
triangles = self.image_interaction.triangulated_polygon_vertices(polygon)
|
|
area = sum(
|
|
abs(
|
|
self.image_interaction._triangle_cross(
|
|
triangles[index],
|
|
triangles[index + 1],
|
|
triangles[index + 2],
|
|
)
|
|
)
|
|
* 0.5
|
|
for index in range(0, len(triangles), 3)
|
|
)
|
|
|
|
self.assertEqual(len(triangles), 18)
|
|
self.assertAlmostEqual(area, 10.0)
|
|
|
|
|
|
def test_image_mask_fill_uses_blender_geometry_without_stopping_the_draw(self):
|
|
mathutils = ModuleType("mathutils")
|
|
mathutils.Vector = lambda values: values
|
|
mathutils.geometry = SimpleNamespace(
|
|
tessellate_polygon=lambda _loops: (
|
|
((0.0, 0.0, 0.0), (2.0, 0.0, 0.0), (0.0, 2.0, 0.0)),
|
|
)
|
|
)
|
|
with patch.dict(sys.modules, {"mathutils": mathutils}):
|
|
triangles = self.image_interaction.triangulated_polygon_vertices(
|
|
((0, 0), (2, 0), (0, 2))
|
|
)
|
|
|
|
self.assertEqual(
|
|
triangles,
|
|
((0.0, 0.0), (2.0, 0.0), (0.0, 2.0)),
|
|
)
|
|
|
|
|
|
def test_image_mask_fill_falls_back_when_blender_tessellation_rejects_path(self):
|
|
mathutils = ModuleType("mathutils")
|
|
mathutils.Vector = lambda values: values
|
|
mathutils.geometry = SimpleNamespace(
|
|
tessellate_polygon=lambda _loops: (_ for _ in ()).throw(TypeError())
|
|
)
|
|
with patch.dict(sys.modules, {"mathutils": mathutils}):
|
|
triangles = self.image_interaction.triangulated_polygon_vertices(
|
|
((0, 0), (2, 0), (0, 2))
|
|
)
|
|
|
|
self.assertEqual(len(triangles), 3)
|
|
|
|
|
|
def test_image_mask_preview_fills_a_self_intersecting_lasso(self):
|
|
polygon = ((0, 0), (4, 4), (0, 4), (4, 0))
|
|
triangles = self.image_interaction.preview_fill_geometry(polygon)[0]
|
|
area = sum(
|
|
abs(
|
|
self.image_interaction._triangle_cross(
|
|
triangles[index],
|
|
triangles[index + 1],
|
|
triangles[index + 2],
|
|
)
|
|
)
|
|
* 0.5
|
|
for index in range(0, len(triangles), 3)
|
|
)
|
|
|
|
self.assertTrue(triangles)
|
|
self.assertAlmostEqual(area, 8.0)
|
|
|
|
|
|
def test_image_mask_preview_detects_only_actual_self_intersections(self):
|
|
self.assertTrue(
|
|
self.image_interaction.polygon_self_intersects(
|
|
((0, 0), (4, 4), (0, 4), (4, 0))
|
|
)
|
|
)
|
|
self.assertFalse(
|
|
self.image_interaction.polygon_self_intersects(
|
|
((0, 0), (4, 0), (2, 2), (4, 4), (0, 4))
|
|
)
|
|
)
|
|
|
|
|
|
def test_mask_alpha_modes_preserve_rgb_and_canvas(self):
|
|
import numpy as np
|
|
|
|
source = np.zeros((2, 3, 4), dtype=np.float32)
|
|
source[:, :, :3] = np.asarray((0.2, 0.4, 0.6), dtype=np.float32)
|
|
source[:, :, 3] = np.asarray(((0.2, 0.5, 0.8), (1.0, 0.4, 0.0)))
|
|
mask = self.image_selection.SelectionMask(
|
|
np.asarray(((0.25, 1.0), (0.5, 0.75)), dtype=np.float32),
|
|
(1, 0, 3, 2),
|
|
)
|
|
full_mask = mask.full_values((3, 2))
|
|
expected_alpha = {
|
|
"SET": source[:, :, 3] * full_mask,
|
|
"ADD": np.maximum(source[:, :, 3], full_mask),
|
|
"SUBTRACT": source[:, :, 3] * (1.0 - full_mask),
|
|
}
|
|
|
|
for mode, expected in expected_alpha.items():
|
|
with self.subTest(mode=mode):
|
|
pixels = self.edit_mask.apply_alpha_mask(source, mask, mode)
|
|
result = np.flipud(pixels.reshape(source.shape))
|
|
np.testing.assert_allclose(result[:, :, :3], source[:, :, :3])
|
|
np.testing.assert_allclose(result[:, :, 3], expected)
|
|
|
|
|
|
def test_mask_alpha_can_be_removed_and_added_again(self):
|
|
import numpy as np
|
|
|
|
source = np.ones((2, 2, 4), dtype=np.float32)
|
|
mask = self.image_selection.SelectionMask(
|
|
np.ones((2, 2), dtype=np.float32),
|
|
(0, 0, 2, 2),
|
|
)
|
|
removed = self.edit_mask.apply_alpha_mask(source, mask, "SUBTRACT")
|
|
transparent = np.flipud(removed.reshape(source.shape))
|
|
self.assertFalse(np.any(transparent[:, :, 3]))
|
|
|
|
added = self.edit_mask.apply_alpha_mask(transparent, mask, "ADD")
|
|
restored = np.flipud(added.reshape(source.shape))
|
|
np.testing.assert_array_equal(restored[:, :, 3], 1.0)
|
|
|
|
|
|
def test_mask_brush_stroke_is_circular_and_continuous(self):
|
|
import numpy as np
|
|
|
|
single = self.edit_mask.rasterize_brush_path(
|
|
(120, 50),
|
|
((20, 25),),
|
|
10,
|
|
lambda path: path,
|
|
)
|
|
stroke = self.edit_mask.rasterize_brush_path(
|
|
(120, 50),
|
|
((20, 25), (100, 25)),
|
|
10,
|
|
lambda path: path,
|
|
)
|
|
single_values = single.full_values((120, 50))
|
|
stroke_values = stroke.full_values((120, 50))
|
|
|
|
self.assertGreater(single_values[25, 20], 0.9)
|
|
self.assertEqual(single_values[0, 0], 0.0)
|
|
self.assertTrue(np.all(stroke_values[25, 20:101] > 0.9))
|
|
|
|
|
|
def test_mask_brush_clips_bounds_and_projects_screen_radius(self):
|
|
clipped = self.edit_mask.rasterize_brush_path(
|
|
(80, 40),
|
|
((0, 0),),
|
|
10,
|
|
lambda path: path,
|
|
)
|
|
projected = self.edit_mask.rasterize_brush_path(
|
|
(160, 80),
|
|
((30, 30),),
|
|
10,
|
|
lambda path: tuple((x * 2.0, y * 0.5) for x, y in path),
|
|
)
|
|
|
|
self.assertEqual(clipped.bounds[:2], (0, 0))
|
|
projected_width = projected.bounds[2] - projected.bounds[0]
|
|
projected_height = projected.bounds[3] - projected.bounds[1]
|
|
self.assertGreater(projected_width, projected_height * 3)
|
|
|
|
|
|
def test_mask_brush_commit_uses_the_shared_viewport_footprint(self):
|
|
import numpy as np
|
|
|
|
polygon = ((1.0, 2.0), (8.0, 2.0), (8.0, 9.0))
|
|
local_mask = self.image_selection.SelectionMask(
|
|
np.ones((2, 2), dtype=np.float32),
|
|
(1, 1, 3, 3),
|
|
)
|
|
|
|
with (
|
|
patch.object(
|
|
self.edit_mask,
|
|
"brush_footprint_polygons",
|
|
return_value=(polygon,),
|
|
) as footprint,
|
|
patch.object(
|
|
self.edit_mask,
|
|
"rasterize_selection_union",
|
|
return_value=local_mask,
|
|
) as rasterize,
|
|
):
|
|
result = self.edit_mask.rasterize_brush_path(
|
|
(20, 20),
|
|
((4.0, 5.0), (7.0, 8.0)),
|
|
6,
|
|
lambda path: tuple((x * 2.0, y * 3.0) for x, y in path),
|
|
)
|
|
|
|
self.assertEqual(result.bounds, local_mask.bounds)
|
|
np.testing.assert_array_equal(result.values, local_mask.values)
|
|
footprint.assert_called_once_with(((4.0, 5.0), (7.0, 8.0)), 6)
|
|
selection_path = rasterize.call_args.args[1][0]
|
|
self.assertEqual(
|
|
selection_path.points,
|
|
((2.0, 6.0), (16.0, 6.0), (16.0, 27.0)),
|
|
)
|
|
self.assertTrue(rasterize.call_args.kwargs["antialias"])
|
|
|
|
|
|
def test_mask_brush_round_turn_stays_inside_the_radius(self):
|
|
polygons = self.image_interaction.brush_footprint_polygons(
|
|
((10.0, 10.0), (30.0, 10.0), (15.0, 11.0)),
|
|
5.0,
|
|
)
|
|
bands = self.image_interaction.scanline_union_bands(polygons)
|
|
center_band = next(
|
|
band for band in bands if band[0] <= 10.0 < band[1]
|
|
)
|
|
|
|
self.assertTrue(
|
|
all(len(intervals) == 1 for _bottom, _top, intervals in bands if intervals)
|
|
)
|
|
self.assertEqual(len(center_band[2]), 1)
|
|
sample_y = (center_band[0] + center_band[1]) / 2
|
|
half_width = (25 - (sample_y - 10) ** 2) ** 0.5
|
|
self.assertAlmostEqual(center_band[2][0][0], 10 - half_width, delta=0.03)
|
|
self.assertAlmostEqual(center_band[2][0][1], 30 + half_width, delta=0.03)
|
|
|
|
|
|
def test_mask_brush_self_intersection_uses_non_zero_winding(self):
|
|
selection_mask = self.edit_mask.rasterize_brush_path(
|
|
(80, 80),
|
|
((15, 15), (65, 65), (15, 65), (65, 15)),
|
|
6,
|
|
lambda path: path,
|
|
)
|
|
|
|
values = selection_mask.full_values((80, 80))
|
|
self.assertGreater(values[40, 40], 0.9)
|
|
|
|
|
|
def test_mask_brush_path_builds_one_coverage_after_release(self):
|
|
import numpy as np
|
|
|
|
selection_mask = self.edit_mask.rasterize_brush_path(
|
|
(80, 40),
|
|
((10, 20), (70, 20)),
|
|
6,
|
|
lambda path: path,
|
|
)
|
|
|
|
full = selection_mask.full_values((80, 40))
|
|
self.assertTrue(np.all(full[20, 10:71] > 0.9))
|
|
self.assertLess(selection_mask.bounds[0], 10)
|
|
self.assertGreater(selection_mask.bounds[2], 70)
|
|
|
|
|
|
def test_mask_brush_completion_projects_and_applies_once(self):
|
|
source = SimpleNamespace(data=SimpleNamespace(size=(8, 6)))
|
|
selection_mask = object()
|
|
operator = SimpleNamespace(
|
|
_path=[(10.0, 20.0), (30.0, 40.0)],
|
|
radius=12,
|
|
_apply_mask=Mock(return_value={"FINISHED"}),
|
|
)
|
|
context = SimpleNamespace(region="region", region_data="region_data")
|
|
|
|
with (
|
|
patch.object(
|
|
self.edit_mask,
|
|
"image_empty_bounds",
|
|
return_value="bounds",
|
|
),
|
|
patch.object(
|
|
self.edit_mask,
|
|
"rasterize_brush_path",
|
|
return_value=selection_mask,
|
|
) as rasterize,
|
|
patch.object(
|
|
self.edit_mask,
|
|
"screen_path_to_image_pixels",
|
|
return_value="projected",
|
|
) as project,
|
|
):
|
|
result = self.edit_mask.EditImageAlpha._complete_brush(
|
|
operator,
|
|
context,
|
|
source,
|
|
"matrix",
|
|
)
|
|
path_projector = rasterize.call_args.args[3]
|
|
self.assertEqual(path_projector(((1.0, 2.0),)), "projected")
|
|
|
|
self.assertEqual(result, {"FINISHED"})
|
|
operator._apply_mask.assert_called_once_with(context, source, selection_mask)
|
|
project.assert_called_once_with(
|
|
((1.0, 2.0),),
|
|
"region",
|
|
"region_data",
|
|
"matrix",
|
|
"bounds",
|
|
(8, 6),
|
|
simplify=False,
|
|
)
|
|
|
|
|
|
def test_mask_brush_drag_only_updates_the_screen_path(self):
|
|
class Operator(self.image_interaction.ImageGesture):
|
|
active_tool_id = "anyimage.mask"
|
|
gesture = "BRUSH"
|
|
|
|
operator = Operator()
|
|
operator._path = [(10.0, 10.0)]
|
|
operator._path_samples = list(operator._path)
|
|
operator._cursor = (10.0, 10.0)
|
|
context = SimpleNamespace(area=SimpleNamespace(tag_redraw=Mock()))
|
|
event = SimpleNamespace(
|
|
type="MOUSEMOVE",
|
|
value="NOTHING",
|
|
mouse_region_x=30,
|
|
mouse_region_y=25,
|
|
)
|
|
|
|
with (
|
|
patch.object(
|
|
self.image_interaction,
|
|
"active_view3d_tool_id",
|
|
return_value=None,
|
|
),
|
|
patch.object(self.edit_mask, "rasterize_brush_path") as rasterize,
|
|
patch.object(self.edit_mask, "image_rgba") as read_pixels,
|
|
):
|
|
result = operator.modal(context, event)
|
|
|
|
self.assertEqual(result, {"RUNNING_MODAL"})
|
|
self.assertEqual(operator._path, [(10.0, 10.0), (30.0, 25.0)])
|
|
rasterize.assert_not_called()
|
|
read_pixels.assert_not_called()
|
|
|
|
|
|
def test_mask_replaces_with_a_same_sized_image(self):
|
|
import numpy as np
|
|
|
|
source_image = SimpleNamespace(size=(8, 6))
|
|
source = SimpleNamespace(data=source_image)
|
|
result_image = SimpleNamespace(users=0)
|
|
mask = self.image_selection.SelectionMask(
|
|
np.ones((2, 3), dtype=np.float32),
|
|
(1, 2, 4, 4),
|
|
)
|
|
operator = SimpleNamespace(mode="SET", report=Mock())
|
|
source_rgba = np.ones((6, 8, 4), dtype=np.float32)
|
|
|
|
with (
|
|
patch.object(self.edit_mask, "image_rgba", return_value=source_rgba),
|
|
patch.object(
|
|
self.edit_mask,
|
|
"create_image_edit_result",
|
|
return_value=result_image,
|
|
) as create,
|
|
patch.object(self.edit_mask, "replace_empty_image") as replace,
|
|
):
|
|
result = self.edit_mask.EditImageAlpha._apply_mask(
|
|
operator,
|
|
SimpleNamespace(),
|
|
source,
|
|
mask,
|
|
)
|
|
|
|
self.assertEqual(result, {"FINISHED"})
|
|
self.assertEqual(create.call_args.args[2], (8, 6))
|
|
replace.assert_called_once_with(source, result_image)
|
|
|
|
|
|
def test_image_selection_uses_white_dashed_segments(self):
|
|
segments = self.image_interaction.dashed_line_segments(
|
|
((0, 0), (10, 0)), dash_length=3.0, gap_length=2.0
|
|
)
|
|
|
|
self.assertEqual(
|
|
segments,
|
|
((0.0, 0.0), (3.0, 0.0), (5.0, 0.0), (8.0, 0.0)),
|
|
)
|
|
|
|
|
|
def test_merged_scanline_outline_omits_internal_band_edges(self):
|
|
segments = self.image_interaction.scanline_outline_segments(
|
|
(
|
|
(0.0, 2.0, ((0.0, 8.0),)),
|
|
(2.0, 4.0, ((0.0, 8.0),)),
|
|
)
|
|
)
|
|
|
|
edges = tuple(zip(segments[::2], segments[1::2]))
|
|
self.assertFalse(
|
|
any(start[1] == end[1] == 2.0 for start, end in edges)
|
|
)
|
|
|
|
|
|
def test_mask_brush_outline_matches_the_screen_radius(self):
|
|
polygons = self.image_interaction.brush_footprint_polygons(
|
|
((10.0, 10.0), (30.0, 10.0)),
|
|
5.0,
|
|
)
|
|
vertices = tuple(point for polygon in polygons for point in polygon)
|
|
|
|
self.assertAlmostEqual(min(point[0] for point in vertices), 5.0)
|
|
self.assertAlmostEqual(max(point[0] for point in vertices), 35.0)
|
|
self.assertAlmostEqual(min(point[1] for point in vertices), 5.0)
|
|
self.assertAlmostEqual(max(point[1] for point in vertices), 15.0)
|
|
|
|
|
|
def test_image_lasso_preview_uses_fill_and_a_closed_dashed_outline(self):
|
|
polygon = ((1.0, 2.0), (8.0, 3.0), (4.0, 9.0))
|
|
operator = SimpleNamespace(
|
|
gesture="LASSO",
|
|
mode="SET",
|
|
_path=list(polygon),
|
|
_area_pointer=1,
|
|
_region_pointer=2,
|
|
_preview_polygon=None,
|
|
_fill_triangles=(),
|
|
_brush_preview=None,
|
|
_outline_segments=(),
|
|
)
|
|
with (
|
|
patch.object(self.image_interaction, "drawing_in_region", return_value=True),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"preview_fill_geometry",
|
|
return_value=(("triangles",), ("segments",)),
|
|
) as build_preview,
|
|
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.ImageGesture._draw_selection(operator)
|
|
self.image_interaction.ImageGesture._draw_selection(operator)
|
|
|
|
build_preview.assert_called_once_with(polygon)
|
|
self.assertEqual(draw_fill.call_count, 2)
|
|
draw_fill.assert_called_with(
|
|
polygon,
|
|
("triangles",),
|
|
(1.0, 1.0, 1.0, 0.16),
|
|
)
|
|
self.assertEqual(draw_outline.call_count, 2)
|
|
draw_outline.assert_called_with((), segments=("segments",))
|
|
|
|
|
|
def test_mask_polyline_modal_adds_removes_and_confirms_points(self):
|
|
redraw = Mock()
|
|
context = SimpleNamespace(area=SimpleNamespace(tag_redraw=redraw))
|
|
confirmed = Mock(return_value={"FINISHED"})
|
|
operator = SimpleNamespace(
|
|
_path=[(10.0, 10.0)],
|
|
_cursor=(10.0, 10.0),
|
|
_confirm_polyline=confirmed,
|
|
)
|
|
|
|
result = self.image_interaction.ImageGesture._modal_polyline(
|
|
operator,
|
|
context,
|
|
SimpleNamespace(
|
|
type="LEFTMOUSE",
|
|
value="PRESS",
|
|
mouse_region_x=30,
|
|
mouse_region_y=10,
|
|
),
|
|
)
|
|
self.assertEqual(result, {"RUNNING_MODAL"})
|
|
self.assertEqual(operator._path, [(10.0, 10.0), (30.0, 10.0)])
|
|
|
|
self.image_interaction.ImageGesture._modal_polyline(
|
|
operator,
|
|
context,
|
|
SimpleNamespace(type="BACK_SPACE", value="PRESS"),
|
|
)
|
|
self.assertEqual(operator._path, [(10.0, 10.0)])
|
|
|
|
operator._path.extend(((30.0, 10.0), (30.0, 30.0)))
|
|
result = self.image_interaction.ImageGesture._modal_polyline(
|
|
operator,
|
|
context,
|
|
SimpleNamespace(
|
|
type="LEFTMOUSE",
|
|
value="PRESS",
|
|
mouse_region_x=12,
|
|
mouse_region_y=11,
|
|
),
|
|
)
|
|
self.assertEqual(result, {"FINISHED"})
|
|
confirmed.assert_called_once_with(context)
|
|
|
|
|
|
def test_mask_polyline_click_uses_close_radius_not_hint_distance(self):
|
|
context = SimpleNamespace(area=SimpleNamespace(tag_redraw=Mock()))
|
|
path = [(10.0, 10.0), (30.0, 10.0), (30.0, 30.0)]
|
|
confirmed = Mock(return_value={"FINISHED"})
|
|
operator = SimpleNamespace(
|
|
_path=list(path),
|
|
_cursor=path[-1],
|
|
_ui_scale=1.0,
|
|
_confirm_polyline=confirmed,
|
|
)
|
|
|
|
result = self.image_interaction.ImageGesture._modal_polyline(
|
|
operator,
|
|
context,
|
|
SimpleNamespace(
|
|
type="LEFTMOUSE",
|
|
value="PRESS",
|
|
mouse_region_x=26,
|
|
mouse_region_y=10,
|
|
),
|
|
)
|
|
|
|
self.assertEqual(result, {"RUNNING_MODAL"})
|
|
self.assertEqual(operator._path[-1], (26.0, 10.0))
|
|
confirmed.assert_not_called()
|
|
|
|
operator._path = list(path)
|
|
result = self.image_interaction.ImageGesture._modal_polyline(
|
|
operator,
|
|
context,
|
|
SimpleNamespace(
|
|
type="LEFTMOUSE",
|
|
value="PRESS",
|
|
mouse_region_x=25,
|
|
mouse_region_y=10,
|
|
),
|
|
)
|
|
|
|
self.assertEqual(result, {"FINISHED"})
|
|
confirmed.assert_called_once_with(context)
|
|
|
|
|
|
def test_mask_polyline_double_click_confirms_without_an_extra_point(self):
|
|
context = SimpleNamespace()
|
|
path = [(10.0, 10.0), (30.0, 10.0), (30.0, 30.0)]
|
|
confirmed = Mock(return_value={"FINISHED"})
|
|
operator = SimpleNamespace(
|
|
_path=list(path),
|
|
_confirm_polyline=confirmed,
|
|
)
|
|
|
|
result = self.image_interaction.ImageGesture._modal_polyline(
|
|
operator,
|
|
context,
|
|
SimpleNamespace(type="LEFTMOUSE", value="DOUBLE_CLICK"),
|
|
)
|
|
|
|
self.assertEqual(result, {"FINISHED"})
|
|
self.assertEqual(operator._path, path)
|
|
confirmed.assert_called_once_with(context)
|
|
|
|
|
|
def test_mask_polyline_consecutive_presses_confirm_double_click(self):
|
|
context = SimpleNamespace(area=SimpleNamespace(tag_redraw=Mock()))
|
|
path = [(10.0, 10.0), (30.0, 10.0), (30.0, 30.0)]
|
|
confirmed = Mock(return_value={"FINISHED"})
|
|
operator = SimpleNamespace(
|
|
_path=list(path),
|
|
_cursor=path[-1],
|
|
_ui_scale=1.0,
|
|
_polyline_double_click_interval=0.35,
|
|
_last_polyline_click_point=None,
|
|
_last_polyline_click_time=None,
|
|
_confirm_polyline=confirmed,
|
|
)
|
|
event = SimpleNamespace(
|
|
type="LEFTMOUSE",
|
|
value="PRESS",
|
|
mouse_region_x=50,
|
|
mouse_region_y=50,
|
|
)
|
|
|
|
with patch.object(
|
|
self.image_interaction.time,
|
|
"monotonic",
|
|
side_effect=(10.0, 10.2),
|
|
):
|
|
first_result = self.image_interaction.ImageGesture._modal_polyline(
|
|
operator, context, event
|
|
)
|
|
second_result = self.image_interaction.ImageGesture._modal_polyline(
|
|
operator, context, event
|
|
)
|
|
|
|
self.assertEqual(first_result, {"RUNNING_MODAL"})
|
|
self.assertEqual(second_result, {"FINISHED"})
|
|
self.assertEqual(operator._path, [*path, (50.0, 50.0)])
|
|
confirmed.assert_called_once_with(context)
|
|
|
|
|
|
def test_polyline_raw_double_click_requires_time_and_proximity(self):
|
|
is_double_click = self.image_interaction.is_polyline_double_click
|
|
|
|
self.assertTrue(is_double_click((10, 10), 1.0, (14, 13), 1.2))
|
|
self.assertFalse(is_double_click((10, 10), 1.0, (16, 10), 1.2))
|
|
self.assertFalse(is_double_click((10, 10), 1.0, (10, 10), 1.4))
|
|
|
|
|
|
def test_mask_polyline_double_click_requires_three_points(self):
|
|
class Operator(self.image_interaction.ImageGesture):
|
|
pass
|
|
|
|
operator = Operator()
|
|
operator._path = [(10.0, 10.0), (30.0, 10.0)]
|
|
operator._complete = Mock(return_value={"FINISHED"})
|
|
|
|
result = operator._modal_polyline(
|
|
SimpleNamespace(),
|
|
SimpleNamespace(type="LEFTMOUSE", value="DOUBLE_CLICK"),
|
|
)
|
|
|
|
self.assertEqual(result, {"RUNNING_MODAL"})
|
|
self.assertEqual(operator._path, [(10.0, 10.0), (30.0, 10.0)])
|
|
operator._complete.assert_not_called()
|
|
|
|
|
|
def test_mask_polyline_enter_requires_three_points(self):
|
|
context = SimpleNamespace()
|
|
complete = Mock(return_value={"FINISHED"})
|
|
operator = SimpleNamespace(_path=[(0, 0), (10, 0)], _complete=complete)
|
|
|
|
self.assertEqual(
|
|
self.image_interaction.ImageGesture._confirm_polyline(
|
|
operator, context
|
|
),
|
|
{"RUNNING_MODAL"},
|
|
)
|
|
operator._path.append((10, 10))
|
|
self.assertEqual(
|
|
self.image_interaction.ImageGesture._confirm_polyline(
|
|
operator, context
|
|
),
|
|
{"FINISHED"},
|
|
)
|
|
complete.assert_called_once_with(context)
|
|
|
|
|
|
def test_mask_polyline_completion_uses_the_lasso_selection_pipeline(self):
|
|
source = SimpleNamespace(data=SimpleNamespace(size=(100, 80)))
|
|
submitted = Mock(return_value={"FINISHED"})
|
|
operator = SimpleNamespace(
|
|
gesture="POLYLINE",
|
|
source_object_name="Source",
|
|
source_matrix_data="matrix",
|
|
_path=[(10.0, 10.0), (60.0, 10.0), (40.0, 50.0)],
|
|
_submit_selection_path=submitted,
|
|
_finish=Mock(),
|
|
)
|
|
context = SimpleNamespace(region=object(), region_data=object())
|
|
|
|
with (
|
|
patch.object(
|
|
self.image_interaction,
|
|
"require_image_empty",
|
|
return_value=source,
|
|
),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"deserialize_matrix",
|
|
return_value="matrix",
|
|
),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"image_empty_bounds",
|
|
return_value=(-0.5, 0.5, -0.5, 0.5),
|
|
),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"screen_path_to_image_pixels",
|
|
return_value=((1, 2), (8, 2), (4, 9)),
|
|
) as project,
|
|
):
|
|
result = self.image_interaction.ImageGesture._complete(
|
|
operator, context
|
|
)
|
|
|
|
self.assertEqual(result, {"FINISHED"})
|
|
self.assertEqual(
|
|
submitted.call_args.args[1].points,
|
|
((1.0, 2.0), (8.0, 2.0), (4.0, 9.0)),
|
|
)
|
|
self.assertEqual(project.call_args.args[0], tuple(operator._path))
|
|
|
|
|
|
def test_mask_polyline_overlay_previews_cursor_and_close_target(self):
|
|
operator = SimpleNamespace(
|
|
gesture="POLYLINE",
|
|
mode="SUBTRACT",
|
|
_path=[(10.0, 10.0), (30.0, 10.0), (30.0, 30.0)],
|
|
_cursor=(12.0, 11.0),
|
|
_ui_scale=1.0,
|
|
_area_pointer=1,
|
|
_region_pointer=2,
|
|
_preview_polygon=None,
|
|
_fill_triangles=(),
|
|
_brush_preview=None,
|
|
_outline_segments=(),
|
|
)
|
|
with (
|
|
patch.object(self.image_interaction, "drawing_in_region", return_value=True),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"preview_fill_geometry",
|
|
return_value=(("triangles",), ("segments",)),
|
|
),
|
|
patch.object(self.image_interaction, "draw_polygon_fill") as fill,
|
|
patch.object(self.image_interaction, "draw_dashed_line"),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"draw_polyline_close_indicator",
|
|
) as close,
|
|
):
|
|
self.image_interaction.ImageGesture._draw_selection(operator)
|
|
|
|
polygon = (*operator._path, operator._cursor)
|
|
fill.assert_called_once_with(
|
|
polygon,
|
|
("triangles",),
|
|
(1.0, 0.25, 0.2, 0.16),
|
|
)
|
|
close.assert_called_once_with(
|
|
operator._path[0],
|
|
self.image_interaction.polyline_close_indicator_radius(
|
|
self.image_interaction.math.dist(
|
|
operator._cursor,
|
|
operator._path[0],
|
|
),
|
|
1.0,
|
|
),
|
|
1.0,
|
|
)
|
|
|
|
|
|
def test_polyline_close_indicator_matches_blender_scale_and_distance(self):
|
|
indicator = self.image_interaction.polyline_close_indicator_radius
|
|
close_radius = self.image_interaction.polyline_close_radius
|
|
|
|
self.assertEqual(close_radius(1.0), 15.0)
|
|
self.assertEqual(indicator(0.0, 1.0), 15.0)
|
|
self.assertGreater(indicator(20.0, 1.0), indicator(120.0, 1.0))
|
|
self.assertIsNone(indicator(225.0, 1.0))
|
|
self.assertEqual(close_radius(2.0), 30.0)
|
|
self.assertEqual(indicator(0.0, 2.0), 30.0)
|
|
self.assertIsNotNone(indicator(899.0, 2.0))
|
|
self.assertIsNone(indicator(900.0, 2.0))
|
|
|
|
|
|
def test_mask_polyline_overlay_hides_close_target_outside_hint_distance(self):
|
|
operator = SimpleNamespace(
|
|
gesture="POLYLINE",
|
|
mode="SUBTRACT",
|
|
_path=[(10.0, 10.0), (30.0, 10.0), (30.0, 30.0)],
|
|
_cursor=(236.0, 10.0),
|
|
_ui_scale=1.0,
|
|
_area_pointer=1,
|
|
_region_pointer=2,
|
|
_preview_polygon=None,
|
|
_fill_triangles=(),
|
|
_brush_preview=None,
|
|
_outline_segments=(),
|
|
)
|
|
with (
|
|
patch.object(self.image_interaction, "drawing_in_region", return_value=True),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"preview_fill_geometry",
|
|
return_value=(("triangles",), ("segments",)),
|
|
),
|
|
patch.object(self.image_interaction, "draw_polygon_fill"),
|
|
patch.object(self.image_interaction, "draw_dashed_line"),
|
|
patch.object(
|
|
self.image_interaction,
|
|
"draw_polyline_close_indicator",
|
|
) as close,
|
|
):
|
|
self.image_interaction.ImageGesture._draw_selection(operator)
|
|
|
|
close.assert_not_called()
|
|
|
|
|
|
def test_mask_brush_overlay_matches_lasso_fill_and_outline(self):
|
|
for mode, color in (
|
|
("SUBTRACT", (1.0, 0.25, 0.2, 0.16)),
|
|
("ADD", (1.0, 1.0, 1.0, 0.16)),
|
|
):
|
|
cache = Mock()
|
|
operator = SimpleNamespace(
|
|
gesture="BRUSH", mode=mode, radius=12,
|
|
_path=[(10.0, 10.0), (20.0, 20.0)],
|
|
_area_pointer=1, _region_pointer=2, _brush_preview=cache,
|
|
)
|
|
with patch.object(self.image_interaction, "drawing_in_region", return_value=True):
|
|
self.image_interaction.ImageGesture._draw_selection(operator)
|
|
cache.update.assert_called_once_with(operator._path, 12)
|
|
cache.draw.assert_called_once_with(color)
|
|
|
|
|
|
def test_mask_brush_retains_the_complete_uncommitted_path(self):
|
|
class Operator(self.image_interaction.ImageGesture):
|
|
active_tool_id = "anyimage.mask"
|
|
gesture = "BRUSH"
|
|
|
|
operator = Operator()
|
|
operator._path = [(10.0, 10.0)]
|
|
operator._path_samples = list(operator._path)
|
|
operator._cursor = (10.0, 10.0)
|
|
context = SimpleNamespace(area=SimpleNamespace(tag_redraw=Mock()))
|
|
event = SimpleNamespace(
|
|
type="MOUSEMOVE",
|
|
value="NOTHING",
|
|
mouse_region_x=20,
|
|
mouse_region_y=25,
|
|
)
|
|
|
|
with patch.object(
|
|
self.image_interaction,
|
|
"active_view3d_tool_id",
|
|
return_value=None,
|
|
):
|
|
result = operator.modal(context, event)
|
|
|
|
self.assertEqual(result, {"RUNNING_MODAL"})
|
|
self.assertEqual(operator._path, [(10.0, 10.0), (20.0, 25.0)])
|
|
|
|
|
|
def test_mask_tool_switch_ends_the_uncommitted_overlay(self):
|
|
class Operator(self.image_interaction.ImageGesture):
|
|
active_tool_id = "anyimage.mask"
|
|
gesture = "BRUSH"
|
|
|
|
operator = Operator()
|
|
operator._finish = Mock()
|
|
|
|
with patch.object(
|
|
self.image_interaction,
|
|
"active_view3d_tool_id",
|
|
return_value="anyimage.frame",
|
|
):
|
|
result = operator.modal(SimpleNamespace(), SimpleNamespace())
|
|
|
|
self.assertEqual(result, {"CANCELLED"})
|
|
operator._finish.assert_called_once()
|
|
|
|
|
|
def test_image_lasso_does_not_add_stationary_or_nearly_collinear_points(self):
|
|
path = [(0.0, 0.0)]
|
|
samples = list(path)
|
|
|
|
self.assertFalse(self.image_interaction.append_lasso_point(path, (0.0, 0.0), samples))
|
|
self.assertEqual(path, [(0.0, 0.0)])
|
|
self.assertTrue(self.image_interaction.append_lasso_point(path, (3.0, 0.0), samples))
|
|
self.assertTrue(self.image_interaction.append_lasso_point(path, (6.0, 0.2), samples))
|
|
self.assertEqual(len(path), 2)
|
|
self.assertEqual(path[-1], (6.0, 0.2))
|
|
self.assertTrue(self.image_interaction.append_lasso_point(path, (6.0, 4.0), samples))
|
|
self.assertEqual(len(path), 3)
|