#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