feat(cutout_tool): Implement cutout region to shape functionality

- Added interaction helpers for cutout region validation and shape menu interaction.
- Created main operator for converting image regions into cutout shapes, including AI artifact generation.
- Introduced object creation logic for cutout shapes, handling depth and normal textures.
- Defined cutout shape identifiers and node groups for various shape types.
- Implemented depth texture writing and prediction artifact generation for MoGe-2.
- Developed server jobs for generating cutout artifacts, depth planes, and refining image regions.
- Added region refinement logic to enhance gesture masks using BEN2.
This commit is contained in:
2026-08-31 11:36:27 +08:00
parent 08309f719d
commit 87d7721128
80 changed files with 4823 additions and 4297 deletions

View File

@@ -84,21 +84,27 @@ class AddonTest(unittest.TestCase):
cls.addon = importlib.import_module("anyimage")
cls.job_runtime = importlib.import_module("anyimage.runtime")
cls.model_operators = importlib.import_module("anyimage.operators.ai_setup")
cls.image_depth = importlib.import_module("anyimage.operators.common.mesh_result")
cls.image_depth = importlib.import_module(
"anyimage.operators.common.mesh_result"
)
cls.image_data = importlib.import_module("anyimage.operators.common.image_data")
cls.object_data = importlib.import_module("anyimage.operators.common.mesh_result")
cls.object_data = importlib.import_module(
"anyimage.operators.common.mesh_result"
)
cls.debug = importlib.import_module("anyimage.operators.debug")
cls.mesh_plane = importlib.import_module("anyimage.operators.convert_to_plane.mesh_plane")
cls.mesh = importlib.import_module("anyimage.operators.convert_to_plane.conversion")
cls.mesh_plane = importlib.import_module(
"anyimage.operators.convert_to_plane.mesh_plane"
)
cls.mesh = importlib.import_module(
"anyimage.operators.convert_to_plane.conversion"
)
cls.cutout_conversion = importlib.import_module(
"anyimage.operators.cutout_tool.region_to_shape"
"anyimage.operators.cutout_tool.main"
)
cls.cutout_build = importlib.import_module(
"anyimage.operators.cutout_tool.region_to_shape"
)
cls.mesh_shapes = importlib.import_module(
"anyimage.operators.cutout_tool.region_to_shape"
"anyimage.operators.cutout_tool.main"
)
cls.mesh_shapes = importlib.import_module("anyimage.operators.cutout_tool.main")
cls.image_background = importlib.import_module(
"anyimage.operators.remove_background"
)
@@ -116,9 +122,7 @@ class AddonTest(unittest.TestCase):
"anyimage.operators.common.image_interaction"
)
cls.image_box = importlib.import_module("anyimage.operators.image_tool.box")
cls.image_lasso = importlib.import_module(
"anyimage.operators.image_tool.lasso"
)
cls.image_lasso = importlib.import_module("anyimage.operators.image_tool.lasso")
cls.image_line = importlib.import_module("anyimage.operators.image_tool.line")
cls.image_polyline = importlib.import_module(
"anyimage.operators.image_tool.polyline"
@@ -127,10 +131,10 @@ class AddonTest(unittest.TestCase):
"anyimage.operators.perspective_tool.edit_perspective"
)
cls.cutout_geometry = importlib.import_module(
"anyimage.operators.cutout_tool.geometry.triangulation"
"anyimage.operators.cutout_tool.geometry"
)
cls.cutout_depth = importlib.import_module(
"anyimage.operators.cutout_tool.geometry.depth_calibration"
"anyimage.operators.cutout_tool.geometry"
)
cls.image_interaction = importlib.import_module(
"anyimage.operators.common.image_interaction"
@@ -525,7 +529,7 @@ class AddonTest(unittest.TestCase):
self.assertNotIn("stage", controller.post_status)
self.assertNotIn("stages", controller.post_status)
def test_conversion_model_lists_moge2_before_da3(self):
def test_production_geometry_models_exclude_da3(self):
properties = importlib.import_module("anyimage.properties")
catalog = (
{
@@ -571,14 +575,12 @@ class AddonTest(unittest.TestCase):
)
with patch.object(properties, "model_catalog", return_value=catalog):
depth_items = properties.depth_plane_model_enum_items(None, None)
mesh_depth_items = properties.mesh_depth_model_enum_items(None, None)
mesh_depth_items = properties.geometry_model_enum_items(None, None)
expected = (
"MOGE2_VITS_NORMAL",
"MOGE2_VITB_NORMAL",
"MOGE2_VITL_NORMAL",
"SMALL",
"BASE",
)
self.assertEqual(tuple(item[0] for item in depth_items[:-1]), expected)
self.assertEqual(tuple(item[0] for item in mesh_depth_items), expected)
@@ -687,11 +689,10 @@ class AddonTest(unittest.TestCase):
self.model_operators.OpenAIEnvironmentSettings,
self.addon.CLASSES,
)
def test_global_ai_input_size_defaults_and_clamps(self):
preference_module = self.addon.preferences
preferences = self.fake_bpy.context.preferences.addons[
"anyimage"
].preferences
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
self.assertEqual(
preference_module.configured_max_ai_input_size(),
@@ -708,6 +709,25 @@ class AddonTest(unittest.TestCase):
256,
)
def test_cutout_mesh_resolution_defaults_and_clamps_independently(self):
preference_module = self.addon.preferences
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
self.assertEqual(
preference_module.configured_max_cutout_mesh_resolution(),
1024,
)
preferences.max_cutout_mesh_resolution = 2048
self.assertEqual(
preference_module.configured_max_cutout_mesh_resolution(),
1024,
)
preferences.max_cutout_mesh_resolution = 1
self.assertEqual(
preference_module.configured_max_cutout_mesh_resolution(),
4,
)
def test_preferences_draws_upscale_model_download(self):
preference_module = importlib.import_module("anyimage.preferences")
downloads = []
@@ -742,7 +762,7 @@ class AddonTest(unittest.TestCase):
preferences = preference_module.AnyImagePreferences()
preferences.layout = Layout()
preferences.mesh_depth_model = "BASE"
preferences.geometry_model = "MOGE2_VITS_NORMAL"
preferences.upscale_model = "SAFMN_REAL_X4"
catalog = tuple(
{
@@ -781,7 +801,7 @@ class AddonTest(unittest.TestCase):
self.assertEqual(
[download.model for download in downloads],
["BASE", "BEN2_BASE", "SAFMN_REAL_X4"],
["MOGE2_VITS_NORMAL", "BEN2_BASE", "SAFMN_REAL_X4"],
)
self.assertEqual(
[
@@ -815,10 +835,10 @@ class AddonTest(unittest.TestCase):
("anyimage_preferences_cutout", False),
],
)
self.assertIn("DA3 models cannot generate normals", labels)
self.assertNotIn("Normal Model", labels)
self.assertIn("upscale_model", properties)
self.assertIn("max_ai_input_size", properties)
self.assertIn("max_cutout_mesh_resolution", properties)
self.assertIn("balloon_profile_algorithm", properties)
balloon_profile = preference_module.AnyImagePreferences.__annotations__[
"balloon_profile_algorithm"
@@ -887,8 +907,8 @@ class AddonTest(unittest.TestCase):
return_value=(1, 0, 24, True),
),
patch.object(self.mesh, "create_image_material", return_value="Material"),
patch.object(self.mesh, "copy_color_image", return_value="Color"),
patch.object(self.mesh, "build_mesh_plane") as build_mesh,
patch.object(self.mesh, "create_color_image", return_value="Color"),
patch.object(self.mesh, "create_plane_object") as build_mesh,
):
result = self.mesh.ConvertToPlane.execute(operator, context)
@@ -905,9 +925,7 @@ class AddonTest(unittest.TestCase):
operator.resolution_level = 5
operator.max_input_size = 1536
context = SimpleNamespace(
scene=SimpleNamespace(
anyimage_settings=SimpleNamespace(video_frames=1)
)
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1))
)
with (
patch.object(self.mesh, "require_environment"),
@@ -924,8 +942,12 @@ class AddonTest(unittest.TestCase):
def test_surface_and_balloon_without_ai_build_locally(self):
import numpy as np
source = SimpleNamespace(data=SimpleNamespace(size=(10, 10)), select_set=lambda _value: None)
context = SimpleNamespace(view_layer=SimpleNamespace(objects=SimpleNamespace(active=None)))
source = SimpleNamespace(
data=SimpleNamespace(size=(10, 10)), select_set=lambda _value: None
)
context = SimpleNamespace(
view_layer=SimpleNamespace(objects=SimpleNamespace(active=None))
)
region_mask = self.cutout_conversion.RegionMask(
np.ones((8, 8), dtype=np.float32), (1, 1, 9, 9)
)
@@ -938,25 +960,46 @@ class AddonTest(unittest.TestCase):
{"points": [[1, 1], [9, 1], [9, 9], [1, 9]]}
),
shape=shape,
mesh_detail=64,
normal_mode="NONE",
refine_selection=False,
mesh_resolution=64,
generate_normal=False,
refine_region=False,
report=lambda *_args: None,
_needs_generation=lambda: False,
)
with (
patch.object(self.cutout_conversion, "require_image_empty", return_value=source),
patch.object(self.cutout_conversion, "video_total_frames", return_value=1),
patch.object(self.cutout_conversion, "image_rgba", return_value=source_rgba),
patch.object(
self.cutout_conversion,
"require_image_empty",
return_value=source,
),
patch.object(
self.cutout_conversion, "video_total_frames", return_value=1
),
patch.object(
self.cutout_conversion, "image_rgba", return_value=source_rgba
),
patch.object(
self.cutout_conversion,
"rasterize_region_path",
return_value=region_mask,
),
patch.object(self.cutout_conversion, "configured_max_ai_input_size", return_value=2048),
patch.object(self.cutout_conversion, "region_to_shape") as convert,
patch.object(
self.cutout_conversion,
"configured_max_ai_input_size",
return_value=2048,
),
patch.object(
self.cutout_conversion,
"configured_max_cutout_mesh_resolution",
return_value=1024,
),
patch.object(
self.cutout_conversion, "create_cutout_shape"
) as convert,
):
result = self.cutout_conversion.CutoutRegionToShape.execute(operator, context)
result = self.cutout_conversion.CutoutRegionToShape.execute(
operator, context
)
self.assertEqual(result, {"FINISHED"})
convert.assert_called_once_with(
@@ -975,16 +1018,24 @@ class AddonTest(unittest.TestCase):
operator.shape = "DEPTH_SURFACE"
operator.model = self.addon.properties.DEFAULT_MOGE2_MODEL_KEY
operator.resolution_level = 5
operator.max_input_size = 2048
operator.normal_mode = "OBJECT"
operator.refine_selection = True
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=SimpleNamespace()))
operator.max_ai_input_size = 2048
operator.generate_normal = True
operator.refine_region = True
context = SimpleNamespace(
scene=SimpleNamespace(anyimage_settings=SimpleNamespace())
)
with (
patch.object(self.cutout_conversion, "require_environment"),
patch.object(self.cutout_conversion, "require_model") as require_model,
patch.object(self.cutout_conversion, "production_device", return_value="CPU"),
patch.object(
self.cutout_conversion, "production_device", return_value="CPU"
),
patch.object(self.cutout_conversion, "source_start_frame", return_value=1),
patch.object(self.cutout_conversion, "moge2_parameters", return_value={"model": operator.model, "resolution_level": 5}),
patch.object(
self.cutout_conversion,
"moge2_parameters",
return_value={"model": operator.model, "resolution_level": 5},
),
):
parameters = operator.request(context)
@@ -993,27 +1044,27 @@ class AddonTest(unittest.TestCase):
[self.addon.properties.BEN2_MODEL_KEY, operator.model],
)
self.assertTrue(parameters["refine_region"])
self.assertTrue(parameters["output_depth"])
self.assertEqual(parameters["depth_mode"], "VECTOR")
self.assertEqual(parameters["normal_mode"], "OBJECT")
self.assertNotIn("polygon", parameters)
self.assertNotIn("shape", parameters)
self.assertNotIn("normal_analysis", parameters)
def test_cutout_moge_settings_falls_back_from_da3(self):
def test_cutout_geometry_settings_use_configured_moge(self):
preferences = SimpleNamespace(
mesh_depth_model="DA3_BASE",
mesh_resolution_level=7,
geometry_model="MOGE2_VITB_NORMAL",
geometry_resolution_level=7,
)
with patch.object(
self.cutout_conversion,
"addon_preferences",
return_value=preferences,
):
settings = self.cutout_conversion.cutout_moge_settings(None)
settings = self.cutout_conversion.cutout_geometry_settings(None)
self.assertEqual(
settings,
(self.addon.properties.DEFAULT_MOGE2_MODEL_KEY, 7),
("MOGE2_VITB_NORMAL", 7),
)
def test_image_menu_keeps_ai_features_clickable_and_offers_models(self):
@@ -1321,9 +1372,7 @@ class AddonTest(unittest.TestCase):
self.assertTrue(nonzero[3, 5])
def preview_area(fill_rule):
triangles = self.image_edit_common.scanline_fill_triangles(
path, fill_rule
)
triangles = self.image_edit_common.scanline_fill_triangles(path, fill_rule)
return sum(
abs(
self.image_edit_common._triangle_cross(
@@ -1336,8 +1385,8 @@ class AddonTest(unittest.TestCase):
for index in range(0, len(triangles), 3)
)
self.assertAlmostEqual(preview_area('EVEN_ODD'), 36.0)
self.assertAlmostEqual(preview_area('NONZERO'), 54.0)
self.assertAlmostEqual(preview_area("EVEN_ODD"), 36.0)
self.assertAlmostEqual(preview_area("NONZERO"), 54.0)
def test_expected_empty_image_edits_are_reported_as_warnings(self):
reports = []
@@ -1370,7 +1419,7 @@ class AddonTest(unittest.TestCase):
def test_image_edit_tools_and_operators_are_registered(self):
operator = self.image_mask.EditImageMask
self.assertIn(operator, self.addon.CLASSES)
self.assertIn(self.image_mask.RefineImageMask, self.addon.CLASSES)
self.assertIn(self.image_mask.RefineImageRegion, self.addon.CLASSES)
self.assertIn(self.image_mask.ActivateImageTool, self.addon.CLASSES)
tool_cases = (
(self.image_box.ImageBoxTool, "BOX", "ops.sculpt.border_mask"),
@@ -1391,24 +1440,36 @@ class AddonTest(unittest.TestCase):
self.assertIn("draw_settings", self.image_box.ImageBoxTool.__dict__)
self.assertIn("draw_settings", self.image_lasso.ImageLassoTool.__dict__)
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=object()))
layout = SimpleNamespace(prop=lambda *_args: None)
drawn_settings = []
layout = SimpleNamespace(
prop=lambda _settings, name: drawn_settings.append(name)
)
with patch.object(self.image_box, "draw_region_settings") as draw:
self.image_box.ImageBoxTool.draw_settings(context, layout, None)
draw.assert_called_once_with(
context, layout, "image_box_refine_selection"
)
draw.assert_called_once_with(context, layout, "image_box_refine_region")
with patch.object(self.image_lasso, "draw_region_settings") as draw:
self.image_lasso.ImageLassoTool.draw_settings(context, layout, None)
draw.assert_called_once_with(
context, layout, "image_lasso_refine_selection"
)
draw.assert_called_once_with(context, layout, "image_lasso_refine_region")
cutout_tool = importlib.import_module(
"anyimage.operators.cutout_tool"
).CutoutTool
cutout_module = sys.modules[cutout_tool.__module__]
with patch.object(cutout_module, "draw_region_settings") as draw:
drawn_settings.clear()
with patch.object(
cutout_module,
"draw_region_settings",
side_effect=lambda _context, _layout, name: drawn_settings.append(name),
) as draw:
cutout_tool.draw_settings(context, layout, None)
draw.assert_called_once_with(context, layout, "cutout_refine_selection")
draw.assert_called_once_with(context, layout, "cutout_refine_region")
self.assertEqual(
drawn_settings,
[
"cutout_refine_region",
"cutout_point_spacing",
"cutout_generate_normal",
],
)
self.assertNotIn("draw_settings", self.image_line.ImageLineTool.__dict__)
self.assertNotIn(
"draw_settings", self.image_polyline.ImagePolylineTool.__dict__
@@ -1419,51 +1480,69 @@ class AddonTest(unittest.TestCase):
self.assertNotIn("padding", operator.__annotations__)
settings = self.addon.properties.AnyImageSettings.__annotations__
for name in (
"cutout_refine_selection",
"image_box_refine_selection",
"image_lasso_refine_selection",
"cutout_refine_region",
"image_box_refine_region",
"image_lasso_refine_region",
):
self.assertFalse(settings[name]["default"])
self.assertNotIn("refine_selection", settings)
self.assertNotIn("image_edit_refine_selection", settings)
self.assertNotIn("refine_region", settings)
self.assertNotIn("image_edit_refine_region", settings)
self.assertEqual(settings["cutout_point_spacing"]["default"], 32)
self.assertEqual(settings["cutout_point_spacing"]["min"], 4)
self.assertEqual(settings["cutout_point_spacing"]["max"], 32)
self.assertEqual(settings["cutout_normal_mode"]["default"], "NONE")
self.assertFalse(settings["cutout_generate_normal"]["default"])
self.assertEqual(settings["cutout_refine_region"]["name"], "Fit to Foreground")
self.assertEqual(
settings["cutout_point_spacing"]["name"], "Mesh Sampling Spacing"
)
self.assertEqual(
settings["cutout_generate_normal"]["name"],
"Generate Normal Map",
)
self.assertNotIn("image_extract_continuous", settings)
self.assertNotIn("image_extract_padding", settings)
self.assertNotIn("image_extract_remove_background", settings)
def test_refine_selection_is_independent_for_each_area_tool(self):
def test_refine_region_is_independent_for_each_area_tool(self):
settings = SimpleNamespace(
cutout_refine_selection=True,
image_box_refine_selection=False,
image_lasso_refine_selection=True,
cutout_refine_region=True,
image_box_refine_region=False,
image_lasso_refine_region=True,
)
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=settings))
with patch.object(self.image_edit_common, "ai_ready", return_value=True):
for gesture in ("LINE", "POLYLINE"):
operator = SimpleNamespace(
gesture=gesture,
refine_selection=True,
refine_region=True,
)
self.image_edit_common.apply_region_settings(operator, context)
self.assertFalse(operator.refine_selection)
self.assertFalse(operator.refine_region)
box = SimpleNamespace(gesture="BOX", refine_selection=True)
box = SimpleNamespace(
gesture="BOX",
refine_region=True,
refine_region_settings={"BOX": "image_box_refine_region"},
)
self.image_edit_common.apply_region_settings(box, context)
self.assertFalse(box.refine_selection)
self.assertFalse(box.refine_region)
lasso = SimpleNamespace(gesture="LASSO", refine_selection=False)
lasso = SimpleNamespace(
gesture="LASSO",
refine_region=False,
refine_region_settings={"LASSO": "image_lasso_refine_region"},
)
self.image_edit_common.apply_region_settings(lasso, context)
self.assertTrue(lasso.refine_selection)
self.assertTrue(lasso.refine_region)
cutout = SimpleNamespace(
gesture="LASSO",
active_tool_id="anyimage.cutout_lasso",
refine_selection=False,
refine_region=False,
refine_region_settings={"LASSO": "cutout_refine_region"},
)
self.image_edit_common.apply_region_settings(cutout, context)
self.assertTrue(cutout.refine_selection)
self.assertTrue(cutout.refine_region)
def test_image_edit_first_click_selects_another_image_without_editing(self):
selected = []
@@ -1652,14 +1731,14 @@ class AddonTest(unittest.TestCase):
self.assertAlmostEqual(area, 10.0)
def test_image_mask_fill_uses_blender_geometry_without_stopping_the_draw(self):
mathutils = ModuleType('mathutils')
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}):
with patch.dict(sys.modules, {"mathutils": mathutils}):
triangles = self.image_edit_common.triangulated_polygon_vertices(
((0, 0), (2, 0), (0, 2))
)
@@ -1670,12 +1749,12 @@ class AddonTest(unittest.TestCase):
)
def test_image_mask_fill_falls_back_when_blender_tessellation_rejects_path(self):
mathutils = ModuleType('mathutils')
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}):
with patch.dict(sys.modules, {"mathutils": mathutils}):
triangles = self.image_edit_common.triangulated_polygon_vertices(
((0, 0), (2, 0), (0, 2))
)
@@ -1684,9 +1763,7 @@ class AddonTest(unittest.TestCase):
def test_image_mask_preview_fills_a_self_intersecting_lasso(self):
polygon = ((0, 0), (4, 4), (0, 4), (4, 0))
triangles = self.image_edit_common.preview_fill_triangles(
polygon, 'NONZERO'
)
triangles = self.image_edit_common.preview_fill_triangles(polygon, "NONZERO")
area = sum(
abs(
self.image_edit_common._triangle_cross(
@@ -1788,9 +1865,10 @@ class AddonTest(unittest.TestCase):
self.assertNotIn("image_perspective_aspect_ratio", settings)
self.assertNotIn("aspect_mode", operator.__annotations__)
self.assertIn("aspect_ratio", operator.__annotations__)
self.assertNotIn("refine_selection", operator.__annotations__)
self.assertNotIn("refine_region", operator.__annotations__)
self.assertNotIn("continuous", operator.__annotations__)
self.assertNotIn("padding", operator.__annotations__)
def test_image_box_builds_a_rectangular_screen_path(self):
self.assertEqual(
self.image_gesture.box_screen_path((2, 3), (8, 9)),
@@ -1830,9 +1908,7 @@ class AddonTest(unittest.TestCase):
return_value=polygon,
),
):
result = self.image_gesture.ImageRegionGesture._complete(
operator, context
)
result = self.image_gesture.ImageRegionGesture._complete(operator, context)
self.assertEqual(result, {"FINISHED"})
self.assertEqual(
@@ -1854,12 +1930,12 @@ class AddonTest(unittest.TestCase):
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',
gesture="LASSO",
invert=False,
_path=[polygon[0]],
_area_pointer=1,
_region_pointer=2,
preview_fill_rule='NONZERO',
preview_fill_rule="NONZERO",
_fill_polygon=None,
_fill_triangles=(),
_outline_polygon=None,
@@ -1867,33 +1943,33 @@ class AddonTest(unittest.TestCase):
_screen_path=lambda: polygon,
)
with (
patch.object(self.image_gesture, 'drawing_in_region', return_value=True),
patch.object(self.image_gesture, "drawing_in_region", return_value=True),
patch.object(
self.image_gesture,
'dashed_line_segments',
return_value=('segments',),
"dashed_line_segments",
return_value=("segments",),
) as build_outline,
patch.object(
self.image_gesture,
'preview_fill_triangles',
return_value=('triangles',),
"preview_fill_triangles",
return_value=("triangles",),
) as build_fill,
patch.object(self.image_gesture, 'draw_polygon_fill') as draw_fill,
patch.object(self.image_gesture, 'draw_dashed_line') as draw_outline,
patch.object(self.image_gesture, "draw_polygon_fill") as draw_fill,
patch.object(self.image_gesture, "draw_dashed_line") as draw_outline,
):
self.image_gesture.ImageRegionGesture._draw_region(operator)
self.image_gesture.ImageRegionGesture._draw_region(operator)
build_fill.assert_called_once_with(polygon, 'NONZERO')
build_fill.assert_called_once_with(polygon, "NONZERO")
build_outline.assert_called_once_with((*polygon, polygon[0]))
self.assertEqual(draw_fill.call_count, 2)
draw_fill.assert_called_with(
polygon,
('triangles',),
("triangles",),
(1.0, 1.0, 1.0, 0.16),
)
self.assertEqual(draw_outline.call_count, 2)
draw_outline.assert_called_with((), segments=('segments',))
draw_outline.assert_called_with((), segments=("segments",))
def test_image_lasso_does_not_add_stationary_or_nearly_collinear_points(self):
path = [(0.0, 0.0)]
@@ -1912,8 +1988,7 @@ class AddonTest(unittest.TestCase):
previous_ops = getattr(self.fake_bpy, "ops", None)
self.fake_bpy.ops = SimpleNamespace(
wm=SimpleNamespace(
tool_set_by_id=lambda **options: calls.append(options)
or {"FINISHED"}
tool_set_by_id=lambda **options: calls.append(options) or {"FINISHED"}
)
)
try:
@@ -1935,9 +2010,7 @@ class AddonTest(unittest.TestCase):
endpoint = self.image_gesture.snapped_line_endpoint((0, 0), (10, 1), True)
self.assertAlmostEqual(math.degrees(math.atan2(endpoint[1], endpoint[0])), 5.0)
preview = self.image_gesture.line_half_plane_polygon(
(0, 5), (10, 5), (10, 10)
)
preview = self.image_gesture.line_half_plane_polygon((0, 5), (10, 5), (10, 10))
image = self.image_gesture.image_line_half_plane_polygon(
((0, 5), (10, 5)), (10, 10)
)
@@ -2191,5 +2264,6 @@ class AddonTest(unittest.TestCase):
self.assertEqual(calls[0][1]["text"], "Working")
self.assertEqual(calls[1], ("operator", "anyimage.cancel_job"))
if __name__ == "__main__":
unittest.main()