Files
AnyImage/tests/test_frame_tool.py
nan 4c902ed7db feat(node_assets): add image plane geometry nodes and validation scripts
- Implemented `image_plane.py` to build the Image Plane Geometry Nodes group with customizable parameters for geometry, subdivide, and thickness.
- Created `image_plane_validation.py` to validate the functionality of the image plane, ensuring correct geometry generation and material assignment.
- Developed `material_layers.py` to construct shader node groups for image materials, including depth and normal handling.
- Authored `plane_layouts.py` to define node arrangements for image geometry assets.
- Introduced `shadeless.py` to create a shadeless shader node group that displays color without scene lighting while preserving shadows and reflections.
2026-09-08 23:24:27 +08:00

911 lines
32 KiB
Python

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