OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / graph / graph_importer.cpp

941 lines · cpp ref:
#include "graph_importer.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/rotation_nodes.h"

// Blender's euler is XYZ, which is godot-cpp's ZYX -- see rotation_nodes.cpp.
#define PN_EULER_ORDER_IMPORT EulerOrder::EULER_ORDER_ZYX
#include "../nodes/selection_nodes.h"
#include "../nodes/utility_nodes.h"
#include "../nodes/volume_nodes.h"
#include "../nodes/volume_grid_nodes.h"
#include "../nodes/zone_nodes.h"

#include <godot_cpp/classes/json.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

namespace godot {

void GraphImporter::_bind_methods() {
	ClassDB::bind_static_method("GraphImporter", D_METHOD("import_from_json", "json"), &GraphImporter::import_from_json);
}

// Factory: create a ProcNode by its type name (matching get_node_type() return values)
static Ref<ProcNode> _create_node_by_type(const String &p_type) {
	// Mesh Primitives
	if (p_type == "MeshGrid") return Ref<ProcNode>(memnew(MeshGridNode));
	if (p_type == "MeshCube") return Ref<ProcNode>(memnew(MeshCubeNode));
	if (p_type == "MeshCircle") return Ref<ProcNode>(memnew(MeshCircleNode));
	if (p_type == "MeshCylinder") return Ref<ProcNode>(memnew(MeshCylinderNode));
	if (p_type == "MeshUVSphere") return Ref<ProcNode>(memnew(MeshUVSphereNode));
	if (p_type == "MeshIcoSphere") return Ref<ProcNode>(memnew(MeshIcoSphereNode));
	if (p_type == "MeshLine") return Ref<ProcNode>(memnew(MeshLineNode));

	// Geometry Operations
	if (p_type == "TransformGeometry") return Ref<ProcNode>(memnew(TransformGeometryNode));
	if (p_type == "SetPosition") return Ref<ProcNode>(memnew(SetPositionNode));
	if (p_type == "JoinGeometry") return Ref<ProcNode>(memnew(JoinGeometryNode));
	if (p_type == "DeleteGeometry") return Ref<ProcNode>(memnew(DeleteGeometryNode));
	if (p_type == "MergeByDistance") return Ref<ProcNode>(memnew(MergeByDistanceNode));
	if (p_type == "BoundingBox") return Ref<ProcNode>(memnew(BoundingBoxNode));
	if (p_type == "SeparateComponents") return Ref<ProcNode>(memnew(SeparateComponentsNode));
	if (p_type == "SeparateGeometry") return Ref<ProcNode>(memnew(SeparateGeometryNode));
	if (p_type == "ConvexHull") return Ref<ProcNode>(memnew(ConvexHullNode));
	if (p_type == "GeometryToInstance") return Ref<ProcNode>(memnew(GeometryToInstanceNode));
	if (p_type == "Viewer") return Ref<ProcNode>(memnew(ViewerNode));

	// Mesh Operations
	if (p_type == "FlipFaces") return Ref<ProcNode>(memnew(FlipFacesNode));
	if (p_type == "SetShadeSmooth") return Ref<ProcNode>(memnew(SetShadeSmoothNode));
	if (p_type == "SubdivideMesh") return Ref<ProcNode>(memnew(SubdivideMeshNode));
	if (p_type == "ExtrudeMesh") return Ref<ProcNode>(memnew(ExtrudeMeshNode));
	if (p_type == "ScaleElements") return Ref<ProcNode>(memnew(ScaleElementsNode));
	if (p_type == "DualMesh") return Ref<ProcNode>(memnew(DualMeshNode));
	if (p_type == "DuplicateElements") return Ref<ProcNode>(memnew(DuplicateElementsNode));
	if (p_type == "MeshBoolean") return Ref<ProcNode>(memnew(MeshBooleanNode));
	if (p_type == "SplitEdges") return Ref<ProcNode>(memnew(SplitEdgesNode));
	if (p_type == "Triangulate") return Ref<ProcNode>(memnew(TriangulateNode));
	if (p_type == "EdgePathsToCurves") return Ref<ProcNode>(memnew(EdgePathsToCurvesNode));

	// Curves
	if (p_type == "CurveCircle") return Ref<ProcNode>(memnew(CurveCircleNode));
	if (p_type == "CurveLine") return Ref<ProcNode>(memnew(CurveLineNode));
	if (p_type == "BezierSegment") return Ref<ProcNode>(memnew(BezierSegmentNode));
	if (p_type == "CurveToMesh") return Ref<ProcNode>(memnew(CurveToMeshNode));
	if (p_type == "CurveToPoints") return Ref<ProcNode>(memnew(CurveToPointsNode));
	if (p_type == "ResampleCurve") return Ref<ProcNode>(memnew(ResampleCurveNode));
	if (p_type == "TrimCurve") return Ref<ProcNode>(memnew(TrimCurveNode));

	// Points
	if (p_type == "Points") return Ref<ProcNode>(memnew(PointsNode));
	if (p_type == "DistributePointsOnFaces") return Ref<ProcNode>(memnew(DistributePointsOnFacesNode));

	// Instances
	if (p_type == "InstanceOnPoints") return Ref<ProcNode>(memnew(InstanceOnPointsNode));
	if (p_type == "RotateInstances") return Ref<ProcNode>(memnew(RotateInstancesNode));
	if (p_type == "ScaleInstances") return Ref<ProcNode>(memnew(ScaleInstancesNode));
	if (p_type == "TranslateInstances") return Ref<ProcNode>(memnew(TranslateInstancesNode));
	if (p_type == "RealizeInstances") return Ref<ProcNode>(memnew(RealizeInstancesNode));

	// Math
	if (p_type == "Math") return Ref<ProcNode>(memnew(MathNode));
	if (p_type == "VectorMath") return Ref<ProcNode>(memnew(VectorMathNode));
	if (p_type == "CombineXYZ") return Ref<ProcNode>(memnew(CombineXYZNode));
	if (p_type == "SeparateXYZ") return Ref<ProcNode>(memnew(SeparateXYZNode));
	if (p_type == "BooleanMath") return Ref<ProcNode>(memnew(BooleanMathNode));
	if (p_type == "Compare") return Ref<ProcNode>(memnew(CompareNode));
	if (p_type == "Clamp") return Ref<ProcNode>(memnew(ClampNode));
	if (p_type == "MapRange") return Ref<ProcNode>(memnew(MapRangeNode));
	if (p_type == "Mix") return Ref<ProcNode>(memnew(MixNode));
	if (p_type == "ColorRamp") return Ref<ProcNode>(memnew(ColorRampNode));
	if (p_type == "FloatCurve") return Ref<ProcNode>(memnew(FloatCurveNode));
	if (p_type == "RGBCurves") return Ref<ProcNode>(memnew(RGBCurvesNode));

	// Input
	if (p_type == "Value") return Ref<ProcNode>(memnew(ValueNode));
	if (p_type == "Vector") return Ref<ProcNode>(memnew(VectorNode));
	if (p_type == "Integer") return Ref<ProcNode>(memnew(IntegerNode));
	if (p_type == "Boolean") return Ref<ProcNode>(memnew(BooleanNode));
	if (p_type == "Color") return Ref<ProcNode>(memnew(ColorNode));
	if (p_type == "String") return Ref<ProcNode>(memnew(StringNode));
	if (p_type == "ObjectInfo") return Ref<ProcNode>(memnew(ObjectInfoNode));
	if (p_type == "Position") return Ref<ProcNode>(memnew(PositionNode));
	if (p_type == "Normal") return Ref<ProcNode>(memnew(NormalNode));
	if (p_type == "Index") return Ref<ProcNode>(memnew(IndexNode));

	// Noise
	if (p_type == "NoiseTexture") return Ref<ProcNode>(memnew(PerlinNoiseNode));
	if (p_type == "SimplexNoise") return Ref<ProcNode>(memnew(NoiseTextureNode));
	if (p_type == "Displace") return Ref<ProcNode>(memnew(DisplaceNode));
	if (p_type == "VoronoiTexture") return Ref<ProcNode>(memnew(VoronoiTextureNode));
	if (p_type == "WaveTexture") return Ref<ProcNode>(memnew(WaveTextureNode));
	if (p_type == "WhiteNoise") return Ref<ProcNode>(memnew(WhiteNoiseNode));
	if (p_type == "GradientTexture") return Ref<ProcNode>(memnew(GradientTextureNode));
	if (p_type == "GradientNoise") return Ref<ProcNode>(memnew(GradientNoiseNode));
	if (p_type == "FractalTerrain") return Ref<ProcNode>(memnew(FractalTerrainNode));
	if (p_type == "UV") return Ref<ProcNode>(memnew(UVNode));
	if (p_type == "ErosionNoise") return Ref<ProcNode>(memnew(ErosionNoiseNode));
	if (p_type == "AltitudeColor") return Ref<ProcNode>(memnew(AltitudeColorNode));
	if (p_type == "SlopeGradient") return Ref<ProcNode>(memnew(SlopeGradientNode));

	// Selection/Utility
	if (p_type == "Switch") return Ref<ProcNode>(memnew(SwitchNode));
	if (p_type == "RandomValue") return Ref<ProcNode>(memnew(RandomValueNode));

	// Attributes
	if (p_type == "StoreNamedAttribute") return Ref<ProcNode>(memnew(StoreNamedAttributeNode));
	if (p_type == "NamedAttribute") return Ref<ProcNode>(memnew(NamedAttributeNode));
	if (p_type == "CaptureAttribute") return Ref<ProcNode>(memnew(CaptureAttributeNode));
	if (p_type == "RemoveNamedAttribute") return Ref<ProcNode>(memnew(RemoveNamedAttributeNode));
	if (p_type == "DomainSize") return Ref<ProcNode>(memnew(DomainSizeNode));

	// Points (additional)
	if (p_type == "PointsToVertices") return Ref<ProcNode>(memnew(PointsToVerticesNode));

	// Curves (additional)
	if (p_type == "SetCurveRadius") return Ref<ProcNode>(memnew(SetCurveRadiusNode));

	// Input (additional)
	if (p_type == "GroupInput") return Ref<ProcNode>(memnew(GroupInputNode));
	if (p_type == "GroupOutput") return Ref<ProcNode>(memnew(GroupOutputNode));

	// Zones
	if (p_type == "RepeatZoneInput") return Ref<ProcNode>(memnew(RepeatZoneInputNode));
	if (p_type == "RepeatZoneOutput") return Ref<ProcNode>(memnew(RepeatZoneOutputNode));
	if (p_type == "ForeachZoneInput") return Ref<ProcNode>(memnew(ForeachZoneInputNode));
	if (p_type == "ForeachZoneOutput") return Ref<ProcNode>(memnew(ForeachZoneOutputNode));
	if (p_type == "SimulationZoneInput") return Ref<ProcNode>(memnew(SimulationZoneInputNode));
	if (p_type == "SimulationZoneOutput") return Ref<ProcNode>(memnew(SimulationZoneOutputNode));

	// Volume
	if (p_type == "SphereSDF") return Ref<ProcNode>(memnew(SphereSdfNode));
	if (p_type == "VolumeBoolean") return Ref<ProcNode>(memnew(VolumeBooleanNode));
	if (p_type == "VolumeToMesh") return Ref<ProcNode>(memnew(VolumeToMeshNode));
	if (p_type == "MeshToSDFGrid") return Ref<ProcNode>(memnew(MeshToSDFGridNode));
	if (p_type == "SDFGridFillet") return Ref<ProcNode>(memnew(SDFGridFilletNode));
	if (p_type == "GridToMesh") return Ref<ProcNode>(memnew(GridToMeshNode));

	// Utility
	if (p_type == "AccumulateField") return Ref<ProcNode>(memnew(AccumulateFieldNode));
	if (p_type == "SampleIndex") return Ref<ProcNode>(memnew(SampleIndexNode));

	// Rotation and matrix family. Registered, and mapped on the Blender side,
	// since the socket types went in -- but never added HERE, so every one of
	// the 18 was dropped on import along with the links either side of it. The
	// mountain's cell-space chain died exactly there.
	if (p_type == "AxisAngleToRotation") return Ref<ProcNode>(memnew(AxisAngleToRotationNode));
	if (p_type == "EulerToRotation") return Ref<ProcNode>(memnew(EulerToRotationNode));
	if (p_type == "QuaternionToRotation") return Ref<ProcNode>(memnew(QuaternionToRotationNode));
	if (p_type == "RotationToAxisAngle") return Ref<ProcNode>(memnew(RotationToAxisAngleNode));
	if (p_type == "RotationToEuler") return Ref<ProcNode>(memnew(RotationToEulerNode));
	if (p_type == "RotationToQuaternion") return Ref<ProcNode>(memnew(RotationToQuaternionNode));
	if (p_type == "InvertRotation") return Ref<ProcNode>(memnew(InvertRotationNode));
	if (p_type == "RotateRotation") return Ref<ProcNode>(memnew(RotateRotationNode));
	if (p_type == "RotateVector") return Ref<ProcNode>(memnew(RotateVectorNode));
	if (p_type == "InputRotation") return Ref<ProcNode>(memnew(InputRotationNode));
	if (p_type == "RotateEuler") return Ref<ProcNode>(memnew(RotateEulerNode));
	if (p_type == "VectorRotate") return Ref<ProcNode>(memnew(VectorRotateNode));
	if (p_type == "CombineMatrix") return Ref<ProcNode>(memnew(CombineMatrixNode));
	if (p_type == "SeparateMatrix") return Ref<ProcNode>(memnew(SeparateMatrixNode));
	if (p_type == "InvertMatrix") return Ref<ProcNode>(memnew(InvertMatrixNode));
	if (p_type == "MatrixDeterminant") return Ref<ProcNode>(memnew(MatrixDeterminantNode));
	if (p_type == "MatrixMultiply") return Ref<ProcNode>(memnew(MatrixMultiplyNode));
	if (p_type == "TransposeMatrix") return Ref<ProcNode>(memnew(TransposeMatrixNode));

	UtilityFunctions::push_warning("GraphImporter: Unknown node type: ", p_type);
	return Ref<ProcNode>();
}

// Apply zone_id to zone nodes via their typed setter
static void _apply_zone_id(const Ref<ProcNode> &p_node, int p_zone_id) {
	// Try each zone node type
	{
		RepeatZoneInputNode *n = Object::cast_to<RepeatZoneInputNode>(p_node.ptr());
		if (n) { n->set_zone_id(p_zone_id); return; }
	}
	{
		RepeatZoneOutputNode *n = Object::cast_to<RepeatZoneOutputNode>(p_node.ptr());
		if (n) { n->set_zone_id(p_zone_id); return; }
	}
	{
		ForeachZoneInputNode *n = Object::cast_to<ForeachZoneInputNode>(p_node.ptr());
		if (n) { n->set_zone_id(p_zone_id); return; }
	}
	{
		ForeachZoneOutputNode *n = Object::cast_to<ForeachZoneOutputNode>(p_node.ptr());
		if (n) { n->set_zone_id(p_zone_id); return; }
	}
	{
		SimulationZoneInputNode *n = Object::cast_to<SimulationZoneInputNode>(p_node.ptr());
		if (n) { n->set_zone_id(p_zone_id); return; }
	}
	{
		SimulationZoneOutputNode *n = Object::cast_to<SimulationZoneOutputNode>(p_node.ptr());
		if (n) { n->set_zone_id(p_zone_id); return; }
	}
}

// Apply default values from JSON to node input sockets
static void _apply_defaults(const Ref<ProcNode> &p_node, const Dictionary &p_defaults) {
	Array keys = p_defaults.keys();
	for (int k = 0; k < keys.size(); k++) {
		String idx_str = keys[k];
		int idx = idx_str.to_int();
		if (idx < 0 || idx >= p_node->get_input_count()) {
			continue;
		}

		Variant val = p_defaults[idx_str];
		SocketType sock_type = p_node->get_input_def(idx).type;

		// Convert JSON arrays to Vector3/Color as needed
		if (sock_type == SocketType::Vector && val.get_type() == Variant::ARRAY) {
			Array arr = val;
			if (arr.size() >= 3) {
				p_node->set_input_default(idx, Vector3((float)arr[0], (float)arr[1], (float)arr[2]));
				continue;
			}
		} else if (sock_type == SocketType::Color && val.get_type() == Variant::ARRAY) {
			Array arr = val;
			if (arr.size() >= 4) {
				p_node->set_input_default(idx, Color((float)arr[0], (float)arr[1], (float)arr[2], (float)arr[3]));
				continue;
			} else if (arr.size() >= 3) {
				p_node->set_input_default(idx, Color((float)arr[0], (float)arr[1], (float)arr[2], 1.0f));
				continue;
			}
		}

		// Scalar types: pass through directly
		p_node->set_input_default(idx, val);
	}
}

Ref<ProcNodeGraph> GraphImporter::import_from_json(const String &p_json) {
	// Parse JSON
	Ref<JSON> json;
	json.instantiate();
	Error err = json->parse(p_json);
	if (err != OK) {
		UtilityFunctions::push_error("GraphImporter: JSON parse error: ", json->get_error_message());
		return Ref<ProcNodeGraph>();
	}

	Variant parsed = json->get_data();
	if (parsed.get_type() != Variant::DICTIONARY) {
		UtilityFunctions::push_error("GraphImporter: Root JSON value is not a dictionary");
		return Ref<ProcNodeGraph>();
	}

	Dictionary data = parsed;
	if (!data.has("nodes") || !data.has("connections")) {
		UtilityFunctions::push_error("GraphImporter: Missing 'nodes' or 'connections' in JSON");
		return Ref<ProcNodeGraph>();
	}

	Ref<ProcNodeGraph> graph;
	graph.instantiate();
	// EVERYTHING THAT ARRIVES THROUGH HERE WAS AUTHORED IN BLENDER, so its
	// arithmetic is in Blender's frame and the result is converted once at the
	// end. Nothing else in the engine changes.
	graph->set_source_frame(1);

	Array nodes_arr = data["nodes"];
	Array conns_arr = data["connections"];

	// Map JSON node ID -> graph node index
	HashMap<int, int> id_to_idx;

	int skipped = 0;

	// Create nodes
	for (int i = 0; i < nodes_arr.size(); i++) {
		Dictionary node_data = nodes_arr[i];

		int json_id = node_data.get("id", -1);
		Variant proc_type_var = node_data.get("proc_type", Variant());

		// Skip unmapped nodes (proc_type is null/empty)
		if (proc_type_var.get_type() == Variant::NIL) {
			String bl_type = node_data.get("bl_type", "unknown");
			UtilityFunctions::push_warning("GraphImporter: Skipping unmapped Blender node: ", bl_type);
			skipped++;
			continue;
		}

		String proc_type = proc_type_var;
		if (proc_type.is_empty()) {
			skipped++;
			continue;
		}

		Ref<ProcNode> node = _create_node_by_type(proc_type);
		if (node.is_null()) {
			skipped++;
			continue;
		}

		// Set node-specific properties
		if (node_data.has("properties")) {
			Dictionary props = node_data["properties"];

			if (props.has("operation")) {
				// MathNode, VectorMathNode, BooleanMathNode, or CompareNode
				MathNode *math = Object::cast_to<MathNode>(node.ptr());
				if (math) {
					math->set_operation((int)props["operation"]);
				} else {
					VectorMathNode *vmath = Object::cast_to<VectorMathNode>(node.ptr());
					if (vmath) {
						vmath->set_operation((int)props["operation"]);
					} else {
						BooleanMathNode *bmath = Object::cast_to<BooleanMathNode>(node.ptr());
						if (bmath) {
							bmath->set_operation((int)props["operation"]);
						} else {
							CompareNode *cmp = Object::cast_to<CompareNode>(node.ptr());
							if (cmp) {
								cmp->set_operation((int)props["operation"]);
							} else {
								MeshBooleanNode *mbool = Object::cast_to<MeshBooleanNode>(node.ptr());
								if (mbool) {
									mbool->set_operation((int)props["operation"]);
								}
							}
						}
					}
				}
			}

			// The Input* nodes carry their value as a node PROPERTY, so it
			// arrives here rather than in `defaults`.
			if (props.has("value")) {
				VectorNode *vec_node = Object::cast_to<VectorNode>(node.ptr());
				if (vec_node) {
					Variant v = props["value"];
					if (v.get_type() == Variant::ARRAY) {
						Array arr = v;
						if (arr.size() >= 3) {
							vec_node->set_value(Vector3((float)arr[0], (float)arr[1], (float)arr[2]));
						}
					} else if (v.get_type() == Variant::VECTOR3) {
						vec_node->set_value(v);
					}
				}
				InputRotationNode *rot_node = Object::cast_to<InputRotationNode>(node.ptr());
				if (rot_node) {
					Variant v = props["value"];
					if (v.get_type() == Variant::ARRAY) {
						Array arr = v;
						if (arr.size() >= 3) {
							// Blender stores this one as an EULER; proc-nodes
							// keeps rotations as quaternions.
							rot_node->set_rotation(Basis::from_euler(
									Vector3((float)arr[0], (float)arr[1], (float)arr[2]),
									PN_EULER_ORDER_IMPORT).get_rotation_quaternion());
						}
					}
				}
			}

			if (props.has("value")) {
				ValueNode *val_node = Object::cast_to<ValueNode>(node.ptr());
				if (val_node) {
					val_node->set_value((float)props["value"]);
				} else {
					IntegerNode *int_node = Object::cast_to<IntegerNode>(node.ptr());
					if (int_node) {
						int_node->set_value((int)props["value"]);
					} else {
						BooleanNode *bool_node = Object::cast_to<BooleanNode>(node.ptr());
						if (bool_node) {
							bool_node->set_value((bool)props["value"]);
						} else {
							StringNode *str_node = Object::cast_to<StringNode>(node.ptr());
							if (str_node) {
								str_node->set_value(props["value"]);
							} else {
								ColorNode *color_node = Object::cast_to<ColorNode>(node.ptr());
								if (color_node) {
									Variant val = props["value"];
									if (val.get_type() == Variant::ARRAY) {
										Array arr = val;
										if (arr.size() >= 4) {
											color_node->set_value(Color((float)arr[0], (float)arr[1], (float)arr[2], (float)arr[3]));
										} else if (arr.size() >= 3) {
											color_node->set_value(Color((float)arr[0], (float)arr[1], (float)arr[2], 1.0f));
										}
									} else if (val.get_type() == Variant::COLOR) {
										color_node->set_value(val);
									}
								}
							}
						}
					}
				}
			}

			{
				MixNode *mx = Object::cast_to<MixNode>(node.ptr());
				if (mx) {
					if (props.has("data_type")) {
						mx->set_data_type((int)props["data_type"]);
					}
					if (props.has("blend_type")) {
						mx->set_blend_type((int)props["blend_type"]);
					}
					if (props.has("factor_mode")) {
						mx->set_factor_mode((int)props["factor_mode"]);
					}
					if (props.has("clamp_factor")) {
						mx->set_clamp_factor((bool)(int)props["clamp_factor"]);
					}
					if (props.has("clamp_result")) {
						mx->set_clamp_result((bool)(int)props["clamp_result"]);
					}
				}
			}

			{
				MapRangeNode *mr = Object::cast_to<MapRangeNode>(node.ptr());
				if (mr) {
					if (props.has("data_type")) {
						mr->set_data_type((int)props["data_type"]);
					}
					if (props.has("interpolation_type")) {
						mr->set_interpolation_type((int)props["interpolation_type"]);
					}
					if (props.has("clamp")) {
						mr->set_clamp((bool)(int)props["clamp"]);
					}
				}
			}

			{
				VoronoiTextureNode *vt = Object::cast_to<VoronoiTextureNode>(node.ptr());
				if (vt) {
					if (props.has("voronoi_dimensions")) {
						vt->set_voronoi_dimensions((int)props["voronoi_dimensions"]);
					}
					if (props.has("feature")) {
						vt->set_feature((int)props["feature"]);
					}
					if (props.has("metric")) {
						vt->set_metric((int)props["metric"]);
					}
					if (props.has("normalize")) {
						vt->set_normalize((bool)(int)props["normalize"]);
					}
				}
			}

			if (props.has("noise_dimensions")) {
				WhiteNoiseNode *wn = Object::cast_to<WhiteNoiseNode>(node.ptr());
				if (wn) {
					wn->set_noise_dimensions((int)props["noise_dimensions"]);
				}
			}

			// Blender's Switch is TYPED. Without this it stays a geometry
			// switch, and a Float switch delivers nothing through it.
			if (props.has("input_type")) {
				SwitchNode *sw = Object::cast_to<SwitchNode>(node.ptr());
				if (sw) {
					sw->set_input_type((int)props["input_type"]);
				}
			}

			// THE FILL OF A CIRCLE OR A CYLINDER DECIDES ITS TOPOLOGY: None
			// is an edge ring with no faces, N-gon one face, Triangle Fan a fan
			// around a centre vertex the mesh does not otherwise have. Blender
			// carries it as a property and it was neither serialized nor
			// applied, so a bridged circle took whatever default this node had
			// -- an N-gon circle arriving as a 6-vertex fan.
			if (props.has("fill_type")) {
				MeshCircleNode *circle = Object::cast_to<MeshCircleNode>(node.ptr());
				if (circle) {
					circle->set_fill_type((int)props["fill_type"]);
				} else {
					MeshCylinderNode *cyl = Object::cast_to<MeshCylinderNode>(node.ptr());
					if (cyl) {
						cyl->set_fill_type((int)props["fill_type"]);
					}
				}
			}

			if (props.has("zone_id")) {
				_apply_zone_id(node, (int)props["zone_id"]);
			}

			{
				WaveTextureNode *wave = Object::cast_to<WaveTextureNode>(node.ptr());
				if (wave) {
					if (props.has("wave_type")) {
						wave->set_wave_type((int)props["wave_type"]);
					}
					if (props.has("bands_direction")) {
						wave->set_bands_direction((int)props["bands_direction"]);
					}
					if (props.has("rings_direction")) {
						wave->set_rings_direction((int)props["rings_direction"]);
					}
					if (props.has("wave_profile")) {
						wave->set_wave_profile((int)props["wave_profile"]);
					}
				}
			}

			{
				VectorRotateNode *vr = Object::cast_to<VectorRotateNode>(node.ptr());
				if (vr) {
					if (props.has("rotation_type")) {
						vr->set_rotation_type((int)props["rotation_type"]);
					}
					if (props.has("invert")) {
						vr->set_invert((bool)(int)props["invert"]);
					}
				}
			}

			if (props.has("gradient_type")) {
				GradientTextureNode *grad = Object::cast_to<GradientTextureNode>(node.ptr());
				if (grad) {
					grad->set_gradient_type((int)props["gradient_type"]);
				}
			}

			// Store Named Attribute is TYPED. data_type and domain were both
			// serialized and applied by nobody, so every bridged attribute was
			// a Float on Point whatever Blender said -- and Value stayed a
			// Float socket, which narrowed a colour to its red channel without
			// a word. data_type BEFORE domain is not significant, but data_type
			// must be applied before defaults, which it is: properties are
			// read before them.
			{
				StoreNamedAttributeNode *store =
						Object::cast_to<StoreNamedAttributeNode>(node.ptr());
				if (store) {
					if (props.has("data_type")) {
						store->set_data_type((int)props["data_type"]);
					}
					// Blender's ATTR_DOMAIN codes are 0..5 in the same order as
					// proc's AttrDomain, so this passes straight through. LAYER
					// (6) has no proc counterpart and is left to the node's own
					// bounds checking rather than silently remapped.
					if (props.has("domain")) {
						store->set_attr_domain((int)props["domain"]);
					}
				}
			}

			// The READ side of the same typing.
			{
				NamedAttributeNode *named =
						Object::cast_to<NamedAttributeNode>(node.ptr());
				if (named && props.has("data_type")) {
					named->set_data_type((int)props["data_type"]);
				}
			}

			// Accumulate Field's Value input and all three outputs take the
			// data_type together; domain is carried for the round trip.
			{
				AccumulateFieldNode *af = Object::cast_to<AccumulateFieldNode>(node.ptr());
				if (af) {
					if (props.has("data_type")) {
						af->set_data_type((int)props["data_type"]);
					}
					if (props.has("domain")) {
						af->set_domain((int)props["domain"]);
					}
				}
			}

			// Extrude Mesh's mode and Mesh Boolean's operation.
			{
				ExtrudeMeshNode *em = Object::cast_to<ExtrudeMeshNode>(node.ptr());
				if (em && props.has("mode")) {
					em->set_mode((int)props["mode"]);
				}
				MeshBooleanNode *mb = Object::cast_to<MeshBooleanNode>(node.ptr());
				if (mb && props.has("operation")) {
					mb->set_operation((int)props["operation"]);
				}
			}

			// FOREACH'S THREE LISTS. The input node takes input_items; the
			// output node takes main_items and generation_items.
			{
				auto pull = [&props](const char *key, PackedStringArray &r_names,
									  PackedInt32Array &r_types) -> bool {
					const String nk = String(key) + "_names";
					if (!props.has(nk)) {
						return false;
					}
					Array a = props[nk];
					r_names.resize(a.size());
					for (int i = 0; i < a.size(); i++) {
						r_names.set(i, String(a[i]));
					}
					const String tk = String(key) + "_types";
					if (props.has(tk)) {
						Array ta = props[tk];
						r_types.resize(ta.size());
						for (int i = 0; i < ta.size(); i++) {
							r_types.set(i, (int)ta[i]);
						}
					}
					return true;
				};
				ForeachZoneInputNode *fi = Object::cast_to<ForeachZoneInputNode>(node.ptr());
				if (fi) {
					PackedStringArray n;
					PackedInt32Array t;
					if (pull("input_item", n, t)) {
						fi->set_input_item_types(t);
						fi->set_input_item_names(n);
					}
				}
				ForeachZoneOutputNode *fo = Object::cast_to<ForeachZoneOutputNode>(node.ptr());
				if (fo) {
					PackedStringArray n;
					PackedInt32Array t;
					if (pull("main_item", n, t)) {
						fo->set_main_item_types(t);
						fo->set_main_item_names(n);
					}
					PackedStringArray gn;
					PackedInt32Array gt;
					if (pull("generation_item", gn, gt)) {
						fo->set_generation_item_types(gt);
						fo->set_generation_item_names(gn);
					}
				}
			}

			// A ZONE'S STATE ITEMS build all four of the pair's socket lists.
			// Applied to BOTH nodes: Blender keeps one list on the output and
			// derives the input's sockets from it, so the serializer sends the
			// same list to each and each rebuilds its own shape.
			{
				PackedStringArray names;
				PackedInt32Array types;
				bool have = props.has("item_names");
				if (have) {
					Array a = props["item_names"];
					names.resize(a.size());
					for (int i = 0; i < a.size(); i++) {
						names.set(i, String(a[i]));
					}
					if (props.has("item_types")) {
						Array ta = props["item_types"];
						types.resize(ta.size());
						for (int i = 0; i < ta.size(); i++) {
							types.set(i, (int)ta[i]);
						}
					}
				}
				if (have) {
					RepeatZoneInputNode *ri = Object::cast_to<RepeatZoneInputNode>(node.ptr());
					if (ri) { ri->set_item_types(types); ri->set_item_names(names); }
					RepeatZoneOutputNode *ro = Object::cast_to<RepeatZoneOutputNode>(node.ptr());
					if (ro) { ro->set_item_types(types); ro->set_item_names(names); }
					SimulationZoneInputNode *si =
							Object::cast_to<SimulationZoneInputNode>(node.ptr());
					if (si) { si->set_item_types(types); si->set_item_names(names); }
					SimulationZoneOutputNode *so =
							Object::cast_to<SimulationZoneOutputNode>(node.ptr());
					if (so) { so->set_item_types(types); so->set_item_names(names); }
				}
			}

			// CAPTURE ATTRIBUTE'S ITEM LIST BUILDS ITS SOCKETS, so it must be
			// applied before defaults and connections -- properties are, which
			// is why this works here and would not later.
			{
				CaptureAttributeNode *cap =
						Object::cast_to<CaptureAttributeNode>(node.ptr());
				if (cap) {
					if (props.has("item_names")) {
						Array a = props["item_names"];
						PackedStringArray names;
						names.resize(a.size());
						for (int i = 0; i < a.size(); i++) {
							names.set(i, String(a[i]));
						}
						PackedInt32Array types;
						if (props.has("item_types")) {
							Array ta = props["item_types"];
							types.resize(ta.size());
							for (int i = 0; i < ta.size(); i++) {
								types.set(i, (int)ta[i]);
							}
						}
						cap->set_item_types(types);
						cap->set_item_names(names);
					}
					if (props.has("domain")) {
						cap->set_domain((int)props["domain"]);
					}
				}
			}

			// Sample Index types its Value input and output together.
			{
				SampleIndexNode *si = Object::cast_to<SampleIndexNode>(node.ptr());
				if (si) {
					if (props.has("data_type")) {
						si->set_data_type((int)props["data_type"]);
					}
					if (props.has("domain")) {
						si->set_domain((int)props["domain"]);
					}
					if (props.has("clamp")) {
						si->set_clamp((bool)(int)props["clamp"]);
					}
				}
			}

			// Random Value's data_type REBUILDS its socket list, not just the
			// types: BOOLEAN has three inputs where the others have four. So a
			// data_type that does not arrive leaves ID and Seed one socket
			// along, and both links land on the wrong thing.
			{
				RandomValueNode *rv = Object::cast_to<RandomValueNode>(node.ptr());
				if (rv && props.has("data_type")) {
					rv->set_data_type((int)props["data_type"]);
				}
			}

			// Float Curve and RGB Curves' CONTROL POINTS. Without these both
			// nodes kept their constructor default, which is the identity --
			// so a bridged curve arrived as a pass-through.
			//
			// Written once against a lambda rather than twice: the two nodes
			// share the encoding but not a base class, and a copy-pasted second
			// copy is how one of them ends up a property behind.
			{
				auto apply_curve = [&props](auto *n) {
					if (props.has("curve_points")) {
						Array pts = props["curve_points"];
						PackedFloat32Array flat;
						flat.resize(pts.size());
						float *w = flat.ptrw();
						for (int i = 0; i < pts.size(); i++) {
							w[i] = (float)(double)pts[i];
						}
						n->set_curve_points(flat);
					}
					if (props.has("curve_handles")) {
						Array h = props["curve_handles"];
						PackedInt32Array arr;
						arr.resize(h.size());
						int *w = arr.ptrw();
						for (int i = 0; i < h.size(); i++) {
							w[i] = (int)h[i];
						}
						n->set_curve_handles(arr);
					}
					if (props.has("curve_counts")) {
						Array c = props["curve_counts"];
						PackedInt32Array arr;
						arr.resize(c.size());
						int *w = arr.ptrw();
						for (int i = 0; i < c.size(); i++) {
							w[i] = (int)c[i];
						}
						n->set_curve_counts(arr);
					}
					if (props.has("extend")) {
						n->set_extend((int)props["extend"]);
					}
					if (props.has("use_clip")) {
						n->set_use_clip((bool)(int)props["use_clip"]);
					}
					if (props.has("clip_rect")) {
						Array r = props["clip_rect"];
						PackedFloat32Array flat;
						flat.resize(r.size());
						float *w = flat.ptrw();
						for (int i = 0; i < r.size(); i++) {
							w[i] = (float)(double)r[i];
						}
						n->set_clip_rect(flat);
					}
				};
				FloatCurveNode *fc = Object::cast_to<FloatCurveNode>(node.ptr());
				if (fc) {
					apply_curve(fc);
				}
				RGBCurvesNode *rc = Object::cast_to<RGBCurvesNode>(node.ptr());
				if (rc) {
					apply_curve(rc);
				}
			}

			// Color Ramp's STOPS -- the node's actual data. Without this the
			// node kept its constructor default and a bridged ramp evaluated
			// as black-to-white whatever it said in Blender.
			{
				ColorRampNode *ramp = Object::cast_to<ColorRampNode>(node.ptr());
				if (ramp) {
					if (props.has("ramp_stops")) {
						// Arrives as a JSON Array of doubles; the property is a
						// PackedFloat32Array, converted here rather than relying
						// on Variant coercion so the narrowing is visible.
						Array stops = props["ramp_stops"];
						PackedFloat32Array flat;
						flat.resize(stops.size());
						float *fw = flat.ptrw();
						for (int s = 0; s < stops.size(); s++) {
							fw[s] = (float)(double)stops[s];
						}
						ramp->set_ramp_stops(flat);
					}
					if (props.has("interpolation")) {
						ramp->set_interpolation((int)props["interpolation"]);
					}
					if (props.has("color_mode")) {
						ramp->set_color_mode((int)props["color_mode"]);
					}
					if (props.has("hue_interpolation")) {
						ramp->set_hue_interpolation((int)props["hue_interpolation"]);
					}
				}
			}

			// Perlin noise node properties (matches Blender's Noise Texture)
			PerlinNoiseNode *perlin = Object::cast_to<PerlinNoiseNode>(node.ptr());
			if (perlin) {
				if (props.has("noise_type")) {
					perlin->set_noise_type((int)props["noise_type"]);
				}
				if (props.has("noise_dimensions")) {
					perlin->set_noise_dimensions((int)props["noise_dimensions"]);
				}
				if (props.has("normalize")) {
					perlin->set_normalize((bool)(int)props["normalize"]);
				}
			}

			// DisplaceNode: no properties (always Perlin fBM)
		}

		// Set graph editor position (Blender Y-up -> Godot Y-down)
		if (node_data.has("position")) {
			Array pos = node_data["position"];
			if (pos.size() >= 2) {
				node->set_graph_position(Vector2((float)pos[0], -(float)pos[1]));
			}
		}

		// THE GRAPH'S INTERFACE. Group Input's output sockets are the graph's
		// parameters, and their values come from the Blender modifier the tree
		// was serialized with. Without this the node keeps its single Geometry
		// output and every parameter link addresses a socket that is not there.
		if (node_data.has("outputs")) {
			GroupInputNode *gi = Object::cast_to<GroupInputNode>(node.ptr());
			if (gi) {
				gi->set_interface(node_data["outputs"]);
			}
		}

		// ...and the OUTPUT side of the same interface. Group Output was built
		// with one Geometry input whatever the tree declared, so a graph
		// returning a value could not say so.
		if (node_data.has("inputs")) {
			GroupOutputNode *go = Object::cast_to<GroupOutputNode>(node.ptr());
			if (go) {
				go->set_interface(node_data["inputs"]);
			}
		}

		// Apply input socket defaults
		if (node_data.has("defaults")) {
			Dictionary defaults = node_data["defaults"];
			_apply_defaults(node, defaults);
		}

		// Keep the authoring id ON the node, not just in a local map that dies
		// with this function. A consumer holding an id -- blender-bridge's
		// parameter deltas do -- needs to reach the node afterwards.
		node->set_source_id(json_id);
		int graph_idx = graph->add_node(node);
		id_to_idx[json_id] = graph_idx;
	}

	// Create connections
	int conn_count = 0;
	for (int i = 0; i < conns_arr.size(); i++) {
		Dictionary conn = conns_arr[i];

		int from_id = conn.get("from_node", -1);
		int from_port = conn.get("from_port", 0);
		int to_id = conn.get("to_node", -1);
		int to_port = conn.get("to_port", 0);

		if (!id_to_idx.has(from_id) || !id_to_idx.has(to_id)) {
			continue; // Node was skipped (unmapped type)
		}

		graph->connect_sockets(id_to_idx[from_id], from_port, id_to_idx[to_id], to_port);
		conn_count++;
	}

	UtilityFunctions::print("GraphImporter: Imported ", graph->get_node_count(), " nodes, ",
			conn_count, " connections (", skipped, " nodes skipped)");

	return graph;
}

} // namespace godot