- Refactor Depth Cutout to reuse the projected outline field, reducing redundant boundary calculations and improving performance. - Derive the center field from shared low-frequency fields, eliminating an unnecessary blur operation. - Implement triangle-only strip cleanup for Depth Cutout, as the input is confirmed to be triangulated. - Short-circuit zero-thickness branches in both Cutout and Depth Cutout to avoid unnecessary geometry generation. - Standardize internal attribute naming with a new `_o_` prefix and implement wildcard cleanup for temporary attributes. - Update relevant tests to ensure functionality remains consistent and validate changes against existing behavior.
221 lines
11 KiB
Python
221 lines
11 KiB
Python
"""Check the saved library's public inventory and persistence."""
|
|
import bpy
|
|
import pytest
|
|
from anyimage.common.node import node_asset_path
|
|
from tools.nodes.check import GEOMETRY_NAMES
|
|
|
|
|
|
def test_saved_assets_evaluate():
|
|
from tools.nodes.check import check_asset_library
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
check_asset_library(node_asset_path())
|
|
|
|
|
|
def test_saved_parameter_descriptions_match_definitions():
|
|
from tools.nodes.build import build_node_groups
|
|
|
|
parameters = {
|
|
"O Image Plane": {"Subdivide"},
|
|
"O Image Depth Plane": {
|
|
"Subdivide", "Thickness", "Depth Split", "Smooth Weight",
|
|
"Depth Mask", "Mask Threshold", "Boundary Smooth",
|
|
},
|
|
"O Image Relief Plane": {
|
|
"Subdivide", "Thickness", "Depth Direction", "Depth Offset",
|
|
},
|
|
"O Image Cutout": {"Thickness"},
|
|
"O Image Depth Cutout": {
|
|
"Thickness", "Depth Split", "Smooth Weight",
|
|
"Boundary Smooth", "Normal Smooth", "Front Inflation",
|
|
},
|
|
"O Image Cutout Symmetry": {
|
|
"Direction", "Offset", "Scale", "Fill Sides", "Fill Smooth",
|
|
},
|
|
"O Image Depth Panorama": {
|
|
"Subdivide", "Depth Scale", "Depth Split", "Smooth Weight",
|
|
"Depth Mask", "Mask Threshold", "Boundary Smooth", "Invalid Distance",
|
|
},
|
|
"O Image Layer": {"Alpha Fix", "Normal Scale", "Bump Scale", "Object Space"},
|
|
"O Image Depth Layer": {"Bump Scale"},
|
|
}
|
|
|
|
def descriptions(groups):
|
|
result = {}
|
|
for group in groups:
|
|
if group.name not in parameters:
|
|
continue
|
|
sockets = [
|
|
item for item in group.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.in_out == "INPUT"
|
|
and item.name in parameters[group.name]
|
|
]
|
|
assert {item.name for item in sockets} == parameters[group.name]
|
|
assert all(item.description for item in sockets), group.name
|
|
input_names = {
|
|
item.name for item in group.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.in_out == "INPUT"
|
|
}
|
|
input_items = [
|
|
item for item in group.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.in_out == "INPUT"
|
|
]
|
|
if group.name in {
|
|
"O Image Cutout", "O Image Depth Cutout", "O Image Depth Plane",
|
|
"O Image Depth Panorama", "O Image Relief Plane",
|
|
}:
|
|
assert "Smooth" not in input_names
|
|
if group.name in {"O Image Cutout", "O Image Relief Plane"}:
|
|
assert "Smooth Weight" not in input_names
|
|
if group.name in {"O Image Depth Plane", "O Image Depth Cutout"}:
|
|
boundary = next(item for item in sockets if item.name == "Boundary Smooth")
|
|
assert boundary.default_value == 4
|
|
assert boundary.min_value == 0
|
|
assert boundary.max_value == 16
|
|
assert boundary.parent.name == "Options"
|
|
if group.name == "O Image Depth Plane":
|
|
depth_scale = next(item for item in input_items if item.name == "Depth Scale")
|
|
assert (depth_scale.min_value, depth_scale.max_value) == (0.0, 2.0)
|
|
if group.name == "O Image Depth Panorama":
|
|
defaults = {item.name: item.default_value for item in sockets
|
|
if item.name in {"Subdivide", "Depth Split", "Boundary Smooth"}}
|
|
assert defaults == pytest.approx({
|
|
"Subdivide": 8, "Depth Split": 0.1, "Boundary Smooth": 2,
|
|
})
|
|
boundary = next(item for item in input_items if item.name == "Boundary Smooth")
|
|
assert (boundary.min_value, boundary.max_value) == (0, 16)
|
|
if group.name == "O Image Relief Plane":
|
|
names = [item.name for item in group.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.in_out == "INPUT"]
|
|
start = names.index("Thickness")
|
|
assert names[start:start + 4] == [
|
|
"Thickness", "Depth Direction", "Depth Offset", "Depth Scale",
|
|
]
|
|
thickness = next(item for item in sockets if item.name == "Thickness")
|
|
direction = next(item for item in sockets if item.name == "Depth Direction")
|
|
offset = next(item for item in sockets if item.name == "Depth Offset")
|
|
assert offset.default_value == pytest.approx(0.0)
|
|
assert (offset.min_value, offset.max_value) == (-10.0, 10.0)
|
|
assert offset.subtype == "DISTANCE"
|
|
assert direction.parent == thickness.parent == offset.parent
|
|
depth_scale = next(item for item in input_items if item.name == "Depth Scale")
|
|
assert (depth_scale.min_value, depth_scale.max_value) == (0.0, 2.0)
|
|
if "Depth Mask" in parameters[group.name]:
|
|
names = [item.name for item in group.interface.items_tree
|
|
if item.item_type == "SOCKET" and item.in_out == "INPUT"]
|
|
assert names.index("Depth Mask") == names.index("Depth Split") + 1
|
|
if group.name in {"O Image Cutout", "O Image Depth Cutout"}:
|
|
mode = next(node for node in group.nodes
|
|
if node.bl_idname == "GeometryNodeMenuSwitch"
|
|
and node.inputs["Menu"].links[0].from_socket.name == "Mode")
|
|
assert [item.name for item in mode.enum_items] == ["Balloon", "Shell"]
|
|
thickness = [item.description for item in sockets if item.name == "Thickness"]
|
|
assert len(thickness) == 2 and len(set(thickness)) == 2
|
|
thicknesses = [item for item in input_items if item.name == "Thickness"]
|
|
balloon = next(item for item in thicknesses if item.subtype == "NONE")
|
|
shell = next(item for item in thicknesses if item.subtype == "DISTANCE")
|
|
assert (balloon.min_value, balloon.max_value) == (0.0, 2.0)
|
|
assert (shell.min_value, shell.max_value) == (0.0, 1.0)
|
|
if group.name == "O Image Depth Cutout":
|
|
assert all(item.default_value == 0 for item in sockets if item.name == "Thickness")
|
|
depth_scale = next(item for item in input_items if item.name == "Depth Scale")
|
|
assert (depth_scale.min_value, depth_scale.max_value) == (0.0, 2.0)
|
|
assert not {"Outline Depth Fix", "Center Smooth", "Center Subdivide", "Show Center"} & {
|
|
item.name for item in group.interface.items_tree if item.item_type == "SOCKET"
|
|
}
|
|
inflation = next(item for item in sockets if item.name == "Front Inflation")
|
|
assert inflation.default_value == pytest.approx(0.2)
|
|
normal_smooth = next(item for item in sockets if item.name == "Normal Smooth")
|
|
assert normal_smooth.default_value == 64
|
|
assert normal_smooth.min_value == 16
|
|
assert normal_smooth.max_value == 512
|
|
assert normal_smooth.parent.name == "Options"
|
|
if group.name == "O Image Cutout Symmetry":
|
|
offset = next(item for item in input_items if item.name == "Offset")
|
|
scale = next(item for item in input_items if item.name == "Scale")
|
|
fill_smooth = next(item for item in input_items if item.name == "Fill Smooth")
|
|
assert (offset.min_value, offset.max_value) == (-10.0, 10.0)
|
|
assert (scale.min_value, scale.max_value) == (0.0, 2.0)
|
|
assert (fill_smooth.min_value, fill_smooth.max_value) == (0, 16)
|
|
result[group.name] = [
|
|
(item.identifier, item.name, item.description) for item in sockets
|
|
]
|
|
assert result.keys() == parameters.keys()
|
|
return result
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
expected = descriptions(build_node_groups())
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
with bpy.data.libraries.load(str(node_asset_path())) as (library, target):
|
|
target.node_groups = list(library.node_groups)
|
|
assert descriptions(target.node_groups) == expected
|
|
|
|
|
|
@pytest.mark.parametrize("source", ["definition", "asset"])
|
|
def test_geometry_nodes_contribute_to_output(source):
|
|
from tools.nodes.build import build_node_groups
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
if source == "definition":
|
|
groups = build_node_groups()
|
|
else:
|
|
with bpy.data.libraries.load(str(node_asset_path())) as (library, target):
|
|
target.node_groups = list(library.node_groups)
|
|
groups = target.node_groups
|
|
for group in groups:
|
|
if group.bl_idname != "GeometryNodeTree":
|
|
continue
|
|
pending = [node for node in group.nodes if node.bl_idname == "NodeGroupOutput"]
|
|
used = set()
|
|
while pending:
|
|
node = pending.pop()
|
|
if node in used:
|
|
continue
|
|
used.add(node)
|
|
pending.extend(link.from_node for socket in node.inputs for link in socket.links)
|
|
pending.extend(candidate for candidate in group.nodes
|
|
if getattr(candidate, "paired_output", None) == node)
|
|
unused = [node.name for node in group.nodes
|
|
if node not in used and node.bl_idname != "NodeFrame"]
|
|
assert not unused, (group.name, unused)
|
|
|
|
|
|
@pytest.mark.parametrize("source", ["definition", "asset"])
|
|
def test_geometry_attribute_contract_includes_nested_groups(source):
|
|
from tools.nodes.build import build_node_groups
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
if source == "definition":
|
|
pending = build_node_groups()
|
|
else:
|
|
with bpy.data.libraries.load(str(node_asset_path())) as (library, target):
|
|
target.node_groups = list(library.node_groups)
|
|
pending = list(target.node_groups)
|
|
seen = set()
|
|
allowed = {
|
|
"UVMap": ("FLOAT2", "CORNER"),
|
|
"o_balloon": ("FLOAT", "POINT"),
|
|
"_o_anyimage_depth_cut": ("BOOLEAN", "CORNER"),
|
|
"o_depth_rotation": ("FLOAT_VECTOR", "FACE"),
|
|
"o_depth_face": ("FLOAT", "FACE"),
|
|
"o_depth_axis": ("FLOAT", "FACE"),
|
|
}
|
|
while pending:
|
|
group = pending.pop()
|
|
if group in seen or group.bl_idname != "GeometryNodeTree":
|
|
continue
|
|
seen.add(group)
|
|
for node in group.nodes:
|
|
assert node.bl_idname != "GeometryNodeCaptureAttribute", group.name
|
|
if node.bl_idname == "GeometryNodeGroup" and node.node_tree:
|
|
pending.append(node.node_tree)
|
|
if node.bl_idname != "GeometryNodeStoreNamedAttribute":
|
|
continue
|
|
name = node.inputs["Name"].default_value
|
|
if name == "o_normal_reduction":
|
|
assert node.data_type == "FLOAT" and node.domain in {"POINT", "FACE"}
|
|
else:
|
|
assert name in allowed, (group.name, name)
|
|
assert (node.data_type, node.domain) == allowed[name]
|
|
assert {group.name for group in seen} == GEOMETRY_NAMES
|