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.
This commit is contained in:
2026-09-12 19:20:06 +08:00
parent e8268504c2
commit 938d95db0f
175 changed files with 3812 additions and 6864 deletions

View File

@@ -51,7 +51,7 @@ def test_shader_editor_pin_and_shared_group_target(shader_editor, tmp_path):
source = nested.image
target = ImageEditTarget.capture(context)
write_result(tmp_path)
target.apply(tmp_path)
target.apply(tmp_path / "foreground.png")
assert nested.image == source
assert tuple(source.size) == (8, 6)
assert second_instance.node_tree.nodes[nested.name].image == source
@@ -98,23 +98,23 @@ def test_texture_poll_applies_busy_and_size_limits(texture, busy, limit, expecte
assert remove_background.RemoveImageBackground.poll(context) == (not busy)
@pytest.mark.parametrize("operation,prefix", [(upscale.UpscaleImage, "upscale."), (remove_background.RunBackgroundRemoval, "image.")])
@pytest.mark.parametrize("operation,filename", [(upscale.UpscaleImage, "upscale.png"), (remove_background.RunBackgroundRemoval, "foreground.png")])
@pytest.mark.parametrize("shared", [False, True])
def test_texture_action_undo_redo_restores_image_and_binding(texture, tmp_path, operation, prefix, shared):
def test_texture_action_undo_redo_restores_image_and_binding(texture, tmp_path, operation, filename, shared):
source_name = texture.image.name
source_pixels = images.image_pixels(texture.image)
if shared:
other = texture.id_data.nodes.new("ShaderNodeTexImage")
other.image = texture.image
other.name = "Other texture"
write_result(tmp_path, prefix)
write_result(tmp_path, filename)
def execute(operator, context):
node = bpy.data.materials["Texture material"].node_tree.nodes["Source texture"]
operator._image_target = ImageEditTarget.capture(texture_context(node))
if operation == upscale.UpscaleImage:
operator.model = "HAT_GAN_X4_SHARPER"
operator.response(context, SimpleNamespace(directory=tmp_path))
operator.response(context, SimpleNamespace(file=lambda key: tmp_path / f"{key}.png"))
return {"FINISHED"}
bpy.context.preferences.edit.use_global_undo = True
@@ -145,22 +145,23 @@ def test_texture_action_undo_redo_restores_image_and_binding(texture, tmp_path,
bpy.utils.unregister_class(operation)
@pytest.mark.parametrize("operation,module,prefix", [
(upscale.UpscaleImage, upscale, "upscale."),
(remove_background.RunBackgroundRemoval, remove_background, "image."),
@pytest.mark.parametrize("operation,module,filename", [
(upscale.UpscaleImage, upscale, "upscale.png"),
(remove_background.RunBackgroundRemoval, remove_background, "foreground.png"),
])
@pytest.mark.parametrize("outcome", ["succeeded", "cancelled", "failed", "invalid_target"])
def test_texture_job_request_and_completion(texture, tmp_path, operation, module, prefix, outcome):
def test_texture_job_request_and_completion(texture, tmp_path, operation, module, filename, outcome):
context = texture_context(texture)
context.window = object()
context.window_manager = SimpleNamespace(
progress_begin=Mock(), progress_end=Mock(), event_timer_add=Mock(return_value=object()),
event_timer_remove=Mock(), modal_handler_add=Mock(),
)
write_result(tmp_path, prefix)
write_result(tmp_path, filename)
controller = SimpleNamespace(
submit=Mock(return_value={"job_id": "texture-job", "directory": str(tmp_path)}),
status=Mock(return_value={"state": "failed" if outcome == "invalid_target" else outcome}),
status=Mock(return_value={"state": "failed" if outcome == "invalid_target" else outcome,
"result": {Path(filename).stem: filename}}),
mark_job_complete=Mock(), cancel=Mock(),
)
original_pixels = images.image_pixels(texture.image)
@@ -183,7 +184,7 @@ def test_texture_job_request_and_completion(texture, tmp_path, operation, module
assert not job.start_thread.is_alive()
kind, parameters = controller.submit.call_args.args
assert kind == operation.job_type
assert parameters["start_frame"] == 1
assert "start_frame" not in parameters and "video_frames" not in parameters
assert parameters["device"] == "cpu"
assert parameters["model"] == ("HAT_GAN_X4_SHARPER" if operation == upscale.UpscaleImage else "BIREFNET_LITE")
input_path = operator.input_path
@@ -193,7 +194,7 @@ def test_texture_job_request_and_completion(texture, tmp_path, operation, module
texture.id_data.nodes.active = other
if outcome == "invalid_target":
texture.image = None
controller.status.return_value = {"state": "succeeded"}
controller.status.return_value = {"state": "succeeded", "result": {Path(filename).stem: filename}}
bpy.ops.ed.undo_push(message="Before asynchronous texture result")
result = operator.modal(context, SimpleNamespace(type="TIMER"))
assert result == ({"FINISHED"} if outcome == "succeeded" else {"CANCELLED"})