from types import SimpleNamespace from unittest.mock import Mock, patch from blender_test_support 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_output_size_uses_viewport_pixels(self): frame = ( (-50.0, 150.0), (150.0, 150.0), (150.0, -50.0), (-50.0, -50.0), ) self.assertEqual( self.image_frame.frame_output_size( frame, (1000, 800), 2048, ), (200, 200), ) def test_frame_output_size_obeys_region_and_preference_limits(self): frame = ( (0.0, 1000.0), (2000.0, 1000.0), (2000.0, 0.0), (0.0, 0.0), ) self.assertEqual( self.image_frame.frame_output_size(frame, (1000, 800), 2048), (1000, 500), ) self.assertEqual( self.image_frame.frame_output_size(frame, (4000, 2000), 500), (500, 250), ) def test_frame_requires_positive_valid_projection_overlap(self): import numpy as np source = self.image_frame.frame_source_projection( np.identity(4), np.identity(4), np.identity(4), (-1.0, 1.0, -1.0, 1.0), (100, 100), perspective=False, ) self.assertTrue( self.image_frame.frame_source_overlap( source, ((50.0, 150.0), (150.0, 150.0), (150.0, 50.0), (50.0, 50.0)), ) ) self.assertFalse( self.image_frame.frame_source_overlap( source, ((100.0, 100.0), (200.0, 100.0), (200.0, 0.0), (100.0, 0.0)), ) ) def test_frame_orthographic_projection_accepts_non_positive_depth(self): import numpy as np matrix_world = np.identity(4) matrix_world[2, 3] = 1.0 source = self.image_frame.frame_source_projection( np.identity(4), np.identity(4), matrix_world, (-1.0, 1.0, -1.0, 1.0), (100, 100), perspective=False, ) source["image_size"] = (100, 100) points, depth, valid, normalized = self.image_frame.frame_source_coordinates( source, np.asarray(((25.0, 75.0),)), ) np.testing.assert_allclose(points[0], (25.0, 25.0)) np.testing.assert_allclose(normalized[0], (0.25, 0.25)) self.assertEqual(depth[0], -1.0) self.assertTrue(valid[0]) self.assertTrue( self.image_frame.frame_source_overlap( source, ((0.0, 100.0), (100.0, 100.0), (100.0, 0.0), (0.0, 0.0)), ) ) def test_frame_rejects_an_orthographic_plane_parallel_to_view_rays(self): import numpy as np matrix_world = np.asarray( ( (0.0, 0.0, 1.0, 0.0), (0.0, 1.0, 0.0, 0.0), (-1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0), ) ) with self.assertRaisesRegex(ValueError, "edge-on"): self.image_frame.frame_source_projection( np.identity(4), np.identity(4), matrix_world, (-1.0, 1.0, -1.0, 1.0), (100, 100), perspective=False, ) def test_frame_perspective_projection_samples_front_of_crossing_image(self): import numpy as np projection = np.identity(4) projection[3] = (0.0, 0.0, -1.0, 0.0) matrix_world = np.identity(4) matrix_world[0, 3] = 1.0 matrix_world[2, 0] = 1.0 matrix_world[2, 3] = 0.2 source = self.image_frame.frame_source_projection( projection, np.identity(4), matrix_world, (-1.0, 1.0, -1.0, 1.0), (100, 100), perspective=True, ) source["image_size"] = (100, 100) front_points, front_depth, front_valid, _normalized = ( self.image_frame.frame_source_coordinates( source, np.asarray(((75.0, 50.0),)), ) ) _rear_points, rear_depth, rear_valid, _normalized = ( self.image_frame.frame_source_coordinates( source, np.asarray(((-50.0, 50.0),)), ) ) overlap = self.image_frame.frame_source_overlap( source, ((50.0, 100.0), (100.0, 100.0), (100.0, 0.0), (50.0, 0.0)), ) self.assertTrue(front_valid[0]) self.assertGreater(front_depth[0], 0.0) self.assertGreaterEqual(front_points[0, 0], 0.0) self.assertFalse(rear_valid[0]) self.assertLess(rear_depth[0], 0.0) self.assertTrue(overlap) _parallel_points, _parallel_depth, parallel_valid, _normalized = ( self.image_frame.frame_source_coordinates( source, np.asarray(((0.0, 50.0),)), ) ) self.assertFalse(parallel_valid[0]) def test_frame_perspective_projection_accepts_extremely_close_image(self): import numpy as np projection = np.identity(4) projection[3] = (0.0, 0.0, -1.0, 0.0) matrix_world = np.identity(4) matrix_world[2, 3] = -1e-14 source = self.image_frame.frame_source_projection( projection, np.identity(4), matrix_world, (-1.0, 1.0, -1.0, 1.0), (100, 100), perspective=True, ) source["image_size"] = (100, 100) _points, depth, valid, _normalized = self.image_frame.frame_source_coordinates( source, np.asarray(((50.0, 50.0),)), ) self.assertTrue(valid[0]) self.assertEqual(depth[0], 1e-14) def test_frame_overlap_does_not_depend_on_output_pixel_centers(self): import numpy as np source = self.image_frame.frame_source_projection( np.identity(4), np.identity(4), np.identity(4), (-1.0, 1.0, -1.0, 1.0), (100, 100), perspective=False, ) self.assertTrue( self.image_frame.frame_source_overlap( source, ( (99.999, 60.0), (100.001, 60.0), (100.001, 40.0), (99.999, 40.0), ), ) ) def test_frame_uses_crossing_active_overlap_for_visible_result_depth(self): import numpy as np projection = np.identity(4) projection[3] = (0.0, 0.0, -1.0, 0.0) matrix_world = np.identity(4) matrix_world[0, 3] = 1.0 matrix_world[2, 0] = 1.0 matrix_world[2, 3] = 0.2 source = self.image_frame.frame_source_projection( projection, np.identity(4), matrix_world, (-1.0, 1.0, -1.0, 1.0), (100, 100), perspective=True, ) source.update( { "image_size": (100, 100), "object": SimpleNamespace(matrix_world=matrix_world), } ) frame = ((50.0, 100.0), (100.0, 100.0), (100.0, 0.0), (50.0, 0.0)) overlap = self.image_frame.frame_source_overlap(source, frame) depth_location = self.image_frame.frame_depth_location(source, overlap) self.assertLess(depth_location[2], 0.0) def test_frame_keeps_positive_active_origin_as_result_depth_location(self): import numpy as np marker = SimpleNamespace(copy=lambda: "active-origin") source = { "object": SimpleNamespace( matrix_world=SimpleNamespace(translation=marker), ), "origin_depth": 1e-14, "perspective": True, } self.assertEqual( self.image_frame.frame_depth_location(source, ((0.0, 0.0),)), "active-origin", ) 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_geometry_faces_the_view_and_uses_frame_center(self): import numpy as np matrix, display_size = self.image_frame.view_facing_frame_geometry( ( (-2.0, 1.0, 5.0), (2.0, 1.0, 5.0), (2.0, -1.0, 5.0), (-2.0, -1.0, 5.0), ), (400, 200), ) np.testing.assert_allclose(matrix[:3, 0], (1.0, 0.0, 0.0)) np.testing.assert_allclose(matrix[:3, 1], (0.0, 1.0, 0.0)) np.testing.assert_allclose(matrix[:3, 2], (0.0, 0.0, 1.0)) np.testing.assert_allclose(matrix[:3, 3], (0.0, 0.0, 5.0)) self.assertEqual(display_size, 4.0) def test_frame_composites_far_to_near_with_active_on_top(self): import numpy as np frame = ((0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)) def source(name, color, depth, active=False): pixels = np.empty((2, 2, 4), dtype=np.float32) pixels[:] = color return { "active": active, "image_size": (2, 2), "name": name, "perspective": True, "pixels": pixels, "rgba": self.image_data.straight_rgba(pixels), "region_size": (2.0, 2.0), "screen_to_source": np.asarray( ((1.0, 0.0, 0.0), (0.0, -1.0, 1.0), (0.0, 0.0, 1.0)) ), "source_to_view": np.asarray( ( (2.0, 0.0, -1.0), (0.0, -2.0, 1.0), (0.0, 0.0, -depth), (0.0, 0.0, 1.0), ) ), } far = source("Far", (1.0, 0.0, 0.0, 1.0), 2.0) active = source("Active", (0.0, 0.0, 0.5, 0.5), 1.0, active=True) pixels = self.image_frame.composite_frame_pixels( (active, far), frame, (2, 2) ) result = np.flipud(np.asarray(pixels).reshape((2, 2, 4))) np.testing.assert_allclose(result[:, :, 0], 0.5, atol=1e-6) np.testing.assert_allclose(result[:, :, 2], 0.5, atol=1e-6) np.testing.assert_allclose(result[:, :, 3], 1.0, atol=1e-6) def test_frame_puts_active_above_an_equal_depth_source(self): import numpy as np frame = ((0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)) screen_to_source = np.asarray( ((1.0, 0.0, 0.0), (0.0, -1.0, 1.0), (0.0, 0.0, 1.0)) ) def source(name, color, active): pixels = np.empty((2, 2, 4), dtype=np.float32) pixels[:] = color return { "active": active, "image_size": (2, 2), "name": name, "perspective": True, "pixels": pixels, "rgba": pixels.copy(), "region_size": (2.0, 2.0), "screen_to_source": screen_to_source, "source_to_view": np.asarray( ( (2.0, 0.0, -1.0), (0.0, -2.0, 1.0), (0.0, 0.0, -1.0), (0.0, 0.0, 1.0), ) ), } pixels = self.image_frame.composite_frame_pixels( ( source("Active", (0.0, 0.0, 1.0, 1.0), True), source("Other", (1.0, 0.0, 0.0, 1.0), False), ), frame, (2, 2), ) result = np.asarray(pixels).reshape((2, 2, 4)) np.testing.assert_allclose(result[:, :, 2], 1.0, atol=1e-6) np.testing.assert_allclose(result[:, :, 0], 0.0, atol=1e-6) def test_frame_composites_orthographic_samples_at_negative_depth(self): import numpy as np pixels = np.empty((2, 2, 4), dtype=np.float32) pixels[:] = (0.25, 0.5, 0.75, 1.0) source = { "active": True, "image_size": (2, 2), "name": "Active", "perspective": False, "pixels": pixels, "rgba": pixels.copy(), "region_size": (2.0, 2.0), "screen_to_source": np.asarray( ((1.0, 0.0, 0.0), (0.0, -1.0, 1.0), (0.0, 0.0, 1.0)) ), "source_to_view": np.asarray( ( (2.0, 0.0, -1.0), (0.0, -2.0, 1.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), ) ), } result = self.image_frame.composite_frame_pixels( (source,), ((0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)), (2, 2), ) np.testing.assert_allclose( np.asarray(result).reshape((2, 2, 4)), pixels, atol=1e-6, ) def test_frame_preserves_masked_rgb_and_can_restore_alpha(self): import numpy as np rgba = np.random.default_rng(7).random((4, 4, 4)).astype(np.float32) mask = self.image_selection.SelectionMask(np.ones((4, 4)), (0, 0, 4, 4)) removed = self.edit_mask.apply_alpha_mask(rgba, mask, "SUBTRACT") source = self._frame_pixel_source(np.flipud(removed.reshape(rgba.shape))) framed = self.image_frame.composite_frame_pixels( (source,), ((0, 4), (4, 4), (4, 0), (0, 0)), (4, 4) ) result = np.flipud(framed.reshape(rgba.shape)) np.testing.assert_allclose(result[..., :3], rgba[..., :3]) np.testing.assert_array_equal(result[..., 3], 0) restored = self.edit_mask.apply_alpha_mask(result, mask, "ADD") restored = np.flipud(restored.reshape(rgba.shape)) np.testing.assert_allclose(restored[..., :3], rgba[..., :3]) np.testing.assert_array_equal(restored[..., 3], 1) def test_frame_hidden_rgb_uses_bottom_valid_depth_and_stable_ties(self): import numpy as np blue = np.tile((0, 0, 1, 0), (2, 2, 1)) red = np.tile((1, 0, 0, 0), (2, 2, 1)) frame = ((0, 2), (2, 2), (2, 0), (0, 0)) for depth in (2.0, 1.0, -1.0): with self.subTest(depth=depth): bottom = self._frame_pixel_source(blue, depth=depth, name="A", active=False) top = self._frame_pixel_source(red, depth=min(depth, 1.0)) result = self.image_frame.composite_frame_pixels((top, bottom), frame, (2, 2)) np.testing.assert_allclose(result.reshape((2, 2, 4)), blue) middle = self._frame_pixel_source(red, name="B", active=False) bottom = self._frame_pixel_source(blue, name="A", active=False) top = self._frame_pixel_source(red) result = self.image_frame.composite_frame_pixels((middle, top, bottom), frame, (2, 2)) np.testing.assert_allclose(result.reshape((2, 2, 4)), blue) bottom["screen_to_source"][0, 2] += 0.5 result = np.flipud(self.image_frame.composite_frame_pixels( (top, bottom), frame, (2, 2) ).reshape((2, 2, 4))) np.testing.assert_allclose(result[:, 0], blue[:, 0]) np.testing.assert_allclose(result[:, 1], red[:, 1]) bottom["perspective"] = True bottom["source_to_view"][2, 2] = 1.0 result = self.image_frame.composite_frame_pixels((top, bottom), frame, (2, 2)) np.testing.assert_allclose(result.reshape((2, 2, 4)), red) def test_frame_hidden_rgb_tracks_crossing_planes(self): import numpy as np bottom = self._frame_pixel_source(np.tile((0, 0, 1, 0), (2, 2, 1)), active=False) top = self._frame_pixel_source(np.tile((1, 0, 0, 0), (2, 2, 1))) bottom["source_to_view"][2] = (2, 0, -2) result = np.flipud(self.image_frame.composite_frame_pixels( (top, bottom), ((0, 2), (2, 2), (2, 0), (0, 0)), (2, 2) ).reshape((2, 2, 4))) np.testing.assert_allclose(result[:, 0], np.tile((0, 0, 1, 0), (2, 1))) np.testing.assert_allclose(result[:, 1], np.tile((1, 0, 0, 0), (2, 1))) def test_frame_blends_rgb_over_bottom_color_with_independent_alpha(self): import numpy as np for bottom_alpha in (0.0, 0.5, 1.0): blue = np.tile((0, 0, 1, bottom_alpha), (2, 5, 1)).astype(np.float32) red = np.tile((1, 0, 0, 0), (2, 5, 1)).astype(np.float32) red[..., 3] = (0, 0.25, 0.5, 0.75, 1) sources = (self._frame_pixel_source(blue, depth=2, active=False), self._frame_pixel_source(red)) result = np.flipud(self.image_frame.composite_frame_pixels( sources, ((0, 2), (5, 2), (5, 0), (0, 0)), (5, 2) ).reshape((2, 5, 4))) alpha = red[..., 3] + bottom_alpha * (1 - red[..., 3]) expected = np.zeros_like(red) expected[..., 0] = red[..., 3] expected[..., 2] = 1 - red[..., 3] np.testing.assert_allclose(result[..., 3], alpha) np.testing.assert_allclose(result[..., :3], expected[..., :3]) def test_frame_three_layers_preserve_foreground_gradient_over_white(self): import numpy as np white = np.tile((1, 1, 1, 0), (2, 6, 1)).astype(np.float32) red = np.tile((1, 0, 0, 0), (2, 6, 1)).astype(np.float32) red[..., 3] = (0, 0.01, 0.1, 0.25, 0.5, 1) blue = np.tile((0, 0, 1, 0.25), (2, 6, 1)).astype(np.float32) sources = ( self._frame_pixel_source(white, depth=3, active=False), self._frame_pixel_source(red, depth=2, active=False), self._frame_pixel_source(blue), ) result = np.flipud(self.image_frame.composite_frame_pixels( sources, ((0, 2), (6, 2), (6, 0), (0, 0)), (6, 2) ).reshape((2, 6, 4))) red_alpha = red[..., 3:4] expected = ((red[..., :3] * red_alpha + 1 - red_alpha) * 0.75 + blue[..., :3] * 0.25) np.testing.assert_allclose(result[..., :3], expected) np.testing.assert_allclose(result[..., 3], 0.25 + red[..., 3] * 0.75) def test_frame_foreground_resampling_excludes_hidden_black(self): import numpy as np white = self._frame_pixel_source( np.tile((1, 1, 1, 0), (1, 2, 1)), depth=2, active=False ) foreground = self._frame_pixel_source( np.asarray([[(1, 0, 0, 1), (0, 0, 0, 0)]]) ) result = self.image_frame.composite_frame_pixels( (foreground, white), ((0, 1), (2, 1), (2, 0), (0, 0)), (1, 1) ) np.testing.assert_allclose(result, (1, 0.5, 0.5, 0.5)) def test_frame_raw_sampling_and_empty_canvas_are_chunk_independent(self): import numpy as np rgba = np.tile((0, 1, 0, 0), (4, 4, 1)).astype(np.float32) rgba[1:3, 2:] = (1, 0, 0, 1) source = self._frame_pixel_source(rgba) frame = ((-3, 7), (7, 7), (7, -3), (-3, -3)) results = [] for rows in (1, 2, 256): with patch.object(self.image_frame, "PROJECTIVE_CHUNK_ROWS", rows): results.append(np.flipud(self.image_frame.composite_frame_pixels( (source,), frame, (10, 10) ).reshape((10, 10, 4)))) for result in results: np.testing.assert_array_equal(result, results[0]) np.testing.assert_allclose(result[3:7, 3:7], rgba) np.testing.assert_array_equal(result[4, 7], 0) np.testing.assert_array_equal(result[0], 0) transparent = np.asarray([[(1, 0, 0, 0), (0, 0, 1, 0)]], dtype=np.float32) source = self._frame_pixel_source(transparent) framed = self.image_frame.composite_frame_pixels( (source,), ((0, 1), (2, 1), (2, 0), (0, 0)), (1, 1) ) np.testing.assert_allclose(framed, (0.5, 0, 0.5, 0)) sampled = self.image_data.sample_rgba( transparent, np.asarray(((1, 0.5), (-1, 0.5))) ) np.testing.assert_allclose(sampled, ((0.5, 0, 0.5, 0), (0, 0, 0, 0))) transparent[0, 0, 3] = 1 source = self._frame_pixel_source(transparent) framed = self.image_frame.composite_frame_pixels( (source,), ((0, 1), (2, 1), (2, 0), (0, 0)), (1, 1) ) np.testing.assert_allclose(framed, (0.5, 0, 0.5, 0.5)) 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) def _frame_pixel_source(self, rgba, *, depth=1.0, name="Active", active=True): import numpy as np rgba = np.asarray(rgba, dtype=np.float32) height, width = rgba.shape[:2] world = np.identity(4) world[2, 3] = -depth source = self.image_frame.frame_source_projection( np.identity(4), np.identity(4), world, (-1.0, 1.0, -1.0, 1.0), (width, height), perspective=False, ) source.update( active=active, name=name, image_size=(width, height), rgba=rgba, pixels=self.image_data.premultiplied_rgba( np.flipud(rgba).ravel(), (width, height) ), ) return source