- 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.
678 lines
23 KiB
Python
678 lines
23 KiB
Python
import sys
|
|
from collections import Counter
|
|
from itertools import pairwise
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
PROJECT_ROOT = Path(__file__).parents[2]
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
|
sys.path.insert(
|
|
0,
|
|
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
|
|
)
|
|
|
|
from node_layout import validate_node_layout
|
|
|
|
from anyimage.common.object import (
|
|
modifier_input_identifier,
|
|
set_modifier_input,
|
|
)
|
|
from anyimage.operators.cutout_tool import object as cutout_objects
|
|
from anyimage.operators.cutout_tool import shape as shapes
|
|
|
|
|
|
def evaluated_mesh(obj):
|
|
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
|
result = evaluated.to_mesh()
|
|
return evaluated, result
|
|
|
|
|
|
def validate_depth_surface_layout(group):
|
|
"""Check the geometry path, stage spacing and drawn node bounds."""
|
|
nodes = [node for node in group.nodes if node.bl_idname != "NodeReroute"]
|
|
main = [
|
|
group.nodes[name]
|
|
for name in (
|
|
"Store Named Attribute",
|
|
"Store Named Attribute.001",
|
|
"Store Named Attribute.002",
|
|
"Store Named Attribute.003",
|
|
"Split Edges",
|
|
"Switch",
|
|
"Delete Geometry",
|
|
"Store Named Attribute.004",
|
|
"Set Position",
|
|
"Store Named Attribute.005",
|
|
"Set Position.001",
|
|
"Store Named Attribute.006",
|
|
"Store Named Attribute.007",
|
|
"Repeat Input",
|
|
"Extrude Mesh",
|
|
"Separate Geometry",
|
|
"Repeat Output",
|
|
"Flip Faces.001",
|
|
"Join Geometry.001",
|
|
"Set Position.003",
|
|
"Merge by Distance",
|
|
"Set Position.004",
|
|
"Switch.002",
|
|
"Remove Named Attribute",
|
|
"Set Shade Smooth",
|
|
"Group Output",
|
|
)
|
|
]
|
|
assert len({node.location.y for node in main}) == 1
|
|
assert all(right.location.x > left.location.x for left, right in pairwise(main))
|
|
for left, right in (
|
|
("Switch", "Delete Geometry"),
|
|
("Delete Geometry", "Store Named Attribute.004"),
|
|
("Set Position", "Store Named Attribute.005"),
|
|
("Switch.002", "Remove Named Attribute"),
|
|
):
|
|
a, b = group.nodes[left], group.nodes[right]
|
|
assert b.location.x - (a.location.x + a.width) >= 280
|
|
assert not any(n.bl_idname == "FunctionNodeInputString" for n in nodes)
|
|
assert min(n.location.x for n in nodes) < main[0].location.x
|
|
|
|
def height(node):
|
|
if node.dimensions.y:
|
|
return node.dimensions.y
|
|
if node.hide or node.bl_idname == "NodeReroute":
|
|
return 30.0
|
|
if node.bl_idname == "GeometryNodeJoinGeometry":
|
|
return 60.0
|
|
return node.height
|
|
|
|
for index, node in enumerate(nodes):
|
|
width = node.dimensions.x or node.width
|
|
for other in nodes[index + 1 :]:
|
|
other_width = other.dimensions.x or other.width
|
|
overlap = (
|
|
node.location.x < other.location.x + other_width
|
|
and node.location.x + width > other.location.x
|
|
and node.location.y - height(node) < other.location.y
|
|
and node.location.y > other.location.y - height(other)
|
|
)
|
|
assert not overlap, (node.name, other.name)
|
|
repeat_input = next(
|
|
node for node in nodes if node.bl_idname == "GeometryNodeRepeatInput"
|
|
)
|
|
repeat_output = repeat_input.paired_output
|
|
inside = {repeat_output}
|
|
pending = [repeat_input]
|
|
while pending:
|
|
node = pending.pop()
|
|
if node in inside:
|
|
continue
|
|
inside.add(node)
|
|
pending.extend(link.to_node for link in group.links if link.from_node == node)
|
|
zone_bottom = min(node.location.y - height(node) for node in inside) - 60.0
|
|
zone_top = max(node.location.y for node in inside) + 60.0
|
|
for node in nodes:
|
|
if repeat_input.location.x <= node.location.x <= repeat_output.location.x:
|
|
assert (
|
|
node in inside
|
|
or node.location.y <= zone_bottom
|
|
or node.location.y - height(node) >= zone_top
|
|
), node.name
|
|
|
|
|
|
def validate_depth_surface_attributes(group):
|
|
"""Check temporary fields are typed, locally read and removed at output."""
|
|
expected = {
|
|
"o_depth_face_center": ("FLOAT_VECTOR", "FACE"),
|
|
"o_depth_face_camera": ("FLOAT_VECTOR", "FACE"),
|
|
"o_depth_corner_camera": ("FLOAT_VECTOR", "CORNER"),
|
|
"o_depth_cut_vertex": ("BOOLEAN", "CORNER"),
|
|
"o_depth_camera": ("FLOAT_VECTOR", "POINT"),
|
|
"o_depth_surface_position": ("FLOAT_VECTOR", "POINT"),
|
|
"o_depth_source_vertex": ("INT", "POINT"),
|
|
"o_depth_wall_layers": ("FLOAT", "POINT"),
|
|
}
|
|
stores = {
|
|
n.inputs["Name"].default_value: (n.data_type, n.domain)
|
|
for n in group.nodes
|
|
if n.bl_idname == "GeometryNodeStoreNamedAttribute"
|
|
}
|
|
assert stores == expected
|
|
assert not any(n.bl_idname == "GeometryNodeCaptureAttribute" for n in group.nodes)
|
|
removes = [n for n in group.nodes if n.bl_idname == "GeometryNodeRemoveAttribute"]
|
|
assert len(removes) == 1
|
|
assert removes[0].pattern_mode == "WILDCARD"
|
|
assert removes[0].inputs["Name"].default_value == "o_depth_*"
|
|
for node in group.nodes:
|
|
if node.bl_idname == "GeometryNodeInputNamedAttribute":
|
|
name = node.inputs["Name"].default_value
|
|
assert name == "UVMap" or node.data_type == expected[name][0]
|
|
assert all(
|
|
abs(link.to_node.location.x - node.location.x) <= 600
|
|
for link in group.links
|
|
if link.from_node == node
|
|
)
|
|
|
|
|
|
def input_names(group):
|
|
return [
|
|
item.name
|
|
for item in group.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.in_out == "INPUT"
|
|
]
|
|
|
|
|
|
def grid_object(name):
|
|
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(name)
|
|
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(name, data)
|
|
bpy.context.scene.collection.objects.link(obj)
|
|
return obj
|
|
|
|
|
|
def add_modifier(obj, shape, thickness):
|
|
group = cutout_objects.load_cutout_node_group()
|
|
modifier = obj.modifiers.new(group.name, "NODES")
|
|
modifier.node_group = group
|
|
for name, value in (
|
|
("Shape", shapes.CUTOUT_SHAPE_INDICES[shape]),
|
|
("Inward Axis", 0),
|
|
("Cleanup", 0.0),
|
|
("Smooth", 0.0),
|
|
("Smooth Iterations", 0),
|
|
("Thickness", thickness),
|
|
):
|
|
set_modifier_input(
|
|
modifier,
|
|
modifier_input_identifier(group, name),
|
|
value,
|
|
)
|
|
return modifier
|
|
|
|
|
|
def main():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
wrapper = cutout_objects.load_cutout_node_group()
|
|
assert all(
|
|
cutout_objects.load_cutout_node_group() == wrapper
|
|
for shape in shapes.CUTOUT_SHAPES
|
|
)
|
|
assert wrapper.name == shapes.CUTOUT_NODE_GROUP_NAME
|
|
assert wrapper.is_modifier
|
|
groups = {
|
|
shape: bpy.data.node_groups[name]
|
|
for shape, name in shapes.CUTOUT_NODE_GROUP_NAMES.items()
|
|
}
|
|
validate_depth_surface_layout(groups["DEPTH_SURFACE"])
|
|
assert all(not group.is_modifier for group in groups.values())
|
|
|
|
menu_switch = next(
|
|
node for node in wrapper.nodes if node.bl_idname == "GeometryNodeMenuSwitch"
|
|
)
|
|
assert [item.name for item in menu_switch.enum_items] == list(
|
|
shapes.CUTOUT_SHAPE_LABELS.values()
|
|
)
|
|
assert any(
|
|
link.from_socket.name == "Shape"
|
|
and link.to_node == menu_switch
|
|
and link.to_socket.name == "Menu"
|
|
for link in wrapper.links
|
|
)
|
|
axis_switch = next(
|
|
node
|
|
for node in wrapper.nodes
|
|
if node.bl_idname == "GeometryNodeMenuSwitch"
|
|
and [item.name for item in node.enum_items] == ["-Z", "+X"]
|
|
)
|
|
assert any(
|
|
link.from_socket.name == "Inward Axis"
|
|
and link.to_node == axis_switch
|
|
and link.to_socket.name == "Menu"
|
|
for link in wrapper.links
|
|
)
|
|
assert all(
|
|
any(
|
|
link.to_node == axis_switch and link.to_socket.name == axis
|
|
for link in wrapper.links
|
|
)
|
|
for axis in ("-Z", "+X")
|
|
)
|
|
negative_z_link = next(
|
|
link
|
|
for link in wrapper.links
|
|
if link.to_node == axis_switch and link.to_socket.name == "-Z"
|
|
)
|
|
positive_x_link = next(
|
|
link
|
|
for link in wrapper.links
|
|
if link.to_node == axis_switch and link.to_socket.name == "+X"
|
|
)
|
|
assert positive_x_link.from_node.bl_idname == "GeometryNodeTransform"
|
|
assert any(
|
|
link.from_socket == negative_z_link.from_socket
|
|
and link.to_node == positive_x_link.from_node
|
|
and link.to_socket.name == "Geometry"
|
|
for link in wrapper.links
|
|
)
|
|
child_nodes = {
|
|
node.node_tree: node
|
|
for node in wrapper.nodes
|
|
if node.bl_idname == "GeometryNodeGroup"
|
|
}
|
|
assert set(child_nodes) == set(groups.values())
|
|
for shape, group in groups.items():
|
|
assert any(
|
|
link.from_node == child_nodes[group]
|
|
and link.to_node == menu_switch
|
|
and link.to_socket.name == shapes.CUTOUT_SHAPE_LABELS[shape]
|
|
for link in wrapper.links
|
|
)
|
|
|
|
assert input_names(wrapper) == [
|
|
"Geometry",
|
|
"Shape",
|
|
"Inward Axis",
|
|
"Thickness",
|
|
"Depth Scale",
|
|
"Double Sided",
|
|
"Smooth Iterations",
|
|
"Cleanup",
|
|
"Smooth",
|
|
"Split",
|
|
"Reference Depth",
|
|
"Depth Direction",
|
|
"Uniform Scale",
|
|
"Depth Image",
|
|
]
|
|
expected_inputs = {
|
|
"SURFACE": ["Geometry", "Thickness"],
|
|
"BALLOON": ["Geometry", "Thickness"],
|
|
"DEPTH_BALLOON": [
|
|
"Geometry",
|
|
"Thickness",
|
|
"Depth Scale",
|
|
"Double Sided",
|
|
"Reference Depth",
|
|
"Depth Direction",
|
|
"Uniform Scale",
|
|
"Depth Image",
|
|
],
|
|
"DEPTH_SURFACE": [
|
|
"Geometry",
|
|
"Thickness",
|
|
"Depth Scale",
|
|
"Split",
|
|
"Cleanup",
|
|
"Reference Depth",
|
|
"Uniform Scale",
|
|
"Depth Image",
|
|
],
|
|
}
|
|
for shape, group in groups.items():
|
|
assert input_names(group) == expected_inputs[shape]
|
|
|
|
dependencies = {}
|
|
for link in wrapper.links:
|
|
if link.from_node.bl_idname != "NodeGroupInput":
|
|
continue
|
|
if link.to_node.bl_idname != "GeometryNodeGroup":
|
|
continue
|
|
shape = next(
|
|
shape for shape, group in groups.items() if group == link.to_node.node_tree
|
|
)
|
|
dependencies.setdefault(link.from_socket.name, set()).add(shape)
|
|
assert dependencies == {
|
|
"Geometry": {"DEPTH_SURFACE"},
|
|
"Thickness": set(shapes.CUTOUT_SHAPES),
|
|
"Depth Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
|
"Split": {"DEPTH_SURFACE"},
|
|
"Double Sided": {"DEPTH_BALLOON"},
|
|
"Reference Depth": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
|
"Depth Direction": {"DEPTH_BALLOON"},
|
|
"Uniform Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
|
"Depth Image": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
|
}
|
|
|
|
repeat_counts = {
|
|
shape: sum(node.bl_idname == "GeometryNodeRepeatInput" for node in group.nodes)
|
|
for shape, group in groups.items()
|
|
}
|
|
assert repeat_counts == {
|
|
"SURFACE": 1,
|
|
"BALLOON": 0,
|
|
"DEPTH_BALLOON": 0,
|
|
"DEPTH_SURFACE": 1,
|
|
}
|
|
assert (
|
|
sum(node.bl_idname == "GeometryNodeRepeatInput" for node in wrapper.nodes) == 1
|
|
)
|
|
assert (
|
|
sum(node.bl_idname == "GeometryNodeInputMeshIsland" for node in wrapper.nodes)
|
|
== 1
|
|
)
|
|
assert not any(
|
|
node.bl_idname == "GeometryNodeAttributeStatistic" for node in wrapper.nodes
|
|
)
|
|
assert all(
|
|
not any(node.bl_idname == "GeometryNodeInputMeshIsland" for node in group.nodes)
|
|
for shape, group in groups.items()
|
|
if shape != "DEPTH_SURFACE"
|
|
)
|
|
assert any(node.bl_idname == "GeometryNodeBlurAttribute" for node in wrapper.nodes)
|
|
smooth = next(
|
|
item
|
|
for item in wrapper.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.name == "Smooth"
|
|
)
|
|
iterations = next(
|
|
item
|
|
for item in wrapper.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.name == "Smooth Iterations"
|
|
)
|
|
cleanup = next(
|
|
item
|
|
for item in wrapper.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.name == "Cleanup"
|
|
)
|
|
assert cleanup.default_value == 0.0
|
|
assert cleanup.subtype == "NONE"
|
|
split_control = next(
|
|
item
|
|
for item in wrapper.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.name == "Split"
|
|
)
|
|
child_split = next(
|
|
item
|
|
for item in groups["DEPTH_SURFACE"].interface.items_tree
|
|
if item.item_type == "SOCKET" and item.name == "Split"
|
|
)
|
|
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 = groups["DEPTH_SURFACE"].nodes
|
|
assert any(
|
|
node.bl_idname == "GeometryNodeSplitEdges" for node in depth_surface_nodes
|
|
)
|
|
validate_depth_surface_attributes(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 groups["DEPTH_SURFACE"].interface.items_tree
|
|
]
|
|
assert all(
|
|
socket.is_linked or socket.hide
|
|
for node in depth_surface_nodes
|
|
for socket in node.outputs
|
|
)
|
|
for node in depth_surface_nodes:
|
|
if node.bl_idname == "GeometryNodeRepeatInput":
|
|
assert node.paired_output.location.x > node.location.x
|
|
cleanup_scale = next(
|
|
link.to_node
|
|
for link in wrapper.links
|
|
if link.from_socket.name == "Cleanup"
|
|
and link.from_node.bl_idname == "NodeGroupInput"
|
|
)
|
|
assert cleanup_scale.bl_idname == "ShaderNodeMath"
|
|
assert cleanup_scale.operation == "MULTIPLY"
|
|
assert abs(cleanup_scale.inputs[1].default_value - 0.01) < 1e-7
|
|
assert smooth.default_value == 1.0
|
|
assert iterations.default_value == 0
|
|
assert iterations.min_value == 0
|
|
assert getattr(iterations.parent, "name", "") == ""
|
|
assert all(
|
|
not any(node.bl_idname == "GeometryNodeBlurAttribute" for node in group.nodes)
|
|
for group in groups.values()
|
|
)
|
|
|
|
for group in (wrapper, *groups.values()):
|
|
validate_node_layout(group)
|
|
for item in group.interface.items_tree:
|
|
if item.item_type == "SOCKET" and item.in_out == "INPUT":
|
|
assert item.structure_type == "SINGLE"
|
|
surface = grid_object("Surface Validation")
|
|
add_modifier(surface, "SURFACE", 2.4)
|
|
evaluated, result = evaluated_mesh(surface)
|
|
try:
|
|
assert len(result.vertices) == 34
|
|
assert len(result.polygons) == 40
|
|
side_faces = [
|
|
polygon for polygon in result.polygons if abs(polygon.normal.z) < 0.1
|
|
]
|
|
assert len(side_faces) == 24
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
balloon = grid_object("Balloon Validation")
|
|
profile = balloon.data.attributes.new(
|
|
shapes.BALLOON_ATTRIBUTE_NAME, "FLOAT", "POINT"
|
|
)
|
|
profile.data.foreach_set(
|
|
"value",
|
|
(0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0),
|
|
)
|
|
modifier = add_modifier(balloon, "BALLOON", 1.0)
|
|
evaluated, result = evaluated_mesh(balloon)
|
|
try:
|
|
initial_faces = len(result.polygons)
|
|
initial_extent = max(abs(vertex.co.z) for vertex in result.vertices)
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
set_modifier_input(
|
|
modifier,
|
|
modifier_input_identifier(modifier.node_group, "Thickness"),
|
|
2.0,
|
|
)
|
|
balloon.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
evaluated, result = evaluated_mesh(balloon)
|
|
try:
|
|
assert len(result.polygons) == initial_faces
|
|
extent = max(abs(vertex.co.z) for vertex in result.vertices)
|
|
assert abs(extent - initial_extent * 2.0) < 1e-5
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
depth_balloon = grid_object("Depth Balloon Metric Validation")
|
|
profile = depth_balloon.data.attributes.new(
|
|
shapes.BALLOON_ATTRIBUTE_NAME, "FLOAT", "POINT"
|
|
)
|
|
profile.data.foreach_set("value", (1.0,) * 9)
|
|
modifier = add_modifier(depth_balloon, "DEPTH_BALLOON", 0.0)
|
|
point_image = bpy.data.images.new(
|
|
"Depth Balloon Points",
|
|
width=3,
|
|
height=3,
|
|
alpha=True,
|
|
float_buffer=True,
|
|
)
|
|
point_image.colorspace_settings.name = "Non-Color"
|
|
point_image.alpha_mode = "CHANNEL_PACKED"
|
|
pixels = []
|
|
for z in range(3):
|
|
for y in range(3):
|
|
if (y, z) == (1, 1):
|
|
depth = 1.5
|
|
elif (y, z) == (0, 0):
|
|
depth = 2.5
|
|
else:
|
|
depth = 2.0
|
|
pixels.extend((0.0, 0.0, depth, 0.0))
|
|
point_image.pixels.foreach_set(pixels)
|
|
for name, value in (
|
|
("Depth Image", point_image),
|
|
("Depth Scale", 1.0),
|
|
("Uniform Scale", 1.0),
|
|
("Reference Depth", 2.0),
|
|
("Depth Direction", (0.0, 0.0, 1.0)),
|
|
):
|
|
set_modifier_input(
|
|
modifier,
|
|
modifier_input_identifier(modifier.node_group, name),
|
|
value,
|
|
)
|
|
depth_balloon.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
evaluated, result = evaluated_mesh(depth_balloon)
|
|
try:
|
|
z_values = [vertex.co.z for vertex in result.vertices]
|
|
assert abs(min(z_values)) < 1e-5
|
|
assert abs(max(z_values) - 0.5) < 1e-5
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
pixels[(1 * 3 + 1) * 4 + 3] = -1.0
|
|
point_image.pixels.foreach_set(pixels)
|
|
point_image.update()
|
|
depth_balloon.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
evaluated, result = evaluated_mesh(depth_balloon)
|
|
try:
|
|
z_values = [vertex.co.z for vertex in result.vertices]
|
|
assert abs(min(z_values)) < 1e-5
|
|
assert abs(max(z_values) - 0.5) < 1e-5
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
depth_surface = grid_object("Depth Surface Projection Validation")
|
|
modifier = add_modifier(depth_surface, "DEPTH_SURFACE", 0.0)
|
|
point_image = bpy.data.images.new(
|
|
"Depth Surface Points",
|
|
width=3,
|
|
height=3,
|
|
alpha=True,
|
|
float_buffer=True,
|
|
)
|
|
point_image.colorspace_settings.name = "Non-Color"
|
|
point_image.alpha_mode = "CHANNEL_PACKED"
|
|
pixels = []
|
|
for y in range(3):
|
|
for x in range(3):
|
|
depth = 7.0 if (x, y) == (0, 0) else 2.0 + x * 0.25
|
|
pixels.extend((x * 0.5, -y * 0.5, depth, 0.0))
|
|
point_image.pixels.foreach_set(pixels)
|
|
for name, value in (
|
|
("Depth Image", point_image),
|
|
("Depth Scale", 1.0),
|
|
("Uniform Scale", 1.0),
|
|
("Reference Depth", 2.0),
|
|
("Smooth Iterations", 0),
|
|
):
|
|
set_modifier_input(
|
|
modifier,
|
|
modifier_input_identifier(modifier.node_group, name),
|
|
value,
|
|
)
|
|
depth_surface.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
evaluated, result = evaluated_mesh(depth_surface)
|
|
try:
|
|
coordinates = [tuple(vertex.co) for vertex in result.vertices]
|
|
assert all(
|
|
all(
|
|
abs(actual - expected) < 1e-5 for actual, expected in zip(point, target)
|
|
)
|
|
for point, target in zip(
|
|
coordinates,
|
|
(
|
|
(
|
|
x * 0.5,
|
|
y * 0.5,
|
|
-5.0 if (x, y) == (0, 0) else -x * 0.25,
|
|
)
|
|
for y in range(3)
|
|
for x in range(3)
|
|
),
|
|
)
|
|
)
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
set_modifier_input(
|
|
modifier,
|
|
modifier_input_identifier(modifier.node_group, "Split"),
|
|
0.6,
|
|
)
|
|
depth_surface.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
evaluated, result = evaluated_mesh(depth_surface)
|
|
try:
|
|
assert len(result.polygons) == 8
|
|
face_edges = {
|
|
tuple(sorted((start, end)))
|
|
for polygon in result.polygons
|
|
for start, end in zip(
|
|
polygon.vertices,
|
|
(*polygon.vertices[1:], polygon.vertices[0]),
|
|
)
|
|
}
|
|
assert all(tuple(sorted(edge.vertices)) in face_edges for edge in result.edges)
|
|
face_vertices = {
|
|
vertex for polygon in result.polygons for vertex in polygon.vertices
|
|
}
|
|
assert face_vertices == set(range(len(result.vertices)))
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
set_modifier_input(
|
|
modifier,
|
|
modifier_input_identifier(modifier.node_group, "Depth Scale"),
|
|
0.0,
|
|
)
|
|
depth_surface.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
evaluated, result = evaluated_mesh(depth_surface)
|
|
try:
|
|
assert len(result.vertices) == 9
|
|
assert len(result.polygons) == 8
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
set_modifier_input(
|
|
modifier, modifier_input_identifier(modifier.node_group, "Depth Scale"), 1.0
|
|
)
|
|
set_modifier_input(
|
|
modifier, modifier_input_identifier(modifier.node_group, "Thickness"), 0.2
|
|
)
|
|
depth_surface.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
evaluated, result = evaluated_mesh(depth_surface)
|
|
try:
|
|
edge_uses = Counter(
|
|
tuple(sorted((a, b)))
|
|
for face in result.polygons
|
|
for a, b in zip(face.vertices, (*face.vertices[1:], face.vertices[0]))
|
|
)
|
|
assert edge_uses and set(edge_uses.values()) == {2}
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
print(
|
|
f"Validated Blender {bpy.app.version_string}: "
|
|
f"{wrapper.name} wrapping "
|
|
f"{', '.join(group.name for group in groups.values())}"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|