- Introduced a new `Boundary Padding` preference in the Cutout Tool with a default of 2 px and a configurable range of 1-4 px. - Updated the Cutout client to establish inward mapping from the final Mask, sampling in a fixed direction and processing independent material RGBA and Depth. - Enhanced the handling of RGBA and Depth during cutout operations, ensuring proper mapping and preservation of source data. - Added tests to validate the new boundary padding functionality and its integration with existing cutout processes. feat(geometry): remove overall mesh smoothing from geometry nodes - Removed the `Smooth` input from several geometry node groups to eliminate unnecessary overall mesh smoothing. - Retained `Smooth Weight` for depth nodes while removing it from Cutout and Relief Plane nodes. - Ensured that boundary, normal, and fill smoothing behaviors remain intact and consistent. - Updated tests and documentation to reflect the changes in node interfaces and behaviors. feat(normals): scale tangent normals with depth - Reused the existing `o_normal_reduction` attribute to adjust Tangent Normal strength based on the geometry's `Depth Scale`. - Ensured that Tangent Normal strength is consistent with the geometry depth, preserving full strength for materials that do not provide the attribute. - Updated tests to verify the correct behavior of normal strength adjustments in both Tangent and Object Space.
75 lines
4.3 KiB
Python
75 lines
4.3 KiB
Python
import tempfile
|
|
from contextlib import ExitStack
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
|
|
from tests.support.blender import BlenderTestCase
|
|
|
|
|
|
class AiInputFailuresTest(BlenderTestCase):
|
|
def test_image_edits_reject_same_input_before_submission(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "input.tiff"
|
|
path.touch()
|
|
messages = []
|
|
for module, operator_type in (
|
|
(self.upscale, self.upscale.UpscaleImage),
|
|
(self.remove_background, self.remove_background.RunBackgroundRemoval),
|
|
):
|
|
operator = operator_type()
|
|
operator.report = Mock()
|
|
target = Mock(image=SimpleNamespace(size=(16, 16)))
|
|
target.prepare.return_value = (path, False)
|
|
context = SimpleNamespace(scene=SimpleNamespace(
|
|
anyimage_settings=SimpleNamespace()))
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch.object(module.ImageEditTarget, "capture", return_value=target))
|
|
stack.enter_context(patch.object(module, "require_environment"))
|
|
stack.enter_context(patch.object(module, "require_model"))
|
|
stack.enter_context(patch.object(self.runtime.runtime, "active_job", None))
|
|
submit = stack.enter_context(patch.object(self.runtime.runtime, "begin_job"))
|
|
if module == self.upscale:
|
|
stack.enter_context(patch.object(module, "configured_upscale_model", return_value="HAT_GAN_X4_SHARPER"))
|
|
stack.enter_context(patch.object(module, "configured_max_ai_input_size", return_value=2048))
|
|
self.assertEqual(operator.execute(context), {"CANCELLED"})
|
|
submit.assert_not_called()
|
|
messages.append(operator.report.call_args.args[1])
|
|
self.assertIsNone(operator._image_target)
|
|
self.assertTrue(path.exists())
|
|
self.assertEqual(messages[0], messages[1])
|
|
self.assertIn("Unsupported input format: .tiff", messages[0])
|
|
|
|
def test_invoke_ai_setup_reports_nested_failure(self):
|
|
owner = SimpleNamespace(report=Mock())
|
|
with (
|
|
patch.object(self.ai, "ai_ready", return_value=False),
|
|
patch.object(self.ai.bpy, "ops", SimpleNamespace(anyimage=SimpleNamespace(
|
|
setup_ai_environment=Mock(side_effect=RuntimeError("Error: setup failed\n")))), create=True),
|
|
):
|
|
self.assertEqual(self.ai.invoke_ai_setup_if_needed(owner), {"CANCELLED"})
|
|
owner.report.assert_called_once_with({"ERROR"}, "setup failed")
|
|
|
|
def test_depth_and_relief_cleanup_on_nested_failure(self):
|
|
module = self.convert_to_plane
|
|
for plane_type in ("DEPTH", "RELIEF"):
|
|
for failure in (RuntimeError("Error: invalid input\n"), None):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "prepared" / "input.png"
|
|
path.parent.mkdir()
|
|
path.touch()
|
|
operator = SimpleNamespace(mesh_detail=4, report=Mock())
|
|
context = SimpleNamespace(object=SimpleNamespace(name="source", as_pointer=lambda: 1,
|
|
data=SimpleNamespace(as_pointer=lambda: 2)), scene=object())
|
|
generate = Mock(side_effect=failure, return_value={"CANCELLED"})
|
|
with (
|
|
patch.object(module, "invoke_ai_setup_if_needed", return_value=None),
|
|
patch.object(module, "prepare_material_color_input", return_value=path),
|
|
patch.object(module, "material_analysis_input", return_value=path),
|
|
patch.object(module, "depth_plane_settings", return_value=("model", 5)),
|
|
patch.object(module, "configured_max_ai_input_size", return_value=2048),
|
|
patch.object(module.bpy, "ops", SimpleNamespace(anyimage=SimpleNamespace(generate_depth_plane=generate)), create=True),
|
|
):
|
|
self.assertEqual(module.start_depth_plane_job(operator, context, plane_type), {"CANCELLED"})
|
|
self.assertFalse(path.parent.exists())
|