OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / editor / proc_graph_edit.cpp

1338 lines · cpp ref:
#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