Add depth surface processing and layout tools
- Implemented `cutout_depth_surface.py` for face separation and connected thickness calculations for depth surface assets. - Created `cutout_depth_surface_layout.py` to arrange nodes for staggered calculation branches in depth surface processing. - Authored `geometry_layouts.py` to define layouts for various geometry assets, including image cutouts and depth planes. - Introduced `node_layout.py` for shared presentation rules, facilitating node arrangement and validation in geometry-node layouts.
This commit is contained in:
@@ -531,7 +531,7 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
"Smooth Iterations",
|
||||
"Cleanup",
|
||||
"Smooth",
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
"Reference Depth",
|
||||
"Depth Direction",
|
||||
"Uniform Scale",
|
||||
@@ -542,7 +542,7 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
assert [item.name for item in panels[0].interface_items] == [
|
||||
"Cleanup",
|
||||
"Smooth",
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
"Reference Depth",
|
||||
"Depth Direction",
|
||||
]
|
||||
@@ -634,7 +634,8 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
"Geometry",
|
||||
"Thickness",
|
||||
"Depth Scale",
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
"Cleanup",
|
||||
"Reference Depth",
|
||||
"Uniform Scale",
|
||||
"Depth Image",
|
||||
@@ -653,9 +654,10 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
)
|
||||
dependencies.setdefault(link.from_socket.name, set()).add(shape)
|
||||
assert dependencies == {
|
||||
"Geometry": {"DEPTH_SURFACE"},
|
||||
"Thickness": set(cutout_shape.CUTOUT_SHAPES),
|
||||
"Depth Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Stretch Limit": {"DEPTH_SURFACE"},
|
||||
"Split": {"DEPTH_SURFACE"},
|
||||
"Double Sided": {"DEPTH_BALLOON"},
|
||||
"Reference Depth": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Depth Direction": {"DEPTH_BALLOON"},
|
||||
@@ -672,7 +674,7 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
)
|
||||
assert all(
|
||||
not any(node.bl_idname == "GeometryNodeInputMeshIsland" for node in group.nodes)
|
||||
for group in child_groups.values()
|
||||
for shape, group in child_groups.items() if shape != "DEPTH_SURFACE"
|
||||
)
|
||||
assert any(node.bl_idname == "GeometryNodeBlurAttribute" for node in cutout.nodes)
|
||||
assert all(
|
||||
@@ -710,112 +712,33 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
)
|
||||
assert cleanup.default_value == 0.0
|
||||
assert cleanup.subtype == "NONE"
|
||||
stretch_limit = next(
|
||||
split_control = next(
|
||||
item
|
||||
for item in cutout.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Stretch Limit"
|
||||
if item.item_type == "SOCKET" and item.name == "Split"
|
||||
)
|
||||
child_stretch_limit = next(
|
||||
child_split = next(
|
||||
item
|
||||
for item in child_groups["DEPTH_SURFACE"].interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Stretch Limit"
|
||||
if item.item_type == "SOCKET" and item.name == "Split"
|
||||
)
|
||||
assert stretch_limit.default_value == 0.0
|
||||
assert stretch_limit.min_value == 0.0
|
||||
assert stretch_limit.max_value == 1.0
|
||||
assert stretch_limit.subtype == "FACTOR"
|
||||
assert getattr(stretch_limit.parent, "name", "") == "Options"
|
||||
assert child_stretch_limit.default_value == 0.0
|
||||
assert child_stretch_limit.min_value == 0.0
|
||||
assert child_stretch_limit.max_value == 1.0
|
||||
assert child_stretch_limit.subtype == "FACTOR"
|
||||
assert getattr(child_stretch_limit.parent, "name", "") == "Options"
|
||||
assert split_control.default_value == 0.0
|
||||
assert split_control.min_value == 0.0
|
||||
assert split_control.max_value == 1.0
|
||||
assert split_control.subtype == "FACTOR"
|
||||
assert getattr(split_control.parent, "name", "") == "Options"
|
||||
assert child_split.default_value == 0.0
|
||||
assert child_split.min_value == 0.0
|
||||
assert child_split.max_value == 1.0
|
||||
assert child_split.subtype == "FACTOR"
|
||||
assert getattr(child_split.parent, "name", "") == "Options"
|
||||
depth_surface_nodes = child_groups["DEPTH_SURFACE"].nodes
|
||||
captures = [
|
||||
node
|
||||
for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeCaptureAttribute"
|
||||
]
|
||||
assert len(captures) == 1
|
||||
assert captures[0].domain == "POINT"
|
||||
assert [item.name for item in captures[0].capture_items] == [
|
||||
"Original Edge Length"
|
||||
]
|
||||
captured_distance = next(
|
||||
link.from_node
|
||||
for link in child_groups["DEPTH_SURFACE"].links
|
||||
if link.to_node == captures[0]
|
||||
and link.to_socket.name == "Original Edge Length"
|
||||
)
|
||||
assert captured_distance.bl_idname == "ShaderNodeVectorMath"
|
||||
assert captured_distance.operation == "DISTANCE"
|
||||
ratio = next(
|
||||
link.to_node
|
||||
for link in child_groups["DEPTH_SURFACE"].links
|
||||
if link.from_node == captured_distance
|
||||
and link.to_node.bl_idname == "ShaderNodeMath"
|
||||
and link.to_node.operation == "DIVIDE"
|
||||
)
|
||||
assert any(
|
||||
link.from_node == captures[0]
|
||||
and link.to_node == ratio
|
||||
and link.to_socket == ratio.inputs[0]
|
||||
for link in child_groups["DEPTH_SURFACE"].links
|
||||
)
|
||||
assert any(
|
||||
link.from_node == captured_distance
|
||||
and link.to_node == ratio
|
||||
and link.to_socket == ratio.inputs[1]
|
||||
for link in child_groups["DEPTH_SURFACE"].links
|
||||
)
|
||||
captured_distance = next(
|
||||
link.from_node
|
||||
for link in child_groups["DEPTH_SURFACE"].links
|
||||
if link.to_node == captures[0]
|
||||
and link.to_socket.name == "Original Edge Length"
|
||||
)
|
||||
assert captured_distance.bl_idname == "ShaderNodeVectorMath"
|
||||
assert captured_distance.operation == "DISTANCE"
|
||||
ratio = next(
|
||||
link.to_node
|
||||
for link in child_groups["DEPTH_SURFACE"].links
|
||||
if link.from_node == captured_distance
|
||||
and link.to_node.bl_idname == "ShaderNodeMath"
|
||||
and link.to_node.operation == "DIVIDE"
|
||||
)
|
||||
assert any(
|
||||
link.from_node == captures[0]
|
||||
and link.to_node == ratio
|
||||
and link.to_socket == ratio.inputs[0]
|
||||
for link in child_groups["DEPTH_SURFACE"].links
|
||||
)
|
||||
assert any(
|
||||
link.from_node == captured_distance
|
||||
and link.to_node == ratio
|
||||
and link.to_socket == ratio.inputs[1]
|
||||
for link in child_groups["DEPTH_SURFACE"].links
|
||||
)
|
||||
samples = [
|
||||
node
|
||||
for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeSampleIndex"
|
||||
]
|
||||
assert not samples
|
||||
delete_domains = [
|
||||
node.domain
|
||||
for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeDeleteGeometry"
|
||||
]
|
||||
assert delete_domains.count("EDGE") == 1
|
||||
assert delete_domains.count("POINT") == 2
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeInputMeshEdgeNeighbors"
|
||||
for node in depth_surface_nodes
|
||||
)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeInputMeshVertexNeighbors"
|
||||
for node in depth_surface_nodes
|
||||
)
|
||||
assert any(node.bl_idname == "GeometryNodeSplitEdges" for node in depth_surface_nodes)
|
||||
from validate_cutout import validate_depth_surface_attributes
|
||||
validate_depth_surface_attributes(child_groups["DEPTH_SURFACE"])
|
||||
assert [node.domain for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeDeleteGeometry"] == ["FACE"]
|
||||
assert "Stretch Limit" not in [item.name for item in child_groups["DEPTH_SURFACE"].interface.items_tree]
|
||||
cleanup_scale = next(
|
||||
link.to_node
|
||||
for link in cutout.links
|
||||
@@ -1547,20 +1470,20 @@ def _add_cutout_modifier(obj, shape):
|
||||
return modifier
|
||||
|
||||
|
||||
def _depth_surface_stretch_object():
|
||||
def _depth_surface_step_object():
|
||||
vertices = [(float(x), float(y), 0.0) for y in range(3) for x in range(3)]
|
||||
faces = []
|
||||
for y in range(2):
|
||||
for x in range(2):
|
||||
a = y * 3 + x
|
||||
faces.extend(((a, a + 1, a + 4), (a, a + 4, a + 3)))
|
||||
data = bpy.data.meshes.new("Depth Surface Stretch")
|
||||
data = bpy.data.meshes.new("Depth Surface Step")
|
||||
data.from_pydata(vertices, (), faces)
|
||||
uv_layer = data.uv_layers.new(name="UVMap")
|
||||
for loop in data.loops:
|
||||
coordinate = data.vertices[loop.vertex_index].co
|
||||
uv_layer.data[loop.index].uv = (coordinate.x * 0.5, coordinate.y * 0.5)
|
||||
obj = bpy.data.objects.new("Depth Surface Stretch", data)
|
||||
obj = bpy.data.objects.new("Depth Surface Step", data)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = _add_cutout_modifier(obj, "DEPTH_SURFACE")
|
||||
|
||||
@@ -1617,40 +1540,17 @@ def _evaluated_topology(obj):
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def test_depth_surface_stretch_limit_tracks_projection_and_cleans_loose_topology():
|
||||
def test_depth_surface_split_preserves_original_faces():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
obj, modifier = _depth_surface_stretch_object()
|
||||
stretch_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Stretch Limit",
|
||||
)
|
||||
depth_scale_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Depth Scale",
|
||||
)
|
||||
|
||||
object_data.set_modifier_input(modifier, stretch_id, 0.0)
|
||||
bpy.context.view_layer.update()
|
||||
assert _evaluated_topology(obj) == (9, 8, set(), set())
|
||||
|
||||
object_data.set_modifier_input(modifier, stretch_id, 0.5)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
assert _evaluated_topology(obj) == (9, 8, set(), set())
|
||||
|
||||
object_data.set_modifier_input(modifier, stretch_id, 0.6)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
vertices, faces, loose_edges, loose_points = _evaluated_topology(obj)
|
||||
assert 0 < faces < 8
|
||||
assert vertices < 9
|
||||
assert not loose_edges
|
||||
assert not loose_points
|
||||
|
||||
object_data.set_modifier_input(modifier, depth_scale_id, 0.0)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
assert _evaluated_topology(obj) == (9, 8, set(), set())
|
||||
obj, modifier = _depth_surface_step_object()
|
||||
split_id = object_data.modifier_input_identifier(modifier.node_group, "Split")
|
||||
for value in (0., .5, 1.):
|
||||
object_data.set_modifier_input(modifier, split_id, value)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
vertices, faces, loose_edges, loose_points = _evaluated_topology(obj)
|
||||
assert vertices >= 9 and faces == 8
|
||||
assert not loose_edges and not loose_points
|
||||
|
||||
|
||||
def test_cutout_thickness_keeps_one_connected_shell():
|
||||
|
||||
321
tests/test_depth_surface_nodes.py
Normal file
321
tests/test_depth_surface_nodes.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""Behavior checks for face separation and connected Depth Surface shells."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "tools" / "node_assets"))
|
||||
from build_cutout import build_node_group, build_cutout_modifier_group
|
||||
|
||||
|
||||
def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
groups = {
|
||||
shape: build_node_group(shape)
|
||||
for shape in ("SURFACE", "BALLOON", "DEPTH_BALLOON", "DEPTH_SURFACE")
|
||||
}
|
||||
group = build_cutout_modifier_group(groups) if wrapper else groups["DEPTH_SURFACE"]
|
||||
# Two columns with a complete depth jump along their common boundary.
|
||||
points = [
|
||||
(x * size / resolution, y * size / resolution, 0)
|
||||
for y in range(resolution + 1)
|
||||
for x in range(2 * resolution + 1)
|
||||
]
|
||||
stride = 2 * resolution + 1
|
||||
faces = [
|
||||
(
|
||||
y * stride + x,
|
||||
y * stride + x + 1,
|
||||
(y + 1) * stride + x + 1,
|
||||
(y + 1) * stride + x,
|
||||
)
|
||||
for y in range(resolution)
|
||||
for x in range(2 * resolution)
|
||||
]
|
||||
mesh = bpy.data.meshes.new("Step")
|
||||
mesh.from_pydata(points, [], faces)
|
||||
uv = mesh.uv_layers.new(name="UVMap")
|
||||
for loop in mesh.loops:
|
||||
x, y, _ = points[loop.vertex_index]
|
||||
uv.data[loop.index].uv = (x / (2 * size), y / size)
|
||||
obj = bpy.data.objects.new("Step", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
image = bpy.data.images.new("Camera", width=64, height=32, float_buffer=True)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.alpha_mode = "CHANNEL_PACKED"
|
||||
yy, xx = np.mgrid[:32, :64]
|
||||
pixels = np.ones((32, 64, 4), np.float32)
|
||||
pixels[..., 0] = (xx + 0.5) / 32
|
||||
pixels[..., 1] = -(yy + 0.5) / 32
|
||||
pixels[..., 2] = 1 + (xx >= 32) * step
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
modifier = obj.modifiers.new("Depth Surface", "NODES")
|
||||
modifier.node_group = group
|
||||
inputs = {
|
||||
s.name: s.identifier
|
||||
for s in group.interface.items_tree
|
||||
if s.item_type == "SOCKET" and s.in_out == "INPUT"
|
||||
}
|
||||
|
||||
def set_value(name, value):
|
||||
modifier[inputs[name]] = value
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
for name, value in (
|
||||
("Depth Image", image),
|
||||
("Uniform Scale", size),
|
||||
("Reference Depth", size),
|
||||
("Depth Scale", 1.0),
|
||||
("Thickness", 0.0),
|
||||
("Cleanup", 0.0),
|
||||
("Split", 0.0),
|
||||
):
|
||||
set_value(name, value)
|
||||
if wrapper:
|
||||
set_value("Shape", 3)
|
||||
set_value("Inward Axis", 0)
|
||||
set_value("Smooth Iterations", 0)
|
||||
return obj, set_value
|
||||
|
||||
|
||||
def evaluated(obj):
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
return np.array([v.co[:] for v in mesh.vertices]), [
|
||||
tuple(p.vertices) for p in mesh.polygons
|
||||
]
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
|
||||
|
||||
def assert_closed(faces):
|
||||
edges = Counter(
|
||||
tuple(sorted((a, b))) for f in faces for a, b in zip(f, (*f[1:], f[0]))
|
||||
)
|
||||
assert edges and set(edges.values()) == {2}, Counter(edges.values())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("thickness", [0.1, 1.4])
|
||||
@pytest.mark.parametrize("split", [0.0, 0.5])
|
||||
def test_surface_caps_and_segmented_walls_are_connected(thickness, split):
|
||||
obj, set_value = surface()
|
||||
set_value("Split", split)
|
||||
set_value("Thickness", thickness)
|
||||
coords, faces = evaluated(obj)
|
||||
assert np.isfinite(coords).all()
|
||||
assert_closed(faces)
|
||||
assert coords[:, 2].min() == pytest.approx(-6 - thickness, abs=1e-5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [0.01, 1.0, 100.0])
|
||||
def test_split_mapping_and_uniform_scale(size):
|
||||
obj, set_value = surface(step=2.0, size=size)
|
||||
# Face centers are size/2 apart: gradient is 4.
|
||||
counts = []
|
||||
for split in (0.0, 0.25, 0.5, 1.0):
|
||||
set_value("Split", split)
|
||||
coords, faces = evaluated(obj)
|
||||
counts.append(len(coords))
|
||||
assert len(faces) == 8
|
||||
assert counts[:3] == [15] * 3
|
||||
assert counts[3] == 18
|
||||
set_value("Depth Scale", 0.0)
|
||||
coords, faces = evaluated(obj)
|
||||
assert len(coords) == 15 and np.abs(coords[:, 2]).max() < 1e-6
|
||||
|
||||
|
||||
def test_midpoint_selects_gradient_six_and_preserves_faces():
|
||||
obj, set_value = surface(step=3.0)
|
||||
set_value("Split", 0.5)
|
||||
coords, faces = evaluated(obj)
|
||||
assert len(coords) == 18 and len(faces) == 8
|
||||
assert set(np.round(coords[:, 2], 4)) == {0.0, -3.0}
|
||||
|
||||
|
||||
def test_cleanup_removes_new_islands_before_thickness():
|
||||
obj, set_value = surface()
|
||||
set_value("Cleanup", 1.1)
|
||||
assert len(evaluated(obj)[1]) == 8
|
||||
set_value("Split", 0.5)
|
||||
assert len(evaluated(obj)[1]) == 0
|
||||
set_value("Thickness", 0.2)
|
||||
assert len(evaluated(obj)[1]) == 0
|
||||
|
||||
|
||||
def test_full_wrapper_smooth_keeps_closed_shell():
|
||||
obj, set_value = surface(wrapper=True)
|
||||
set_value("Split", 0.5)
|
||||
set_value("Thickness", 0.2)
|
||||
set_value("Smooth Iterations", 2)
|
||||
coords, faces = evaluated(obj)
|
||||
assert np.isfinite(coords).all()
|
||||
assert_closed(faces)
|
||||
|
||||
|
||||
def component_count(faces):
|
||||
adjacency = {}
|
||||
for face in faces:
|
||||
for a, b in zip(face, (*face[1:], face[0])):
|
||||
adjacency.setdefault(a, set()).add(b)
|
||||
adjacency.setdefault(b, set()).add(a)
|
||||
remaining = set(adjacency)
|
||||
count = 0
|
||||
while remaining:
|
||||
count += 1
|
||||
pending = [remaining.pop()]
|
||||
while pending:
|
||||
for neighbor in adjacency[pending.pop()] & remaining:
|
||||
remaining.remove(neighbor)
|
||||
pending.append(neighbor)
|
||||
return count
|
||||
|
||||
|
||||
def test_coincident_independent_sheets_do_not_weld_together():
|
||||
obj, set_value = surface(step=0.0)
|
||||
mesh = obj.data
|
||||
vertices = [tuple(v.co) for v in mesh.vertices]
|
||||
faces = [tuple(f.vertices) for f in mesh.polygons]
|
||||
n = len(vertices)
|
||||
duplicate = bpy.data.meshes.new("Close sheets")
|
||||
duplicate.from_pydata(
|
||||
vertices + [(x, y, z + 1e-8) for x, y, z in vertices],
|
||||
[],
|
||||
faces + [tuple(v + n for v in face) for face in faces],
|
||||
)
|
||||
uv = duplicate.uv_layers.new(name="UVMap")
|
||||
for loop in duplicate.loops:
|
||||
x, y, z = duplicate.vertices[loop.vertex_index].co
|
||||
uv.data[loop.index].uv = (x / 2, y)
|
||||
obj.data = duplicate
|
||||
set_value("Thickness", 0.1)
|
||||
coords, faces = evaluated(obj)
|
||||
assert_closed(faces)
|
||||
assert component_count(faces) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("resolution", [1, 2, 4])
|
||||
def test_planar_depth_has_no_split_at_any_density(resolution):
|
||||
obj, set_value = surface(step=0.0, resolution=resolution)
|
||||
original, original_faces = evaluated(obj)
|
||||
set_value("Split", 1.0)
|
||||
coords, faces = evaluated(obj)
|
||||
assert len(coords) == len(obj.data.vertices)
|
||||
assert len(faces) == len(obj.data.polygons)
|
||||
np.testing.assert_allclose(coords, original, atol=1e-6)
|
||||
|
||||
|
||||
def test_small_positive_split_preserves_unsplit_projection():
|
||||
obj, set_value = surface(step=2.0)
|
||||
original, original_faces = evaluated(obj)
|
||||
set_value("Split", 0.01)
|
||||
coords, faces = evaluated(obj)
|
||||
np.testing.assert_allclose(coords, original, atol=1e-6)
|
||||
assert faces == original_faces
|
||||
|
||||
|
||||
def test_cut_vertices_preserve_camera_rays_and_other_vertices():
|
||||
obj, set_value = surface(step=3.0)
|
||||
original, original_faces = evaluated(obj)
|
||||
set_value("Split", 0.5)
|
||||
coords, faces = evaluated(obj)
|
||||
for source_face, result_face in zip(original_faces, faces):
|
||||
for source_index, result_index in zip(source_face, result_face):
|
||||
before, after = original[source_index], coords[result_index]
|
||||
if obj.data.vertices[source_index].co.x != 1.0:
|
||||
np.testing.assert_allclose(after, before, atol=1e-6)
|
||||
before_depth, after_depth = 1.0 - before[2], 1.0 - after[2]
|
||||
np.testing.assert_allclose(
|
||||
after[:2] / after_depth, before[:2] / before_depth, atol=1e-6
|
||||
)
|
||||
|
||||
|
||||
def test_stored_point_positions_prevent_depth_resampling_on_side_walls():
|
||||
obj, set_value = surface(step=3.0)
|
||||
set_value("Split", 0.5)
|
||||
set_value("Thickness", 0.1)
|
||||
coords, faces = evaluated(obj)
|
||||
assert_closed(faces)
|
||||
assert set(np.round(coords[:, 2], 4)) == {0.0, -0.1, -3.0, -3.1}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("thickness", [0.0, 0.2])
|
||||
@pytest.mark.parametrize("split", [0.0, 0.5])
|
||||
def test_temporary_attributes_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")
|
||||
for value in attribute.data:
|
||||
value.value = 0.75
|
||||
set_value("Split", split)
|
||||
set_value("Thickness", thickness)
|
||||
set_value("Smooth Iterations", 2)
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
assert not any(a.name.startswith("o_depth_") for a in mesh.attributes)
|
||||
assert "UVMap" in mesh.attributes
|
||||
assert all(
|
||||
v.value == pytest.approx(0.75)
|
||||
for v in mesh.attributes["o_user_weight"].data
|
||||
)
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
|
||||
|
||||
def test_zero_center_distance_does_not_split_or_produce_nan():
|
||||
obj, set_value = surface(step=6.0)
|
||||
mesh = bpy.data.meshes.new("Coincident face centers")
|
||||
mesh.from_pydata(
|
||||
[(0, 0, 0), (1, 0, 0), (0.5, 1, 0), (0.5, 1, 0)], [], [(0, 1, 2), (1, 0, 3)]
|
||||
)
|
||||
uv = mesh.uv_layers.new(name="UVMap")
|
||||
for face in mesh.polygons:
|
||||
for li in face.loop_indices:
|
||||
uv.data[li].uv = (0.25 if face.index == 0 else 0.75, 0.5)
|
||||
obj.data = mesh
|
||||
set_value("Split", 1.0)
|
||||
coords, faces = evaluated(obj)
|
||||
assert len(coords) == 4 and len(faces) == 2
|
||||
assert np.isfinite(coords).all()
|
||||
|
||||
|
||||
def test_triangle_corner_weights_do_not_open_sloped_side_walls():
|
||||
obj, set_value = surface(step=3.0)
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
image = bpy.data.images["Camera"]
|
||||
pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4)
|
||||
pixels[..., 2] += np.arange(32)[:, None] * 0.02
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
image.update()
|
||||
set_value("Split", 0.5)
|
||||
set_value("Thickness", 0.8)
|
||||
coords, faces = evaluated(obj)
|
||||
assert_closed(faces)
|
||||
assert component_count(faces) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("resolution", [1, 2, 4])
|
||||
def test_linear_depth_slope_has_density_independent_selection(resolution):
|
||||
obj, set_value = surface(step=0.0, resolution=resolution)
|
||||
image = bpy.data.images["Camera"]
|
||||
pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4)
|
||||
pixels[..., 2] = 1 + 4 * pixels[..., 0]
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
image.update()
|
||||
set_value("Split", 0.5)
|
||||
assert len(evaluated(obj)[0]) == len(obj.data.vertices)
|
||||
set_value("Split", 1.0)
|
||||
assert len(evaluated(obj)[0]) > len(obj.data.vertices)
|
||||
@@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -7,6 +8,26 @@ SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
|
||||
|
||||
|
||||
class NodeAssetsTest(unittest.TestCase):
|
||||
def test_geometry_assets_have_readable_local_layouts(self):
|
||||
import bpy
|
||||
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "tools" / "node_assets"))
|
||||
from node_layout import validate_node_layout
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
with bpy.data.libraries.load(
|
||||
str(SOURCE_ROOT / "assets" / "O_AnyImage.blend")
|
||||
) as (source, target):
|
||||
target.node_groups = list(source.node_groups)
|
||||
groups = [
|
||||
group
|
||||
for group in target.node_groups
|
||||
if group.bl_idname == "GeometryNodeTree"
|
||||
]
|
||||
self.assertEqual(len(groups), 7)
|
||||
for group in groups:
|
||||
validate_node_layout(group)
|
||||
|
||||
def test_production_code_does_not_create_node_groups(self):
|
||||
offenders = []
|
||||
for path in SOURCE_ROOT.rglob("*.py"):
|
||||
@@ -15,9 +36,9 @@ class NodeAssetsTest(unittest.TestCase):
|
||||
self.assertEqual(offenders, [])
|
||||
|
||||
def test_runtime_node_group_entries_load_assets(self):
|
||||
material_source = (
|
||||
SOURCE_ROOT / "common" / "material.py"
|
||||
).read_text(encoding="utf-8")
|
||||
material_source = (SOURCE_ROOT / "common" / "material.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
mesh_plane_source = (
|
||||
SOURCE_ROOT / "operators" / "convert_to_plane" / "mesh_plane.py"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user