Add cutout shell and volume node assets for depth volume processing

- Implemented `cutout_shell.py` to build front normals and shell positions for balloon geometry.
- Created `cutout_volume.py` to establish smooth center fields and manage balloon surface attributes.
- Introduced `pisa_sr_export.py` for exporting PiSA-SR components with dependencies and asset finalization.
This commit is contained in:
2026-09-08 17:18:03 +08:00
parent 7dec3b7ca4
commit 196b964efc
71 changed files with 3440 additions and 1988 deletions

View File

@@ -1,676 +1,92 @@
"""Validate direct Cutout assets and their evaluated defaults."""
from pathlib import Path
import sys
from collections import Counter
from itertools import pairwise
from pathlib import Path
import bpy
import numpy as np
PROJECT_ROOT = Path(__file__).parents[2]
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "src"))
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"))),
str(next((ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.cutout_tool.object import load_cutout_node_group
from anyimage.common.object import modifier_input_identifier, set_modifier_input
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()):
groups = {shape: load_cutout_node_group(shape) for shape in ("FLAT", "SOLID", "DEPTH_SOLID", "DEPTH_BALLOON")}
assert groups["FLAT"] == groups["SOLID"]
assert len(set(groups.values())) == 3
for group in set(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
assert group.is_modifier and group.get("anyimage_asset_revision") == 11
assert not any(node.bl_idname == "GeometryNodeGroup" for node in group.nodes)
inputs = {item.name: item for item in group.interface.items_tree if item.item_type == "SOCKET" and item.in_out == "INPUT"}
assert "Shape" not in inputs and "Rim Thickness" not in inputs
assert next(s for s in group.interface.items_tree if s.name == "Thickness").default_value == 1
assert all(item.structure_type == "SINGLE" for item in inputs.values())
if "Mode" in inputs:
assert inputs["Thickness"].subtype == "DISTANCE"
assert inputs["Mode"].default_value == "Balloon"
if "Double Sided" in inputs:
assert inputs["Double Sided"].default_value
if group == groups["DEPTH_SOLID"]:
assert np.isclose(inputs["Edge Round"].default_value, 0.2)
assert inputs["Edge Round"].parent.name == "Options"
assert list(inputs)[list(inputs).index("Front Inflation") - 1] == "Edge Round"
assert inputs["Normal Smooth"].default_value == 50
assert inputs["Front Inflation"].default_value == 1
assert inputs["Front Inflation"].parent.name == "Options"
assert not set(inputs) & {"Edge Round Range", "Back Rim Range", "Normal Influence", "Edge Round Amount", "Back Rim Fullness"}
options = [s.name for s in inputs.values() if s.parent and s.parent.name == "Options"]
expected = ["Cleanup Threshold"]
if group == groups["DEPTH_SOLID"]:
expected += ["Split Threshold", "Reference Depth", "Edge Round", "Front Inflation", "Normal Smooth"]
elif group == groups["DEPTH_BALLOON"]:
expected += ["Reference Depth"]
expected += ["Smooth Weight"]
if group == groups["DEPTH_BALLOON"]:
expected += ["Depth Direction"]
assert options == expected, (group.name, options)
assert inputs["Smooth"].socket_type == "NodeSocketInt" and inputs["Smooth"].default_value == 0
assert inputs["Smooth Weight"].default_value == 1
incoming = [link for link in group.links if link.from_node.bl_idname == "NodeGroupInput" and link.from_socket.name == "Geometry"]
assert len(incoming) == 1 and incoming[0].to_node.bl_idname == "GeometryNodeDeleteGeometry"
mesh = bpy.data.meshes.new("Profile")
mesh.from_pydata([(-1,-1,0), (1,-1,0), (1,1,0), (-1,1,0), (0,0,0)], [], [(0,1,4), (1,2,4), (2,3,4), (3,0,4)])
profile = mesh.attributes.new("o_balloon", "FLOAT", "POINT")
profile.data.foreach_set("value", [0,0,0,0,0.6])
obj = bpy.data.objects.new("Profile", mesh)
bpy.context.collection.objects.link(obj)
modifier = obj.modifiers.new("Cutout", "NODES")
modifier.node_group = groups["SOLID"]
for mode, thickness in ((0, 0), (0, 1), (1, 0), (1, 0.4)):
for name, value in (("Mode", mode), ("Thickness", thickness)):
subtype = ("DISTANCE" if mode else "NONE") if name == "Thickness" else None
set_modifier_input(modifier, modifier_input_identifier(modifier.node_group, name, subtype=subtype), value)
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
result = evaluated.to_mesh()
try:
z = np.array([vertex.co.z for vertex in result.vertices])
assert np.isfinite(z).all() and len(z)
if thickness == 0:
assert len(result.polygons) == 4 and np.max(abs(z)) < 1e-7
assert all(abs(item.value - 1) < 1e-7 for item in result.attributes["o_normal_scale"].data)
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())}"
)
radius = 0.6 if mode == 0 else 0.2
assert np.isclose(z.min(), -radius) and np.isclose(z.max(), radius)
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 set(uses.values()) == {2}
finally:
evaluated.to_mesh_clear()
print("Validated three Cutout assets, four presets, both thickness modes and closed defaults")
if __name__ == "__main__":