#ifdef TOOLS_ENABLED
#include "proc_graph_edit.h"
#include "../graph/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/selection_nodes.h"
#include "../nodes/zone_nodes.h"
#include <godot_cpp/classes/editor_interface.hpp>
#include <godot_cpp/classes/h_box_container.hpp>
#include <godot_cpp/classes/input_event_key.hpp>
#include <godot_cpp/classes/input_event_mouse_button.hpp>
#include <godot_cpp/classes/label.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/classes/resource_saver.hpp>
#include <godot_cpp/classes/v_box_container.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/classes/style_box_empty.hpp>
namespace godot {
// ---------------------------------------------------------------------------
// Socket colors (matching Blender Geometry Nodes convention)
// ---------------------------------------------------------------------------
static const Color COLOR_FLOAT(0.631, 0.631, 0.631); // #A1A1A1
static const Color COLOR_INT(0.298, 0.686, 0.314); // #4CAF50
static const Color COLOR_BOOL(1.0, 0.596, 0.0); // #FF9800
static const Color COLOR_VECTOR(0.612, 0.153, 0.690); // #9C27B0
static const Color COLOR_COLOR(1.0, 0.922, 0.231); // #FFEB3B
static const Color COLOR_STRING(0.129, 0.588, 0.953); // #2196F3
static const Color COLOR_GEOMETRY(0.0, 0.737, 0.831); // #00BCD4
static const Color COLOR_OBJECT(0.957, 0.263, 0.212); // #F44336
// ---------------------------------------------------------------------------
// Node factory — creates ProcNode by type name (same as graph_importer.cpp)
// ---------------------------------------------------------------------------
static Ref<ProcNode> _create_proc_node(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
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));
// Mesh Ops
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 == "MergeByDistance") return Ref<ProcNode>(memnew(MergeByDistanceNode));
// 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));
if (p_type == "SetCurveRadius") return Ref<ProcNode>(memnew(SetCurveRadiusNode));
// Points
if (p_type == "Points") return Ref<ProcNode>(memnew(PointsNode));
if (p_type == "PointsToVertices") return Ref<ProcNode>(memnew(PointsToVerticesNode));
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 == "GroupInput") return Ref<ProcNode>(memnew(GroupInputNode));
if (p_type == "GroupOutput") return Ref<ProcNode>(memnew(GroupOutputNode));
// Attributes
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));
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));
// Selection/Utility
if (p_type == "DeleteGeometry") return Ref<ProcNode>(memnew(DeleteGeometryNode));
if (p_type == "BoundingBox") return Ref<ProcNode>(memnew(BoundingBoxNode));
if (p_type == "SeparateComponents") return Ref<ProcNode>(memnew(SeparateComponentsNode));
if (p_type == "Switch") return Ref<ProcNode>(memnew(SwitchNode));
if (p_type == "RandomValue") return Ref<ProcNode>(memnew(RandomValueNode));
// Noise
if (p_type == "NoiseTexture") return Ref<ProcNode>(memnew(NoiseTextureNode));
if (p_type == "Displace") return Ref<ProcNode>(memnew(DisplaceNode));
if (p_type == "UV") return Ref<ProcNode>(memnew(UVNode));
if (p_type == "GradientNoise") return Ref<ProcNode>(memnew(GradientNoiseNode));
if (p_type == "FractalTerrain") return Ref<ProcNode>(memnew(FractalTerrainNode));
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));
if (p_type == "Viewer") return Ref<ProcNode>(memnew(ViewerNode));
// 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));
return Ref<ProcNode>();
}
// ---------------------------------------------------------------------------
// Bind methods
// ---------------------------------------------------------------------------
void ProcGraphEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("_on_connection_request", "from", "from_port", "to", "to_port"), &ProcGraphEdit::_on_connection_request);
ClassDB::bind_method(D_METHOD("_on_disconnection_request", "from", "from_port", "to", "to_port"), &ProcGraphEdit::_on_disconnection_request);
ClassDB::bind_method(D_METHOD("_on_delete_nodes_request", "nodes"), &ProcGraphEdit::_on_delete_nodes_request);
ClassDB::bind_method(D_METHOD("_on_node_selected", "node"), &ProcGraphEdit::_on_node_selected);
ClassDB::bind_method(D_METHOD("_on_end_node_move"), &ProcGraphEdit::_on_end_node_move);
ClassDB::bind_method(D_METHOD("_on_palette_item_activated"), &ProcGraphEdit::_on_palette_item_activated);
ClassDB::bind_method(D_METHOD("_on_palette_search_changed", "text"), &ProcGraphEdit::_on_palette_search_changed);
ClassDB::bind_method(D_METHOD("_on_new_pressed"), &ProcGraphEdit::_on_new_pressed);
ClassDB::bind_method(D_METHOD("_on_load_pressed"), &ProcGraphEdit::_on_load_pressed);
ClassDB::bind_method(D_METHOD("_on_save_pressed"), &ProcGraphEdit::_on_save_pressed);
ClassDB::bind_method(D_METHOD("_on_load_file_selected", "path"), &ProcGraphEdit::_on_load_file_selected);
ClassDB::bind_method(D_METHOD("_on_save_file_selected", "path"), &ProcGraphEdit::_on_save_file_selected);
ClassDB::bind_method(D_METHOD("_on_bake_pressed"), &ProcGraphEdit::_on_bake_pressed);
ClassDB::bind_method(D_METHOD("_on_default_value_changed", "value", "node_idx", "socket_idx"), &ProcGraphEdit::_on_default_value_changed);
ClassDB::bind_method(D_METHOD("_on_node_property_changed", "value", "node_idx", "property"), &ProcGraphEdit::_on_node_property_changed);
ClassDB::bind_method(D_METHOD("_do_refresh"), &ProcGraphEdit::_do_refresh);
ClassDB::bind_method(D_METHOD("_sync_defaults_from_graph"), &ProcGraphEdit::_sync_defaults_from_graph);
}
// ---------------------------------------------------------------------------
// Constructor / Destructor
// ---------------------------------------------------------------------------
ProcGraphEdit::ProcGraphEdit() {
set_right_disconnects(true);
set_show_minimap_button(true);
set_minimap_enabled(true);
set_snapping_distance(20);
set_snapping_enabled(true);
// Register valid connection types
_register_connection_types();
// Build node palette entries
_build_node_entries();
// Create palette as PopupPanel (auto-closes on click outside)
palette_popup = memnew(PopupPanel);
palette_popup->set_size(Vector2i(300, 400));
VBoxContainer *palette_vbox = memnew(VBoxContainer);
palette_vbox->set_custom_minimum_size(Vector2(290, 380));
palette_popup->add_child(palette_vbox);
palette_search = memnew(LineEdit);
palette_search->set_placeholder("Search nodes...");
palette_search->set_clear_button_enabled(true);
palette_vbox->add_child(palette_search);
palette_tree = memnew(Tree);
palette_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
palette_tree->set_hide_root(true);
palette_vbox->add_child(palette_tree);
add_child(palette_popup);
// Toolbar — graph management
HBoxContainer *toolbar = memnew(HBoxContainer);
toolbar->add_theme_constant_override("separation", 6);
new_button = memnew(Button);
new_button->set_text("New");
new_button->set_tooltip_text("Create empty graph");
toolbar->add_child(new_button);
load_button = memnew(Button);
load_button->set_text("Load");
load_button->set_tooltip_text("Load graph from .tres file");
toolbar->add_child(load_button);
save_button = memnew(Button);
save_button->set_text("Save");
save_button->set_tooltip_text("Save graph to .tres file");
toolbar->add_child(save_button);
Control *sep = memnew(Control);
sep->set_custom_minimum_size(Vector2(16, 0));
toolbar->add_child(sep);
bake_button = memnew(Button);
bake_button->set_text("Bake Mesh");
bake_button->set_tooltip_text("Evaluate graph and save result as .tres ArrayMesh");
toolbar->add_child(bake_button);
get_menu_hbox()->add_child(toolbar);
// File dialogs (children of this control so they show in the editor)
load_dialog = memnew(FileDialog);
load_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE);
load_dialog->set_title("Load Proc Graph");
load_dialog->add_filter("*.tres", "Proc Node Graph");
load_dialog->set_access(FileDialog::ACCESS_RESOURCES);
add_child(load_dialog);
save_dialog = memnew(FileDialog);
save_dialog->set_file_mode(FileDialog::FILE_MODE_SAVE_FILE);
save_dialog->set_title("Save Proc Graph");
save_dialog->add_filter("*.tres", "Proc Node Graph");
save_dialog->set_access(FileDialog::ACCESS_RESOURCES);
add_child(save_dialog);
}
ProcGraphEdit::~ProcGraphEdit() {
}
// ---------------------------------------------------------------------------
// Notification
// ---------------------------------------------------------------------------
void ProcGraphEdit::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
connect("connection_request", Callable(this, "_on_connection_request"));
connect("disconnection_request", Callable(this, "_on_disconnection_request"));
connect("delete_nodes_request", Callable(this, "_on_delete_nodes_request"));
connect("end_node_move", Callable(this, "_on_end_node_move"));
palette_tree->connect("item_activated", Callable(this, "_on_palette_item_activated"));
palette_search->connect("text_changed", Callable(this, "_on_palette_search_changed"));
new_button->connect("pressed", Callable(this, "_on_new_pressed"));
load_button->connect("pressed", Callable(this, "_on_load_pressed"));
save_button->connect("pressed", Callable(this, "_on_save_pressed"));
bake_button->connect("pressed", Callable(this, "_on_bake_pressed"));
load_dialog->connect("file_selected", Callable(this, "_on_load_file_selected"));
save_dialog->connect("file_selected", Callable(this, "_on_save_file_selected"));
} break;
}
}
// ---------------------------------------------------------------------------
// Right-click → show palette (replaces removed popup_request signal)
// ---------------------------------------------------------------------------
void ProcGraphEdit::_gui_input(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_RIGHT && mb->is_pressed()) {
Vector2 local_pos = mb->get_position();
Vector2 graph_pos = (get_scroll_offset() + local_pos) / get_zoom();
Vector2 screen_pos = get_screen_position() + local_pos;
_show_palette(screen_pos, graph_pos);
accept_event();
return;
}
// Copy/Paste
Ref<InputEventKey> key = p_event;
if (key.is_valid() && key->is_pressed() && key->is_command_or_control_pressed()) {
if (key->get_keycode() == KEY_C) {
// Copy selected nodes
if (!edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) return;
clipboard_nodes.clear();
clipboard_connections.clear();
clipboard_center = Vector2();
// Find selected node indices
Vector<int> selected_indices;
for (int i = 0; i < get_child_count(); i++) {
GraphNode *gn = Object::cast_to<GraphNode>(get_child(i));
if (gn && gn->is_selected()) {
int idx = String(gn->get_name()).get_slice("_", 1).to_int();
selected_indices.push_back(idx);
}
}
if (selected_indices.is_empty()) return;
// Compute center of selected nodes
for (int i = 0; i < selected_indices.size(); i++) {
Ref<ProcNode> node = graph->get_node(selected_indices[i]);
if (node.is_valid()) {
clipboard_center += node->get_graph_position();
}
}
clipboard_center /= (float)selected_indices.size();
// Duplicate nodes into clipboard
HashMap<int, int> idx_map; // old index -> clipboard index
for (int i = 0; i < selected_indices.size(); i++) {
Ref<ProcNode> node = graph->get_node(selected_indices[i]);
if (node.is_valid()) {
Ref<Resource> dup = node->duplicate();
Ref<ProcNode> copy = Ref<ProcNode>(Object::cast_to<ProcNode>(dup.ptr()));
idx_map[selected_indices[i]] = clipboard_nodes.size();
clipboard_nodes.push_back(copy);
}
}
// Copy connections between selected nodes
const Vector<ProcConnection> &conns = graph->get_connections();
for (int c = 0; c < conns.size(); c++) {
const ProcConnection &conn = conns[c];
if (idx_map.has(conn.from_node) && idx_map.has(conn.to_node)) {
ProcConnection new_conn;
new_conn.from_node = idx_map[conn.from_node];
new_conn.from_socket = conn.from_socket;
new_conn.to_node = idx_map[conn.to_node];
new_conn.to_socket = conn.to_socket;
clipboard_connections.push_back(new_conn);
}
}
accept_event();
} else if (key->get_keycode() == KEY_V) {
// Paste clipboard nodes
if (clipboard_nodes.is_empty()) return;
if (!edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) {
graph.instantiate();
edited_node->set_graph(graph);
}
// Paste at offset from center
Vector2 paste_offset = Vector2(50, 50);
EditorUndoRedoManager *ur = _get_undo_redo();
ur->create_action("Paste Proc Nodes");
int base_idx = graph->get_node_count();
for (int i = 0; i < clipboard_nodes.size(); i++) {
Ref<Resource> dup = clipboard_nodes[i]->duplicate();
Ref<ProcNode> copy = Ref<ProcNode>(Object::cast_to<ProcNode>(dup.ptr()));
Vector2 pos = copy->get_graph_position() - clipboard_center + clipboard_center + paste_offset;
copy->set_graph_position(pos);
ur->add_do_method(graph.ptr(), "add_node", copy);
}
for (int c = 0; c < clipboard_connections.size(); c++) {
const ProcConnection &conn = clipboard_connections[c];
ur->add_do_method(graph.ptr(), "connect_sockets",
base_idx + conn.from_node, conn.from_socket,
base_idx + conn.to_node, conn.to_socket);
}
ur->add_do_method(this, "_do_refresh");
// Undo: remove pasted nodes (in reverse)
for (int i = clipboard_nodes.size() - 1; i >= 0; i--) {
ur->add_undo_method(graph.ptr(), "remove_node", base_idx + i);
}
ur->add_undo_method(this, "_do_refresh");
ur->commit_action();
accept_event();
}
}
}
// ---------------------------------------------------------------------------
// Show palette popup at screen position, spawn at graph position
// ---------------------------------------------------------------------------
void ProcGraphEdit::_show_palette(const Vector2 &p_screen_position, const Vector2 &p_graph_position) {
palette_spawn_position = p_graph_position;
palette_search->set_text("");
_populate_palette_tree();
palette_popup->set_position(Vector2i((int)p_screen_position.x, (int)p_screen_position.y));
palette_popup->reset_size();
palette_popup->popup();
palette_search->grab_focus();
}
// ---------------------------------------------------------------------------
// Socket color mapping
// ---------------------------------------------------------------------------
Color ProcGraphEdit::_socket_color(SocketType p_type) const {
switch (p_type) {
case SocketType::Float: return COLOR_FLOAT;
case SocketType::Int: return COLOR_INT;
case SocketType::Bool: return COLOR_BOOL;
case SocketType::Vector: return COLOR_VECTOR;
case SocketType::Color: return COLOR_COLOR;
case SocketType::String: return COLOR_STRING;
case SocketType::Geometry: return COLOR_GEOMETRY;
case SocketType::Object: return COLOR_OBJECT;
}
return COLOR_FLOAT;
}
// ---------------------------------------------------------------------------
// Register compatible connection types
// ---------------------------------------------------------------------------
void ProcGraphEdit::_register_connection_types() {
// Same type always valid
for (int t = 0; t <= (int)SocketType::Object; t++) {
add_valid_connection_type(t, t);
}
// Float <-> Int
add_valid_connection_type((int)SocketType::Float, (int)SocketType::Int);
add_valid_connection_type((int)SocketType::Int, (int)SocketType::Float);
// Float <-> Bool
add_valid_connection_type((int)SocketType::Float, (int)SocketType::Bool);
add_valid_connection_type((int)SocketType::Bool, (int)SocketType::Float);
// Int <-> Bool
add_valid_connection_type((int)SocketType::Int, (int)SocketType::Bool);
add_valid_connection_type((int)SocketType::Bool, (int)SocketType::Int);
}
// ---------------------------------------------------------------------------
// Set edited node
// ---------------------------------------------------------------------------
void ProcGraphEdit::set_edited_node(ProcGeometryNode *p_node) {
// Disconnect from previous node
if (edited_node && edited_node->is_connected("defaults_changed", Callable(this, "_sync_defaults_from_graph"))) {
edited_node->disconnect("defaults_changed", Callable(this, "_sync_defaults_from_graph"));
}
edited_node = p_node;
_rebuild_graph();
// Connect all node "changed" signals to invalidate_plan for live preview
if (edited_node) {
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_valid()) {
Callable on_changed = Callable(edited_node, "invalidate_plan");
for (int i = 0; i < graph->get_node_count(); i++) {
Ref<ProcNode> node = graph->get_node(i);
if (node.is_valid() && !node->is_connected("changed", on_changed)) {
node->connect("changed", on_changed);
}
}
}
// Connect defaults_changed for external param delta sync (blender-bridge)
if (!edited_node->is_connected("defaults_changed", Callable(this, "_sync_defaults_from_graph"))) {
edited_node->connect("defaults_changed", Callable(this, "_sync_defaults_from_graph"));
}
}
}
// ---------------------------------------------------------------------------
// Build node palette entries
// ---------------------------------------------------------------------------
void ProcGraphEdit::_build_node_entries() {
node_entries.clear();
auto add = [&](const String &type, const String &title, const String &cat) {
ProcNodeEntry e;
e.type_name = type;
e.title = title;
e.category = cat;
node_entries.push_back(e);
};
// Input
add("Value", "Value", "Input");
add("Vector", "Vector", "Input");
add("Integer", "Integer", "Input");
add("Boolean", "Boolean", "Input");
add("Color", "Color", "Input");
add("String", "String", "Input");
add("ObjectInfo", "Object Info", "Input");
add("GroupInput", "Group Input", "Input");
add("GroupOutput", "Group Output", "Input");
// Math
add("Math", "Math", "Math");
add("VectorMath", "Vector Math", "Math");
add("BooleanMath", "Boolean Math", "Math");
add("Compare", "Compare", "Math");
add("CombineXYZ", "Combine XYZ", "Math");
add("SeparateXYZ", "Separate XYZ", "Math");
add("Clamp", "Clamp", "Math");
add("MapRange", "Map Range", "Math");
add("Mix", "Mix", "Math");
add("ColorRamp", "Color Ramp", "Math");
add("FloatCurve", "Float Curve", "Math");
add("RGBCurves", "RGB Curves", "Math");
// Mesh Primitives
add("MeshCube", "Cube", "Mesh");
add("MeshGrid", "Grid", "Mesh");
add("MeshCircle", "Circle", "Mesh");
add("MeshCylinder", "Cylinder", "Mesh");
add("MeshUVSphere", "UV Sphere", "Mesh");
add("MeshIcoSphere", "Ico Sphere", "Mesh");
add("MeshLine", "Line", "Mesh");
// Geometry
add("TransformGeometry", "Transform", "Geometry");
add("SetPosition", "Set Position", "Geometry");
add("JoinGeometry", "Join Geometry", "Geometry");
// Mesh Operations
add("FlipFaces", "Flip Faces", "Mesh Ops");
add("SetShadeSmooth", "Set Shade Smooth", "Mesh Ops");
add("SubdivideMesh", "Subdivide", "Mesh Ops");
add("ExtrudeMesh", "Extrude", "Mesh Ops");
add("ScaleElements", "Scale Elements", "Mesh Ops");
add("DualMesh", "Dual Mesh", "Mesh Ops");
add("MergeByDistance", "Merge By Distance", "Mesh Ops");
// Curves
add("CurveCircle", "Curve Circle", "Curves");
add("CurveLine", "Curve Line", "Curves");
add("BezierSegment", "Bezier Segment", "Curves");
add("CurveToMesh", "Curve to Mesh", "Curves");
add("CurveToPoints", "Curve to Points", "Curves");
add("ResampleCurve", "Resample Curve", "Curves");
add("TrimCurve", "Trim Curve", "Curves");
add("SetCurveRadius", "Set Curve Radius", "Curves");
// Points
add("Points", "Points", "Points");
add("PointsToVertices", "Points to Vertices", "Points");
add("DistributePointsOnFaces", "Distribute on Faces", "Points");
// Instances
add("InstanceOnPoints", "Instance on Points", "Instances");
add("RotateInstances", "Rotate Instances", "Instances");
add("ScaleInstances", "Scale Instances", "Instances");
add("TranslateInstances", "Translate Instances", "Instances");
add("RealizeInstances", "Realize Instances", "Instances");
// Attributes
add("Position", "Position", "Attributes");
add("Normal", "Normal", "Attributes");
add("Index", "Index", "Attributes");
add("StoreNamedAttribute", "Store Named Attribute", "Attributes");
add("NamedAttribute", "Named Attribute", "Attributes");
add("CaptureAttribute", "Capture Attribute", "Attributes");
add("RemoveNamedAttribute", "Remove Named Attribute", "Attributes");
add("DomainSize", "Domain Size", "Attributes");
// Selection/Utility
add("DeleteGeometry", "Delete Geometry", "Selection");
add("BoundingBox", "Bounding Box", "Selection");
add("SeparateComponents", "Separate Components", "Selection");
add("Switch", "Switch", "Selection");
add("RandomValue", "Random Value", "Selection");
// Noise
add("NoiseTexture", "Noise Texture", "Noise");
add("Displace", "Displace", "Noise");
add("UV", "UV", "Input");
add("GradientNoise", "Gradient Noise", "Noise");
add("FractalTerrain", "Fractal Terrain", "Noise");
add("ErosionNoise", "Erosion Noise", "Noise");
add("AltitudeColor", "Altitude Color", "Utility");
add("SlopeGradient", "Slope Gradient", "Geometry");
add("Viewer", "Viewer", "Utility");
// Zones
add("RepeatZoneInput", "Repeat Zone", "Zones");
add("RepeatZoneOutput", "Repeat Zone Output", "Zones");
add("ForeachZoneInput", "Foreach Zone", "Zones");
add("ForeachZoneOutput", "Foreach Zone Output", "Zones");
add("SimulationZoneInput", "Simulation Zone", "Zones");
add("SimulationZoneOutput", "Simulation Zone Output", "Zones");
}
// ---------------------------------------------------------------------------
// Populate palette tree with optional filter
// ---------------------------------------------------------------------------
void ProcGraphEdit::_populate_palette_tree(const String &p_filter) {
palette_tree->clear();
TreeItem *root = palette_tree->create_item();
// Group by category
HashMap<String, TreeItem *> categories;
for (int i = 0; i < node_entries.size(); i++) {
const ProcNodeEntry &entry = node_entries[i];
// Filter check
if (!p_filter.is_empty()) {
if (entry.title.findn(p_filter) == -1 &&
entry.type_name.findn(p_filter) == -1 &&
entry.category.findn(p_filter) == -1) {
continue;
}
}
// Get or create category
if (!categories.has(entry.category)) {
TreeItem *cat_item = palette_tree->create_item(root);
cat_item->set_text(0, entry.category);
cat_item->set_selectable(0, false);
categories[entry.category] = cat_item;
}
TreeItem *item = palette_tree->create_item(categories[entry.category]);
item->set_text(0, entry.title);
item->set_metadata(0, entry.type_name);
}
// Expand all categories
TreeItem *child = root->get_first_child();
while (child) {
child->set_collapsed(false);
child = child->get_next();
}
}
// ---------------------------------------------------------------------------
// Create an inline editor widget for a socket's default value
// ---------------------------------------------------------------------------
static Control *_create_default_editor(const SocketDef &p_def, int p_node_idx, int p_socket_idx, ProcGraphEdit *p_owner) {
Callable on_changed = Callable(p_owner, "_on_default_value_changed").bind(p_node_idx, p_socket_idx);
const Variant &def = p_def.default_value;
switch (p_def.type) {
case SocketType::Float: {
SpinBox *sb = memnew(SpinBox);
sb->set_min(-10000);
sb->set_max(10000);
sb->set_step(0.01);
sb->set_value(def.get_type() != Variant::NIL ? (double)def : 0.0);
sb->set_custom_minimum_size(Vector2(70, 0));
sb->connect("value_changed", on_changed);
return sb;
}
case SocketType::Int: {
SpinBox *sb = memnew(SpinBox);
sb->set_min(-10000);
sb->set_max(10000);
sb->set_step(1);
sb->set_value(def.get_type() != Variant::NIL ? (double)def : 0.0);
sb->set_custom_minimum_size(Vector2(70, 0));
sb->connect("value_changed", on_changed);
return sb;
}
case SocketType::Bool: {
CheckBox *cb = memnew(CheckBox);
cb->set_pressed(def.get_type() != Variant::NIL ? (bool)def : false);
// CheckBox emits toggled(bool) — wrap to Variant
cb->connect("toggled", on_changed);
return cb;
}
case SocketType::Color: {
ColorPickerButton *cpb = memnew(ColorPickerButton);
cpb->set_pick_color(def.get_type() == Variant::COLOR ? (Color)def : Color(1, 1, 1));
cpb->set_custom_minimum_size(Vector2(40, 0));
cpb->connect("color_changed", on_changed);
return cpb;
}
default:
break;
}
return nullptr;
}
// ---------------------------------------------------------------------------
// Check if a specific input socket is connected
// ---------------------------------------------------------------------------
bool _is_input_connected(const ProcNodeGraph *p_graph, int p_node_idx, int p_socket_idx) {
if (!p_graph) return false;
return p_graph->get_input_source_node(p_node_idx, p_socket_idx) >= 0;
}
// ---------------------------------------------------------------------------
// Create a visual GraphNode for a ProcNode
// ---------------------------------------------------------------------------
GraphNode *ProcGraphEdit::_create_graph_node(int p_node_idx, const Ref<ProcNode> &p_node) {
GraphNode *gn = memnew(GraphNode);
gn->set_title(p_node->get_node_title());
gn->set_name("node_" + itos(p_node_idx));
gn->set_position_offset(p_node->get_graph_position());
// Get graph for connection checks
const ProcNodeGraph *graph_ptr = nullptr;
if (edited_node && edited_node->get_graph().is_valid()) {
graph_ptr = edited_node->get_graph().ptr();
}
// Special case: ValueNode — no inputs, one output, show inline editor for its value property
ValueNode *val_node = Object::cast_to<ValueNode>(p_node.ptr());
VectorNode *vec_node = Object::cast_to<VectorNode>(p_node.ptr());
if (val_node && p_node->get_input_count() == 0) {
HBoxContainer *row = memnew(HBoxContainer);
SpinBox *sb = memnew(SpinBox);
sb->set_min(-10000);
sb->set_max(10000);
sb->set_step(0.01);
sb->set_value(val_node->get_value());
sb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
sb->connect("value_changed", Callable(this, "_on_node_property_changed").bind(p_node_idx, "value"));
row->add_child(sb);
Label *out_lbl = memnew(Label);
out_lbl->set_text("Value");
out_lbl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
row->add_child(out_lbl);
gn->add_child(row);
gn->set_slot(0, false, 0, Color(1, 1, 1),
true, (int)SocketType::Float, _socket_color(SocketType::Float));
gn->connect("node_selected", Callable(this, "_on_node_selected").bind(gn));
return gn;
}
if (vec_node && p_node->get_input_count() == 0) {
Vector3 v = vec_node->get_value();
const char *labels[] = { "X", "Y", "Z" };
float vals[] = { v.x, v.y, v.z };
for (int c = 0; c < 3; c++) {
HBoxContainer *row = memnew(HBoxContainer);
Label *lbl = memnew(Label);
lbl->set_text(labels[c]);
row->add_child(lbl);
SpinBox *sb = memnew(SpinBox);
sb->set_min(-10000);
sb->set_max(10000);
sb->set_step(0.01);
sb->set_value(vals[c]);
sb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
sb->connect("value_changed", Callable(this, "_on_node_property_changed").bind(p_node_idx, labels[c]));
row->add_child(sb);
if (c == 0) {
Label *out_lbl = memnew(Label);
out_lbl->set_text("Vector");
out_lbl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
row->add_child(out_lbl);
}
gn->add_child(row);
gn->set_slot(c, false, 0, Color(1, 1, 1),
c == 0, (int)SocketType::Vector, _socket_color(SocketType::Vector));
}
gn->connect("node_selected", Callable(this, "_on_node_selected").bind(gn));
return gn;
}
// General case: inputs with optional inline editors + outputs
int max_slots = MAX(p_node->get_input_count(), p_node->get_output_count());
if (max_slots == 0) {
max_slots = 1;
}
for (int s = 0; s < max_slots; s++) {
bool has_left = s < p_node->get_input_count();
bool has_right = s < p_node->get_output_count();
HBoxContainer *row = memnew(HBoxContainer);
if (has_left) {
const SocketDef &idef = p_node->get_input_def(s);
bool connected = _is_input_connected(graph_ptr, p_node_idx, s);
Label *lbl = memnew(Label);
lbl->set_text(String(idef.name));
row->add_child(lbl);
// Only show inline editor when NOT connected
if (!connected) {
Control *editor = _create_default_editor(idef, p_node_idx, s, this);
if (editor) {
editor->set_h_size_flags(Control::SIZE_EXPAND_FILL);
row->add_child(editor);
}
}
}
if (has_left && has_right) {
Control *spacer = memnew(Control);
spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
spacer->set_custom_minimum_size(Vector2(8, 0));
row->add_child(spacer);
}
if (has_right) {
const SocketDef &odef = p_node->get_output_def(s);
Label *lbl = memnew(Label);
lbl->set_text(String(odef.name));
lbl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
if (!has_left) {
lbl->set_h_size_flags(Control::SIZE_EXPAND_FILL);
}
row->add_child(lbl);
}
gn->add_child(row);
int left_type = has_left ? (int)p_node->get_input_def(s).type : 0;
int right_type = has_right ? (int)p_node->get_output_def(s).type : 0;
Color left_color = has_left ? _socket_color(p_node->get_input_def(s).type) : Color(1, 1, 1);
Color right_color = has_right ? _socket_color(p_node->get_output_def(s).type) : Color(1, 1, 1);
gn->set_slot(s, has_left, left_type, left_color, has_right, right_type, right_color);
}
gn->connect("node_selected", Callable(this, "_on_node_selected").bind(gn));
return gn;
}
// ---------------------------------------------------------------------------
// Rebuild the entire visual graph from the ProcNodeGraph resource
// ---------------------------------------------------------------------------
void ProcGraphEdit::_rebuild_graph() {
// Clear existing visual nodes
Vector<Node *> to_remove;
for (int i = 0; i < get_child_count(); i++) {
GraphNode *gn = Object::cast_to<GraphNode>(get_child(i));
if (gn) {
to_remove.push_back(gn);
}
GraphFrame *gf = Object::cast_to<GraphFrame>(get_child(i));
if (gf) {
to_remove.push_back(gf);
}
}
for (int i = 0; i < to_remove.size(); i++) {
remove_child(to_remove[i]);
memdelete(to_remove[i]);
}
// Clear connections
clear_connections();
if (!edited_node) {
return;
}
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) {
return;
}
// Create visual nodes
for (int i = 0; i < graph->get_node_count(); i++) {
Ref<ProcNode> node = graph->get_node(i);
if (node.is_null()) {
continue;
}
GraphNode *gn = _create_graph_node(i, node);
add_child(gn);
}
// Create connections
const Vector<ProcConnection> &conns = graph->get_connections();
for (int i = 0; i < conns.size(); i++) {
const ProcConnection &c = conns[i];
StringName from_name = "node_" + itos(c.from_node);
StringName to_name = "node_" + itos(c.to_node);
connect_node(from_name, c.from_socket, to_name, c.to_socket);
}
// Build zone frames
_rebuild_zone_frames();
}
// ---------------------------------------------------------------------------
// Zone frames
// ---------------------------------------------------------------------------
void ProcGraphEdit::_rebuild_zone_frames() {
if (!edited_node) {
return;
}
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) {
return;
}
// Detect zone pairs by scanning for zone input/output nodes with matching zone_id
HashMap<int, int> zone_input_map; // zone_id -> node_idx
HashMap<int, int> zone_output_map; // zone_id -> node_idx
for (int i = 0; i < graph->get_node_count(); i++) {
Ref<ProcNode> node = graph->get_node(i);
if (node.is_null()) continue;
StringName type = node->get_node_type();
int zone_id = -1;
String type_str = String(type);
if (type_str == "RepeatZoneInput" || type_str == "ForeachZoneInput" || type_str == "SimulationZoneInput") {
RepeatZoneInputNode *rzi = Object::cast_to<RepeatZoneInputNode>(node.ptr());
ForeachZoneInputNode *fzi = Object::cast_to<ForeachZoneInputNode>(node.ptr());
SimulationZoneInputNode *szi = Object::cast_to<SimulationZoneInputNode>(node.ptr());
if (rzi) zone_id = rzi->get_zone_id();
else if (fzi) zone_id = fzi->get_zone_id();
else if (szi) zone_id = szi->get_zone_id();
if (zone_id >= 0) zone_input_map[zone_id] = i;
}
if (type_str == "RepeatZoneOutput" || type_str == "ForeachZoneOutput" || type_str == "SimulationZoneOutput") {
RepeatZoneOutputNode *rzo = Object::cast_to<RepeatZoneOutputNode>(node.ptr());
ForeachZoneOutputNode *fzo = Object::cast_to<ForeachZoneOutputNode>(node.ptr());
SimulationZoneOutputNode *szo = Object::cast_to<SimulationZoneOutputNode>(node.ptr());
if (rzo) zone_id = rzo->get_zone_id();
else if (fzo) zone_id = fzo->get_zone_id();
else if (szo) zone_id = szo->get_zone_id();
if (zone_id >= 0) zone_output_map[zone_id] = i;
}
}
// Create frames for each matched zone pair
for (const KeyValue<int, int> &kv : zone_input_map) {
int zone_id = kv.key;
if (!zone_output_map.has(zone_id)) continue;
int input_idx = kv.value;
int output_idx = zone_output_map[zone_id];
Ref<ProcNode> input_node = graph->get_node(input_idx);
String input_type_str = String(input_node->get_node_type());
Color tint;
String frame_title;
if (input_type_str == "RepeatZoneInput") {
tint = Color(0.2, 0.4, 0.8, 0.15);
frame_title = "Repeat Zone";
} else if (input_type_str == "ForeachZoneInput") {
tint = Color(0.2, 0.7, 0.3, 0.15);
frame_title = "Foreach Zone";
} else {
tint = Color(0.9, 0.5, 0.1, 0.15);
frame_title = "Simulation Zone";
}
GraphFrame *frame = memnew(GraphFrame);
frame->set_name("zone_frame_" + itos(zone_id));
frame->set_title(frame_title);
frame->set_tint_color(tint);
frame->set_tint_color_enabled(true);
add_child(frame);
StringName in_name = "node_" + itos(input_idx);
StringName out_name = "node_" + itos(output_idx);
attach_graph_element_to_frame(in_name, frame->get_name());
attach_graph_element_to_frame(out_name, frame->get_name());
}
}
// ---------------------------------------------------------------------------
// Schedule a deferred graph refresh
// ---------------------------------------------------------------------------
void ProcGraphEdit::_schedule_refresh() {
if (!_refresh_pending) {
_refresh_pending = true;
call_deferred("_do_refresh");
}
}
void ProcGraphEdit::_do_refresh() {
_refresh_pending = false;
_rebuild_graph();
if (edited_node) {
edited_node->invalidate_plan();
// Ensure all ProcNode resources forward "changed" to the ProcGeometryNode
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_valid()) {
Callable on_changed = Callable(edited_node, "invalidate_plan");
for (int i = 0; i < graph->get_node_count(); i++) {
Ref<ProcNode> node = graph->get_node(i);
if (node.is_valid() && !node->is_connected("changed", on_changed)) {
node->connect("changed", on_changed);
}
}
}
}
}
void ProcGraphEdit::_sync_defaults_from_graph() {
_rebuild_graph();
}
// ---------------------------------------------------------------------------
// Graph management (New / Save / Load)
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_new_pressed() {
if (!edited_node) return;
Ref<ProcNodeGraph> graph;
graph.instantiate();
edited_node->set_graph(graph);
_current_graph_path = "";
_rebuild_graph();
UtilityFunctions::print("ProcNodes: New empty graph");
}
void ProcGraphEdit::_on_load_pressed() {
if (!edited_node) return;
load_dialog->popup_centered(Vector2(600, 400));
}
void ProcGraphEdit::_on_save_pressed() {
if (!edited_node || edited_node->get_graph().is_null()) return;
if (_current_graph_path.is_empty()) {
save_dialog->popup_centered(Vector2(600, 400));
} else {
_on_save_file_selected(_current_graph_path);
}
}
void ProcGraphEdit::_on_load_file_selected(const String &p_path) {
if (!edited_node) return;
Ref<ProcNodeGraph> graph = ResourceLoader::get_singleton()->load(p_path);
if (graph.is_valid()) {
edited_node->set_graph(graph);
_current_graph_path = p_path;
_rebuild_graph();
UtilityFunctions::print("ProcNodes: Loaded graph from ", p_path,
" (", graph->get_node_count(), " nodes)");
} else {
UtilityFunctions::push_error("ProcNodes: Failed to load graph: ", p_path);
}
}
void ProcGraphEdit::_on_save_file_selected(const String &p_path) {
if (!edited_node || edited_node->get_graph().is_null()) return;
Error err = ResourceSaver::get_singleton()->save(edited_node->get_graph(), p_path);
if (err == OK) {
_current_graph_path = p_path;
UtilityFunctions::print("ProcNodes: Saved graph to ", p_path);
} else {
UtilityFunctions::push_error("ProcNodes: Failed to save graph: ", p_path, " (error ", (int)err, ")");
}
}
// ---------------------------------------------------------------------------
// Undo/Redo helper
// ---------------------------------------------------------------------------
EditorUndoRedoManager *ProcGraphEdit::_get_undo_redo() const {
EditorInterface *ei = EditorInterface::get_singleton();
if (ei) {
return ei->get_editor_undo_redo();
}
return nullptr;
}
// ---------------------------------------------------------------------------
// Connection request
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_connection_request(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) {
if (!edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) return;
int from_idx = String(p_from).get_slice("_", 1).to_int();
int to_idx = String(p_to).get_slice("_", 1).to_int();
EditorUndoRedoManager *ur = _get_undo_redo();
ur->create_action("Connect Proc Nodes");
int prev_source = graph->get_input_source_node(to_idx, p_to_port);
int prev_socket = graph->get_input_source_socket(to_idx, p_to_port);
ur->add_do_method(graph.ptr(), "connect_sockets", from_idx, p_from_port, to_idx, p_to_port);
ur->add_do_method(this, "_do_refresh");
if (prev_source >= 0) {
ur->add_undo_method(graph.ptr(), "connect_sockets", prev_source, prev_socket, to_idx, p_to_port);
} else {
ur->add_undo_method(graph.ptr(), "disconnect_sockets", from_idx, p_from_port, to_idx, p_to_port);
}
ur->add_undo_method(this, "_do_refresh");
ur->commit_action();
}
// ---------------------------------------------------------------------------
// Disconnection request
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_disconnection_request(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) {
if (!edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) return;
int from_idx = String(p_from).get_slice("_", 1).to_int();
int to_idx = String(p_to).get_slice("_", 1).to_int();
EditorUndoRedoManager *ur = _get_undo_redo();
ur->create_action("Disconnect Proc Nodes");
ur->add_do_method(graph.ptr(), "disconnect_sockets", from_idx, p_from_port, to_idx, p_to_port);
ur->add_do_method(this, "_do_refresh");
ur->add_undo_method(graph.ptr(), "connect_sockets", from_idx, p_from_port, to_idx, p_to_port);
ur->add_undo_method(this, "_do_refresh");
ur->commit_action();
}
// ---------------------------------------------------------------------------
// Delete nodes request
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_delete_nodes_request(const TypedArray<StringName> &p_nodes) {
if (!edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) return;
Vector<int> indices;
for (int i = 0; i < p_nodes.size(); i++) {
String name = p_nodes[i];
int idx = name.get_slice("_", 1).to_int();
indices.push_back(idx);
}
indices.sort();
indices.reverse();
EditorUndoRedoManager *ur = _get_undo_redo();
ur->create_action("Delete Proc Nodes");
Vector<ProcConnection> all_conns = graph->get_connections();
for (int i = 0; i < indices.size(); i++) {
ur->add_do_method(graph.ptr(), "remove_node", indices[i]);
}
ur->add_do_method(this, "_do_refresh");
for (int i = indices.size() - 1; i >= 0; i--) {
int idx = indices[i];
Ref<ProcNode> node = graph->get_node(idx);
if (node.is_valid()) {
ur->add_undo_method(graph.ptr(), "add_node", node);
}
}
for (int c = 0; c < all_conns.size(); c++) {
const ProcConnection &conn = all_conns[c];
ur->add_undo_method(graph.ptr(), "connect_sockets", conn.from_node, conn.from_socket, conn.to_node, conn.to_socket);
}
ur->add_undo_method(this, "_do_refresh");
ur->commit_action();
}
// ---------------------------------------------------------------------------
// Palette item double-clicked → add node
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_palette_item_activated() {
TreeItem *selected = palette_tree->get_selected();
if (!selected) return;
String type_name = selected->get_metadata(0);
if (type_name.is_empty()) return;
if (!edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
// Auto-create graph if none exists
if (graph.is_null()) {
graph.instantiate();
edited_node->set_graph(graph);
}
Ref<ProcNode> new_node = _create_proc_node(type_name);
if (new_node.is_null()) return;
new_node->set_graph_position(palette_spawn_position);
EditorUndoRedoManager *ur = _get_undo_redo();
ur->create_action("Add Proc Node: " + type_name);
ur->add_do_method(graph.ptr(), "add_node", new_node);
ur->add_do_method(this, "_do_refresh");
int expected_idx = graph->get_node_count();
ur->add_undo_method(graph.ptr(), "remove_node", expected_idx);
ur->add_undo_method(this, "_do_refresh");
ur->commit_action();
palette_popup->hide();
}
// ---------------------------------------------------------------------------
// Palette search
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_palette_search_changed(const String &p_text) {
_populate_palette_tree(p_text);
}
// ---------------------------------------------------------------------------
// Node selected → edit in inspector
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_node_selected(Node *p_node) {
GraphNode *gn = Object::cast_to<GraphNode>(p_node);
if (!gn || !edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) return;
int idx = String(gn->get_name()).get_slice("_", 1).to_int();
Ref<ProcNode> proc_node = graph->get_node(idx);
if (proc_node.is_null()) return;
// inspect_object shows properties without changing the edited object,
// so the plugin stays visible (edit_resource would trigger _handles→_make_visible(false))
EditorInterface *ei = EditorInterface::get_singleton();
if (ei) {
ei->inspect_object(proc_node.ptr(), "", true);
}
}
// ---------------------------------------------------------------------------
// End node move → save positions
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_end_node_move() {
if (!edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) return;
for (int i = 0; i < get_child_count(); i++) {
GraphNode *gn = Object::cast_to<GraphNode>(get_child(i));
if (!gn) continue;
int idx = String(gn->get_name()).get_slice("_", 1).to_int();
Ref<ProcNode> proc_node = graph->get_node(idx);
if (proc_node.is_valid()) {
proc_node->set_graph_position(gn->get_position_offset());
}
}
// Mark the graph resource as changed so Godot persists positions to disk.
graph->emit_changed();
}
// ---------------------------------------------------------------------------
// Bake mesh
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_bake_pressed() {
if (!edited_node) return;
Ref<ArrayMesh> mesh = edited_node->bake_mesh();
if (mesh.is_null()) {
UtilityFunctions::push_warning("ProcNodes: Bake failed — no mesh produced");
return;
}
String path = "res://baked_proc_mesh.tres";
Error err = ResourceSaver::get_singleton()->save(mesh, path);
if (err == OK) {
UtilityFunctions::print("ProcNodes: Baked mesh saved to ", path);
} else {
UtilityFunctions::push_error("ProcNodes: Failed to save baked mesh to ", path);
}
}
// ---------------------------------------------------------------------------
// Default value changed on inline editor
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_default_value_changed(const Variant &p_value, int p_node_idx, int p_socket_idx) {
if (!edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) return;
Ref<ProcNode> node = graph->get_node(p_node_idx);
if (node.is_null()) return;
node->set_input_default(p_socket_idx, p_value);
edited_node->push_param_delta(p_node_idx, p_socket_idx, p_value);
// Must recompile plan — GPU shaders inline parameter values
edited_node->invalidate_plan();
}
// ---------------------------------------------------------------------------
// Node property changed (ValueNode.value, VectorNode X/Y/Z)
// ---------------------------------------------------------------------------
void ProcGraphEdit::_on_node_property_changed(const Variant &p_value, int p_node_idx, const String &p_property) {
if (!edited_node) return;
Ref<ProcNodeGraph> graph = edited_node->get_graph();
if (graph.is_null()) return;
Ref<ProcNode> node = graph->get_node(p_node_idx);
if (node.is_null()) return;
ValueNode *val_node = Object::cast_to<ValueNode>(node.ptr());
VectorNode *vec_node = Object::cast_to<VectorNode>(node.ptr());
if (val_node && p_property == "value") {
val_node->set_value((float)(double)p_value);
edited_node->push_param_delta(p_node_idx, 0, Variant((double)val_node->get_value()));
} else if (vec_node) {
Vector3 v = vec_node->get_value();
float fval = (float)(double)p_value;
if (p_property == "X") v.x = fval;
else if (p_property == "Y") v.y = fval;
else if (p_property == "Z") v.z = fval;
vec_node->set_value(v);
edited_node->push_param_delta(p_node_idx, 0, v);
}
edited_node->invalidate_plan();
}
} // namespace godot
#endif // TOOLS_ENABLED