refactor: incorporate Depth Scale into Depth Split criteria for consistent geometry handling

This commit is contained in:
2026-09-17 00:39:45 +08:00
parent c681d51fd1
commit efd117dd9b
19 changed files with 225 additions and 32 deletions

View File

@@ -85,7 +85,9 @@ def diagonal_surface(triangles=False):
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Depth Split", 0.5)
set_value("Depth Scale", 0)
# A shallow display keeps the cut nearly flat so smoothing cannot fold the
# sawtooth slivers this fixture exists to exercise.
set_value("Depth Scale", 0.05)
return obj, set_value
@@ -201,7 +203,6 @@ def test_smoothing_reduces_stairs_without_leaving_two_ring_band(triangles):
before.append(np.linalg.norm(original[adjacent].mean(axis=0) - original[i]))
after.append(np.linalg.norm(smoothed[adjacent].mean(axis=0) - smoothed[i]))
assert sum(after) < 0.8 * sum(before)
np.testing.assert_array_equal(smoothed[:, 1], original[:, 1])
set_value("Boundary Smooth", 0)
np.testing.assert_array_equal(evaluated(obj)[0], original)
@@ -393,4 +394,3 @@ def test_junction_band_keeps_the_sharpness_weight():
creased_step = abs(creased[4][1] + 0.5)
assert flat_step > 1e-3
assert creased_step < flat_step * 0.99

View File

@@ -20,6 +20,18 @@ def profile_values(obj):
result.to_mesh_clear()
def vertex_uv(obj):
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
mesh = result.to_mesh()
try:
uv = np.zeros((len(mesh.vertices), 2))
for loop in mesh.loops:
uv[loop.vertex_index] = mesh.uv_layers["UVMap"].data[loop.index].uv
return uv
finally:
result.to_mesh_clear()
def test_boundary_triangle_cleanup_is_one_pass():
obj, _ = surface(resolution=2)
group = obj.modifiers[0].node_group
@@ -62,12 +74,14 @@ def test_split_profile_tapers_within_boundary_band(resolution, aspect):
image.update()
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set(
"value", np.full(len(obj.data.vertices), .3, dtype=np.float32))
set_value("Depth Scale", 0)
set_value("Depth Scale", 1)
np.testing.assert_allclose(profile_values(obj), .3)
set_value("Depth Split", .1)
points, faces = evaluated(obj)
boundary = boundary_neighbors(faces)
cut = [i for i in boundary if 0 < points[i,2] < 1 and .5*aspect < points[i,0] < 1.5*aspect]
uv = vertex_uv(obj)
# The depth step sits at u=0.5, so that column is the split line.
cut = [i for i in boundary if abs(uv[i,0] - .5) < 1e-4 and 1e-4 < uv[i,1] < 1 - 1e-4]
assert cut
band = edge_band(faces, cut)
outside = [i for i in range(len(points)) if i not in band]

View File

@@ -131,6 +131,26 @@ def test_depth_split_preserves_faces_and_closes_each_shell(surface, valid_only):
assert restored[1] == base[1]
def test_depth_scale_gates_depth_split(surface):
obj, set_value, _, _, image = surface
pixels = np.asarray(image.pixels[:], dtype=np.float32).reshape(8, 16, 4)
pixels[:, 8:, 2] += 10
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Depth Scale", 1.0)
whole = evaluated(obj)
set_value("Depth Split", 1.0)
split = evaluated(obj)
assert len(split[0]) > len(whole[0])
set_value("Depth Scale", 0.0)
set_value("Depth Split", 0.0)
flat = evaluated(obj)
set_value("Depth Split", 1.0)
cancelled = evaluated(obj)
assert cancelled[1] == flat[1]
np.testing.assert_allclose(cancelled[0], flat[0], atol=1e-6)
def test_depth_jump_keeps_rectangular_faces(surface):
obj, set_value, _, _, image = surface
base = evaluated(obj)

View File

@@ -132,6 +132,7 @@ def test_zero_split_bypasses_cleanup_and_positive_split_can_empty_mesh(kind):
obj, set_value, _, _, _ = create_surface("DEPTH")
set_value("Subdivide", 0)
set_value("Depth Scale", 1.0)
original, faces = evaluated(obj)
assert len(faces) == 2
set_value("Depth Split", 0.001)

View File

@@ -75,8 +75,8 @@ def test_split_mapping_and_uniform_scale(size):
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[:, 1]).max() < 1e-6
coords, _ = evaluated(obj)
assert len(coords) == counts[0] and np.abs(coords[:, 1]).max() < 1e-6
def test_midpoint_selects_relative_ray_jump_and_preserves_faces():
@@ -87,16 +87,33 @@ def test_midpoint_selects_relative_ray_jump_and_preserves_faces():
assert set(np.round(coords[:, 1], 4)) == {2.0, -2.0}
@pytest.mark.parametrize("depth_scale", [0.0, 2.0])
def test_split_topology_uses_depth_before_display_scaling(depth_scale):
@pytest.mark.parametrize("depth_scale", [0.0, 1.0])
def test_depth_scale_scales_split_selection(depth_scale):
obj, set_value = surface(step=4.0)
set_value("Depth Split", 0.5)
original, original_faces = evaluated(obj)
set_value("Depth Scale", depth_scale)
set_value("Depth Split", 0.0)
whole, whole_faces = evaluated(obj)
set_value("Depth Split", 0.5)
coords, faces = evaluated(obj)
assert len(coords) == len(original) == 18
assert faces == original_faces
np.testing.assert_allclose(coords[:, 1], original[:, 1] * depth_scale, atol=1e-6)
assert np.isfinite(coords).all()
if depth_scale == 0.0:
# A flat projection keeps every face, so there is nothing to separate.
np.testing.assert_allclose(coords, whole, atol=1e-6)
assert faces == whole_faces
else:
assert len(coords) > len(whole)
def test_depth_scale_multiplies_split_strength():
obj, set_value = surface(step=4.0)
set_value("Depth Scale", 0.5)
set_value("Depth Split", 1.0)
scaled, scaled_faces = evaluated(obj)
set_value("Depth Scale", 1.0)
set_value("Depth Split", 0.5)
reference, reference_faces = evaluated(obj)
assert len(scaled) == len(reference) > 15
assert scaled_faces == reference_faces
def test_full_wrapper_keeps_closed_shell():
@@ -314,6 +331,7 @@ def test_split_uses_ray_distance_and_ignores_absolute_distance_scale(kind):
if kind == "plane":
obj, set_value, _, _, image = create_surface("DEPTH")
set_value("Subdivide", 1)
set_value("Depth Scale", 1)
else:
obj, set_value = surface(step=0)
image = bpy.data.images["Camera"]

View File

@@ -194,6 +194,21 @@ def test_material_uv_matches_radial_sampling_and_faces_point_inward():
result.to_mesh_clear()
def test_depth_scale_gates_split_selection():
depth = np.ones((64, 128))
depth[:, 64:] = 7
obj, set_value = panorama(depth)
set_value("Depth Scale", 0)
whole = evaluated(obj)
set_value("Depth Split", 1)
flat = evaluated(obj)
# A flat projection keeps the sphere whole, whatever the split strength is.
assert flat[1] == whole[1]
np.testing.assert_allclose(flat[0], whole[0], atol=1e-6)
set_value("Depth Scale", 1)
assert len(evaluated(obj)[0]) > len(flat[0])
def test_split_strength_and_distance_scale_invariance():
depth = np.ones((64, 128))
depth[:, 64:] = 7

View File

@@ -14,6 +14,7 @@ def test_depth_attribute_cleanup_has_no_warnings(kind):
obj, set_value = surface()
elif kind == "plane":
obj, set_value, *_ = create_surface("DEPTH")
set_value("Depth Scale", 1)
else:
obj, set_value = panorama(subdivide=2)
modifier = obj.modifiers[0]