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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)], [], [])
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user