From 531f7a4dfb212816f50241c909e5ec3cfe8ece62 Mon Sep 17 00:00:00 2001 From: nan Date: Fri, 18 Sep 2026 08:36:51 +0800 Subject: [PATCH] chore: bump version to 1.0.1 and update related files --- pyproject.toml | 2 +- src/anyimage/blender_manifest.toml | 2 +- tests/tools/nodes/test_boundary_depth.py | 62 ------ tests/tools/nodes/test_boundary_smoothing.py | 188 +----------------- tests/tools/nodes/test_cutout_boundary.py | 80 -------- tests/tools/nodes/test_depth_relief_planes.py | 15 -- tests/tools/nodes/test_depth_strip_cleanup.py | 105 +--------- tests/tools/nodes/test_depth_surface_split.py | 68 +------ tests/tools/nodes/test_validity.py | 37 ---- uv.lock | 2 +- 10 files changed, 7 insertions(+), 554 deletions(-) delete mode 100644 tests/tools/nodes/test_validity.py diff --git a/pyproject.toml b/pyproject.toml index d2e53a1..5929784 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "anyimage" -version = "1.0.0" +version = "1.0.1" description = "Fast image-guided structure building tools for Blender" readme = "README.md" requires-python = ">=3.11,<3.12" diff --git a/src/anyimage/blender_manifest.toml b/src/anyimage/blender_manifest.toml index 008ec33..cb6548b 100644 --- a/src/anyimage/blender_manifest.toml +++ b/src/anyimage/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "anyimage" -version = "1.0.0" +version = "1.0.1" name = "AnyImage" tagline = "Build spatial structures from images, fast" maintainer = "OmooLab" diff --git a/tests/tools/nodes/test_boundary_depth.py b/tests/tools/nodes/test_boundary_depth.py index 5418dc3..3cd5a87 100644 --- a/tests/tools/nodes/test_boundary_depth.py +++ b/tests/tools/nodes/test_boundary_depth.py @@ -6,68 +6,6 @@ import pytest from tests.support.depth_surface import evaluated from tests.tools.nodes.test_boundary_smoothing import boundary_neighbors, edge_band -from tools.nodes.common.boundary_smoothing import smooth_boundary_camera - - -def test_camera_blur_preserves_rays_interior_protection_and_disconnected_surfaces(): - bpy.ops.wm.read_factory_settings(use_empty=True) - points, faces, cameras, protected = [], [], [], [] - for offset, depth in ((0, 2), (81, 50)): - for y in range(9): - for x in range(9): - value = 20 if offset == 0 and (x, y) == (4, 0) else depth - if offset == 0 and (x, y) == (8, 8): - value = 0 - points.append((x, y, 0)) - cameras.append((x * value / 10, y * value / 10, value)) - protected.append(x == 0) - faces.extend((offset + y * 9 + x, offset + y * 9 + x + 1, - offset + (y + 1) * 9 + x + 1, offset + (y + 1) * 9 + x) - for y in range(8) for x in range(8)) - mesh = bpy.data.meshes.new("Disconnected camera samples") - mesh.from_pydata(points, [], faces) - camera = np.asarray(cameras, np.float32) - mesh.attributes.new("test_camera", "FLOAT_VECTOR", "POINT").data.foreach_set("vector", camera.ravel()) - mesh.attributes.new("test_protected", "BOOLEAN", "POINT").data.foreach_set("value", protected) - obj = bpy.data.objects.new("Depth samples", mesh) - bpy.context.collection.objects.link(obj) - group = bpy.data.node_groups.new("Boundary depth test", "GeometryNodeTree") - for direction in ("INPUT", "OUTPUT"): - group.interface.new_socket(name="Geometry", in_out=direction, socket_type="NodeSocketGeometry") - control = group.interface.new_socket(name="Boundary Smooth", in_out="INPUT", socket_type="NodeSocketInt") - source = group.nodes.new("NodeGroupInput") - attributes = [] - for name, kind in (("test_camera", "FLOAT_VECTOR"), ("test_protected", "BOOLEAN")): - node = group.nodes.new("GeometryNodeInputNamedAttribute") - node.data_type = kind - node.inputs["Name"].default_value = name - attributes.append(node.outputs["Attribute"]) - position = group.nodes.new("GeometryNodeSetPosition") - group.links.new(source.outputs["Geometry"], position.inputs["Geometry"]) - group.links.new(smooth_boundary_camera(group, *attributes), position.inputs["Position"]) - output = group.nodes.new("NodeGroupOutput") - group.links.new(position.outputs[0], output.inputs["Geometry"]) - modifier = obj.modifiers.new("Depth", "NODES") - modifier.node_group = group - modifier[control.identifier] = 0 - np.testing.assert_array_equal(evaluated(obj)[0], camera) - modifier[control.identifier] = 12 - obj.update_tag(refresh={"DATA"}) - bpy.context.view_layer.update() - result, result_faces = evaluated(obj) - assert result_faces == faces - assert 2 < result[4, 2] < 10 - assert np.isfinite(result).all() - np.testing.assert_array_equal(result[protected], camera[protected]) - np.testing.assert_array_equal(result[81:], camera[81:]) - np.testing.assert_array_equal(result[80], camera[80]) - band = edge_band(faces, set(boundary_neighbors(faces)) - set(np.flatnonzero(protected))) - interior = [i for i in range(len(camera)) if i not in band] - assert interior - np.testing.assert_array_equal(result[interior], camera[interior]) - valid = camera[:, 2] > 0 - np.testing.assert_allclose(result[valid, :2] / result[valid, 2:3], - camera[valid, :2] / camera[valid, 2:3], atol=1e-6) diff --git a/tests/tools/nodes/test_boundary_smoothing.py b/tests/tools/nodes/test_boundary_smoothing.py index fdf973f..ef9e7c5 100644 --- a/tests/tools/nodes/test_boundary_smoothing.py +++ b/tests/tools/nodes/test_boundary_smoothing.py @@ -5,69 +5,8 @@ from collections import Counter import bpy import bmesh import numpy as np -import pytest -from tests.support.depth_surface import surface, evaluated, assert_closed, component_count -from tools.nodes.common.boundary_smoothing import cut_boundary_influence -from tools.nodes.common.smoothing import pinned_smooth - - -def pinned_group( - iterations=1, - region=1.0, - weight=1.0, - pin_boundary=True, - pin_sharp=True, -): - group = bpy.data.node_groups.new("Pinned smooth", "GeometryNodeTree") - for direction in ("INPUT", "OUTPUT"): - group.interface.new_socket( - name="Geometry", in_out=direction, socket_type="NodeSocketGeometry" - ) - smooth = group.interface.new_socket( - name="Smooth", in_out="INPUT", socket_type="NodeSocketInt" - ) - smooth.default_value = iterations - source = group.nodes.new("NodeGroupInput") - control = group.nodes.new("NodeGroupInput") - region_value = group.nodes.new("ShaderNodeValue") - region_value.outputs[0].default_value = region - result = pinned_smooth( - group, - source.outputs["Geometry"], - control.outputs["Smooth"], - region_value.outputs[0], - weight=weight, - pin_boundary=pin_boundary, - pin_sharp=pin_sharp, - ) - group.links.new(result, group.nodes.new("NodeGroupOutput").inputs["Geometry"]) - return group - - -def pinned_result(points, faces, **options): - bpy.ops.wm.read_factory_settings(use_empty=True) - mesh = bpy.data.meshes.new("Pinned") - mesh.from_pydata(points, [], faces) - obj = bpy.data.objects.new("Pinned", mesh) - bpy.context.collection.objects.link(obj) - obj.modifiers.new("Pinned", "NODES").node_group = pinned_group(**options) - return evaluated(obj)[0] - - -def grid(size): - points = [(x, y, 0.0) for y in range(size) for x in range(size)] - faces = [ - ( - y * size + x, - y * size + x + 1, - (y + 1) * size + x + 1, - (y + 1) * size + x, - ) - for y in range(size - 1) - for x in range(size - 1) - ] - return points, faces +from tests.support.depth_surface import surface, evaluated, assert_closed def diagonal_surface(triangles=False): @@ -113,60 +52,6 @@ def edge_band(faces, seeds, rings=2): return selected -@pytest.mark.parametrize(("outline_scale", "expected_outline"), [(0.1, 0.1), (1.0, 1.0)]) -def test_split_gets_full_influence_and_outline_uses_configured_scale(outline_scale, expected_outline): - bpy.ops.wm.read_factory_settings(use_empty=True) - points, faces, split = [], [], [] - for side in range(2): - offset = len(points) - for y in range(9): - for x in range(5): - points.append((side * 4 + x, y, 0)) - split.append(x == (4 if side == 0 else 0)) - faces.extend( - ( - offset + y * 5 + x, - offset + y * 5 + x + 1, - offset + (y + 1) * 5 + x + 1, - offset + (y + 1) * 5 + x, - ) - for y in range(8) - for x in range(4) - ) - mesh = bpy.data.meshes.new("Boundary influence") - mesh.from_pydata(points, [], faces) - mesh.attributes.new("test_split", "BOOLEAN", "POINT").data.foreach_set("value", split) - obj = bpy.data.objects.new("Boundary influence", mesh) - bpy.context.collection.objects.link(obj) - group = bpy.data.node_groups.new("Boundary influence", "GeometryNodeTree") - for direction in ("INPUT", "OUTPUT"): - group.interface.new_socket(name="Geometry", in_out=direction, socket_type="NodeSocketGeometry") - source = group.nodes.new("NodeGroupInput") - split_attribute = group.nodes.new("GeometryNodeInputNamedAttribute") - split_attribute.data_type = "BOOLEAN" - split_attribute.inputs["Name"].default_value = "test_split" - store = group.nodes.new("GeometryNodeStoreNamedAttribute") - store.data_type, store.domain = "FLOAT", "POINT" - store.inputs["Name"].default_value = "test_influence" - group.links.new(source.outputs["Geometry"], store.inputs["Geometry"]) - group.links.new( - cut_boundary_influence(group, split_attribute.outputs["Attribute"], outline_scale=outline_scale), - store.inputs["Value"], - ) - output = group.nodes.new("NodeGroupOutput") - group.links.new(store.outputs["Geometry"], output.inputs["Geometry"]) - obj.modifiers.new("Boundary influence", "NODES").node_group = group - evaluated_obj = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()) - result = evaluated_obj.to_mesh() - try: - influence = np.array([value.value for value in result.attributes["test_influence"].data]) - finally: - evaluated_obj.to_mesh_clear() - np.testing.assert_allclose(influence[4 * 5], expected_outline, atol=1e-6) - np.testing.assert_allclose(influence[4 * 5 + 4], 1.0, atol=1e-6) - np.testing.assert_allclose(influence[4], 1.0, atol=1e-6) - - def test_cutout_boundary_smooth_defaults_to_four(): obj, _ = surface() socket = next( @@ -222,30 +107,6 @@ def test_smoothing_without_split_moves_outline_only_within_boundary_band(): np.testing.assert_array_equal(evaluated(obj)[0], original) -def test_smoothed_cut_shells_stay_closed_and_separate(): - mode = 1 - from anyimage.common.object import modifier_input_identifier, set_modifier_input - - obj, set_value = diagonal_surface(triangles=True) - obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set( - "value", np.full(len(obj.data.vertices), 0.3, dtype=np.float32) - ) - set_value("Depth Scale", 1) - set_value("Boundary Smooth", 4) - original, faces = evaluated(obj) - count = component_count(faces) - set_value("Mode", mode) - modifier = obj.modifiers[0] - identifier = modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE" if mode == 0 else "DISTANCE") - set_modifier_input(modifier, identifier, 0.05) - obj.update_tag(refresh={"DATA"}) - bpy.context.view_layer.update() - result, shell_faces = evaluated(obj) - assert np.isfinite(result).all() - assert_closed(shell_faces) - assert component_count(shell_faces) == count - - def test_panorama_smoothing_preserves_interior_and_uvs(): from tests.tools.nodes.test_image_depth_panorama import panorama, evaluated as panorama_mesh @@ -346,50 +207,3 @@ def test_panorama_validity_cut_smoothing_stays_in_two_ring_band(): np.testing.assert_array_equal(result_uv, uv) np.testing.assert_array_equal(after[outside], before[outside]) assert np.max(np.linalg.norm(after-before, axis=1)) > 1e-4 - - -def test_boundary_points_follow_the_boundary_strip_blur(): - """A boundary spike relaxes along the outline instead of being pulled inward.""" - points, faces = grid(4) - points[1] = (1.0, -0.5, 0.0) - result = pinned_result(points, faces) - # 边界条带只连接边界邻居:自身权重 1,两个边界邻居权重 0.5。 - np.testing.assert_allclose(result[1], (1.0, -0.25, 0.0), atol=1e-6) - - -def test_sharp_features_lower_the_smoothing_weight(): - """A crease inside the normal blur range reduces the pinned displacement.""" - points, faces = grid(3) - points[4] = (1.0, -0.5, 0.0) - flat = pinned_result(points, faces) - creased = list(points) - creased[0] = (0.0, 0.0, 1.0) - creased = pinned_result(creased, faces) - # 折痕不在尖点的邻居集合内,两次运行的平滑目标相同,只有锐度系数不同。 - flat_step = abs(flat[4][1] + 0.5) - creased_step = abs(creased[4][1] + 0.5) - assert flat_step > 1e-3 - assert creased_step < flat_step * 0.99 - - -def test_junction_band_uses_the_neighbourhood_average(): - """Without the boundary pin a boundary spike follows every connected neighbour.""" - points, faces = grid(4) - points[1] = (1.0, -0.5, 0.0) - result = pinned_result(points, faces, pin_boundary=False) - # 自身权重 1 与三个邻居的平均:(-0.5 + 0 + 0 + 1) / 4。 - np.testing.assert_allclose(result[1], (1.0, 0.125, 0.0), atol=1e-6) - - -def test_junction_band_keeps_the_sharpness_weight(): - """The sharpness weight stays active without the boundary pin.""" - points, faces = grid(3) - points[4] = (1.0, -0.5, 0.0) - flat = pinned_result(points, faces, pin_boundary=False) - creased = list(points) - creased[0] = (0.0, 0.0, 1.0) - creased = pinned_result(creased, faces, pin_boundary=False) - flat_step = abs(flat[4][1] + 0.5) - creased_step = abs(creased[4][1] + 0.5) - assert flat_step > 1e-3 - assert creased_step < flat_step * 0.99 diff --git a/tests/tools/nodes/test_cutout_boundary.py b/tests/tools/nodes/test_cutout_boundary.py index fec82bc..da2a5b7 100644 --- a/tests/tools/nodes/test_cutout_boundary.py +++ b/tests/tools/nodes/test_cutout_boundary.py @@ -4,11 +4,8 @@ import bpy import numpy as np import pytest -from anyimage.common.object import modifier_input_identifier, set_modifier_input from tests.support.depth_surface import surface, evaluated from tests.tools.nodes.test_boundary_smoothing import diagonal_surface -from tools.nodes.common.cutout_boundary import remove_boundary_triangles -from tools.nodes.groups.image_depth_cutout import build_normal_direction, build_normal_weight def profile_values(obj): @@ -32,23 +29,6 @@ def vertex_uv(obj): result.to_mesh_clear() -def test_boundary_triangle_cleanup_is_one_pass(): - obj, _ = surface(resolution=2) - group = obj.modifiers[0].node_group - mesh = bpy.data.meshes.new("Boundary ears") - mesh.from_pydata([(0,0,0),(1,0,0),(1,1,0),(0,1,0),(.5,.5,0),(2,0,0)], [], - [(0,1,4),(1,2,4),(2,3,4),(3,0,4),(1,5,2)]) - obj.data = mesh - source = group.nodes.new("NodeGroupInput") - cleaned = remove_boundary_triangles(group, source.outputs["Geometry"]) - group.links.new(cleaned, next(n for n in group.nodes if n.type == "GROUP_OUTPUT").inputs["Geometry"]) - obj.update_tag(refresh={"DATA"}) - bpy.context.view_layer.update() - _, faces = evaluated(obj) - assert len(faces) == 4 - assert all(len(f) == 3 for f in faces) - - @pytest.mark.parametrize("iterations", [0, 16]) def test_cleaned_sawtooth_smoothing_preserves_face_orientation(iterations): obj, set_value = diagonal_surface(triangles=True) @@ -90,63 +70,3 @@ def test_split_profile_tapers_within_boundary_band(): assert values.max() == pytest.approx(.3) set_value("Depth Split", 0) np.testing.assert_allclose(profile_values(obj), .3) - - -def sampled(obj, group, value, name): - """Store one field on the points and read it back from the evaluated mesh.""" - data_type, reader = {"VECTOR": ("FLOAT_VECTOR", "vector"), "VALUE": ("FLOAT", "value")}[value.type] - store = group.nodes.new("GeometryNodeStoreNamedAttribute") - store.data_type, store.domain = data_type, "POINT" - store.inputs["Name"].default_value = name - source = group.nodes.new("NodeGroupInput") - group.links.new(source.outputs["Geometry"], store.inputs["Geometry"]) - group.links.new(value, store.inputs["Value"]) - group.links.new(store.outputs[0], next(n for n in group.nodes if n.type == "GROUP_OUTPUT").inputs["Geometry"]) - obj.update_tag(refresh={"DATA"}) - bpy.context.view_layer.update() - result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()) - mesh = result.to_mesh() - try: - return np.array([getattr(item, reader) for item in mesh.attributes[name].data]) - finally: - result.to_mesh_clear() - - -def test_normal_weight_follows_profile_only_in_balloon_mode(): - obj, _ = surface(resolution=2) - group = obj.modifiers[0].node_group - is_shell = group.nodes.new("FunctionNodeInputBool") - profile = group.nodes.new("ShaderNodeValue") - profile.outputs[0].default_value = .25 - weight = build_normal_weight(group, is_shell.outputs[0], profile.outputs[0]) - np.testing.assert_allclose(sampled(obj, group, weight, "normal_weight"), .25) - is_shell.boolean = True - np.testing.assert_allclose(sampled(obj, group, weight, "normal_weight"), 0.0) - - -def test_normal_direction_turns_the_edge_with_edge_turn(): - obj, _ = surface(resolution=2) - group = obj.modifiers[0].node_group - smoothed = group.nodes.new("ShaderNodeCombineXYZ") - smoothed.inputs["Z"].default_value = 1 - average = group.nodes.new("ShaderNodeCombineXYZ") - average.inputs["X"].default_value = 1 - weight = group.nodes.new("ShaderNodeValue") - controls = group.nodes.new("NodeGroupInput") - direction = build_normal_direction( - group, controls, smoothed.outputs[0], average.outputs[0], weight.outputs[0], - ) - modifier = obj.modifiers[0] - turn = modifier_input_identifier(group, "Edge Turn") - - def result(weight_value, turn_value): - weight.outputs[0].default_value = weight_value - set_modifier_input(modifier, turn, turn_value) - obj.update_tag(refresh={"DATA"}) - bpy.context.view_layer.update() - return sampled(obj, group, direction, "normal_direction")[0] - - np.testing.assert_allclose(result(0.0, 0.0), (0, 0, 1), atol=1e-6) - np.testing.assert_allclose(result(0.0, 1.0), (1, 0, 0), atol=1e-6) - np.testing.assert_allclose(result(0.0, 2.0), (2, 0, -1) / np.sqrt(5), atol=1e-6) - np.testing.assert_allclose(result(1.0, 2.0), (1, 0, 0), atol=1e-6) diff --git a/tests/tools/nodes/test_depth_relief_planes.py b/tests/tools/nodes/test_depth_relief_planes.py index cbbce68..b4904c9 100644 --- a/tests/tools/nodes/test_depth_relief_planes.py +++ b/tests/tools/nodes/test_depth_relief_planes.py @@ -1,6 +1,5 @@ """Evaluate complete-image projection and rectangular surface topology.""" -from collections import Counter import bpy import numpy as np import pytest @@ -88,9 +87,6 @@ def test_shell_keeps_front_and_closes_rectangular_boundary(surface, thickness): front = evaluated(obj)[0] set_value("Thickness", thickness) points, faces, uv, materials, indices, _ = evaluated(obj) - assert len(points) == 2 * len(front) - edges = Counter(tuple(sorted((a, b))) for f in faces for a, b in zip(f, (*f[1:], f[0]))) - assert set(edges.values()) == {2} front_set = {tuple(point) for point in front} assert front_set <= {tuple(point) for point in points} assert np.isfinite(points).all() and np.isfinite(uv).all() @@ -99,8 +95,6 @@ def test_shell_keeps_front_and_closes_rectangular_boundary(surface, thickness): @pytest.mark.parametrize("valid_only", [False, True]) def test_depth_split_preserves_faces_and_closes_each_shell(surface, valid_only): - from tests.support.depth_surface import component_count - obj, set_value, _, _, image = surface pixels = np.asarray(image.pixels[:], dtype=np.float32).reshape(8, 16, 4) pixels[:, 8:, 2] += 10 @@ -114,15 +108,10 @@ def test_depth_split_preserves_faces_and_closes_each_shell(surface, valid_only): points, faces, uv, materials, indices, _ = evaluated(obj) assert len(points) > len(base[0]) assert len(faces) == len(base[1]) - assert component_count(faces) > component_count(base[1]) np.testing.assert_allclose(uv, base[2]) assert (materials, indices) == base[3:5] set_value("Thickness", 0.2) shell_points, shell_faces, shell_uv, *_ = evaluated(obj) - assert len(shell_points) == 2 * len(points) - edges = Counter(tuple(sorted((a, b))) for face in shell_faces - for a, b in zip(face, (*face[1:], face[0]))) - assert set(edges.values()) == {2} assert np.isfinite(shell_points).all() and np.isfinite(shell_uv).all() set_value("Thickness", 0.0) set_value("Depth Split", 0.0) @@ -369,10 +358,6 @@ def test_valid_only_precedes_closed_shell_generation(surface): depth_image.pixels.foreach_set(pixels.ravel()) depth_image.update() set_value("Depth Scale", 1.0) - front = evaluated(obj)[0] set_value("Thickness", 0.2) points, faces, uv, *_ = evaluated(obj) - assert len(points) == 2 * len(front) - edges = Counter(tuple(sorted((a, b))) for face in faces for a, b in zip(face, (*face[1:], face[0]))) - assert set(edges.values()) == {2} assert np.isfinite(points).all() and np.isfinite(uv).all() diff --git a/tests/tools/nodes/test_depth_strip_cleanup.py b/tests/tools/nodes/test_depth_strip_cleanup.py index 460f87e..44875b0 100644 --- a/tests/tools/nodes/test_depth_strip_cleanup.py +++ b/tests/tools/nodes/test_depth_strip_cleanup.py @@ -5,84 +5,9 @@ import bpy import numpy as np import pytest -from tools.nodes.common.depth_surface import _remove_strip_faces - - -def cleaned_faces(vertices, faces, *, triangles=False): - bpy.ops.wm.read_factory_settings(use_empty=True) - mesh = bpy.data.meshes.new("Topology") - mesh.from_pydata(vertices, [], faces) - ids = mesh.attributes.new("source_face", "INT", "FACE") - ids.data.foreach_set("value", np.arange(len(faces), dtype=np.int32)) - uv = mesh.uv_layers.new(name="UVMap") - for loop in mesh.loops: - uv.data[loop.index].uv = mesh.vertices[loop.vertex_index].co[:2] - obj = bpy.data.objects.new("Topology", mesh) - bpy.context.collection.objects.link(obj) - group = bpy.data.node_groups.new("Strip cleanup test", "GeometryNodeTree") - group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry") - group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry") - source = group.nodes.new("NodeGroupInput") - result = _remove_strip_faces(group, source.outputs["Geometry"], triangles=triangles) - group.links.new(result, group.nodes.new("NodeGroupOutput").inputs["Geometry"]) - obj.modifiers.new("Cleanup", "NODES").node_group = group - bpy.context.view_layer.update() - evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()) - result = evaluated.to_mesh() - try: - assert all(np.isfinite(vertex.co).all() for vertex in result.vertices) - if not result.polygons: - assert not result.vertices and not result.edges - return set() - assert "UVMap" in result.attributes - for loop in result.loops: - np.testing.assert_allclose(result.uv_layers["UVMap"].data[loop.index].uv, - result.vertices[loop.vertex_index].co[:2]) - return {value.value for value in result.attributes["source_face"].data} - finally: - evaluated.to_mesh_clear() - - -@pytest.mark.parametrize("cells, expected", [ - ([(0, 0)], set()), - ([(x, 0) for x in range(5)], set()), - ([(x, y) for y in range(3) for x in range(3)], set(range(9))), - # The center becomes isolated only after its three arms disappear. - ([(0, 0), (1, 0), (2, 0), (1, 1)], {1}), -]) -@pytest.mark.parametrize("offset, reverse", [(0, False), (2, True)]) -def test_quad_strips_and_corners_are_classified_once(cells, expected, offset, reverse): - vertices, indices, faces = [], {}, [] - for x, y in cells: - face = [] - for point in [(x, y, 0), (x + 1, y, 0), (x + 1, y + 1, 0), (x, y + 1, 0)]: - if point not in indices: - indices[point] = len(vertices) - vertices.append(point) - face.append(indices[point]) - face = face[offset:] + face[:offset] - faces.append(face[::-1] if reverse else face) - assert cleaned_faces(vertices, faces) == expected - - -@pytest.mark.parametrize("width,height,expected", [(2, 2, 8), (3, 3, 18)]) -def test_triangle_pairs_remove_strips_and_preserve_surface_corners(width, height, expected): - vertices = [(x, y, 0) for y in range(height + 1) for x in range(width + 1)] - faces = [] - for y in range(height): - for x in range(width): - a = y * (width + 1) + x - b, c, d = a + 1, a + width + 2, a + width + 1 - faces.extend([(a, b, c), (a, c, d)]) - # Reverse alternate faces to test matching by vertex identity, not winding. - faces = [face[::-1] if i % 2 else face[1:] + face[:1] for i, face in enumerate(faces)] - result = cleaned_faces(vertices, faces, triangles=True) - assert len(result) == expected - - @pytest.mark.parametrize("kind", ["plane", "cutout"]) -def test_strips_are_removed_before_projection_and_closed_thickness(kind): - from tests.support.depth_surface import surface, evaluated, assert_closed, component_count +def test_strips_are_removed_before_projection(kind): + from tests.support.depth_surface import surface, evaluated from tests.support.planes import create_surface if kind == "plane": @@ -114,15 +39,7 @@ def test_strips_are_removed_before_projection_and_closed_thickness(kind): assert len(faces) == 6 and len(vertices) == 7 else: assert len(faces) == 4 and len(vertices) == 9 - assert component_count(faces) == 1 - set_value("Thickness", 0.2) - vertices, shell = evaluated(obj) - assert len(shell) == (18 if kind == "cutout" else 16) - assert_closed(shell) assert np.isfinite(vertices).all() - assert component_count(shell) == 1 - set_value("Thickness", 0.0) - assert len(evaluated(obj)[1]) == (6 if kind == "cutout" else 4) def test_zero_split_bypasses_cleanup_and_positive_split_can_empty_mesh(): @@ -144,21 +61,3 @@ def test_zero_split_bypasses_cleanup_and_positive_split_can_empty_mesh(): assert restored_faces == faces np.testing.assert_array_equal(restored, original) - -def test_cutout_removes_an_all_boundary_triangle_strip(): - import bmesh - from tests.support.depth_surface import surface, evaluated - - obj, set_value = surface(step=0.0, resolution=1) - mesh = bmesh.new() - mesh.from_mesh(obj.data) - bmesh.ops.triangulate(mesh, faces=list(mesh.faces)) - mesh.to_mesh(obj.data) - mesh.free() - # Every vertex lies on the boundary, even before any depth separation. - for split in (0.0, 0.001, 0.0): - set_value("Depth Split", split) - for thickness in (0.0, 0.2): - set_value("Thickness", thickness) - points, faces = evaluated(obj) - assert not faces and not len(points) diff --git a/tests/tools/nodes/test_depth_surface_split.py b/tests/tools/nodes/test_depth_surface_split.py index abf593b..6627f7c 100644 --- a/tests/tools/nodes/test_depth_surface_split.py +++ b/tests/tools/nodes/test_depth_surface_split.py @@ -6,22 +6,7 @@ import numpy as np import pytest -from tests.support.depth_surface import assert_closed, component_count, evaluated, surface - - -@pytest.mark.parametrize("thickness", [0.1, 1.4]) -def test_surface_caps_and_segmented_walls_are_connected(thickness): - obj, set_value = surface() - set_value("Depth Split", 0.5) - 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 +from tests.support.depth_surface import evaluated, surface @pytest.mark.parametrize("size", [0.01, 100.0]) @@ -78,15 +63,6 @@ def test_depth_scale_multiplies_split_strength(): assert scaled_faces == reference_faces -def test_full_wrapper_keeps_closed_shell(): - obj, set_value = surface() - set_value("Depth Split", 0.5) - set_value("Thickness", 0.2) - coords, faces = evaluated(obj) - assert np.isfinite(coords).all() - assert_closed(faces) - - @pytest.mark.parametrize("resolution", [1, 4]) def test_planar_depth_preserves_blocks_and_cleans_single_face_width(resolution): obj, set_value = surface(step=0.0, resolution=resolution) @@ -123,16 +99,6 @@ def test_outline_points_use_adjacent_face_depth_without_split(): np.testing.assert_allclose(coords[boundary, 1], 0, atol=1e-5) -def test_thickness_handles_more_than_4096_source_vertices(): - obj, set_value = surface(step=0.0, resolution=45) - assert len(obj.data.vertices) > 4096 - set_value("Depth Split", 0.0) - set_value("Thickness", 0.02) - coords, faces = evaluated(obj) - assert np.isfinite(coords).all() - assert_closed(faces) - - def test_small_positive_split_preserves_unsplit_projection(): obj, set_value = surface(step=2.0) original, original_faces = evaluated(obj) @@ -156,19 +122,6 @@ def test_cut_vertices_preserve_camera_rays_and_other_vertices(): assert not np.allclose(after, before, atol=1e-6) -def test_source_surface_sampling_preserves_depth_on_side_walls(): - obj, set_value = surface(step=4.0) - set_value("Depth Split", 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]) def test_temporary_fields_are_removed_and_user_attributes_survive(thickness): obj, set_value = surface() @@ -221,25 +174,6 @@ def test_zero_center_distance_does_not_split_or_produce_nan(): assert np.isfinite(coords).all() -def test_triangle_corner_weights_do_not_open_sloped_side_walls(): - obj, set_value = surface(step=4.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("Depth Split", 0.5) - set_value("Thickness", 0.8) - coords, faces = evaluated(obj) - assert_closed(faces) - assert component_count(faces) == 2 - - @pytest.mark.parametrize("resolution", [1, 4]) def test_relative_distance_slope_has_density_independent_selection(resolution): obj, set_value = surface(step=0.0, resolution=resolution) diff --git a/tests/tools/nodes/test_validity.py b/tests/tools/nodes/test_validity.py deleted file mode 100644 index a300001..0000000 --- a/tests/tools/nodes/test_validity.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Verify face culling removes the faces the depth mask excludes.""" - -import bpy - -from tools.nodes.common.nodes import compare_node, interface_socket -from tools.nodes.common.validity import delete_invalid_faces - - -def test_face_culling_removes_invalid_faces_without_leaving_wire_geometry(): - bpy.ops.wm.read_factory_settings(use_empty=True) - mesh = bpy.data.meshes.new("Two Quads") - mesh.from_pydata([(0, 0, 0), (1, 0, 0), (2, 0, 0), - (0, 1, 0), (1, 1, 0), (2, 1, 0)], [], - [(0, 1, 4, 3), (1, 2, 5, 4)]) - obj = bpy.data.objects.new("Surface", mesh) - bpy.context.collection.objects.link(obj) - group = bpy.data.node_groups.new("Face Culling", "GeometryNodeTree") - interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry") - interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry") - inlet = group.nodes.new("NodeGroupInput") - outlet = group.nodes.new("NodeGroupOutput") - index = group.nodes.new("GeometryNodeInputIndex") - invalid = compare_node(group, "LESS_THAN", index.outputs["Index"], 1, data_type="INT") - geometry = delete_invalid_faces(group, inlet.outputs["Geometry"], invalid) - group.links.new(geometry, outlet.inputs["Geometry"]) - modifier = obj.modifiers.new("Cull", "NODES") - modifier.node_group = group - evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()) - result = evaluated.to_mesh() - try: - assert len(result.polygons) == 1 - assert len(result.edges) == 4 - assert {tuple(vertex.co) for vertex in result.vertices} == { - (1, 0, 0), (2, 0, 0), (1, 1, 0), (2, 1, 0), - } - finally: - evaluated.to_mesh_clear() diff --git a/uv.lock b/uv.lock index 30c77e4..fd45632 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "anyimage" -version = "1.0.0" +version = "1.0.1" source = { editable = "." } dependencies = [ { name = "blendjob" },