Add Chromium-only Blender WebEngine parity work
This commit is contained in:
231
blender-5.2.0/tests/python/bl_node_tool.py
Normal file
231
blender-5.2.0/tests/python/bl_node_tool.py
Normal file
@@ -0,0 +1,231 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import unittest
|
||||
import bpy
|
||||
|
||||
|
||||
def create_object_mode_mesh_tool_tree(name, tool_idname):
|
||||
tree = bpy.data.node_groups.new(name, "GeometryNodeTree")
|
||||
tree.is_tool = True
|
||||
tree.is_mode_object = True
|
||||
tree.is_type_mesh = True
|
||||
tree.node_tool_idname = tool_idname
|
||||
return tree
|
||||
|
||||
|
||||
def create_join_geometry_tool(tool_idname):
|
||||
"""
|
||||
Create a geometry node group set up as an object-mode tool.
|
||||
Returns the node tree and the identifier of the Object input socket.
|
||||
"""
|
||||
tree = create_object_mode_mesh_tool_tree("TestNodeTool", tool_idname)
|
||||
|
||||
tree.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
|
||||
obj_socket = tree.interface.new_socket("Object", in_out='INPUT', socket_type='NodeSocketObject')
|
||||
tree.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
|
||||
|
||||
group_input = tree.nodes.new("NodeGroupInput")
|
||||
group_output = tree.nodes.new("NodeGroupOutput")
|
||||
obj_info = tree.nodes.new("GeometryNodeObjectInfo")
|
||||
join_geo = tree.nodes.new("GeometryNodeJoinGeometry")
|
||||
|
||||
tree.links.new(group_input.outputs["Geometry"], join_geo.inputs["Geometry"])
|
||||
tree.links.new(group_input.outputs["Object"], obj_info.inputs["Object"])
|
||||
tree.links.new(obj_info.outputs["Geometry"], join_geo.inputs["Geometry"])
|
||||
tree.links.new(join_geo.outputs["Geometry"], group_output.inputs["Geometry"])
|
||||
|
||||
return tree, obj_socket.identifier
|
||||
|
||||
|
||||
class TestNodeTool(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
def test_join_geometry_with_object_input(self):
|
||||
from bpy.types import WindowManager
|
||||
# Add a cube (the active object the tool will run on).
|
||||
bpy.ops.mesh.primitive_cube_add()
|
||||
cube = bpy.context.active_object
|
||||
cube_vertex_count = len(cube.data.vertices)
|
||||
|
||||
# Add a monkey/Suzanne (the tool's object input, joined into the cube).
|
||||
bpy.ops.mesh.primitive_monkey_add()
|
||||
monkey = bpy.context.active_object
|
||||
monkey_vertex_count = len(monkey.data.vertices)
|
||||
|
||||
# Make the cube the active, selected object.
|
||||
bpy.context.view_layer.objects.active = cube
|
||||
for obj in bpy.context.scene.objects:
|
||||
obj.select_set(obj == cube)
|
||||
|
||||
_tree, obj_input_identifier = create_join_geometry_tool("geometry.test_node_tool_join_geometry")
|
||||
|
||||
WindowManager.register_node_group_operators()
|
||||
|
||||
bpy.ops.geometry.test_node_tool_join_geometry(
|
||||
'EXEC_DEFAULT',
|
||||
inputs={obj_input_identifier: {"value": monkey.name}},
|
||||
)
|
||||
|
||||
# The cube's mesh should now contain geometry from both the cube and the
|
||||
# monkey, so the vertex count should be the sum of both.
|
||||
expected_vertex_count = cube_vertex_count + monkey_vertex_count
|
||||
self.assertEqual(len(cube.data.vertices), expected_vertex_count)
|
||||
|
||||
def test_string_inputs_store_named_attribute(self):
|
||||
from bpy.types import WindowManager
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add()
|
||||
cube = bpy.context.active_object
|
||||
|
||||
tree = create_object_mode_mesh_tool_tree(
|
||||
"TestNodeToolStringInputs", "geometry.test_node_tool_string_inputs"
|
||||
)
|
||||
tree.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
|
||||
str_default_socket = tree.interface.new_socket(
|
||||
"Name With Default", in_out='INPUT', socket_type='NodeSocketString')
|
||||
str_default_socket.default_value = "attr_with_default"
|
||||
str_no_default_socket = tree.interface.new_socket(
|
||||
"Name Without Default", in_out='INPUT', socket_type='NodeSocketString')
|
||||
tree.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
|
||||
|
||||
group_input = tree.nodes.new("NodeGroupInput")
|
||||
group_output = tree.nodes.new("NodeGroupOutput")
|
||||
store_default = tree.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
store_no_default = tree.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
|
||||
tree.links.new(group_input.outputs["Geometry"], store_default.inputs["Geometry"])
|
||||
tree.links.new(store_default.outputs["Geometry"], store_no_default.inputs["Geometry"])
|
||||
tree.links.new(store_no_default.outputs["Geometry"], group_output.inputs["Geometry"])
|
||||
tree.links.new(group_input.outputs["Name With Default"], store_default.inputs["Name"])
|
||||
tree.links.new(group_input.outputs["Name Without Default"], store_no_default.inputs["Name"])
|
||||
|
||||
WindowManager.register_node_group_operators()
|
||||
|
||||
# The string with a default value is left at its default ("attr_with_default").
|
||||
# The string without a default is explicitly set when calling the tool.
|
||||
bpy.ops.geometry.test_node_tool_string_inputs(
|
||||
'EXEC_DEFAULT',
|
||||
inputs={str_no_default_socket.identifier: {"value": "attr_without_default"}},
|
||||
)
|
||||
|
||||
attribute_names = {attr.name for attr in cube.data.attributes}
|
||||
self.assertIn("attr_with_default", attribute_names)
|
||||
self.assertIn("attr_without_default", attribute_names)
|
||||
|
||||
def test_menu_input(self):
|
||||
from bpy.types import WindowManager
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add()
|
||||
cube = bpy.context.active_object
|
||||
|
||||
tree = create_object_mode_mesh_tool_tree(
|
||||
"TestNodeToolMenu", "geometry.test_node_tool_menu"
|
||||
)
|
||||
tree.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
|
||||
mode_socket = tree.interface.new_socket(
|
||||
"Mode", in_out='INPUT', socket_type='NodeSocketMenu')
|
||||
tree.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
|
||||
|
||||
group_input = tree.nodes.new("NodeGroupInput")
|
||||
group_output = tree.nodes.new("NodeGroupOutput")
|
||||
menu_switch = tree.nodes.new("GeometryNodeMenuSwitch")
|
||||
menu_switch.data_type = 'VECTOR'
|
||||
transform = tree.nodes.new("GeometryNodeTransform")
|
||||
|
||||
# The menu switch starts with default items "A" and "B". Rename and add a third so the items become X, Y, Z.
|
||||
menu_switch.enum_items[0].name = "X"
|
||||
menu_switch.enum_items[1].name = "Y"
|
||||
menu_switch.enum_items.new("Z")
|
||||
|
||||
menu_switch.inputs["X"].default_value = (10.0, 0.0, 0.0)
|
||||
menu_switch.inputs["Y"].default_value = (0.0, 10.0, 0.0)
|
||||
menu_switch.inputs["Z"].default_value = (0.0, 0.0, 10.0)
|
||||
|
||||
tree.links.new(group_input.outputs["Geometry"], transform.inputs["Geometry"])
|
||||
tree.links.new(group_input.outputs["Mode"], menu_switch.inputs["Menu"])
|
||||
tree.links.new(menu_switch.outputs["Output"], transform.inputs["Translation"])
|
||||
tree.links.new(transform.outputs["Geometry"], group_output.inputs["Geometry"])
|
||||
|
||||
WindowManager.register_node_group_operators()
|
||||
|
||||
bpy.ops.geometry.test_node_tool_menu(
|
||||
'EXEC_DEFAULT',
|
||||
inputs={mode_socket.identifier: {"value": "Y"}},
|
||||
)
|
||||
|
||||
verts = cube.data.vertices
|
||||
n = len(verts)
|
||||
center_x = sum(v.co.x for v in verts) / n
|
||||
center_y = sum(v.co.y for v in verts) / n
|
||||
center_z = sum(v.co.z for v in verts) / n
|
||||
self.assertAlmostEqual(center_x, 0.0, places=4)
|
||||
self.assertAlmostEqual(center_y, 10.0, places=4)
|
||||
self.assertAlmostEqual(center_z, 0.0, places=4)
|
||||
|
||||
def test_image_input_sample_texture(self):
|
||||
from bpy.types import WindowManager
|
||||
|
||||
# Build a 32x32 image: left half white, right half black.
|
||||
size = 32
|
||||
image = bpy.data.images.new("TestImage", width=size, height=size)
|
||||
white = [1.0, 1.0, 1.0, 1.0]
|
||||
black = [0.0, 0.0, 0.0, 1.0]
|
||||
row = (white * (size // 2)) + (black * (size // 2))
|
||||
pixels = row * size
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
|
||||
bpy.ops.mesh.primitive_plane_add()
|
||||
plane = bpy.context.active_object
|
||||
|
||||
tree = create_object_mode_mesh_tool_tree(
|
||||
"TestNodeToolImage", "geometry.test_node_tool_image"
|
||||
)
|
||||
tree.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
|
||||
image_socket = tree.interface.new_socket(
|
||||
"Image", in_out='INPUT', socket_type='NodeSocketImage')
|
||||
tree.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
|
||||
|
||||
group_input = tree.nodes.new("NodeGroupInput")
|
||||
group_output = tree.nodes.new("NodeGroupOutput")
|
||||
|
||||
uv_attr = tree.nodes.new("GeometryNodeInputNamedAttribute")
|
||||
uv_attr.data_type = 'FLOAT_VECTOR'
|
||||
uv_attr.inputs["Name"].default_value = "UVMap"
|
||||
|
||||
image_texture = tree.nodes.new("GeometryNodeImageTexture")
|
||||
# Avoid edge interpolation/wrap effects so the leftmost vertices sample pure white.
|
||||
image_texture.interpolation = 'Closest'
|
||||
image_texture.extension = 'EXTEND'
|
||||
|
||||
store_attr = tree.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
store_attr.data_type = 'FLOAT'
|
||||
store_attr.inputs["Name"].default_value = "sampled_value"
|
||||
|
||||
tree.links.new(group_input.outputs["Geometry"], store_attr.inputs["Geometry"])
|
||||
tree.links.new(group_input.outputs["Image"], image_texture.inputs["Image"])
|
||||
tree.links.new(uv_attr.outputs["Attribute"], image_texture.inputs["Vector"])
|
||||
tree.links.new(image_texture.outputs["Color"], store_attr.inputs["Value"])
|
||||
tree.links.new(store_attr.outputs["Geometry"], group_output.inputs["Geometry"])
|
||||
|
||||
WindowManager.register_node_group_operators()
|
||||
|
||||
bpy.ops.geometry.test_node_tool_image(
|
||||
'EXEC_DEFAULT',
|
||||
inputs={image_socket.identifier: {"value": image.name}},
|
||||
)
|
||||
|
||||
value_attr = plane.data.attributes["sampled_value"]
|
||||
left_vertex_indices = [i for i, v in enumerate(plane.data.vertices) if v.co.x < 0.0]
|
||||
self.assertGreater(len(left_vertex_indices), 0)
|
||||
for i in left_vertex_indices:
|
||||
self.assertAlmostEqual(value_attr.data[i].value, 1.0, places=2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user