chore: bump version to 1.0.1 and update related files

This commit is contained in:
2026-09-18 08:36:51 +08:00
parent 0e0287d340
commit 531f7a4dfb
10 changed files with 7 additions and 554 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

2
uv.lock generated
View File

@@ -13,7 +13,7 @@ wheels = [
[[package]]
name = "anyimage"
version = "1.0.0"
version = "1.0.1"
source = { editable = "." }
dependencies = [
{ name = "blendjob" },