Files
AnyImage/tests/nodes/test_depth_cutout.py
nan 198fcbf55b
Some checks failed
Test / test (push) Has been cancelled
Initial public release
2026-09-19 21:31:36 +08:00

181 lines
7.0 KiB
Python

"""Behavior checks for the Depth Cutout balloon and shell shapes."""
import bpy
import numpy as np
import pytest
from anyimage.common.object import modifier_input_identifier, set_modifier_input
@pytest.mark.parametrize("mode", [0, 1])
def test_edge_turn_reshapes_thickness_without_moving_zero_thickness(mode):
"""Edge Turn swings the wall direction and leaves zero thickness untouched."""
from tests.support.depth_surface import surface, evaluated
obj, set_value = surface(step=1, resolution=10)
xz = np.array([(v.co.x, v.co.z) for v in obj.data.vertices])
profile = .3 * np.sin(np.pi * xz[:, 0] / 2) * np.sin(np.pi * xz[:, 1])
profile[abs(profile) < 1e-7] = 0
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
set_value("Mode", mode)
set_value("Front Inflation", .2)
modifier = obj.modifiers[0]
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE" if mode else "NONE")
turn = modifier_input_identifier(modifier.node_group, "Edge Turn")
def result(amount, edge_turn):
set_modifier_input(modifier, thickness, amount)
set_modifier_input(modifier, turn, edge_turn)
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
return evaluated(obj)
plain, faces = result(0, 0)
turned, turned_faces = result(0, 2)
np.testing.assert_array_equal(plain, turned)
assert faces == turned_faces
plain, faces = result(1, 0)
turned, turned_faces = result(1, 2)
assert faces == turned_faces
assert np.isfinite(turned).all()
assert np.max(np.abs(plain - turned)) > 1e-5
def test_shell_offset_scales_with_thickness_and_ignores_reference():
"""A thin Shell moves a distance proportional to its thickness."""
from tests.support.depth_surface import surface, evaluated
obj, set_value = surface(step=0, resolution=12)
image = bpy.data.images['Camera']
pixels = np.array(image.pixels[:], dtype=np.float32).reshape(32, 64, 4)
pixels[..., 2] = 1 + .15 * np.sin(pixels[..., 0] * 6) * np.cos(pixels[..., 1] * 5)
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value('Reference Depth', 10.)
set_value('Thickness', 0.)
front, _ = evaluated(obj)
def offsets(thickness, reference=10.):
set_value('Thickness', thickness)
set_value('Reference Depth', reference)
points, faces = evaluated(obj)
adjusted_front = front + (0, 10 - reference, 0)
from scipy.spatial import cKDTree
distances, indices = cKDTree(adjusted_front).query(points)
np.testing.assert_allclose(np.sort(distances)[:len(front)], 0, atol=2e-6)
rear = distances > thickness * .5
assert rear.sum() == len(front)
order = np.argsort(indices[rear])
return ((points[rear] - adjusted_front[indices[rear]]) / thickness)[order], faces
smooth, faces = offsets(.02)
thinner, thinner_faces = offsets(.01)
distant, _ = offsets(.02, 20.)
np.testing.assert_allclose(thinner, smooth, atol=2e-4)
np.testing.assert_allclose(distant, smooth, atol=2e-4)
assert thinner_faces == faces
def test_shell_thickness_extends_behind_the_tilted_front():
from tests.support.depth_surface import surface, evaluated
obj, set_value = surface(step=0, resolution=4)
image = bpy.data.images["Camera"]
pixels = np.array(image.pixels[:], dtype=np.float32).reshape(32, 64, 4)
pixels[..., 2] = 1 + 0.4 * pixels[..., 0]
image.pixels.foreach_set(pixels.ravel())
image.update()
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
front, _ = evaluated(obj)
set_value("Thickness", 0.2)
set_value("Front Inflation", 0.5)
points, _ = evaluated(obj)
front_set = {tuple(point) for point in front}
assert front_set <= {tuple(point) for point in points}
assert not np.allclose(points[len(front) :], front, atol=1e-6)
def test_balloon_shell_converges_to_front_with_thickness():
from scipy.spatial import cKDTree
from tests.support.depth_surface import surface, evaluated
obj, set_value = surface(step=1, resolution=10)
xz = np.array([(v.co.x, v.co.z) for v in obj.data.vertices])
profile = 0.3 * np.sin(np.pi * xz[:, 0] / 2) * np.sin(np.pi * xz[:, 1])
profile[abs(profile) < 1e-7] = 0
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
set_value("Mode", 0)
modifier = obj.modifiers[0]
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE")
def amount(value):
set_modifier_input(modifier, thickness, value)
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
return evaluated(obj)
front, _ = amount(0)
tree = cKDTree(front)
gaps = []
for value in (0.01, 0.001, 0.0001):
points, _ = amount(value)
gaps.append(tree.query(points)[0].max())
assert gaps[0] > gaps[1] > gaps[2]
np.testing.assert_allclose(gaps[1] / gaps[0], 0.1, rtol=0.02)
def test_depth_cutout_normal_reduction_uses_point_domain():
from tests.support.depth_surface import surface
obj, set_value = surface()
set_value("Thickness", 0.3)
bpy.context.view_layer.update()
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
mesh = evaluated.to_mesh()
try:
attribute = mesh.attributes["o_normal_reduction"]
assert attribute.domain == "POINT"
values = [item.value for item in attribute.data]
assert 0.0 in values and 1.0 in values
assert all(np.isfinite(values))
finally:
evaluated.to_mesh_clear()
def test_zero_thickness_short_circuits_shell_without_warnings():
from tests.support.depth_surface import surface, evaluated
obj, set_value = surface()
set_value("Thickness", 0.0)
points, faces = evaluated(obj)
assert len(faces) == len(obj.data.polygons)
modifier = obj.modifiers[0]
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
result.to_mesh()
try:
assert not list(modifier.node_warnings), [
warning.message for warning in modifier.node_warnings
]
finally:
result.to_mesh_clear()
@pytest.mark.parametrize("thickness,inflation", [
(0.0, 1.0), (0.02, 0.5), (0.1, 1.0),
])
def test_shell_ignores_front_inflation(thickness, inflation):
from tests.support.depth_surface import surface, evaluated
obj, set_value = surface(step=0, resolution=10)
xz = np.array([(v.co.x, v.co.z) for v in obj.data.vertices])
profile = 0.3 * np.sin(np.pi * xz[:, 0] / 2) * np.sin(np.pi * xz[:, 1])
profile[abs(profile) < 1e-7] = 0
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
set_value("Thickness", thickness)
set_value("Front Inflation", 0.0)
baseline, baseline_faces = evaluated(obj)
set_value("Front Inflation", inflation)
inflated, inflated_faces = evaluated(obj)
assert baseline_faces == inflated_faces
np.testing.assert_array_equal(inflated, baseline)