Files
AnyImage/tests/anyimage/operators/test_frame_tool.py
nan 938d95db0f Add new functionality for image rectification and depth metadata handling
- Implemented a preview tool for rectifying images with interactive aspect controls in `preview.py`.
- Added tests for depth metadata loading to ensure consistent schema usage in `test_depth_metadata.py`.
- Developed comprehensive tests for cutout calibration processes in `test_cutout_calibration.py`, including stability checks and depth calibration.
- Created tests for frame compositing operations in `test_frame_compositing.py`, ensuring correct handling of depth and transparency.
- Introduced material slot inheritance functionality in `material_slots.py` to manage geometry material assignments.
- Built shared shader nodes for material processing in `shader.py`, including roughness and bump mapping setups.
2026-09-12 19:20:06 +08:00

328 lines
11 KiB
Python

from types import SimpleNamespace
from unittest.mock import Mock, patch
from tests.support.blender import BlenderTestCase
class FrameToolTest(BlenderTestCase):
def test_frame_builds_a_canonical_rectangular_screen_path(self):
self.assertEqual(
self.image_interaction.rectangle_screen_path((2, 3), (8, 9)),
((2.0, 9.0), (8.0, 9.0), (8.0, 3.0), (2.0, 3.0)),
)
self.assertEqual(
self.image_interaction.rectangle_screen_path((2, 3), (3, 9)),
(),
)
def test_frame_reports_missing_active_overlap_as_warning(self):
active = SimpleNamespace(name="Active")
context = SimpleNamespace(
active_object=active,
region=object(),
region_data=SimpleNamespace(view_matrix="view-matrix"),
)
descriptor = {
"active": True,
}
with (
patch.object(self.image_frame, "_frame_source", return_value=descriptor),
patch.object(self.image_frame, "frame_source_overlap", return_value=()),
self.assertRaises(self.image_selection.ImageEditWarning),
):
self.image_frame.apply_frame(
context,
(active,),
((2, 3), (3, 3), (3, 2), (2, 2)),
)
def test_frame_uses_selected_still_images_and_ignores_other_objects(self):
active = SimpleNamespace(
name="Active",
type="EMPTY",
empty_display_type="IMAGE",
data=SimpleNamespace(source="FILE"),
)
other = SimpleNamespace(
name="Other",
type="EMPTY",
empty_display_type="IMAGE",
data=SimpleNamespace(source="FILE"),
)
mesh = SimpleNamespace(name="Mesh", type="MESH")
context = SimpleNamespace(
active_object=active,
selected_objects=(mesh, active, other),
)
self.assertEqual(
self.image_frame.selected_frame_sources(context),
(other, active),
)
def test_frame_rejects_active_image_that_is_not_selected(self):
active = SimpleNamespace(
name="Active",
type="EMPTY",
empty_display_type="IMAGE",
data=SimpleNamespace(source="FILE"),
)
other = SimpleNamespace(
name="Other",
type="EMPTY",
empty_display_type="IMAGE",
data=SimpleNamespace(source="FILE"),
)
context = SimpleNamespace(
active_object=active,
selected_objects=(other,),
)
with self.assertRaisesRegex(
self.image_frame.ImageEditWarning,
"Select an active Image Empty",
):
self.image_frame.selected_frame_sources(context)
def test_frame_warns_when_active_is_not_an_image(self):
image = SimpleNamespace(
name="Image",
type="EMPTY",
empty_display_type="IMAGE",
data=SimpleNamespace(source="FILE"),
)
mesh = SimpleNamespace(name="Mesh", type="MESH")
context = SimpleNamespace(
active_object=mesh,
selected_objects=(image, mesh),
area=SimpleNamespace(type="VIEW_3D"),
)
operator = self.image_frame.FrameImages()
operator.report = Mock()
self.assertTrue(operator.poll(context))
self.assertEqual(operator.invoke(context, object()), {"CANCELLED"})
operator.report.assert_called_once_with(
{"WARNING"},
"Select an active Image Empty to use an Image tool",
)
def test_frame_modal_can_start_outside_source_projection(self):
active = SimpleNamespace(name="Active")
other = SimpleNamespace(name="Other")
redraw = Mock()
status = Mock()
modal_handler_add = Mock()
context = SimpleNamespace(
active_object=active,
area=SimpleNamespace(as_pointer=lambda: 1, tag_redraw=redraw),
region=SimpleNamespace(as_pointer=lambda: 2),
workspace=SimpleNamespace(status_text_set=status),
window_manager=SimpleNamespace(modal_handler_add=modal_handler_add),
)
event = SimpleNamespace(mouse_region_x=-20, mouse_region_y=300)
space = SimpleNamespace(
draw_handler_add=Mock(return_value="handle"),
draw_handler_remove=Mock(),
)
operator = self.image_frame.FrameImages()
with (
patch.object(
self.image_frame,
"selected_frame_sources",
return_value=(other, active),
),
patch.object(
self.image_frame.bpy.types,
"SpaceView3D",
space,
create=True,
),
):
result = operator.invoke(context, event)
operator.cancel(context)
self.assertEqual(result, {"RUNNING_MODAL"})
self.assertEqual(operator._active_name, "Active")
self.assertEqual(operator._source_names, ("Other", "Active"))
self.assertEqual(operator._start, (-20.0, 300.0))
modal_handler_add.assert_called_once_with(operator)
space.draw_handler_remove.assert_called_once_with("handle", "WINDOW")
def test_frame_rejects_an_animated_selected_image(self):
active = SimpleNamespace(
name="Active",
type="EMPTY",
empty_display_type="IMAGE",
data=SimpleNamespace(source="FILE"),
)
video = SimpleNamespace(
name="Video",
type="EMPTY",
empty_display_type="IMAGE",
data=SimpleNamespace(source="MOVIE", frame_duration=2),
)
context = SimpleNamespace(
active_object=active,
selected_objects=(active, video),
)
with self.assertRaisesRegex(ValueError, "still images"):
self.image_frame.selected_frame_sources(context)
def test_frame_keeps_active_object_and_removes_other_image_empties(self):
import numpy as np
class MatrixStub:
translation = SimpleNamespace(copy=lambda: "active-origin")
def copy(self):
return "old-matrix"
active_image = Mock(users=1, use_fake_user=False)
other_image = Mock(users=1, use_fake_user=False)
active = SimpleNamespace(
name="Active",
data=active_image,
matrix_world=MatrixStub(),
empty_display_size=1.0,
empty_image_offset=(-0.5, -0.5),
)
other = SimpleNamespace(name="Other", data=other_image)
context = SimpleNamespace(
active_object=active,
region=SimpleNamespace(width=100, height=100),
region_data=SimpleNamespace(view_matrix="view-matrix"),
)
frame = ((0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0))
descriptors = iter(
(
{
"active": False,
"image_size": (2, 2),
},
{
"active": True,
"image_size": (2, 2),
},
)
)
result_image = Mock(users=1)
remove_object = Mock()
def replace(source, result, *, removed_objects):
self.assertEqual(removed_objects, (other,))
source.data = result
with (
patch.object(self.image_frame, "_frame_source", side_effect=descriptors),
patch.object(
self.image_frame,
"frame_source_overlap",
return_value=frame,
),
patch.object(self.image_frame, "frame_output_size", return_value=(2, 2)),
patch.object(
self.image_frame,
"composite_frame_pixels",
return_value=np.zeros(16, dtype=np.float32),
),
patch.object(
self.image_frame,
"frame_result_transform",
return_value=("new-matrix", 2.0),
),
patch.object(
self.image_frame,
"frame_depth_location",
return_value="active-origin",
),
patch.object(
self.image_frame,
"create_image_edit_result",
return_value=result_image,
),
patch.object(self.image_frame, "replace_empty_image", side_effect=replace),
patch.object(
self.image_frame.bpy.data,
"objects",
SimpleNamespace(remove=remove_object),
),
patch.object(
self.image_frame.bpy.data,
"images",
SimpleNamespace(remove=Mock()),
create=True,
),
):
result = self.image_frame.apply_frame(
context,
(other, active),
frame,
)
self.assertIs(result, active)
self.assertIs(active.data, result_image)
self.assertEqual(active.matrix_world, "new-matrix")
self.assertEqual(active.empty_display_size, 2.0)
self.assertEqual(active.empty_image_offset, (-0.5, -0.5))
remove_object.assert_called_once_with(other, do_unlink=True)
def test_frame_invalid_result_depth_does_not_create_or_replace_image(self):
import numpy as np
active_image = Mock(users=1, use_fake_user=False)
active = SimpleNamespace(
name="Active",
data=active_image,
matrix_world=SimpleNamespace(
translation=SimpleNamespace(copy=lambda: "active-origin")
),
)
context = SimpleNamespace(
active_object=active,
region=SimpleNamespace(width=100, height=100),
region_data=SimpleNamespace(view_matrix="view-matrix"),
)
frame = ((0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0))
descriptor = {"active": True, "image_size": (2, 2)}
create_result = Mock()
replace_result = Mock()
with (
patch.object(self.image_frame, "_frame_source", return_value=descriptor),
patch.object(self.image_frame, "frame_source_overlap", return_value=frame),
patch.object(self.image_frame, "frame_output_size", return_value=(2, 2)),
patch.object(
self.image_frame,
"composite_frame_pixels",
return_value=np.zeros(16, dtype=np.float32),
),
patch.object(
self.image_frame,
"frame_depth_location",
side_effect=ValueError("The Frame result depth is invalid"),
),
patch.object(
self.image_frame,
"create_image_edit_result",
create_result,
),
patch.object(self.image_frame, "replace_empty_image", replace_result),
self.assertRaisesRegex(ValueError, "result depth"),
):
self.image_frame.apply_frame(context, (active,), frame)
create_result.assert_not_called()
replace_result.assert_not_called()
self.assertIs(active.data, active_image)