#include "graph_exporter.h"
#include "../nodes/attribute_nodes.h"
#include "../nodes/curve_nodes.h"
#include "../nodes/geometry_nodes.h"
#include "../nodes/input_nodes.h"
#include "../nodes/instance_nodes.h"
#include "../nodes/math_nodes.h"
#include "../nodes/mesh_ops_nodes.h"
#include "../nodes/mesh_primitive_nodes.h"
#include "../nodes/noise_nodes.h"
#include "../nodes/point_nodes.h"
#include "../nodes/selection_nodes.h"
#include "../nodes/utility_nodes.h"
#include "../nodes/volume_nodes.h"
#include "../nodes/zone_nodes.h"
#include <godot_cpp/classes/json.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
namespace godot {
void GraphExporter::_bind_methods() {
ClassDB::bind_static_method("GraphExporter", D_METHOD("export_to_json", "graph"), &GraphExporter::export_to_json);
}
// Build the proc_type → Blender bl_type reverse map.
// Mirrors GEOMETRY_NODE_MAP from geometry_serializer.py.
static HashMap<StringName, String> _build_reverse_type_map() {
HashMap<StringName, String> m;
// Mesh Primitives
m["MeshGrid"] = "GeometryNodeMeshGrid";
// The SDF-grid family. Godot -> Blender must know these too, or the export
// side and the import side disagree about what crosses -- which is what
// test_node_maps asserts and what caught this.
m["MeshToSDFGrid"] = "GeometryNodeMeshToSDFGrid";
m["SDFGridFillet"] = "GeometryNodeSDFGridFillet";
m["GridToMesh"] = "GeometryNodeGridToMesh";
m["MeshCube"] = "GeometryNodeMeshCube";
m["MeshCircle"] = "GeometryNodeMeshCircle";
m["MeshCylinder"] = "GeometryNodeMeshCylinder";
m["MeshUVSphere"] = "GeometryNodeMeshUVSphere";
m["MeshIcoSphere"] = "GeometryNodeMeshIcoSphere";
m["MeshLine"] = "GeometryNodeMeshLine";
// Geometry Operations
m["SetPosition"] = "GeometryNodeSetPosition";
m["TransformGeometry"] = "GeometryNodeTransform";
m["JoinGeometry"] = "GeometryNodeJoinGeometry";
m["DeleteGeometry"] = "GeometryNodeDeleteGeometry";
m["MergeByDistance"] = "GeometryNodeMergeByDistance";
m["BoundingBox"] = "GeometryNodeBoundBox";
m["SeparateComponents"] = "GeometryNodeSeparateComponents";
m["SeparateGeometry"] = "GeometryNodeSeparateGeometry";
m["DuplicateElements"] = "GeometryNodeDuplicateElements";
// Mesh Operations
m["FlipFaces"] = "GeometryNodeFlipFaces";
m["SetShadeSmooth"] = "GeometryNodeSetShadeSmooth";
// SIMPLE subdivision, not Catmull-Clark. SubdivideMeshNode splits each
// triangle at its edge midpoints and moves nothing, which is exactly
// GeometryNodeSubdivideMesh; GeometryNodeSubdivisionSurface is a different
// operation and naming it here claimed a smoothing we do not do.
m["SubdivideMesh"] = "GeometryNodeSubdivideMesh";
// The graph's entry and exit. Both proc types have existed all along.
m["GroupInput"] = "NodeGroupInput";
m["GroupOutput"] = "NodeGroupOutput";
m["ExtrudeMesh"] = "GeometryNodeExtrudeMesh";
m["ScaleElements"] = "GeometryNodeScaleElements";
m["DualMesh"] = "GeometryNodeDualMesh";
// Curves
m["CurveCircle"] = "GeometryNodeCurvePrimitiveCircle";
m["CurveLine"] = "GeometryNodeCurvePrimitiveLine";
m["BezierSegment"] = "GeometryNodeCurvePrimitiveBezierSegment";
m["CurveToMesh"] = "GeometryNodeCurveToMesh";
m["CurveToPoints"] = "GeometryNodeCurveToPoints";
m["ResampleCurve"] = "GeometryNodeResampleCurve";
m["TrimCurve"] = "GeometryNodeTrimCurve";
// Points
m["DistributePointsOnFaces"] = "GeometryNodeDistributePointsOnFaces";
m["Points"] = "GeometryNodePoints";
// Instances
m["InstanceOnPoints"] = "GeometryNodeInstanceOnPoints";
m["RotateInstances"] = "GeometryNodeRotateInstances";
m["ScaleInstances"] = "GeometryNodeScaleInstances";
m["TranslateInstances"] = "GeometryNodeTranslateInstances";
m["RealizeInstances"] = "GeometryNodeRealizeInstances";
// Math
m["Math"] = "ShaderNodeMath";
m["VectorMath"] = "ShaderNodeVectorMath";
m["CombineXYZ"] = "ShaderNodeCombineXYZ";
m["SeparateXYZ"] = "ShaderNodeSeparateXYZ";
// Input
m["Value"] = "ShaderNodeValue";
m["Vector"] = "FunctionNodeInputVector";
// Noise / Texture
m["NoiseTexture"] = "ShaderNodeTexNoise";
m["VoronoiTexture"] = "ShaderNodeTexVoronoi";
m["WaveTexture"] = "ShaderNodeTexWave";
m["WhiteNoise"] = "ShaderNodeTexWhiteNoise";
m["GradientTexture"] = "ShaderNodeTexGradient";
// Attribute / Field inputs
m["Position"] = "GeometryNodeInputPosition";
m["Normal"] = "GeometryNodeInputNormal";
m["Index"] = "GeometryNodeInputIndex";
// Zones
m["RepeatZoneInput"] = "GeometryNodeRepeatInput";
m["RepeatZoneOutput"] = "GeometryNodeRepeatOutput";
m["SimulationZoneInput"] = "GeometryNodeSimulationInput";
m["SimulationZoneOutput"] = "GeometryNodeSimulationOutput";
m["ForeachZoneInput"] = "GeometryNodeForeachGeometryElementInput";
m["ForeachZoneOutput"] = "GeometryNodeForeachGeometryElementOutput";
// Field, utility and attribute layer -- mirrors the block added to
// GEOMETRY_NODE_MAP. Both maps must agree; test/test_node_maps.py checks it.
m["Compare"] = "FunctionNodeCompare";
m["RandomValue"] = "FunctionNodeRandomValue";
m["AccumulateField"] = "GeometryNodeAccumulateField";
m["CaptureAttribute"] = "GeometryNodeCaptureAttribute";
m["MeshBoolean"] = "GeometryNodeMeshBoolean";
m["SampleIndex"] = "GeometryNodeSampleIndex";
m["StoreNamedAttribute"] = "GeometryNodeStoreNamedAttribute";
m["Switch"] = "GeometryNodeSwitch";
m["Clamp"] = "ShaderNodeClamp";
m["FloatCurve"] = "ShaderNodeFloatCurve";
m["MapRange"] = "ShaderNodeMapRange";
m["Mix"] = "ShaderNodeMix";
m["DomainSize"] = "GeometryNodeAttributeDomainSize";
m["NamedAttribute"] = "GeometryNodeInputNamedAttribute";
m["RemoveNamedAttribute"] = "GeometryNodeRemoveAttribute";
m["ColorRamp"] = "ShaderNodeValToRGB";
m["RGBCurves"] = "ShaderNodeRGBCurve";
// Rotation and matrix family.
m["AxisAngleToRotation"] = "FunctionNodeAxisAngleToRotation";
m["EulerToRotation"] = "FunctionNodeEulerToRotation";
m["QuaternionToRotation"] = "FunctionNodeQuaternionToRotation";
m["RotationToAxisAngle"] = "FunctionNodeRotationToAxisAngle";
m["RotationToEuler"] = "FunctionNodeRotationToEuler";
m["RotationToQuaternion"] = "FunctionNodeRotationToQuaternion";
m["InvertRotation"] = "FunctionNodeInvertRotation";
m["RotateRotation"] = "FunctionNodeRotateRotation";
m["RotateVector"] = "FunctionNodeRotateVector";
m["InputRotation"] = "FunctionNodeInputRotation";
m["RotateEuler"] = "FunctionNodeRotateEuler";
m["VectorRotate"] = "ShaderNodeVectorRotate";
m["CombineMatrix"] = "FunctionNodeCombineMatrix";
m["SeparateMatrix"] = "FunctionNodeSeparateMatrix";
m["InvertMatrix"] = "FunctionNodeInvertMatrix";
m["MatrixDeterminant"] = "FunctionNodeMatrixDeterminant";
m["MatrixMultiply"] = "FunctionNodeMatrixMultiply";
m["TransposeMatrix"] = "FunctionNodeTransposeMatrix";
// Types that existed all along and were simply never mapped.
m["BooleanMath"] = "FunctionNodeBooleanMath";
m["ConvexHull"] = "GeometryNodeConvexHull";
m["EdgePathsToCurves"] = "GeometryNodeEdgePathsToCurves";
m["GeometryToInstance"] = "GeometryNodeGeometryToInstance";
m["PointsToVertices"] = "GeometryNodePointsToVertices";
m["SetCurveRadius"] = "GeometryNodeSetCurveRadius";
m["SplitEdges"] = "GeometryNodeSplitEdges";
m["Triangulate"] = "GeometryNodeTriangulate";
return m;
}
// Convert a Variant default value to a JSON-friendly Variant (arrays for Vector3/Color).
static Variant _default_to_json(const Variant &p_val, SocketType p_type) {
switch (p_type) {
case SocketType::Vector: {
if (p_val.get_type() == Variant::VECTOR3) {
Vector3 v = p_val;
Array a;
a.push_back(v.x);
a.push_back(v.y);
a.push_back(v.z);
return a;
}
return p_val;
}
case SocketType::Color: {
if (p_val.get_type() == Variant::COLOR) {
Color c = p_val;
Array a;
a.push_back(c.r);
a.push_back(c.g);
a.push_back(c.b);
a.push_back(c.a);
return a;
}
return p_val;
}
default:
return p_val;
}
}
// Extract node-specific properties into a Dictionary.
// This is the reverse of the property-application code in graph_importer.cpp.
static Dictionary _extract_properties(const Ref<ProcNode> &p_node) {
Dictionary props;
// --- operation property (MathNode, VectorMathNode, BooleanMathNode, CompareNode, MeshBooleanNode, VolumeBooleanNode) ---
{
MathNode *n = Object::cast_to<MathNode>(p_node.ptr());
if (n) { props["operation"] = n->get_operation(); }
}
if (!props.has("operation")) {
VectorMathNode *n = Object::cast_to<VectorMathNode>(p_node.ptr());
if (n) { props["operation"] = n->get_operation(); }
}
if (!props.has("operation")) {
BooleanMathNode *n = Object::cast_to<BooleanMathNode>(p_node.ptr());
if (n) { props["operation"] = n->get_operation(); }
}
if (!props.has("operation")) {
CompareNode *n = Object::cast_to<CompareNode>(p_node.ptr());
if (n) { props["operation"] = n->get_operation(); }
}
if (!props.has("operation")) {
MeshBooleanNode *n = Object::cast_to<MeshBooleanNode>(p_node.ptr());
if (n) { props["operation"] = n->get_operation(); }
}
if (!props.has("operation")) {
VolumeBooleanNode *n = Object::cast_to<VolumeBooleanNode>(p_node.ptr());
if (n) { props["operation"] = n->get_operation(); }
}
// --- value property (ValueNode, IntegerNode, BooleanNode, StringNode, ColorNode, VectorNode) ---
{
ValueNode *n = Object::cast_to<ValueNode>(p_node.ptr());
if (n) { props["value"] = n->get_value(); }
}
if (!props.has("value")) {
IntegerNode *n = Object::cast_to<IntegerNode>(p_node.ptr());
if (n) { props["value"] = n->get_value(); }
}
if (!props.has("value")) {
BooleanNode *n = Object::cast_to<BooleanNode>(p_node.ptr());
if (n) { props["value"] = n->get_value(); }
}
if (!props.has("value")) {
StringNode *n = Object::cast_to<StringNode>(p_node.ptr());
if (n) { props["value"] = Variant(n->get_value()); }
}
if (!props.has("value")) {
ColorNode *n = Object::cast_to<ColorNode>(p_node.ptr());
if (n) {
Color c = n->get_value();
Array a;
a.push_back(c.r);
a.push_back(c.g);
a.push_back(c.b);
a.push_back(c.a);
props["value"] = a;
}
}
if (!props.has("value")) {
VectorNode *n = Object::cast_to<VectorNode>(p_node.ptr());
if (n) {
Vector3 v = n->get_value();
Array a;
a.push_back(v.x);
a.push_back(v.y);
a.push_back(v.z);
props["value"] = a;
}
}
// --- zone_id property (all zone node types) ---
{
RepeatZoneInputNode *n = Object::cast_to<RepeatZoneInputNode>(p_node.ptr());
if (n) { props["zone_id"] = n->get_zone_id(); }
}
if (!props.has("zone_id")) {
RepeatZoneOutputNode *n = Object::cast_to<RepeatZoneOutputNode>(p_node.ptr());
if (n) { props["zone_id"] = n->get_zone_id(); }
}
if (!props.has("zone_id")) {
ForeachZoneInputNode *n = Object::cast_to<ForeachZoneInputNode>(p_node.ptr());
if (n) { props["zone_id"] = n->get_zone_id(); }
}
if (!props.has("zone_id")) {
ForeachZoneOutputNode *n = Object::cast_to<ForeachZoneOutputNode>(p_node.ptr());
if (n) { props["zone_id"] = n->get_zone_id(); }
}
if (!props.has("zone_id")) {
SimulationZoneInputNode *n = Object::cast_to<SimulationZoneInputNode>(p_node.ptr());
if (n) { props["zone_id"] = n->get_zone_id(); }
}
if (!props.has("zone_id")) {
SimulationZoneOutputNode *n = Object::cast_to<SimulationZoneOutputNode>(p_node.ptr());
if (n) { props["zone_id"] = n->get_zone_id(); }
}
// --- wave_type ---
{
WaveTextureNode *n = Object::cast_to<WaveTextureNode>(p_node.ptr());
if (n) { props["wave_type"] = n->get_wave_type(); }
}
// --- gradient_type ---
{
GradientTextureNode *n = Object::cast_to<GradientTextureNode>(p_node.ptr());
if (n) { props["gradient_type"] = n->get_gradient_type(); }
}
// --- Perlin noise properties (noise_type, noise_dimensions, normalize) ---
{
PerlinNoiseNode *n = Object::cast_to<PerlinNoiseNode>(p_node.ptr());
if (n) {
props["noise_type"] = n->get_noise_type();
props["noise_dimensions"] = n->get_noise_dimensions();
props["normalize"] = n->get_normalize() ? 1 : 0;
}
}
// --- Color Ramp's stops, and the three enums that read them ---
//
// The Godot -> Blender direction. Without this a ramp authored here reached
// Blender as whatever a fresh ShaderNodeValToRGB happens to be.
{
ColorRampNode *n = Object::cast_to<ColorRampNode>(p_node.ptr());
if (n) {
// Sent as an Array of numbers, which is what the JSON encoder and
// the Python side both expect; a PackedFloat32Array would serialize
// as a Godot-specific type.
PackedFloat32Array flat = n->get_ramp_stops();
Array stops;
stops.resize(flat.size());
for (int s = 0; s < flat.size(); s++) {
stops[s] = flat[s];
}
props["ramp_stops"] = stops;
props["interpolation"] = n->get_interpolation();
props["color_mode"] = n->get_color_mode();
props["hue_interpolation"] = n->get_hue_interpolation();
}
}
// --- Float Curve / RGB Curves control points, the Godot -> Blender way ---
{
auto emit_curve = [&props](auto *n) {
PackedFloat32Array pts = n->get_curve_points();
Array a_pts;
a_pts.resize(pts.size());
for (int i = 0; i < pts.size(); i++) {
a_pts[i] = pts[i];
}
props["curve_points"] = a_pts;
PackedInt32Array h = n->get_curve_handles();
Array a_h;
a_h.resize(h.size());
for (int i = 0; i < h.size(); i++) {
a_h[i] = h[i];
}
props["curve_handles"] = a_h;
PackedInt32Array c = n->get_curve_counts();
Array a_c;
a_c.resize(c.size());
for (int i = 0; i < c.size(); i++) {
a_c[i] = c[i];
}
props["curve_counts"] = a_c;
props["extend"] = n->get_extend();
props["use_clip"] = n->get_use_clip() ? 1 : 0;
PackedFloat32Array r = n->get_clip_rect();
Array a_r;
a_r.resize(r.size());
for (int i = 0; i < r.size(); i++) {
a_r[i] = r[i];
}
props["clip_rect"] = a_r;
};
FloatCurveNode *fc = Object::cast_to<FloatCurveNode>(p_node.ptr());
if (fc) {
emit_curve(fc);
}
RGBCurvesNode *rc = Object::cast_to<RGBCurvesNode>(p_node.ptr());
if (rc) {
emit_curve(rc);
}
}
// DisplaceNode: no properties to extract (always Perlin fBM)
return props;
}
String GraphExporter::export_to_json(const Ref<ProcNodeGraph> &p_graph) {
if (p_graph.is_null()) {
UtilityFunctions::push_error("GraphExporter: graph is null");
return "";
}
static HashMap<StringName, String> reverse_map = _build_reverse_type_map();
const Vector<Ref<ProcNode>> &nodes = p_graph->get_nodes();
const Vector<ProcConnection> &connections = p_graph->get_connections();
// --- Nodes ---
Array nodes_arr;
for (int i = 0; i < nodes.size(); i++) {
const Ref<ProcNode> &node = nodes[i];
if (node.is_null()) {
continue;
}
Dictionary node_dict;
node_dict["id"] = i;
StringName proc_type = node->get_node_type();
node_dict["proc_type"] = String(proc_type);
// Reverse-map to Blender type
if (reverse_map.has(proc_type)) {
node_dict["bl_type"] = reverse_map[proc_type];
} else {
node_dict["bl_type"] = ""; // Godot-only node, no Blender equivalent
}
node_dict["label"] = node->get_node_title();
// Position: Godot Y-down → Blender Y-up (negate Y)
Vector2 pos = node->get_graph_position();
Array pos_arr;
pos_arr.push_back(pos.x);
pos_arr.push_back(-pos.y);
node_dict["position"] = pos_arr;
// Properties
Dictionary props = _extract_properties(node);
node_dict["properties"] = props;
// Export ALL defaults (not just modified ones) because Blender's
// constructor defaults may differ from Godot's
Dictionary defaults;
for (int j = 0; j < node->get_input_count(); j++) {
const SocketDef &def = node->get_input_def(j);
if (def.default_value.get_type() != Variant::NIL) {
defaults[String::num_int64(j)] = _default_to_json(def.default_value, def.type);
}
}
node_dict["defaults"] = defaults;
// Socket NAMES, so the far side can resolve a socket by name instead of
// trusting its index. An index does not identify a socket across the
// bridge: an audit of the 53 node types mapped to Blender found 27 whose
// layouts do not line up, most because Blender carries a Selection
// socket at index 1 that proc-nodes has no equivalent for. SetPosition
// is the plain case -- ours is (Geometry, Offset), Blender's is
// (Geometry, Selection, Position, Offset), so our index 1 is its
// Selection.
Array inputs_arr;
for (int j = 0; j < node->get_input_count(); j++) {
const SocketDef &def = node->get_input_def(j);
Dictionary sd;
sd["name"] = String(def.name);
sd["type"] = String(socket_type_name(def.type));
inputs_arr.push_back(sd);
}
node_dict["inputs"] = inputs_arr;
Array outputs_arr;
for (int j = 0; j < node->get_output_count(); j++) {
const SocketDef &def = node->get_output_def(j);
Dictionary sd;
sd["name"] = String(def.name);
sd["type"] = String(socket_type_name(def.type));
outputs_arr.push_back(sd);
}
node_dict["outputs"] = outputs_arr;
nodes_arr.push_back(node_dict);
}
// --- Connections ---
Array conns_arr;
for (int i = 0; i < connections.size(); i++) {
const ProcConnection &conn = connections[i];
Dictionary conn_dict;
conn_dict["from_node"] = conn.from_node;
conn_dict["from_port"] = conn.from_socket;
conn_dict["to_node"] = conn.to_node;
conn_dict["to_port"] = conn.to_socket;
conns_arr.push_back(conn_dict);
}
// --- Root dictionary ---
Dictionary root;
// 2: nodes carry "inputs"/"outputs" socket names. A reader that predates
// this still works -- it just goes on resolving by index, which is what
// version 1 meant.
root["version"] = 2;
root["nodes"] = nodes_arr;
root["connections"] = conns_arr;
String result = JSON::stringify(root);
UtilityFunctions::print("GraphExporter: Exported ", nodes_arr.size(), " nodes, ",
conns_arr.size(), " connections");
return result;
}
} // namespace godot