- 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.
234 lines
7.6 KiB
Python
234 lines
7.6 KiB
Python
import tempfile
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
from blender_test_support import BlenderTestCase
|
|
|
|
|
|
class ImageEditToolTest(BlenderTestCase):
|
|
def test_image_edit_tools_share_the_plain_pixel_edit_result_creator(self):
|
|
self.assertIs(
|
|
self.edit_mask.create_image_edit_result,
|
|
self.image_data.create_image_edit_result,
|
|
)
|
|
self.assertIs(
|
|
self.rectify.create_image_edit_result,
|
|
self.image_data.create_image_edit_result,
|
|
)
|
|
self.assertFalse(hasattr(self.image_data, "create_pixel_result_image"))
|
|
|
|
|
|
def test_packed_image_export_does_not_use_scene_render_transform(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
output_directory = Path(directory)
|
|
calls = []
|
|
|
|
class Image:
|
|
file_format = "JPEG"
|
|
|
|
def save(self, **options):
|
|
calls.append(options)
|
|
Path(options["filepath"]).write_bytes(b"raw image")
|
|
|
|
image = Image()
|
|
scene = SimpleNamespace(
|
|
view_settings=SimpleNamespace(look="AgX - Medium High Contrast")
|
|
)
|
|
with patch.object(
|
|
self.image_data.tempfile,
|
|
"mkdtemp",
|
|
return_value=str(output_directory),
|
|
):
|
|
output = self.image_data.save_packed_image(image, scene)
|
|
|
|
self.assertEqual(output, output_directory / "input.png")
|
|
self.assertEqual(
|
|
calls,
|
|
[{"filepath": str(output), "save_copy": True}],
|
|
)
|
|
self.assertEqual(image.file_format, "JPEG")
|
|
|
|
|
|
def test_expected_empty_image_edits_are_reported_as_warnings(self):
|
|
reports = []
|
|
operator = SimpleNamespace(
|
|
report=lambda level, message: reports.append((level, message))
|
|
)
|
|
|
|
self.image_interaction.report_image_edit_exception(
|
|
operator,
|
|
self.image_interaction.ImageEditWarning(
|
|
"The edit contains no visible image pixels"
|
|
),
|
|
)
|
|
self.image_interaction.report_image_edit_exception(
|
|
operator,
|
|
ValueError("Unexpected invalid data"),
|
|
)
|
|
|
|
self.assertEqual(
|
|
reports,
|
|
[
|
|
(
|
|
{"WARNING"},
|
|
"The edit contains no visible image pixels",
|
|
),
|
|
({"ERROR"}, "Unexpected invalid data"),
|
|
],
|
|
)
|
|
|
|
|
|
def test_image_edit_first_click_selects_another_image_without_editing(self):
|
|
selected = []
|
|
|
|
def image(name):
|
|
return SimpleNamespace(
|
|
name=name,
|
|
type="EMPTY",
|
|
empty_display_type="IMAGE",
|
|
data=object(),
|
|
select_set=lambda value: selected.append((name, value)),
|
|
)
|
|
|
|
active = image("Active")
|
|
clicked = image("Clicked")
|
|
context = SimpleNamespace(
|
|
active_object=active,
|
|
selected_objects=(active,),
|
|
view_layer=SimpleNamespace(objects=SimpleNamespace(active=active)),
|
|
)
|
|
event = SimpleNamespace(mouse_region_x=10, mouse_region_y=20)
|
|
previous_ops = getattr(self.fake_bpy, "ops", None)
|
|
previous_objects = self.fake_bpy.data.objects
|
|
|
|
def select(**_kwargs):
|
|
context.active_object = clicked
|
|
|
|
self.fake_bpy.ops = SimpleNamespace(
|
|
view3d=SimpleNamespace(select=select),
|
|
object=SimpleNamespace(select_all=lambda **_kwargs: None),
|
|
)
|
|
self.fake_bpy.data.objects = {
|
|
active.name: active,
|
|
clicked.name: clicked,
|
|
}
|
|
try:
|
|
source, should_edit = self.image_interaction.resolve_image_edit_click(
|
|
context, event, lambda *_args: None
|
|
)
|
|
finally:
|
|
self.fake_bpy.data.objects = previous_objects
|
|
if previous_ops is None:
|
|
del self.fake_bpy.ops
|
|
else:
|
|
self.fake_bpy.ops = previous_ops
|
|
|
|
self.assertIs(source, clicked)
|
|
self.assertFalse(should_edit)
|
|
self.assertEqual(selected, [])
|
|
|
|
|
|
def test_image_edit_first_click_selects_another_object_without_editing(self):
|
|
active = SimpleNamespace(
|
|
name="Active",
|
|
type="EMPTY",
|
|
empty_display_type="IMAGE",
|
|
data=object(),
|
|
)
|
|
clicked = SimpleNamespace(name="Mesh", type="MESH", data=object())
|
|
context = SimpleNamespace(
|
|
active_object=active,
|
|
selected_objects=(active,),
|
|
)
|
|
event = SimpleNamespace(mouse_region_x=10, mouse_region_y=20)
|
|
previous_ops = getattr(self.fake_bpy, "ops", None)
|
|
select_options = []
|
|
|
|
def select(**options):
|
|
select_options.append(options)
|
|
context.active_object = clicked
|
|
|
|
self.fake_bpy.ops = SimpleNamespace(
|
|
view3d=SimpleNamespace(select=select),
|
|
)
|
|
try:
|
|
source, should_edit = self.image_interaction.resolve_image_edit_click(
|
|
context, event, lambda *_args: None
|
|
)
|
|
finally:
|
|
if previous_ops is None:
|
|
del self.fake_bpy.ops
|
|
else:
|
|
self.fake_bpy.ops = previous_ops
|
|
|
|
self.assertIs(source, clicked)
|
|
self.assertFalse(should_edit)
|
|
self.assertEqual(select_options[0]["deselect_all"], False)
|
|
|
|
|
|
def test_image_edit_empty_click_keeps_active_image_and_starts_editing(self):
|
|
active = SimpleNamespace(
|
|
name="Active",
|
|
type="EMPTY",
|
|
empty_display_type="IMAGE",
|
|
data=object(),
|
|
)
|
|
context = SimpleNamespace(
|
|
active_object=active,
|
|
selected_objects=(active,),
|
|
)
|
|
event = SimpleNamespace(mouse_region_x=10, mouse_region_y=20)
|
|
previous_ops = getattr(self.fake_bpy, "ops", None)
|
|
select_options = []
|
|
self.fake_bpy.ops = SimpleNamespace(
|
|
view3d=SimpleNamespace(
|
|
select=lambda **options: select_options.append(options)
|
|
),
|
|
)
|
|
try:
|
|
source, should_edit = self.image_interaction.resolve_image_edit_click(
|
|
context, event, lambda *_args: None
|
|
)
|
|
finally:
|
|
if previous_ops is None:
|
|
del self.fake_bpy.ops
|
|
else:
|
|
self.fake_bpy.ops = previous_ops
|
|
|
|
self.assertIs(source, active)
|
|
self.assertTrue(should_edit)
|
|
self.assertIs(context.active_object, active)
|
|
self.assertEqual(select_options[0]["deselect_all"], False)
|
|
|
|
|
|
def test_image_edit_source_requires_active_image_to_be_selected(self):
|
|
active = SimpleNamespace(
|
|
name="Active",
|
|
type="EMPTY",
|
|
empty_display_type="IMAGE",
|
|
data=object(),
|
|
)
|
|
context = SimpleNamespace(
|
|
active_object=active,
|
|
selected_objects=(),
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
self.image_interaction.ImageEditWarning,
|
|
"Select an active Image Empty",
|
|
):
|
|
self.image_interaction.active_image_edit_source(context)
|
|
|
|
|
|
def test_blender_image_exports_do_not_use_scene_render_color_management(self):
|
|
source_root = PROJECT_ROOT / "src" / "anyimage"
|
|
offenders = []
|
|
for path in source_root.rglob("*.py"):
|
|
if ".save_render(" in path.read_text(encoding="utf-8"):
|
|
offenders.append(path.relative_to(source_root))
|
|
self.assertEqual(offenders, [])
|