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:
2026-09-09 22:18:59 +08:00
parent 89087fc2e2
commit f48bbb3d05
97 changed files with 436 additions and 705 deletions

View File

@@ -0,0 +1,269 @@
"""Behavior checks for face separation and connected Depth Surface shells."""
from collections import Counter
import bpy
import bmesh
import numpy as np
import pytest
from tests.support.nodes import CUTOUT_BUILDERS
from tests.support.depth_surface import assert_closed, component_count, evaluated, surface
@pytest.mark.parametrize("mode", [0, 1])
@pytest.mark.parametrize("split", [0.0, 0.5])
def test_shell_walls_directly_connect_caps(mode, split):
from anyimage.common.object import modifier_input_identifier, set_modifier_input
obj, set_value = surface(resolution=4)
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set(
"value", np.full(len(obj.data.vertices), 0.3)
)
set_value("Split Threshold", split)
front, faces = evaluated(obj)
edges = Counter(tuple(sorted((a, b))) for face in faces for a, b in zip(face, (*face[1:], face[0])))
boundary_count = sum(count == 1 for count in edges.values())
set_value("Mode", mode)
modifier = obj.modifiers[0]
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE" if mode == 0 else "DISTANCE")
for amount in (0.01, 2.0):
set_modifier_input(modifier, thickness, amount)
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
points, shell_faces = evaluated(obj)
assert len(points) == 2 * len(front)
assert len(shell_faces) == 2 * len(faces) + boundary_count
assert all(len(face) == 4 for face in shell_faces)
assert np.isfinite(points).all()
assert_closed(shell_faces)
@pytest.mark.parametrize("thickness", [0.1, 1.4])
@pytest.mark.parametrize("split", [0.0, 0.5])
def test_surface_caps_and_segmented_walls_are_connected(thickness, split):
obj, set_value = surface()
set_value("Split Threshold", split)
set_value("Thickness", thickness)
coords, faces = evaluated(obj)
assert np.isfinite(coords).all()
assert_closed(faces)
set_value("Thickness", 0.0)
front, _ = evaluated(obj)
from scipy.spatial import cKDTree
assert cKDTree(front).query(coords)[0].max() <= thickness + 1e-5
assert cKDTree(coords).query(front)[0].max() < 1e-5
@pytest.mark.parametrize("size", [0.01, 1.0, 100.0])
def test_split_mapping_and_uniform_scale(size):
obj, set_value = surface(step=2.0, size=size)
# Face centers are size/2 apart: gradient is 4.
counts = []
for split in (0.0, 0.25, 0.5, 1.0):
set_value("Split Threshold", split)
coords, faces = evaluated(obj)
counts.append(len(coords))
assert len(faces) == 8
assert counts[:3] == [15] * 3
assert counts[3] == 18
set_value("Depth Scale", 0.0)
coords, faces = evaluated(obj)
assert len(coords) == 18 and np.abs(coords[:, 2]).max() < 1e-6
def test_midpoint_selects_gradient_six_and_preserves_faces():
obj, set_value = surface(step=3.0)
set_value("Split Threshold", 0.5)
coords, faces = evaluated(obj)
assert len(coords) == 18 and len(faces) == 8
assert set(np.round(coords[:, 2], 4)) == {0.0, -3.0}
def test_cleanup_keeps_split_parts_of_a_large_source_island():
obj, set_value = surface(wrapper=True)
set_value("Cleanup Threshold", 110.0)
assert len(evaluated(obj)[1]) == 8
set_value("Split Threshold", 0.5)
assert len(evaluated(obj)[1]) == 8
set_value("Thickness", 0.2)
assert_closed(evaluated(obj)[1])
set_value("Cleanup Threshold", 210.0)
assert len(evaluated(obj)[1]) == 0
@pytest.mark.parametrize("depth_scale", [0.0, 0.25, 1.0, 2.0, 4.0])
def test_split_topology_uses_depth_before_display_scaling(depth_scale):
obj, set_value = surface(step=3.0)
set_value("Split Threshold", 0.5)
original, original_faces = evaluated(obj)
set_value("Depth Scale", depth_scale)
coords, faces = evaluated(obj)
assert len(coords) == len(original) == 18
assert faces == original_faces
np.testing.assert_allclose(coords[:, 2], original[:, 2] * depth_scale, atol=1e-6)
@pytest.mark.parametrize("shape", ["SOLID", "DEPTH_SOLID", "DEPTH_BALLOON"])
def test_each_asset_cleans_the_original_geometry_before_shaping(shape):
bpy.ops.wm.read_factory_settings(use_empty=True)
group = CUTOUT_BUILDERS[shape]()
links = [link for link in group.links if link.from_node.bl_idname == "NodeGroupInput" and link.from_socket.name == "Geometry"]
assert len(links) == 1
assert links[0].to_node.bl_idname == "GeometryNodeDeleteGeometry"
def test_full_wrapper_smooth_keeps_closed_shell():
obj, set_value = surface(wrapper=True)
set_value("Split Threshold", 0.5)
set_value("Thickness", 0.2)
set_value("Smooth", 2)
coords, faces = evaluated(obj)
assert np.isfinite(coords).all()
assert_closed(faces)
def test_coincident_independent_sheets_do_not_weld_together():
obj, set_value = surface(step=0.0)
mesh = obj.data
vertices = [tuple(v.co) for v in mesh.vertices]
faces = [tuple(f.vertices) for f in mesh.polygons]
n = len(vertices)
duplicate = bpy.data.meshes.new("Close sheets")
duplicate.from_pydata(
vertices + [(x, y, z + 1e-8) for x, y, z in vertices],
[],
faces + [tuple(v + n for v in face) for face in faces],
)
uv = duplicate.uv_layers.new(name="UVMap")
for loop in duplicate.loops:
x, y, z = duplicate.vertices[loop.vertex_index].co
uv.data[loop.index].uv = (x / 2, y)
obj.data = duplicate
set_value("Thickness", 0.1)
coords, faces = evaluated(obj)
assert_closed(faces)
assert component_count(faces) == 2
@pytest.mark.parametrize("resolution", [1, 2, 4])
def test_planar_depth_has_no_split_at_any_density(resolution):
obj, set_value = surface(step=0.0, resolution=resolution)
original, original_faces = evaluated(obj)
set_value("Split Threshold", 1.0)
coords, faces = evaluated(obj)
assert len(coords) == len(obj.data.vertices)
assert len(faces) == len(obj.data.polygons)
np.testing.assert_allclose(coords, original, atol=1e-6)
def test_small_positive_split_preserves_unsplit_projection():
obj, set_value = surface(step=2.0)
original, original_faces = evaluated(obj)
set_value("Split Threshold", 0.01)
coords, faces = evaluated(obj)
np.testing.assert_allclose(coords, original, atol=1e-6)
assert faces == original_faces
def test_cut_vertices_preserve_camera_rays_and_other_vertices():
obj, set_value = surface(step=3.0)
original, original_faces = evaluated(obj)
set_value("Split Threshold", 0.5)
coords, faces = evaluated(obj)
for source_face, result_face in zip(original_faces, faces):
for source_index, result_index in zip(source_face, result_face):
before, after = original[source_index], coords[result_index]
if obj.data.vertices[source_index].co.x != 1.0:
np.testing.assert_allclose(after, before, atol=1e-6)
before_depth, after_depth = 1.0 - before[2], 1.0 - after[2]
np.testing.assert_allclose(
after[:2] / after_depth, before[:2] / before_depth, atol=1e-6
)
def test_stored_point_positions_prevent_depth_resampling_on_side_walls():
obj, set_value = surface(step=3.0)
set_value("Split Threshold", 0.5)
set_value("Thickness", 0.1)
coords, faces = evaluated(obj)
assert_closed(faces)
set_value("Thickness", 0.0)
front, _ = evaluated(obj)
from scipy.spatial import cKDTree
assert cKDTree(front).query(coords)[0].max() <= 0.1 + 1e-5
assert cKDTree(coords).query(front)[0].max() < 1e-5
@pytest.mark.parametrize("thickness", [0.0, 0.2])
@pytest.mark.parametrize("split", [0.0, 0.5])
def test_temporary_attributes_are_removed_and_user_attributes_survive(thickness, split):
obj, set_value = surface(wrapper=True)
obj.data.attributes.new("o_depth_probe", "FLOAT", "POINT")
attribute = obj.data.attributes.new("o_user_weight", "FLOAT", "POINT")
for value in attribute.data:
value.value = 0.75
set_value("Split Threshold", split)
set_value("Thickness", thickness)
set_value("Smooth", 2)
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
mesh = result.to_mesh()
try:
assert not any(a.name.startswith("o_depth_") for a in mesh.attributes)
assert "UVMap" in mesh.attributes
assert all(
v.value == pytest.approx(0.75)
for v in mesh.attributes["o_user_weight"].data
)
finally:
result.to_mesh_clear()
def test_zero_center_distance_does_not_split_or_produce_nan():
obj, set_value = surface(step=6.0)
mesh = bpy.data.meshes.new("Coincident face centers")
mesh.from_pydata(
[(0, 0, 0), (1, 0, 0), (0.5, 1, 0), (0.5, 1, 0)], [], [(0, 1, 2), (1, 0, 3)]
)
uv = mesh.uv_layers.new(name="UVMap")
for face in mesh.polygons:
for li in face.loop_indices:
uv.data[li].uv = (0.25 if face.index == 0 else 0.75, 0.5)
obj.data = mesh
set_value("Split Threshold", 1.0)
coords, faces = evaluated(obj)
assert len(coords) == 4 and len(faces) == 2
assert np.isfinite(coords).all()
def test_triangle_corner_weights_do_not_open_sloped_side_walls():
obj, set_value = surface(step=3.0)
bm = bmesh.new()
bm.from_mesh(obj.data)
bmesh.ops.triangulate(bm, faces=list(bm.faces))
bm.to_mesh(obj.data)
bm.free()
image = bpy.data.images["Camera"]
pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4)
pixels[..., 2] += np.arange(32)[:, None] * 0.02
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Split Threshold", 0.5)
set_value("Thickness", 0.8)
coords, faces = evaluated(obj)
assert_closed(faces)
assert component_count(faces) == 2
@pytest.mark.parametrize("resolution", [1, 2, 4])
def test_linear_depth_slope_has_density_independent_selection(resolution):
obj, set_value = surface(step=0.0, resolution=resolution)
image = bpy.data.images["Camera"]
pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4)
pixels[..., 2] = 1 + 4 * pixels[..., 0]
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Split Threshold", 0.5)
assert len(evaluated(obj)[0]) == len(obj.data.vertices)
set_value("Split Threshold", 1.0)
assert len(evaluated(obj)[0]) > len(obj.data.vertices)