from collections import Counter import bpy import numpy as np import pytest from tests.support.nodes import CUTOUT_BUILDERS from tools.nodes.arrangement.flow import origin_socket from anyimage.operators.cutout_tool.object import load_cutout_node_group from anyimage.common.object import modifier_input_identifier, set_modifier_input from tests.support.nodes import validate_node_structure @pytest.mark.parametrize("source", ["definition", "asset"]) def test_cutout_interfaces_and_thickness_modes(source): bpy.ops.wm.read_factory_settings(use_empty=True) groups = {shape: (CUTOUT_BUILDERS[shape]() if source == "definition" else 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_structure(group) 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 += ["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.to_node.bl_idname != "NodeReroute" and origin_socket(link.from_socket).node.bl_idname == "NodeGroupInput" and origin_socket(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: 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()