diff --git a/src/anyimage/assets/O_AnyImage.blend b/src/anyimage/assets/O_AnyImage.blend index 93837fa..7aca491 100644 --- a/src/anyimage/assets/O_AnyImage.blend +++ b/src/anyimage/assets/O_AnyImage.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96dbc2b540c6e0b329b98b7fdbae2cf9b8fef43aca9d9b2d551f8e5130a90072 -size 7757917 +oid sha256:2a4691d0c5656653c090d8a0f086f4f300b2e8ad4305103f5877c1c191eb4486 +size 7786499 diff --git a/tests/tools/nodes/test_depth_surface_split.py b/tests/tools/nodes/test_depth_surface_split.py index c274013..7060953 100644 --- a/tests/tools/nodes/test_depth_surface_split.py +++ b/tests/tools/nodes/test_depth_surface_split.py @@ -145,6 +145,34 @@ def test_planar_depth_preserves_blocks_and_cleans_single_face_width(resolution): np.testing.assert_allclose(coords, original, atol=1e-6) +def test_outline_points_use_adjacent_face_depth_without_split(): + obj, set_value = surface(step=0.0, resolution=2) + image = bpy.data.images["Camera"] + pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4) + pixels[..., 2] = 1 + # Spike both edge rows to cover Blender's image origin without relying on it. + pixels[(0, -1), :, 2] = 10 + image.pixels.foreach_set(pixels.ravel()) + image.update() + obj.update_tag(refresh={"DATA"}) + bpy.context.view_layer.update() + set_value("Depth Split", 0.0) + coords, faces = evaluated(obj) + boundary = np.isclose(coords[:, 2], 0, atol=0.05) | np.isclose(coords[:, 2], 1, atol=0.05) + assert boundary.any() + 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) diff --git a/tools/nodes/common/depth_surface.py b/tools/nodes/common/depth_surface.py index 141327b..8378514 100644 --- a/tools/nodes/common/depth_surface.py +++ b/tools/nodes/common/depth_surface.py @@ -240,13 +240,21 @@ def split_depth_surface(group, geometry, face_camera, strength, reference_depth, return choose.outputs["Output"], face_camera, cut -def build_surface_camera(group, original, separated, cut_vertex, strength): +def build_surface_camera(group, original, separated, cut_vertex, strength, *, always_vertex=None): """Change cut depth along the original ray, preserving every other vertex.""" enabled = group.nodes.new("GeometryNodeSwitch") enabled.input_type = "BOOLEAN" group.links.new(_math(group, "GREATER_THAN", strength, 0.0), enabled.inputs["Switch"]) enabled.inputs["False"].default_value = False group.links.new(cut_vertex, enabled.inputs["True"]) + if always_vertex is not None: + combined = group.nodes.new("FunctionNodeBooleanMath") + combined.operation = "OR" + group.links.new(enabled.outputs["Output"], combined.inputs[0]) + group.links.new(always_vertex, combined.inputs[1]) + enabled = combined.outputs["Boolean"] + else: + enabled = enabled.outputs["Output"] original_axes = group.nodes.new("ShaderNodeSeparateXYZ") separated_axes = group.nodes.new("ShaderNodeSeparateXYZ") group.links.new(original, original_axes.inputs[0]) @@ -260,7 +268,7 @@ def build_surface_camera(group, original, separated, cut_vertex, strength): group.links.new(ratio, projected.inputs[3]) choose = group.nodes.new("GeometryNodeSwitch") choose.input_type = "VECTOR" - group.links.new(enabled.outputs["Output"], choose.inputs["Switch"]) + group.links.new(enabled, choose.inputs["Switch"]) group.links.new(original, choose.inputs["False"]) group.links.new(projected.outputs[0], choose.inputs["True"]) return evaluate_field(group, choose.outputs["Output"], "FLOAT_VECTOR", "POINT") @@ -275,7 +283,7 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *, # Encode identity in the construction geometry before extrusion. The source # surface remains a separate sampling input; no position or ID is stored. identity = nodes.new("ShaderNodeCombineXYZ") - for operation, axis in (("MODULO", "X"), ("DIVIDE", "Y")): + for operation, axis in (("MODULO", "X"), ("DIVIDE", "Z")): part = nodes.new("FunctionNodeIntegerMath") part.operation = operation part.inputs[1].default_value = 4096 diff --git a/tools/nodes/groups/image_depth_cutout.py b/tools/nodes/groups/image_depth_cutout.py index 7e935e5..801ba64 100644 --- a/tools/nodes/groups/image_depth_cutout.py +++ b/tools/nodes/groups/image_depth_cutout.py @@ -269,7 +269,10 @@ def _build_depth_surface(group, geometry, controls): group, geometry, sample_face_camera(group, image), split, controls.outputs["Reference Depth"], triangles=True, ) geometry = remove_boundary_triangles(group, geometry) - camera = build_surface_camera(group, _position_field(group, image), corner_camera, cut_vertex, split) + outline = evaluate_field(group, edge_boundary_field(group.nodes, group.links), "BOOLEAN", "POINT") + camera = build_surface_camera( + group, _position_field(group, image), corner_camera, cut_vertex, split, always_vertex=outline, + ) projection = project_depth_surface( group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"], split, )