## Evaluates proc-nodes value nodes from a JSON spec, for cross-engine parity tests.
##
## Reads a spec, builds each graph, evaluates the requested output sockets via
## ProcNodeGraph.evaluate_socket, and writes the results as flat float arrays so
## the other side can compare numbers without knowing Godot's types.
##
## Quaternion -> [x, y, z, w] Projection -> 16 floats, column-major
## Vector3 -> [x, y, z] Color -> [r, g, b, a]
## float/int/bool -> [v]
##
## Usage: godot --headless --path demo --script res://parity_driver.gd -- <spec> <out>
extends SceneTree
func _flatten(v: Variant) -> Array:
match typeof(v):
TYPE_QUATERNION:
return [v.x, v.y, v.z, v.w]
TYPE_VECTOR3:
return [v.x, v.y, v.z]
TYPE_COLOR:
return [v.r, v.g, v.b, v.a]
TYPE_PROJECTION:
var out := []
for c in range(4):
var col: Vector4 = v[c]
for r in range(4):
out.append(col[r])
return out
TYPE_BOOL:
return [1.0 if v else 0.0]
TYPE_INT, TYPE_FLOAT:
return [float(v)]
return []
## Convert a JSON default to the socket's DECLARED type.
##
## Guessing from the array length cannot work: a Color and a Rotation are both
## four floats, and a Vector and a Color are told apart only by what the socket
## says it is. Keying on the length silently handed a Color socket a raw Array,
## which reads back as black -- so every colour case compared black against
## Blender and failed for a reason that had nothing to do with the node.
func _to_default(v: Variant, socket_type: int) -> Variant:
if typeof(v) != TYPE_ARRAY:
return v
match socket_type:
3: # Vector
if v.size() >= 3:
return Vector3(v[0], v[1], v[2])
4: # Color
if v.size() >= 4:
return Color(v[0], v[1], v[2], v[3])
if v.size() == 3:
return Color(v[0], v[1], v[2], 1.0)
8: # Rotation
if v.size() >= 4:
return Quaternion(v[0], v[1], v[2], v[3])
if v.size() == 3:
return Vector3(v[0], v[1], v[2])
return v
func _init() -> void:
var args := OS.get_cmdline_user_args()
if args.size() < 2:
printerr("need <spec.json> <out.json>")
quit(1)
return
var spec: Array = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
var results := {}
for case in spec:
var g := ProcNodeGraph.new()
var idx := []
for nd in case["nodes"]:
var obj = ClassDB.instantiate(nd["cls"])
if obj == null:
printerr("cannot instantiate ", nd["cls"])
quit(1)
return
var i: int = g.add_node(obj)
idx.append(i)
# Node PROPERTIES -- gradient_type, operation, input_type. A node
# whose behaviour is chosen by a property cannot be parity-checked
# without them, and the driver used to carry defaults only.
for pk in nd.get("props", {}).keys():
obj.set(pk, nd["props"][pk])
for k in nd.get("defaults", {}).keys():
var si := int(k)
g.get_node(i).set_input_default(
si, _to_default(nd["defaults"][k], g.get_node(i).get_input_type(si)))
for l in case.get("links", []):
g.connect_sockets(idx[int(l[0])], int(l[1]), idx[int(l[2])], int(l[3]))
var vals := []
for rd in case["read"]:
vals.append_array(_flatten(g.evaluate_socket(idx[int(rd[0])], int(rd[1]))))
results[case["name"]] = vals
var f := FileAccess.open(args[1], FileAccess.WRITE)
f.store_string(JSON.stringify(results))
f.close()
print("PARITY_DRIVER wrote ", results.size(), " cases")
quit()