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:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user