"""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_preserves_blocks_and_cleans_single_face_width(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) if resolution == 1: assert len(coords) == 0 and not faces return 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 output_split_mesh(obj): """Inspect separation before boundary strip cleanup.""" group = obj.modifiers[0].node_group split = next(node for node in group.nodes if node.bl_idname == "GeometryNodeSplitEdges") output = next(node for node in group.nodes if node.bl_idname == "NodeGroupOutput") group.links.new(split.outputs["Mesh"], output.inputs["Geometry"]) def test_zero_center_distance_does_not_split_or_produce_nan(): obj, set_value = surface(step=6.0) output_split_mesh(obj) 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) output_split_mesh(obj) 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] = 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)