Add comprehensive tests for edge depth extension, empty image edits, and material view adaptation
- Introduced `test_edge_depth.py` to validate the functionality of edge depth extension, ensuring core alpha preservation, proper depth handling, and edge padding behavior. - Created `test_empty_image_edits.py` to verify the behavior of image edits in Blender, focusing on shared edits, preservation of image properties, and handling of failed operations. - Developed `test_material_view_adaptation.py` to test material adaptation preferences, ensuring color space and alpha settings are correctly applied and preserved across various scenarios.
This commit is contained in:
@@ -340,7 +340,7 @@ def image_path(image):
|
||||
filepath_from_user = getattr(image, "filepath_from_user", None)
|
||||
filepath = (
|
||||
filepath_from_user()
|
||||
if callable(filepath_from_user)
|
||||
if callable(filepath_from_user) and getattr(image, "source", None) != "SEQUENCE"
|
||||
else getattr(image, "filepath", "")
|
||||
)
|
||||
if not filepath:
|
||||
@@ -558,7 +558,7 @@ def configure_image_user(
|
||||
|
||||
def pack_sequence_if_possible(image, expected_frames):
|
||||
"""Pack a frame sequence when Blender supports it; otherwise keep external files."""
|
||||
if expected_frames <= 1:
|
||||
if expected_frames <= 1 or not image.has_data:
|
||||
return False
|
||||
try:
|
||||
image.pack()
|
||||
@@ -655,71 +655,149 @@ def create_image_edit_result(source_image, pixels, output_size):
|
||||
return image
|
||||
|
||||
|
||||
def replace_empty_image(source_object, result_image, *, placement_bounds=None):
|
||||
def has_other_image_empty(source_object, *, removed_objects=()):
|
||||
"""Check surviving image Empty objects for the same Image datablock."""
|
||||
return any(
|
||||
candidate != source_object
|
||||
and candidate not in removed_objects
|
||||
and is_image_empty(candidate)
|
||||
and candidate.data == source_object.data
|
||||
for candidate in bpy.data.objects
|
||||
)
|
||||
|
||||
|
||||
def image_content_state(image):
|
||||
"""Capture encoded content and unsaved pixels for an Image transaction."""
|
||||
import numpy as np
|
||||
|
||||
pixels = None
|
||||
if image.is_dirty:
|
||||
pixels = np.empty(len(image.pixels), dtype=np.float32)
|
||||
image.pixels.foreach_get(pixels)
|
||||
return {
|
||||
"source": image.source,
|
||||
"filepath": image.filepath_raw,
|
||||
"colorspace": image.colorspace_settings.name,
|
||||
"alpha_mode": image.alpha_mode,
|
||||
"file_format": image.file_format,
|
||||
"packed": bytes(image.packed_file.data) if image.packed_file else None,
|
||||
"pixels": pixels,
|
||||
"size": tuple(image.size),
|
||||
"generated": {
|
||||
name: getattr(image, name)
|
||||
for name in ("generated_width", "generated_height", "generated_type",
|
||||
"use_generated_float")
|
||||
},
|
||||
"generated_color": tuple(image.generated_color),
|
||||
}
|
||||
|
||||
|
||||
def restore_image_content(image, state):
|
||||
"""Apply captured image content while retaining the destination ID."""
|
||||
if image.packed_file:
|
||||
image.source = "FILE"
|
||||
image.unpack(method="REMOVE")
|
||||
image.filepath_raw = state["filepath"]
|
||||
image.source = state["source"]
|
||||
if state["source"] == "GENERATED":
|
||||
for name, value in state["generated"].items():
|
||||
setattr(image, name, value)
|
||||
image.generated_color = state["generated_color"]
|
||||
image.colorspace_settings.name = state["colorspace"]
|
||||
image.alpha_mode = state["alpha_mode"]
|
||||
image.file_format = state["file_format"]
|
||||
if state["packed"] is not None:
|
||||
data = state["packed"]
|
||||
image.source = "FILE"
|
||||
image.pack(data=data, data_len=len(data))
|
||||
image.source = state["source"]
|
||||
image.reload()
|
||||
if state["pixels"] is not None:
|
||||
if tuple(image.size) != state["size"]:
|
||||
image.scale(*state["size"])
|
||||
image.pixels.foreach_set(state["pixels"])
|
||||
image.update()
|
||||
|
||||
|
||||
def replace_empty_image(source_object, result_image, *, placement_bounds=None,
|
||||
removed_objects=()):
|
||||
"""Replace one Empty image, retaining shared source images."""
|
||||
source_image = source_object.data
|
||||
source_name = source_image.name
|
||||
original_fake_user = source_image.use_fake_user
|
||||
original_blending = getattr(source_object, "use_empty_image_alpha", False)
|
||||
image_user = getattr(source_object, "image_user", None)
|
||||
original_timing = image_user_settings(source_object) if image_user is not None else None
|
||||
frame_count = (
|
||||
video_total_frames(result_image)
|
||||
if is_animated_image(result_image)
|
||||
else None
|
||||
)
|
||||
alignment = (
|
||||
empty_display_alignment(
|
||||
source_object,
|
||||
tuple(source_image.size),
|
||||
tuple(result_image.size),
|
||||
placement_bounds,
|
||||
)
|
||||
if placement_bounds is not None
|
||||
else None
|
||||
)
|
||||
if frame_count is not None:
|
||||
(
|
||||
original_frame_start,
|
||||
_original_frame_offset,
|
||||
_original_frame_duration,
|
||||
original_use_cyclic,
|
||||
) = image_user_settings(source_object)
|
||||
|
||||
source_image.use_fake_user = True
|
||||
original_alignment = (
|
||||
(source_object.empty_display_size, tuple(source_object.empty_image_offset))
|
||||
if alignment is not None else None
|
||||
)
|
||||
try:
|
||||
source_object.data = result_image
|
||||
source_object.use_empty_image_alpha = True
|
||||
if frame_count is not None:
|
||||
configure_image_user(
|
||||
image_user,
|
||||
frame_count,
|
||||
frame_start=original_frame_start,
|
||||
use_cyclic=original_use_cyclic,
|
||||
source_image = source_object.data
|
||||
original_fake_user = source_image.use_fake_user
|
||||
original_blending = getattr(source_object, "use_empty_image_alpha", False)
|
||||
image_user = getattr(source_object, "image_user", None)
|
||||
original_timing = image_user_settings(source_object) if image_user is not None else None
|
||||
original_auto_refresh = getattr(image_user, "use_auto_refresh", False)
|
||||
frame_count = (
|
||||
video_total_frames(result_image)
|
||||
if is_animated_image(result_image)
|
||||
else None
|
||||
)
|
||||
alignment = (
|
||||
empty_display_alignment(
|
||||
source_object,
|
||||
tuple(source_image.size),
|
||||
tuple(result_image.size),
|
||||
placement_bounds,
|
||||
)
|
||||
if alignment is not None:
|
||||
source_object.empty_display_size = alignment[0]
|
||||
source_object.empty_image_offset = alignment[1]
|
||||
except Exception:
|
||||
source_object.data = source_image
|
||||
source_object.use_empty_image_alpha = original_blending
|
||||
if original_alignment is not None:
|
||||
source_object.empty_display_size, source_object.empty_image_offset = original_alignment
|
||||
if original_timing is not None:
|
||||
(image_user.frame_start, image_user.frame_offset,
|
||||
image_user.frame_duration, image_user.use_cyclic) = original_timing
|
||||
source_image.use_fake_user = original_fake_user
|
||||
raise
|
||||
if placement_bounds is not None
|
||||
else None
|
||||
)
|
||||
if frame_count is not None:
|
||||
(
|
||||
original_frame_start,
|
||||
_original_frame_offset,
|
||||
_original_frame_duration,
|
||||
original_use_cyclic,
|
||||
) = image_user_settings(source_object)
|
||||
|
||||
source_image.use_fake_user = original_fake_user
|
||||
if not original_fake_user and int(source_image.users) == 0:
|
||||
bpy.data.images.remove(source_image, do_unlink=True)
|
||||
result_image.name = source_name
|
||||
return result_image
|
||||
source_image.use_fake_user = True
|
||||
original_alignment = (
|
||||
(source_object.empty_display_size, tuple(source_object.empty_image_offset))
|
||||
if alignment is not None else None
|
||||
)
|
||||
original_content = None
|
||||
try:
|
||||
shared = has_other_image_empty(source_object, removed_objects=removed_objects)
|
||||
if shared:
|
||||
final_image = result_image
|
||||
else:
|
||||
result_content = image_content_state(result_image)
|
||||
original_content = image_content_state(source_image)
|
||||
restore_image_content(source_image, result_content)
|
||||
final_image = source_image
|
||||
source_object.data = final_image
|
||||
source_object.use_empty_image_alpha = True
|
||||
if frame_count is not None:
|
||||
configure_image_user(
|
||||
image_user,
|
||||
frame_count,
|
||||
frame_start=original_frame_start,
|
||||
use_cyclic=original_use_cyclic,
|
||||
)
|
||||
if alignment is not None:
|
||||
source_object.empty_display_size = alignment[0]
|
||||
source_object.empty_image_offset = alignment[1]
|
||||
except Exception:
|
||||
if original_content is not None:
|
||||
restore_image_content(source_image, original_content)
|
||||
source_object.data = source_image
|
||||
source_object.use_empty_image_alpha = original_blending
|
||||
if original_alignment is not None:
|
||||
source_object.empty_display_size, source_object.empty_image_offset = original_alignment
|
||||
if original_timing is not None:
|
||||
(image_user.frame_start, image_user.frame_offset,
|
||||
image_user.frame_duration, image_user.use_cyclic) = original_timing
|
||||
image_user.use_auto_refresh = original_auto_refresh
|
||||
source_image.use_fake_user = original_fake_user
|
||||
raise
|
||||
|
||||
source_image.use_fake_user = original_fake_user
|
||||
return final_image
|
||||
finally:
|
||||
if result_image.users == 0:
|
||||
bpy.data.images.remove(result_image)
|
||||
|
||||
|
||||
def _load_packed_result_image(
|
||||
|
||||
Reference in New Issue
Block a user