Add tests and implement boundary smoothing and cutout functionality
- Introduced tests for boundary smoothing in `test_boundary_smoothing.py`, verifying that smoothing reduces stair-step artifacts while preserving boundary integrity. - Added tests for cutout boundary behavior in `test_cutout_boundary.py`, ensuring proper handling of boundary triangles and normal blending. - Created `test_temporary_attributes.py` to check the cleanup of temporary attributes across depth modifier branches, ensuring no warnings are raised. - Implemented point culling functionality in `test_validity.py`, confirming that invalid points are removed without affecting adjacent geometry. - Developed boundary smoothing logic in `boundary_smoothing.py`, allowing for depth smoothing while maintaining edge integrity. - Added cutout boundary management in `cutout_boundary.py`, including functions for removing boundary triangles and tapering thickness profiles. - Implemented validity checks in `validity.py` to ensure proper deletion of invalid vertices and associated wire edges.
This commit is contained in:
@@ -24,6 +24,14 @@ def test_shell_walls_directly_connect_caps(mode, split):
|
||||
front, faces = evaluated(obj)
|
||||
edges = Counter(tuple(sorted((a, b))) for face in faces for a, b in zip(face, (*face[1:], face[0])))
|
||||
boundary_count = sum(count == 1 for count in edges.values())
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
welded = {i for i, value in enumerate(mesh.attributes["o_balloon"].data)
|
||||
if mode == 0 and value.value == 0}
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
collapsed_walls = sum(count == 1 and a in welded and b in welded for (a, b), count in edges.items())
|
||||
set_value("Mode", mode)
|
||||
modifier = obj.modifiers[0]
|
||||
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE" if mode == 0 else "DISTANCE")
|
||||
@@ -32,9 +40,9 @@ def test_shell_walls_directly_connect_caps(mode, split):
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
points, shell_faces = evaluated(obj)
|
||||
assert len(points) == 2 * len(front)
|
||||
assert len(shell_faces) == 2 * len(faces) + boundary_count
|
||||
assert all(len(face) == 4 for face in shell_faces)
|
||||
assert len(points) == 2 * len(front) - len(welded)
|
||||
assert len(shell_faces) == 2 * len(faces) + boundary_count - collapsed_walls
|
||||
assert all(len(face) in (3, 4) for face in shell_faces)
|
||||
assert np.isfinite(points).all()
|
||||
assert_closed(shell_faces)
|
||||
|
||||
@@ -146,6 +154,31 @@ def test_coincident_independent_sheets_do_not_weld_together():
|
||||
assert component_count(faces) == 2
|
||||
|
||||
|
||||
def test_shell_source_indices_cross_identity_grid_rows():
|
||||
from scipy.spatial import cKDTree
|
||||
from tools.nodes.common.depth_surface import thicken_depth_surface
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
group = bpy.data.node_groups.new("Indexed Shell", "GeometryNodeTree")
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
grid = group.nodes.new("GeometryNodeMeshGrid")
|
||||
grid.inputs["Vertices X"].default_value = 65
|
||||
grid.inputs["Vertices Y"].default_value = 65
|
||||
amount = group.nodes.new("ShaderNodeValue")
|
||||
amount.outputs[0].default_value = 0.1
|
||||
shell = thicken_depth_surface(group, grid.outputs["Mesh"], amount.outputs[0])
|
||||
group.links.new(shell["geometry"], group.nodes.new("NodeGroupOutput").inputs["Geometry"])
|
||||
obj = bpy.data.objects.new("Indexed Shell", bpy.data.meshes.new("Empty"))
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.modifiers.new("Shell", "NODES").node_group = group
|
||||
points, faces = evaluated(obj)
|
||||
expected = np.array([(x / 64 - .5, y / 64 - .5, z)
|
||||
for z in (0, -.1) for y in range(65) for x in range(65)])
|
||||
assert len(points) == len(expected)
|
||||
assert cKDTree(points).query(expected)[0].max() < 1e-6
|
||||
assert_closed(faces)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("resolution", [1, 2, 4])
|
||||
def test_planar_depth_preserves_blocks_and_cleans_single_face_width(resolution):
|
||||
obj, set_value = surface(step=0.0, resolution=resolution)
|
||||
@@ -185,7 +218,7 @@ def test_cut_vertices_preserve_camera_rays_and_other_vertices():
|
||||
)
|
||||
|
||||
|
||||
def test_stored_point_positions_prevent_depth_resampling_on_side_walls():
|
||||
def test_source_surface_sampling_preserves_depth_on_side_walls():
|
||||
obj, set_value = surface(step=3.0)
|
||||
set_value("Split Threshold", 0.5)
|
||||
set_value("Thickness", 0.1)
|
||||
@@ -200,10 +233,11 @@ def test_stored_point_positions_prevent_depth_resampling_on_side_walls():
|
||||
|
||||
@pytest.mark.parametrize("thickness", [0.0, 0.2])
|
||||
@pytest.mark.parametrize("split", [0.0, 0.5])
|
||||
def test_internal_fields_stay_anonymous_and_user_attributes_survive(thickness, split):
|
||||
def test_temporary_fields_are_removed_and_user_attributes_survive(thickness, split):
|
||||
obj, set_value = surface(wrapper=True)
|
||||
obj.data.attributes.new("o_depth_probe", "FLOAT", "POINT")
|
||||
attribute = obj.data.attributes.new("o_user_weight", "FLOAT", "POINT")
|
||||
obj.data.attributes.new(".o_anyimage_user_weight", "FLOAT", "POINT")
|
||||
for value in attribute.data:
|
||||
value.value = 0.75
|
||||
set_value("Split Threshold", split)
|
||||
@@ -214,6 +248,9 @@ def test_internal_fields_stay_anonymous_and_user_attributes_survive(thickness, s
|
||||
try:
|
||||
assert {a.name for a in mesh.attributes if a.name.startswith(("o_depth_", "o_balloon_surface_"))} == {"o_depth_probe"}
|
||||
assert "UVMap" in mesh.attributes
|
||||
assert ".o_anyimage_user_weight" in mesh.attributes
|
||||
assert ".o_anyimage_original_boundary" not in mesh.attributes
|
||||
assert ".o_anyimage_depth_cut" not in mesh.attributes
|
||||
assert all(
|
||||
v.value == pytest.approx(0.75)
|
||||
for v in mesh.attributes["o_user_weight"].data
|
||||
|
||||
Reference in New Issue
Block a user