Add tests for image layer, image plane, inventory, saved layout, commands, documentation, manifest, and packaging
- Implement tests for image layer functionality including alpha curve and depth layer behavior. - Create tests for image plane evaluation and modifier input handling. - Add tests to verify the saved asset inventory and its persistence. - Introduce tests for the documentation command to ensure correct project version deployment. - Validate the extension manifest structure and required permissions. - Develop tests for the packaging process, ensuring correct platform handling and dependency management. - Enhance image depth plane group functionality with new split threshold input and surface processing.
This commit is contained in:
246
tests/tools/nodes/test_depth_cutout.py
Normal file
246
tests/tools/nodes/test_depth_cutout.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""Contracts for smooth depth volume, editable presets and normal masks."""
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from tools.nodes.groups.image_depth_cutout import build_volume_fields
|
||||
from tools.nodes.groups.image_depth_cutout import build_volume_rounding
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
|
||||
|
||||
@pytest.mark.parametrize("iterations", [0, 50])
|
||||
def test_uniform_thickness_follows_the_tilted_front_normal(iterations):
|
||||
from scipy.spatial import cKDTree
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
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()
|
||||
set_value("Normal Smooth", iterations)
|
||||
front, _ = evaluated(obj)
|
||||
set_value("Thickness", 0.2)
|
||||
points, faces = evaluated(obj)
|
||||
expected = front - 0.2 * np.array((0.4, 0, 1)) / np.sqrt(1.16)
|
||||
assert cKDTree(points).query(expected)[0].max() < 1e-5
|
||||
assert cKDTree(points).query(front)[0].max() < 1e-5
|
||||
assert_closed(faces)
|
||||
|
||||
|
||||
def test_balloon_shell_converges_to_front_with_thickness():
|
||||
from scipy.spatial import cKDTree
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
obj, set_value = surface(step=1, resolution=10)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
|
||||
profile[abs(profile) < 1e-7] = 0
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
set_value("Mode", 0)
|
||||
set_value("Edge Round", 0.0)
|
||||
set_value("Normal Smooth", 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, faces = amount(value)
|
||||
assert_closed(faces)
|
||||
gaps.append(tree.query(points)[0].max())
|
||||
np.testing.assert_allclose(np.array(gaps[1:]) / gaps[:-1], 0.1, rtol=0.02)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("amount", [0.0, 1.0])
|
||||
def test_rounding_shares_front_and_back_falloff(amount):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
n = 15
|
||||
xy = np.array([(x, y) for y in np.linspace(-1, 1, n) for x in np.linspace(-1, 1, n)])
|
||||
balloon = 0.4 * np.maximum(0, (1 - xy[:, 0]**2) * (1 - xy[:, 1]**2))
|
||||
depth = 0.2 * xy[:, 0] + 0.3 * xy[:, 1] + 1.4 * balloon
|
||||
faces = [(y*n+x, y*n+x+1, (y+1)*n+x+1, (y+1)*n+x) for y in range(n-1) for x in range(n-1)]
|
||||
mesh = bpy.data.meshes.new("Round profile")
|
||||
mesh.from_pydata(np.column_stack((xy, depth)), [], faces)
|
||||
mesh.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", balloon)
|
||||
obj = bpy.data.objects.new("Round profile", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
group = bpy.data.node_groups.new("Round field test", "GeometryNodeTree")
|
||||
for name, kind, default in (("Geometry", "NodeSocketGeometry", None), ("Thickness", "NodeSocketFloat", 1), ("Edge Round", "NodeSocketFloat", amount), ("Normal Smooth", "NodeSocketInt", 20)):
|
||||
socket = group.interface.new_socket(name=name, in_out="INPUT", socket_type=kind)
|
||||
if default is not None:
|
||||
socket.default_value = default
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
source = group.nodes.new("NodeGroupInput")
|
||||
result = build_volume_fields(group, source.outputs["Geometry"])
|
||||
result = build_volume_rounding(group, result)
|
||||
group.links.new(result, group.nodes.new("NodeGroupOutput").inputs["Geometry"])
|
||||
obj.modifiers.new("Round", "NODES").node_group = group
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
def values(name):
|
||||
return np.array([v.value for v in result.attributes["o_balloon_surface_" + name].data])
|
||||
center = values("center")
|
||||
u = balloon / balloon.max()
|
||||
weight = amount * (1 - u**3 * (u * (6*u - 15) + 10))
|
||||
delta = (center + balloon - depth) * weight
|
||||
original = np.maximum(depth - center + balloon, 0.05 * balloon)
|
||||
np.testing.assert_allclose(values("edge_weight"), weight, atol=1e-6)
|
||||
np.testing.assert_allclose(values("thickness"), original + delta + balloon * weight, atol=1e-6)
|
||||
np.testing.assert_allclose([v.co.z for v in result.vertices], depth + delta, atol=1e-6)
|
||||
normals = np.array([v.vector[:] for v in result.attributes["o_balloon_surface_normal"].data])
|
||||
np.testing.assert_allclose(np.linalg.norm(normals, axis=1), 1, atol=1e-6)
|
||||
assert np.max(abs(normals[:, :2])) > 0.1
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def test_smooth_back_rejects_front_noise_and_preserves_center_depth():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
n = 41
|
||||
points = np.array([(x, y) for y in np.linspace(-1, 1, n) for x in np.linspace(-1, 1, n)])
|
||||
profile = np.sqrt(np.maximum(0, np.cos(points[:, 0]*np.pi/2)*np.cos(points[:, 1]*np.pi/2))) * 0.6
|
||||
boundary = np.max(abs(points), axis=1) > 0.999
|
||||
profile[boundary] = 0
|
||||
center = 0.3*points[:, 0] + 0.6*points[:, 1]
|
||||
noise = 0.08*np.sin(points[:, 0]*23)*np.sin(points[:, 1]*19)
|
||||
noise[boundary] = 0
|
||||
faces = [(y*n+x, y*n+x+1, (y+1)*n+x+1) for y in range(n-1) for x in range(n-1)]
|
||||
faces += [(y*n+x, (y+1)*n+x+1, (y+1)*n+x) for y in range(n-1) for x in range(n-1)]
|
||||
mesh = bpy.data.meshes.new("Noisy front")
|
||||
mesh.from_pydata(np.column_stack((points, center + profile + noise)), [], faces)
|
||||
mesh.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile.astype(np.float32))
|
||||
obj = bpy.data.objects.new("Noisy front", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
group = bpy.data.node_groups.new("Volume field test", "GeometryNodeTree")
|
||||
for name, kind in (("Geometry", "NodeSocketGeometry"), ("Thickness", "NodeSocketFloat")):
|
||||
socket = group.interface.new_socket(name=name, in_out="INPUT", socket_type=kind)
|
||||
if name == "Thickness":
|
||||
socket.default_value = 1
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
source = group.nodes.new("NodeGroupInput")
|
||||
result = build_volume_fields(group, source.outputs["Geometry"])
|
||||
group.links.new(result, group.nodes.new("NodeGroupOutput").inputs["Geometry"])
|
||||
obj.modifiers.new("Volume", "NODES").node_group = group
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
thickness = np.array([item.value for item in result.attributes["o_balloon_surface_thickness"].data])
|
||||
rear = center + profile + noise - thickness
|
||||
error = rear - (center - profile)
|
||||
assert max(abs(error)) < 1e-5
|
||||
assert np.sqrt(np.mean(error**2)) < np.sqrt(np.mean(noise**2))*0.01
|
||||
assert max(abs(thickness[boundary])) == 0
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def test_depth_cutout_normal_map_only_affects_front_faces():
|
||||
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_scale"]
|
||||
assert attribute.domain == "FACE"
|
||||
weights = [item.value for item in attribute.data]
|
||||
assert weights.count(1) == len(obj.data.polygons)
|
||||
assert weights.count(0) == len(mesh.polygons) - len(obj.data.polygons)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("structure", [0, 1, 4])
|
||||
@pytest.mark.parametrize("thickness", [0.0, 0.02, 0.1])
|
||||
def test_uniform_front_inflation_scales_with_distance(thickness, structure):
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
obj, set_value = surface(step=0, resolution=10)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
|
||||
profile[abs(profile) < 1e-7] = 0
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
group = obj.modifiers[0].node_group
|
||||
store = next(n for n in group.nodes if n.bl_idname == "GeometryNodeStoreNamedAttribute" and n.inputs["Name"].default_value == "o_balloon_surface_structure")
|
||||
for link in list(store.inputs["Value"].links):
|
||||
group.links.remove(link)
|
||||
store.inputs["Value"].default_value = structure
|
||||
set_value("Thickness", thickness)
|
||||
set_value("Front Inflation", 0.0)
|
||||
baseline, faces = evaluated(obj)
|
||||
set_value("Front Inflation", 1.0)
|
||||
inflated, inflated_faces = evaluated(obj)
|
||||
assert faces == inflated_faces
|
||||
delta = np.linalg.norm(inflated - baseline, axis=1)
|
||||
front = np.isclose(baseline[:, 2], baseline[:, 2].max(), atol=1e-6)
|
||||
np.testing.assert_allclose(delta[front], thickness, atol=2e-6)
|
||||
np.testing.assert_allclose(delta[~front], 0, atol=2e-6)
|
||||
if thickness:
|
||||
assert_closed(inflated_faces)
|
||||
else:
|
||||
np.testing.assert_array_equal(inflated, baseline)
|
||||
|
||||
|
||||
def test_normal_smooth_controls_front_and_rear_directions():
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
obj, set_value = surface(step=1, resolution=10)
|
||||
set_value("Front Inflation", 1.0)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
|
||||
profile[abs(profile) < 1e-7] = 0
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
group = obj.modifiers[0].node_group
|
||||
set_value("Mode", 0)
|
||||
set_modifier_input(obj.modifiers[0], modifier_input_identifier(group, "Thickness", subtype="NONE"), 1.0)
|
||||
masks = []
|
||||
coordinates = []
|
||||
for iterations in (0, 50):
|
||||
set_value("Normal Smooth", iterations)
|
||||
points, faces = evaluated(obj)
|
||||
ev = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = ev.to_mesh()
|
||||
mask = [v.value for v in mesh.attributes["o_normal_scale"].data]
|
||||
front = {v for face, weight in zip(faces, mask) if weight > 0.5 for v in face}
|
||||
rear = set(range(len(points))) - front
|
||||
ev.to_mesh_clear()
|
||||
coordinates.append(points)
|
||||
masks.append((front, rear))
|
||||
assert masks[0] == masks[1]
|
||||
for ids in masks[0]:
|
||||
assert np.max(abs(coordinates[1][list(ids)] - coordinates[0][list(ids)])) > 1e-5
|
||||
|
||||
|
||||
def test_balloon_suppression_is_stronger_without_a_startup_threshold():
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
obj, set_value = surface(step=0, resolution=10)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
|
||||
profile[abs(profile) < 1e-7] = 0
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
group = obj.modifiers[0].node_group
|
||||
store = next(n for n in group.nodes if n.bl_idname == "GeometryNodeStoreNamedAttribute" and n.inputs["Name"].default_value == "o_balloon_surface_structure")
|
||||
for link in list(store.inputs["Value"].links):
|
||||
group.links.remove(link)
|
||||
store.inputs["Value"].default_value = 1
|
||||
set_value("Mode", 0)
|
||||
set_value("Edge Round", 0)
|
||||
thickness_id = modifier_input_identifier(group, "Thickness", subtype="NONE")
|
||||
previous = 0
|
||||
for thickness in (0.001, 0.01, 0.1, 0.5, 1):
|
||||
set_modifier_input(obj.modifiers[0], thickness_id, thickness)
|
||||
set_value("Front Inflation", 0)
|
||||
baseline, _ = evaluated(obj)
|
||||
set_value("Front Inflation", 1)
|
||||
inflated, _ = evaluated(obj)
|
||||
growth = np.linalg.norm(inflated - baseline, axis=1).max() / profile.max()
|
||||
assert growth > previous
|
||||
previous = growth
|
||||
assert growth == pytest.approx(0.323, abs=0.001)
|
||||
Reference in New Issue
Block a user