from pathlib import Path import sys import bpy PROJECT_ROOT = Path(__file__).parents[1] sys.path.insert(0, str(PROJECT_ROOT / "src")) sys.path.insert( 0, str(next((PROJECT_ROOT / "src" / "snapform" / "wheels").glob("blendjob-*.whl"))), ) from snapform.operators.balloon.depth import ( BALLOON_DEPTH_ATTRIBUTE, add_balloon_shape_modifier, balloon_shape_node_group, ) from snapform.operators.balloon.operator import ( mark_balloon_material, merge_balloon_normal_image, reusable_balloon_material, ) from snapform.operators.material import ( BALLOON_MATERIAL_NODE_GROUP_NAME, material_node_group, set_modifier_input, ) def input_socket(group, name): return next( item for item in group.interface.items_tree if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name == name ) def main(): bpy.ops.wm.read_factory_settings(use_empty=True) group = balloon_shape_node_group() assert group.is_modifier assert input_socket(group, "Double Sided Depth").default_value is False smoother = next( node for node in group.nodes if node.bl_idname == "ShaderNodeMapRange" and node.interpolation_type == "SMOOTHERSTEP" ) assert smoother.clamp assert not any( node.bl_idname == "ShaderNodeMath" and node.operation == "MAXIMUM" and node.label == "Clamp Depth" for node in group.nodes ) mesh = bpy.data.meshes.new("Balloon Validation Mesh") mesh.from_pydata( ( (-1.0, 0.0, 0.0), (-0.5, 0.0, 0.0), (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), ), (), (), ) depth = mesh.attributes.new(BALLOON_DEPTH_ATTRIBUTE, "FLOAT", "POINT") depth.data.foreach_set("value", (0.0, 2.0, 2.0, 2.0)) obj = bpy.data.objects.new("Balloon Validation", mesh) bpy.context.scene.collection.objects.link(obj) modifier = add_balloon_shape_modifier(obj, 1.0) evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()) result = evaluated.to_mesh() try: x = [vertex.co.x for vertex in result.vertices] assert abs(x[0] + 1.0) < 1e-5 assert abs(x[1] + 1.25) < 1e-5 assert abs(x[2]) < 1e-5 assert abs(x[3] - 1.0) < 1e-5 finally: evaluated.to_mesh_clear() set_modifier_input(modifier, input_socket(group, "Thickness").identifier, 2.0) obj.update_tag(refresh={"DATA"}) bpy.context.view_layer.update() evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()) result = evaluated.to_mesh() try: x = [vertex.co.x for vertex in result.vertices] assert abs(x[0] + 1.0) < 1e-5 assert abs(x[3] - 2.0) < 1e-5 finally: evaluated.to_mesh_clear() material_group = material_node_group( surface_group_name=BALLOON_MATERIAL_NODE_GROUP_NAME, ) normal_map = next( node for node in material_group.nodes if node.bl_idname == "ShaderNodeNormalMap" ) assert normal_map.space == "TANGENT" assert abs(input_socket(material_group, "Normal Strength").default_value - 0.5) < 1e-6 source_image = bpy.data.images.new("Reuse Source", 2, 1) shared_normal = bpy.data.images.new("Shared Normal", 2, 1) shared_normal.pixels.foreach_set( (128 / 255, 128 / 255, 1, 1, 128 / 255, 128 / 255, 1, 1) ) material = bpy.data.materials.new("Unrelated Material Name") mark_balloon_material(material, source_image, shared_normal) assert reusable_balloon_material( "unused", "unused", source_image, normal_required=True, ) is material update_normal = bpy.data.images.new("Normal Update", 2, 1) update_normal.pixels.foreach_set( (1, 0.25, 0.75, 1, 128 / 255, 128 / 255, 1, 1) ) merge_balloon_normal_image(shared_normal, update_normal) pixels = list(shared_normal.pixels) assert abs(pixels[0] - 1.0) < 1e-5 assert abs(pixels[4] - 128 / 255) < 1e-5 print(f"Validated Blender {bpy.app.version_string}: {group.name}") if __name__ == "__main__": main()