256 lines
9.1 KiB
Python
256 lines
9.1 KiB
Python
"""Compare Shriinivas cartesian source graphs against Geometry Script translations.
|
|
|
|
Run with Blender from the repository root:
|
|
|
|
blender --background experiments/source-blends/shriinivas-cartesian.blend --python tools/verify_cartesian_translations.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import bpy
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
TRANSLATION = ROOT / "examples" / "geometry_script" / "shriinivas_cartesian_helper.py"
|
|
|
|
|
|
HELPER_INPUTS = {
|
|
"Count": 25,
|
|
"Start": (-0.45, 0.0, 0.0),
|
|
"End": (0.45, 0.0, 0.0),
|
|
"r": 0.6,
|
|
"Value": 1.25,
|
|
}
|
|
|
|
GRAPH_CASES = [
|
|
{
|
|
"case": "line",
|
|
"source": "Geometry Nodes Line",
|
|
"translated": "VG Cartesian Line",
|
|
"inputs": {
|
|
"Resolution": 17,
|
|
"Length": 1.15,
|
|
"m": -0.35,
|
|
"Value": 0.25,
|
|
"Thickness": 0.035,
|
|
},
|
|
},
|
|
{
|
|
"case": "parabola",
|
|
"source": "Geometry Nodes Parabola",
|
|
"translated": "VG Cartesian Parabola",
|
|
"inputs": {
|
|
"Resolution": 31,
|
|
"Sample Resolution": 31,
|
|
"Start": -0.7,
|
|
"End": 0.8,
|
|
"a": 0.55,
|
|
"b": -0.25,
|
|
"c": 0.1,
|
|
"Thickness": 0.025,
|
|
},
|
|
},
|
|
{
|
|
"case": "circle",
|
|
"source": "Geometry Nodes Circle",
|
|
"translated": "VG Cartesian Circle",
|
|
"inputs": {
|
|
"Resolution": 37,
|
|
"r": 0.65,
|
|
"Thickness": 0.02,
|
|
},
|
|
},
|
|
]
|
|
|
|
|
|
def _load_translation() -> None:
|
|
spec = importlib.util.spec_from_file_location("vg_cartesian_translation", TRANSLATION)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError(f"Could not load translation script: {TRANSLATION}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
|
|
|
|
def _new_geometry_group(name: str) -> bpy.types.GeometryNodeTree:
|
|
if name in bpy.data.node_groups:
|
|
bpy.data.node_groups.remove(bpy.data.node_groups[name])
|
|
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
|
|
group.is_modifier = True
|
|
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
|
|
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
|
return group
|
|
|
|
|
|
def _socket_by_name(sockets: Any, name: str):
|
|
for socket in sockets:
|
|
if socket.name == name:
|
|
return socket
|
|
raise KeyError(name)
|
|
|
|
|
|
def _set_inputs(group_node: bpy.types.GeometryNode, values: dict[str, object]) -> None:
|
|
for socket in group_node.inputs:
|
|
if socket.name not in values:
|
|
continue
|
|
value = values[socket.name]
|
|
if isinstance(value, list):
|
|
socket.default_value = value.pop(0)
|
|
else:
|
|
socket.default_value = value
|
|
|
|
|
|
def _build_helper_eval_wrapper(name: str, helper_group: bpy.types.GeometryNodeTree) -> bpy.types.GeometryNodeTree:
|
|
wrapper = _new_geometry_group(name)
|
|
nodes = wrapper.nodes
|
|
links = wrapper.links
|
|
|
|
group_output = nodes.new("NodeGroupOutput")
|
|
helper = nodes.new("GeometryNodeGroup")
|
|
helper.node_tree = helper_group
|
|
_set_inputs(helper, HELPER_INPUTS)
|
|
|
|
set_position = nodes.new("GeometryNodeSetPosition")
|
|
profile = nodes.new("GeometryNodeCurvePrimitiveCircle")
|
|
_socket_by_name(profile.inputs, "Resolution").default_value = 8
|
|
_socket_by_name(profile.inputs, "Radius").default_value = 0.01
|
|
curve_to_mesh = nodes.new("GeometryNodeCurveToMesh")
|
|
|
|
links.new(_socket_by_name(helper.outputs, "Curve"), _socket_by_name(set_position.inputs, "Geometry"))
|
|
links.new(_socket_by_name(helper.outputs, "Vector"), _socket_by_name(set_position.inputs, "Position"))
|
|
links.new(_socket_by_name(set_position.outputs, "Geometry"), _socket_by_name(curve_to_mesh.inputs, "Curve"))
|
|
links.new(_socket_by_name(profile.outputs, "Curve"), _socket_by_name(curve_to_mesh.inputs, "Profile Curve"))
|
|
links.new(_socket_by_name(curve_to_mesh.outputs, "Mesh"), _socket_by_name(group_output.inputs, "Geometry"))
|
|
return wrapper
|
|
|
|
|
|
def _build_group_eval_wrapper(
|
|
name: str,
|
|
graph_group: bpy.types.GeometryNodeTree,
|
|
inputs: dict[str, object],
|
|
) -> bpy.types.GeometryNodeTree:
|
|
wrapper = _new_geometry_group(name)
|
|
nodes = wrapper.nodes
|
|
links = wrapper.links
|
|
|
|
group_input = nodes.new("NodeGroupInput")
|
|
group_output = nodes.new("NodeGroupOutput")
|
|
graph = nodes.new("GeometryNodeGroup")
|
|
graph.node_tree = graph_group
|
|
_set_inputs(graph, inputs)
|
|
|
|
if any(socket.name == "Geometry" for socket in graph.inputs):
|
|
links.new(_socket_by_name(group_input.outputs, "Geometry"), _socket_by_name(graph.inputs, "Geometry"))
|
|
links.new(_socket_by_name(graph.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
|
|
return wrapper
|
|
|
|
|
|
def _evaluated_vertices(group: bpy.types.GeometryNodeTree) -> list[tuple[float, float, float]]:
|
|
mesh = bpy.data.meshes.new(group.name + " Input Mesh")
|
|
obj = bpy.data.objects.new(group.name + " Object", mesh)
|
|
bpy.context.collection.objects.link(obj)
|
|
modifier = obj.modifiers.new(group.name + " Modifier", "NODES")
|
|
modifier.node_group = group
|
|
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
evaluated = obj.evaluated_get(depsgraph)
|
|
evaluated_mesh = evaluated.to_mesh()
|
|
try:
|
|
return [tuple(vertex.co) for vertex in evaluated_mesh.vertices]
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
|
|
def _bounds(vertices: list[tuple[float, float, float]]) -> dict[str, list[float]]:
|
|
return {
|
|
"min": [min(vertex[i] for vertex in vertices) for i in range(3)],
|
|
"max": [max(vertex[i] for vertex in vertices) for i in range(3)],
|
|
}
|
|
|
|
|
|
def _compare(case: str, source_vertices, translated_vertices) -> dict[str, object]:
|
|
if len(source_vertices) != len(translated_vertices):
|
|
return {
|
|
"case": case,
|
|
"ok": False,
|
|
"reason": "vertex_count_mismatch",
|
|
"source_vertex_count": len(source_vertices),
|
|
"translated_vertex_count": len(translated_vertices),
|
|
"source_bounds": _bounds(source_vertices) if source_vertices else None,
|
|
"translated_bounds": _bounds(translated_vertices) if translated_vertices else None,
|
|
}
|
|
max_delta = 0.0
|
|
total_delta = 0.0
|
|
for source, translated in zip(source_vertices, translated_vertices):
|
|
delta = math.dist(source, translated)
|
|
max_delta = max(max_delta, delta)
|
|
total_delta += delta
|
|
sorted_source = sorted(source_vertices)
|
|
sorted_translated = sorted(translated_vertices)
|
|
max_sorted_delta = 0.0
|
|
total_sorted_delta = 0.0
|
|
for source, translated in zip(sorted_source, sorted_translated):
|
|
delta = math.dist(source, translated)
|
|
max_sorted_delta = max(max_sorted_delta, delta)
|
|
total_sorted_delta += delta
|
|
return {
|
|
"case": case,
|
|
"ok": max_delta <= 1e-6 or max_sorted_delta <= 1e-6,
|
|
"source_vertex_count": len(source_vertices),
|
|
"translated_vertex_count": len(translated_vertices),
|
|
"max_vertex_delta": max_delta,
|
|
"mean_vertex_delta": total_delta / len(source_vertices) if source_vertices else 0.0,
|
|
"max_sorted_vertex_delta": max_sorted_delta,
|
|
"mean_sorted_vertex_delta": total_sorted_delta / len(source_vertices) if source_vertices else 0.0,
|
|
"source_bounds": _bounds(source_vertices),
|
|
"translated_bounds": _bounds(translated_vertices),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
_load_translation()
|
|
results = []
|
|
|
|
source_helper = bpy.data.node_groups.get("NodeGroup.001")
|
|
translated_helper = bpy.data.node_groups.get("VG Cartesian Helper")
|
|
if source_helper is None or translated_helper is None:
|
|
raise RuntimeError("Source or translated helper group was not found.")
|
|
results.append(
|
|
_compare(
|
|
"helper",
|
|
_evaluated_vertices(_build_helper_eval_wrapper("VG Verify Source Helper", source_helper)),
|
|
_evaluated_vertices(_build_helper_eval_wrapper("VG Verify Translated Helper", translated_helper)),
|
|
)
|
|
)
|
|
|
|
for case in GRAPH_CASES:
|
|
source = bpy.data.node_groups.get(case["source"])
|
|
translated = bpy.data.node_groups.get(case["translated"])
|
|
if source is None or translated is None:
|
|
raise RuntimeError(f"Missing group for case {case['case']}: {case['source']} / {case['translated']}")
|
|
results.append(
|
|
_compare(
|
|
case["case"],
|
|
_evaluated_vertices(
|
|
_build_group_eval_wrapper("VG Verify Source " + case["case"], source, case["inputs"])
|
|
),
|
|
_evaluated_vertices(
|
|
_build_group_eval_wrapper("VG Verify Translated " + case["case"], translated, case["inputs"])
|
|
),
|
|
)
|
|
)
|
|
|
|
ok = all(result["ok"] for result in results)
|
|
print("VG_CARTESIAN_TRANSLATION_BEHAVIOR " + json.dumps({"ok": ok, "results": results}, sort_keys=True))
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|