Refactor depth node groups and simplify options

- Removed `Smooth Weight` input from `O Image Depth Plane`, `O Image Depth Cutout`, and `O Image Depth Panorama` to streamline the interface and maintain consistent boundary smoothing behavior.
- Adjusted the `Reference Depth` input to appear last in the options panel for better organization, reflecting its role as a calibration value.
- Consolidated the `expand_smooth()` function to only accept necessary parameters, eliminating unused branches related to `Smooth Weight` and `pin_sharp`.
- Updated related tests and node assets to ensure functionality remains intact and consistent with the new structure.
- Introduced new specifications to document the changes and ensure compliance with the updated requirements.
This commit is contained in:
2026-09-16 13:12:12 +08:00
parent 6ca084ee4a
commit 7f69630371
26 changed files with 741 additions and 178 deletions

View File

@@ -90,7 +90,6 @@ def test_cutout_fine_outline_smooths_only_within_boundary_band():
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Boundary Smooth", 0)
set_value("Smooth Weight", 1)
before, original_faces = evaluated(obj)
set_value("Boundary Smooth", 12)
after, result_faces = evaluated(obj)
@@ -114,7 +113,7 @@ def test_plane_validity_hole_blurs_depth_and_protects_rectangle():
pixels[1:3, 7:9, 2] = 12
image.pixels.foreach_set(pixels.ravel())
image.update()
for name, value in (("Subdivide", 5), ("Depth Scale", 1), ("Smooth Weight", 0)):
for name, value in (("Subdivide", 5), ("Depth Scale", 1)):
set_value(name, value)
before, faces, uv, *_ = plane_mesh(obj)
set_value("Boundary Smooth", 12)
@@ -126,8 +125,11 @@ def test_plane_validity_hole_blurs_depth_and_protects_rectangle():
corners = np.concatenate(faces)
outer = np.unique(corners[protected])
np.testing.assert_array_equal(after[outer], before[outer])
np.testing.assert_allclose(after[:, [0, 2]] / (1.2 + after[:, 1:2]),
before[:, [0, 2]] / (1.2 + before[:, 1:2]), atol=1e-6)
# Position smoothing moves the cut band off its projection ray by design.
band = edge_band(faces, boundary_neighbors(faces))
kept = [i for i in range(len(before)) if i not in band]
np.testing.assert_allclose(after[kept][:, [0, 2]] / (1.2 + after[kept][:, 1:2]),
before[kept][:, [0, 2]] / (1.2 + before[kept][:, 1:2]), atol=1e-6)
set_value("Depth Mask", False)
intact = plane_mesh(obj)[0]
set_value("Boundary Smooth", 0)
@@ -144,7 +146,6 @@ def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(region):
spike = ((distance < 4) & (yy > 40) & (yy < 44)) if region == "seam" else ((yy > 50) & (xx < 8))
depth = np.where(spike, 10., 3.)
obj, set_value = panorama(depth, (~hole).astype(float), subdivide=5)
set_value("Smooth Weight", 0)
before, faces, uv = panorama_mesh(obj)
set_value("Boundary Smooth", 12)
after, result_faces, result_uv = panorama_mesh(obj)
@@ -154,18 +155,28 @@ def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(region):
smoothed = np.linalg.norm(after, axis=1)
rim = list(boundary_neighbors(faces))
assert smoothed[rim].max() < radii[rim].max() - .1
np.testing.assert_allclose(after / smoothed[:, None], before / radii[:, None], atol=1e-6)
band = edge_band(faces, boundary_neighbors(faces))
interior = [i for i in range(len(before)) if i not in band]
assert interior
# Position smoothing moves the opening rim off its radial direction by design.
np.testing.assert_allclose(after[interior] / smoothed[interior, None],
before[interior] / radii[interior, None], atol=1e-6)
np.testing.assert_array_equal(after[interior], before[interior])
set_value("Depth Mask", False)
near = panorama_mesh(obj)[0]
set_value("Invalid Distance", 250)
far = panorama_mesh(obj)[0]
far, far_faces, _ = panorama_mesh(obj)
valid = np.linalg.norm(near, axis=1) < 50
assert valid.any() and (~valid).any()
np.testing.assert_array_equal(far[valid], near[valid])
np.testing.assert_allclose(np.linalg.norm(far[~valid], axis=1), 250, atol=5e-5)
# The smoothed rim blends into its neighbours, so only the interior keeps the exact distance.
far_band = edge_band(far_faces, boundary_neighbors(far_faces))
kept = [i for i in range(len(far)) if i not in far_band and not valid[i]]
assert kept
np.testing.assert_allclose(np.linalg.norm(far[kept], axis=1), 250, atol=5e-5)
set_value("Depth Scale", 0)
np.testing.assert_allclose(np.linalg.norm(panorama_mesh(obj)[0], axis=1), 1, atol=1e-6)
sphere, sphere_faces, _ = panorama_mesh(obj)
sphere_band = edge_band(sphere_faces, boundary_neighbors(sphere_faces))
interior = [i for i in range(len(sphere)) if i not in sphere_band]
assert interior
np.testing.assert_allclose(np.linalg.norm(sphere[interior], axis=1), 1, atol=1e-6)

View File

@@ -143,9 +143,6 @@ def test_smoothing_reduces_stairs_without_leaving_two_ring_band(triangles):
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("Smooth Weight", 0)
np.testing.assert_array_equal(evaluated(obj)[0], original)
set_value("Smooth Weight", 1)
set_value("Boundary Smooth", 0)
np.testing.assert_array_equal(evaluated(obj)[0], original)

View File

@@ -9,7 +9,7 @@ from scipy.spatial import cKDTree
from anyimage.common.object import modifier_input_identifier, set_modifier_input
from anyimage.operators.cutout_tool.object import load_cutout_node_group
from tests.support.depth_surface import assert_closed, evaluated
from tests.support.depth_surface import assert_closed, assert_single_vertex_fans, evaluated
from tools.nodes.common.normal_map import AXIS_ATTRIBUTE, FACE_ATTRIBUTE, ROTATION_ATTRIBUTE
from tools.nodes.groups.image_depth_cutout import build_image_depth_cutout_group
from tools.nodes.groups.image_cutout_symmetry import build_image_cutout_symmetry_group
@@ -100,6 +100,7 @@ def symmetry(request):
def test_symmetry_interface_and_cutout_defaults(symmetry):
_obj, cutout, mirror, _cutout_setter, _mirror_setter = symmetry
assert not any(node.bl_idname == "GeometryNodeMeshBoolean" for node in mirror.node_group.nodes)
mirror_inputs = [
item
for item in mirror.node_group.interface.items_tree
@@ -146,6 +147,102 @@ def test_final_symmetry_axis_is_x_and_z_is_up(symmetry):
assert cKDTree(points).query(points * (-1, 1, 1))[0].max() < 1e-5
assert np.ptp(points[:, 2]) > 1.0
assert_closed(faces)
assert_single_vertex_fans(faces)
def _tolerance(points):
size = float(np.linalg.norm(points.max(axis=0) - points.min(axis=0)))
return max(size, 1e-8) * 1e-7
def _evaluated(obj):
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
mesh = result.to_mesh()
try:
points = np.array([vertex.co[:] for vertex in mesh.vertices])
faces = [tuple(polygon.vertices) for polygon in mesh.polygons]
marker = np.array([item.value for item in mesh.attributes[FACE_ATTRIBUTE].data])
finally:
result.to_mesh_clear()
return points, faces, marker
def _open_edges(faces):
counts = Counter(
tuple(sorted((a, b))) for f in faces for a, b in zip(f, (*f[1:], f[0]))
)
return [edge for edge, count in counts.items() if count == 1]
def _marked_vertices(points, faces, marker, value):
indices = {index for face, item in zip(faces, marker) if item == value for index in face}
return points[sorted(indices)]
def _front_vertices(points, faces, marker, predicate):
return np.array(
[point for point in _marked_vertices(points, faces, marker, 1) if predicate(point)]
)
def _positions(points):
return {tuple(np.round(point, 6)) for point in points}
def _front_rim(points, faces, marker, tolerance):
return _front_vertices(points, faces, marker, lambda point: abs(point[0]) <= tolerance)
def test_seam_clamps_geometry_onto_the_symmetry_plane(symmetry):
obj, _cutout, _mirror, _cutout_setter, _mirror_setter = symmetry
points, faces, marker = _evaluated(obj)
tolerance = _tolerance(points)
plane = np.abs(points[:, 0]) <= tolerance
assert _marked_vertices(points, faces, marker, 1)[:, 0].min() >= -tolerance
assert _marked_vertices(points, faces, marker, 2)[:, 0].max() <= tolerance
assert plane.any()
assert not any(all(plane[index] for index in face) for face in faces)
# Kept straddling faces fold onto the plane instead of being discarded.
assert any(plane[list(face)].any() and not plane[list(face)].all() for face in faces)
def test_disabled_fill_keeps_the_welded_seam(symmetry):
obj, _cutout, _mirror, _cutout_setter, mirror_setter = symmetry
mirror_setter("Fill Sides", False)
points, faces = evaluated(obj)
tolerance = _tolerance(points)
plane = np.abs(points[:, 0]) <= tolerance
open_edges = _open_edges(faces)
assert open_edges
assert not any(plane[edge[0]] and plane[edge[1]] for edge in open_edges)
def test_fill_smooth_only_relaxes_the_seam(symmetry):
obj, _cutout, _mirror, _cutout_setter, mirror_setter = symmetry
mirror_setter("Fill Sides", True)
mirror_setter("Fill Smooth", 0)
plain_points, plain_faces, plain_marker = _evaluated(obj)
mirror_setter("Fill Smooth", 4)
smooth_points, smooth_faces, smooth_marker = _evaluated(obj)
tolerance = _tolerance(plain_points)
for points, faces in ((plain_points, plain_faces), (smooth_points, smooth_faces)):
plane = np.abs(points[:, 0]) <= tolerance
assert plane.any()
assert not any(all(plane[index] for index in face) for face in faces)
plain_interior = _front_vertices(
plain_points, plain_faces, plain_marker, lambda point: point[0] > tolerance
)
smooth_interior = _front_vertices(
smooth_points, smooth_faces, smooth_marker, lambda point: point[0] > tolerance
)
assert _positions(plain_interior) == _positions(smooth_interior)
plain_rim = _front_rim(plain_points, plain_faces, plain_marker, tolerance)
smooth_rim = _front_rim(smooth_points, smooth_faces, smooth_marker, tolerance)
assert plain_rim.shape == smooth_rim.shape
assert not np.allclose(np.sort(plain_rim[:, 1:], axis=0), np.sort(smooth_rim[:, 1:], axis=0))
def test_object_normal_attributes_follow_final_orientation(symmetry):

View File

@@ -132,7 +132,8 @@ def test_controls_and_material_slot_inheritance():
assert inputs["Invalid Distance"].parent.name == "Options"
assert inputs["Invalid Distance"].default_value == 100
assert inputs["Invalid Distance"].subtype == "DISTANCE"
assert inputs["Smooth Weight"].parent.name == "Options"
assert inputs["Boundary Smooth"].parent.name == "Options"
assert "Smooth Weight" not in inputs
assert inputs["Depth Mask"].parent == inputs["Subdivide"].parent
assert inputs["Depth Mask"].default_value
obj.data.from_pydata([(0, 0, 0)], [], [])

View File

@@ -18,7 +18,7 @@ def test_saved_parameter_descriptions_match_definitions():
parameters = {
"O Image Plane": {"Subdivide"},
"O Image Depth Plane": {
"Subdivide", "Thickness", "Depth Split", "Smooth Weight",
"Subdivide", "Thickness", "Depth Split",
"Depth Mask", "Mask Threshold", "Boundary Smooth",
},
"O Image Relief Plane": {
@@ -26,14 +26,14 @@ def test_saved_parameter_descriptions_match_definitions():
},
"O Image Cutout": {"Thickness"},
"O Image Depth Cutout": {
"Thickness", "Depth Split", "Smooth Weight",
"Thickness", "Depth Split",
"Boundary Smooth", "Normal Smooth", "Front Inflation",
},
"O Image Cutout Symmetry": {
"Direction", "Offset", "Scale", "Fill Sides", "Fill Smooth",
},
"O Image Depth Panorama": {
"Subdivide", "Depth Scale", "Depth Split", "Smooth Weight",
"Subdivide", "Depth Scale", "Depth Split",
"Depth Mask", "Mask Threshold", "Boundary Smooth", "Invalid Distance",
},
"O Image Layer": {"Alpha Fix", "Normal Scale", "Bump Scale", "Object Space"},
@@ -65,8 +65,21 @@ def test_saved_parameter_descriptions_match_definitions():
"O Image Depth Panorama", "O Image Relief Plane",
}:
assert "Smooth" not in input_names
if group.name in {"O Image Cutout", "O Image Relief Plane"}:
assert "Smooth Weight" not in input_names
if group.name in {
"O Image Depth Cutout", "O Image Depth Plane", "O Image Relief Plane",
}:
options = next(
item for item in group.interface.items_tree
if item.item_type == "PANEL" and item.name == "Options"
)
reference = next(item for item in input_items if item.name == "Reference Depth")
assert reference.parent == options
siblings = [
item for item in group.interface.items_tree
if item.parent == options and item.item_type == "SOCKET"
]
assert siblings[-1].name == "Reference Depth"
if group.name in {"O Image Depth Plane", "O Image Depth Cutout"}:
boundary = next(item for item in sockets if item.name == "Boundary Smooth")
assert boundary.default_value == 4