- Introduced a new `Boundary Padding` preference in the Cutout Tool with a default of 2 px and a configurable range of 1-4 px. - Updated the Cutout client to establish inward mapping from the final Mask, sampling in a fixed direction and processing independent material RGBA and Depth. - Enhanced the handling of RGBA and Depth during cutout operations, ensuring proper mapping and preservation of source data. - Added tests to validate the new boundary padding functionality and its integration with existing cutout processes. feat(geometry): remove overall mesh smoothing from geometry nodes - Removed the `Smooth` input from several geometry node groups to eliminate unnecessary overall mesh smoothing. - Retained `Smooth Weight` for depth nodes while removing it from Cutout and Relief Plane nodes. - Ensured that boundary, normal, and fill smoothing behaviors remain intact and consistent. - Updated tests and documentation to reflect the changes in node interfaces and behaviors. feat(normals): scale tangent normals with depth - Reused the existing `o_normal_reduction` attribute to adjust Tangent Normal strength based on the geometry's `Depth Scale`. - Ensured that Tangent Normal strength is consistent with the geometry depth, preserving full strength for materials that do not provide the attribute. - Updated tests to verify the correct behavior of normal strength adjustments in both Tangent and Object Space.
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(1.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
|