Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
NodeGraphPanel.cpp
Go to the documentation of this file.
1/*
2 * Olympe Blueprint Editor - Node Graph Panel Implementation
3 */
4
5#include "NodeGraphPanel.h"
6#include "BlueprintEditor.h"
7#include "InspectorPanel.h"
9#include "Clipboard.h"
10#include "EditorContext.h"
12#include "BTNodeGraphManager.h"
13#include "EnumCatalogManager.h"
14#include "BPCommandSystem.h"
15#include "NodeStyleRegistry.h"
16#include "../system/system_consts.h"
17#include "../TaskSystem/AtomicTaskRegistry.h"
18#include "../TaskSystem/TaskGraphTypes.h"
19#include "../third_party/imgui/imgui.h"
20#include "../third_party/imnodes/imnodes.h"
21#include <iostream>
22#include <vector>
23#include <cstring>
24#include <cmath>
25#include <algorithm>
26#include <cctype>
27#include <memory>
28#include "../NodeGraphShared/BlueprintAdapter.h"
29
30// Use Blueprint namespace for command classes
31using namespace Olympe::Blueprint;
32
33namespace
34{
35 // UID generation constants for ImNodes
36 // These ensure unique IDs across multiple open graphs
37 constexpr int GRAPH_ID_MULTIPLIER = 10000; // Multiplier for graph ID in node UID calculation
38 constexpr int ATTR_ID_MULTIPLIER = 100; // Multiplier for node UID in attribute UID calculation
39 constexpr int LINK_ID_MULTIPLIER = 100000; // Multiplier for graph ID in link UID calculation
40
41 // Helper function to convert screen space coordinates to grid space coordinates
42 // Screen space: origin at upper-left corner of the window
43 // Grid space: origin at upper-left corner of the node editor, adjusted by panning
45 {
46 // Get the editor's screen space position
47 ImVec2 editorPos = ImGui::GetCursorScreenPos();
48
49 // Get the current panning offset
50 ImVec2 panning = ImNodes::EditorContextGetPanning();
51
52 // Convert: subtract editor position to get editor space, then subtract panning to get grid space
53 return ImVec2(screenPos.x - editorPos.x - panning.x,
54 screenPos.y - editorPos.y - panning.y);
55 }
56}
57
58namespace Olympe
59{
60
61// ============================================================================
62// Static member definitions
63// ============================================================================
64
66
67// ============================================================================
68// SetActiveDebugNode
69// ============================================================================
70
75
77 {
78 m_NodeNameBuffer[0] = '\0';
79 m_ContextMenuSearch[0] = '\0';
81
82 // Phase 8: seed the tab list with the root graph tab.
83 m_SubgraphTabs.emplace_back("root", "Root", "root");
85 }
86
90
92 {
93 std::cout << "[NodeGraphPanel] Initialized\n";
94
95 // Phase 35.0: Create dedicated imnodes context for this panel instance
96 m_imnodesContext = ImNodes::EditorContextCreate();
97
98 // Phase 36: Canvas editor created lazily in RenderGraph() when canvas size is known
99
100 // Set up autosave timing only. The per-save lambda overload of
101 // ScheduleSave() is used at each change site so that serialization
102 // happens on the UI thread and the background task only does I/O.
103 m_autosave.Init(nullptr, 1.5f, 60.0f);
104 }
105
107 {
108 // Phase 35.0: Free imnodes context
110 {
111 ImNodes::EditorContextFree(m_imnodesContext);
112 m_imnodesContext = nullptr;
113 }
114
116 std::cout << "[NodeGraphPanel] Shutdown\n";
117 }
118
120 {
121 ImGui::Begin("Node Graph Editor");
123 ImGui::End();
124 }
125
127 {
128 // Advance autosave timers each frame.
129 m_autosave.Tick(static_cast<double>(ImGui::GetTime()));
130
131 // Handle keyboard shortcuts
133
134 // View toggles: Snap-to-grid and Minimap
135 ImGui::Checkbox("Snap", &m_SnapToGrid);
136 if (ImGui::IsItemHovered())
137 ImGui::SetTooltip("Snap-to-grid (Ctrl+G)");
138 ImGui::SameLine();
139 ImGui::SetNextItemWidth(60.0f);
140 ImGui::DragFloat("Grid", &m_SnapGridSize, 1.0f, 4.0f, 128.0f, "%.0f");
141 if (ImGui::IsItemHovered())
142 ImGui::SetTooltip("Grid cell size");
143 ImGui::SameLine();
144
145 // Phase 36: Minimap toggle through canvas editor
146 bool minimapVisible = m_canvasEditor ? m_canvasEditor->IsMinimapVisible() : false;
147 if (ImGui::Checkbox("Map", &minimapVisible))
148 {
149 if (m_canvasEditor)
150 m_canvasEditor->SetMinimapVisible(minimapVisible);
151 }
152 if (ImGui::IsItemHovered())
153 ImGui::SetTooltip("Minimap (Ctrl+M)");
154 ImGui::SameLine();
155
156 // Phase 36: Minimap size control (Step 8)
158 {
159 ImGui::SetNextItemWidth(50.0f);
160 float minimapSize = m_canvasEditor->GetMinimapSize();
161 if (ImGui::DragFloat("##mmsize", &minimapSize, 0.01f, 0.05f, 0.5f, "%.2f"))
162 {
163 m_canvasEditor->SetMinimapSize(minimapSize);
164 }
165 if (ImGui::IsItemHovered())
166 ImGui::SetTooltip("Minimap scale (0.05-0.5)");
167 ImGui::SameLine();
168
169 // Phase 36: Minimap position control (Step 9)
170 ImGui::SetNextItemWidth(80.0f);
171 int minimapPosition = m_canvasEditor->GetMinimapPosition();
172 const char* positionNames[] = {"TopLeft", "TopRight", "BottomLeft", "BottomRight"};
173 if (ImGui::Combo("##mmpos", &minimapPosition, positionNames, IM_ARRAYSIZE(positionNames)))
174 {
175 m_canvasEditor->SetMinimapPosition(minimapPosition);
176 }
177 if (ImGui::IsItemHovered())
178 ImGui::SetTooltip("Minimap position");
179 ImGui::SameLine();
180 }
181 ImGui::SameLine();
182
183 // Debug info when runtime overlay is active
184 if (s_ActiveDebugNodeId >= 0)
185 {
186 ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.2f, 1.0f), " [DBG node %d]", s_ActiveDebugNodeId);
187 }
188
189 //ImGui::Separator();
190 ImGui::SameLine();
191
192 // Toolbar with Save/Save As buttons
194 if (activeGraph)
195 {
196 // Save button
197 bool canSave = activeGraph->HasFilepath();
198 if (!canSave)
199 ImGui::BeginDisabled();
200
201 if (ImGui::Button("Save"))
202 {
203 // Validate before saving
204 std::string validationError;
205 if (!activeGraph->ValidateGraph(validationError))
206 {
207 // Show validation error popup
208 ImGui::OpenPopup("ValidationError");
209 }
210 else
211 {
212 int graphId = NodeGraphManager::Get().GetActiveGraphId();
213 // Sync node positions from ImNodes before saving
215 const std::string& filepath = activeGraph->GetFilepath();
216 if (NodeGraphManager::Get().SaveGraph(graphId, filepath))
217 {
218 std::cout << "[NodeGraphPanel] Saved graph to: " << filepath << std::endl;
219 }
220 else
221 {
222 std::cout << "[NodeGraphPanel] Failed to save graph!" << std::endl;
223 }
224 }
225 }
226
227 if (!canSave)
228 ImGui::EndDisabled();
229
230 if (!canSave && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
231 {
232 ImGui::SetTooltip("No filepath set. Use 'Save As...' first.");
233 }
234
235 ImGui::SameLine();
236
237 // Save As button
238 if (ImGui::Button("Save As..."))
239 {
240 // TODO: Open file dialog to select save location
241 // For now, show popup to enter filename
242 ImGui::OpenPopup("SaveAsPopup");
243 }
244
245 // Show dirty indicator
246 ImGui::SameLine();
247 if (activeGraph->IsDirty())
248 {
249 ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "*");
250 if (ImGui::IsItemHovered())
251 {
252 ImGui::SetTooltip("Unsaved changes");
253 }
254 }
255
256 // Show currently selected entity at the top (informational only, doesn't block rendering)
258 if (selectedEntity != 0)
259 {
261 ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f),
262 "Editing for Entity: %s (ID: %llu)", info.name.c_str(), selectedEntity);
263 }
264 else
265 {
266 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.9f, 1.0f),
267 "Editing BehaviorTree Asset (no entity context)");
268 }
269
270
271 // Save As popup (simple text input for now)
272 static bool saveAsPopupOpen = false;
273 static char filepathBuffer[512] = "";
274
275 if (ImGui::BeginPopup("SaveAsPopup"))
276 {
277 // Clear buffer when popup first opens
278 if (!saveAsPopupOpen)
279 {
280 filepathBuffer[0] = '\0';
281 saveAsPopupOpen = true;
282 }
283
284 ImGui::Text("Save graph as:");
285 ImGui::InputText("Filepath", filepathBuffer, sizeof(filepathBuffer));
286
287 if (ImGui::Button("Save", ImVec2(120, 0)))
288 {
289 std::string filepath(filepathBuffer);
290 if (!filepath.empty())
291 {
292 // Validate before saving
293 std::string validationError;
294 if (!activeGraph->ValidateGraph(validationError))
295 {
296 // Show validation error
297 saveAsPopupOpen = false;
298 ImGui::CloseCurrentPopup();
299 ImGui::OpenPopup("ValidationError");
300 }
301 else
302 {
303 // Ensure .json extension (check that it ends with .json)
304 if (filepath.size() < 5 || filepath.substr(filepath.size() - 5) != ".json")
305 filepath += ".json";
306
307 int graphId = NodeGraphManager::Get().GetActiveGraphId();
308 // Sync node positions from ImNodes before saving
310 if (NodeGraphManager::Get().SaveGraph(graphId, filepath))
311 {
312 std::cout << "[NodeGraphPanel] Saved graph as: " << filepath << std::endl;
313 saveAsPopupOpen = false;
314 ImGui::CloseCurrentPopup();
315 }
316 else
317 {
318 std::cout << "[NodeGraphPanel] Failed to save graph!" << std::endl;
319 }
320 }
321 }
322 }
323 ImGui::SameLine();
324 if (ImGui::Button("Cancel", ImVec2(120, 0)))
325 {
326 saveAsPopupOpen = false;
327 ImGui::CloseCurrentPopup();
328 }
329 ImGui::EndPopup();
330 }
331 else
332 {
333 saveAsPopupOpen = false;
334 }
335
336 // Validation error popup
337 if (ImGui::BeginPopupModal("ValidationError", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
338 {
339 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Cannot save: Graph validation failed!");
340 ImGui::Separator();
341
342 std::string validationError;
343 if (!activeGraph->ValidateGraph(validationError))
344 {
345 ImGui::TextWrapped("%s", validationError.c_str());
346 }
347
348 ImGui::Separator();
349 if (ImGui::Button("OK", ImVec2(120, 0)))
350 {
351 ImGui::CloseCurrentPopup();
352 }
353 ImGui::EndPopup();
354 }
355
356 ImGui::Separator();
357 }
358
359 // Render graph tabs (unless suppressed by external renderer like BehaviorTreeRenderer)
361 {
363 ImGui::Separator();
364 }
365
366 // Render the active graph
368 if (activeGraph)
369 {
370 RenderGraph();
371 }
372 else
373 {
374 // Legacy UI disabled - BehaviorTreeRenderer and other editors should ensure
375 // a graph is set active before rendering begins.
376 ImGui::TextDisabled("No graph active. Create a new graph from the menu or load an existing one.");
377 }
378
379 // Render node edit modal
381 }
382
384 {
387
388 // Track which graph was requested to close
389 // Using static but ensuring cleanup to prevent issues with multiple rapid closes
390 static int graphToClose = -1;
391 static bool confirmationOpen = false;
392
393 if (ImGui::BeginTabBar("GraphTabs"))
394 {
395 for (int graphId : graphIds)
396 {
397 std::string graphName = NodeGraphManager::Get().GetGraphName(graphId);
398
399 // Add dirty indicator to tab name
401 if (graph && graph->IsDirty())
402 graphName += " *";
403
404 // Only set ImGuiTabItemFlags_SetSelected if this is the active graph
405 // This ensures the tab is selected visually without forcing re-selection each frame
407 if (graphId == currentActiveId)
408 {
410 }
411
412 // Enable close button for tabs
413 bool tabOpen = true;
414 if (ImGui::BeginTabItem(graphName.c_str(), &tabOpen, flags))
415 {
416 // Only change active graph if user clicked this tab (and it's not already active)
417 // BeginTabItem returns true when the tab content should be shown
418 if (currentActiveId != graphId)
419 {
421 }
422 ImGui::EndTabItem();
423 }
424
425 // If tab was closed (X button clicked)
426 if (!tabOpen)
427 {
428 // Only process if no confirmation dialog is currently open
429 if (!confirmationOpen)
430 {
431 // Check if graph has unsaved changes
432 if (graph && graph->IsDirty())
433 {
434 graphToClose = graphId;
435 confirmationOpen = true;
436 ImGui::OpenPopup("ConfirmCloseUnsaved");
437 }
438 else
439 {
440 // Close immediately if no unsaved changes
442 }
443 }
444 }
445 }
446
447 // Add "+" button for new graph
448 if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing))
449 {
450 ImGui::OpenPopup("CreateGraphPopup");
451 }
452
453 ImGui::EndTabBar();
454 }
455
456 // Confirmation popup for closing unsaved graph
457 if (ImGui::BeginPopupModal("ConfirmCloseUnsaved", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
458 {
459 ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "Warning: Unsaved Changes!");
460 ImGui::Separator();
461
462 std::string graphName = NodeGraphManager::Get().GetGraphName(graphToClose);
463 ImGui::TextWrapped("The graph '%s' has unsaved changes.", graphName.c_str());
464 ImGui::TextWrapped("Do you want to save before closing?");
465
466 ImGui::Separator();
467
468 // Save and Close button
469 if (ImGui::Button("Save and Close", ImVec2(120, 0)))
470 {
472 if (graph && graph->HasFilepath())
473 {
474 // Validate before saving
475 std::string validationError;
476 if (!graph->ValidateGraph(validationError))
477 {
478 // Show validation error
479 ImGui::CloseCurrentPopup();
480 ImGui::OpenPopup("ValidationError");
481 }
482 else
483 {
484 // Save and close
485 if (NodeGraphManager::Get().SaveGraph(graphToClose, graph->GetFilepath()))
486 {
488 graphToClose = -1;
489 confirmationOpen = false;
490 ImGui::CloseCurrentPopup();
491 }
492 }
493 }
494 else
495 {
496 // No filepath - need Save As
497 confirmationOpen = false;
498 ImGui::CloseCurrentPopup();
499 ImGui::OpenPopup("SaveAsPopup");
500 }
501 }
502
503 ImGui::SameLine();
504
505 // Close without saving button
506 if (ImGui::Button("Close Without Saving", ImVec2(150, 0)))
507 {
509 graphToClose = -1;
510 confirmationOpen = false;
511 ImGui::CloseCurrentPopup();
512 }
513
514 ImGui::SameLine();
515
516 // Cancel button
517 if (ImGui::Button("Cancel", ImVec2(120, 0)))
518 {
519 graphToClose = -1;
520 confirmationOpen = false;
521 ImGui::CloseCurrentPopup();
522 }
523
524 ImGui::EndPopup();
525 }
526 else
527 {
528 // Popup closed without action - reset state
529 if (confirmationOpen && graphToClose >= 0)
530 {
531 confirmationOpen = false;
532 graphToClose = -1;
533 }
534 }
535
536 // Create graph popup
537 if (ImGui::BeginPopup("CreateGraphPopup"))
538 {
539 if (ImGui::MenuItem("New Behavior Tree"))
540 {
541 NodeGraphManager::Get().CreateGraph("New Behavior Tree", "BehaviorTree");
542 }
543 if (ImGui::MenuItem("New HFSM"))
544 {
545 NodeGraphManager::Get().CreateGraph("New HFSM", "HFSM");
546 }
547 ImGui::EndPopup();
548 }
549 }
550
552 {
554 if (!graph)
555 return;
556
557 // Get the Graph ID for creating unique UIDs
558 int graphID = NodeGraphManager::Get().GetActiveGraphId();
559 if (graphID < 0)
560 {
561 std::cerr << "[NodeGraphPanel] Invalid graph ID" << std::endl;
562 return;
563 }
564
565 // Ensure canvas has valid size (minimum 1px to render)
566 constexpr float MIN_CANVAS_SIZE = 1.0f;
567 ImVec2 canvasSize = ImGui::GetContentRegionAvail();
568 if (canvasSize.x < MIN_CANVAS_SIZE || canvasSize.y < MIN_CANVAS_SIZE)
569 {
570 ImGui::Text("Canvas too small to render graph");
571 return;
572 }
573
574 // Phase 36: Initialize or update canvas editor for minimap support
575 if (!m_canvasEditor)
576 {
577 ImVec2 canvasScreenPos = ImGui::GetCursorScreenPos();
578 m_canvasEditor = std::make_unique<ImNodesCanvasEditor>(
579 "BehaviorTreeEditor",
581 canvasSize,
583 );
584 }
585
586 // Phase 35.0: Set this panel's imnodes context active before rendering
587 // Prevents viewport state collision with other graph renderers (e.g., VisualScriptEditorPanel)
589 {
590 ImNodes::EditorContextSet(m_imnodesContext);
591 }
592
593 ImNodes::BeginNodeEditor();
594
595 // Render all nodes
596 auto nodes = graph->GetAllNodes();
597
598 // Clear positioned-nodes tracking when the active graph changes so that
599 // the initial positions of a newly-opened graph are applied correctly.
600 if (graphID != m_lastActiveGraphId)
601 {
602 m_positionedNodes.clear();
603 m_lastActiveGraphId = graphID;
604 }
605
606 // Build set of connected attribute IDs from all graph links so that
607 // connected pins render filled and unconnected pins render outlined.
608 std::unordered_set<int> connectedAttrIDs;
609 {
610 auto allLinks = graph->GetAllLinks();
611 for (size_t li = 0; li < allLinks.size(); ++li)
612 {
613 int fromUID = (graphID * GRAPH_ID_MULTIPLIER) + allLinks[li].fromNode;
614 int toUID = (graphID * GRAPH_ID_MULTIPLIER) + allLinks[li].toNode;
615 connectedAttrIDs.insert(fromUID * ATTR_ID_MULTIPLIER + 2); // output attr
616 connectedAttrIDs.insert(toUID * ATTR_ID_MULTIPLIER + 1); // input attr
617 }
618 }
619
620 for (GraphNode* node : nodes)
621 {
622 // Generate a global unique UID for ImNodes
623 // Format: graphID * GRAPH_ID_MULTIPLIER + nodeID
624 // This ensures no node from different graphs has the same UID
625 int globalNodeUID = (graphID * GRAPH_ID_MULTIPLIER) + node->id;
626
627 // Set node position BEFORE rendering (ImNodes requirement), but only
628 // once per node so that subsequent user drags are not overridden.
630 {
631 ImNodes::SetNodeGridSpacePos(globalNodeUID, ImVec2(node->posX, node->posY));
633 }
634
635 // Apply per-type title-bar colours from NodeStyleRegistry
636 const NodeStyle& style = NodeStyleRegistry::Get().GetStyle(node->type);
637
638 // Debug overlay: tint the active node bright yellow
639 ImU32 headerColor = style.headerColor;
640 ImU32 headerHoveredColor = style.headerHoveredColor;
641 ImU32 headerSelectedColor = style.headerSelectedColor;
642
643 // Phase 38b: Visual distinction for OnEvent and Root nodes
644 // Check if this is a Root node or OnEvent node based on eventType field
645 bool isRootNode = (node->id == graph->GetRootNodeId());
646 bool isOnEventNode = (node->type == NodeType::BT_OnEvent);
647
648 if (isRootNode)
649 {
650 // Root node: green color (from BT_ROOT_NODE_COLOR)
652 headerHoveredColor = IM_COL32(100, 255, 100, 255); // Lighter green
653 headerSelectedColor = IM_COL32(150, 255, 150, 255); // Even lighter green
654 }
655
656 if (s_ActiveDebugNodeId == node->id)
657 {
658 headerColor = IM_COL32(200, 180, 20, 255);
659 headerHoveredColor = IM_COL32(220, 200, 40, 255);
660 headerSelectedColor = IM_COL32(240, 220, 60, 255);
661 }
662
663 ImNodes::PushColorStyle(ImNodesCol_TitleBar, headerColor);
664 ImNodes::PushColorStyle(ImNodesCol_TitleBarHovered, headerHoveredColor);
665 ImNodes::PushColorStyle(ImNodesCol_TitleBarSelected, headerSelectedColor);
666
667 ImNodes::BeginNode(globalNodeUID);
668
670
671 ImNodes::EndNode();
672
673 ImNodes::PopColorStyle();
674 ImNodes::PopColorStyle();
675 ImNodes::PopColorStyle();
676 }
677
678 // Render all links with global UIDs.
679 // Pass 1: draw only inactive links (baseline blue).
680 // Active links are skipped here; RenderActiveLinks() draws them with glow.
681 auto links = graph->GetAllLinks();
682 for (size_t i = 0; i < links.size(); ++i)
683 {
684 const GraphLink& link = links[i];
685
686 // Generate global UIDs for the attributes
687 int fromNodeUID = (graphID * GRAPH_ID_MULTIPLIER) + link.fromNode;
688 int toNodeUID = (graphID * GRAPH_ID_MULTIPLIER) + link.toNode;
689
690 int fromAttrUID = fromNodeUID * ATTR_ID_MULTIPLIER + 2; // Output attribute
691 int toAttrUID = toNodeUID * ATTR_ID_MULTIPLIER + 1; // Input attribute
692
693 // Link ID must also be unique globally
694 int globalLinkUID = (graphID * LINK_ID_MULTIPLIER) + (int)i + 1;
695
696 // Skip active links in this baseline pass to avoid double-draw;
697 // RenderActiveLinks() will overlay the glow for them.
698 bool isActive = (s_ActiveDebugNodeId >= 0 &&
699 (link.fromNode == s_ActiveDebugNodeId ||
700 link.toNode == s_ActiveDebugNodeId));
701 if (isActive)
702 continue;
703
704 ImNodes::Link(globalLinkUID, fromAttrUID, toAttrUID);
705 }
706
707 // Phase 36: Render minimap through canvas editor interface
708 if (m_canvasEditor)
709 {
710 m_canvasEditor->RenderMinimap();
711 }
712
713 ImNodes::EndNodeEditor();
714
715 // Overlay Bezier glow for links connected to the active debug node.
716 // Must be called after EndNodeEditor() so screen-space positions are valid.
717 RenderActiveLinks(graph, graphID);
718
719 // Phase 38: Render execution indices on connections for Sequence/Selector children
721
722 // Handle node interactions with UID mapping
723 HandleNodeInteractions(graphID);
724
725 // Handle link selection
726 int numSelectedLinks = ImNodes::NumSelectedLinks();
727 if (numSelectedLinks > 0)
728 {
729 std::vector<int> selectedLinks(numSelectedLinks);
730 ImNodes::GetSelectedLinks(selectedLinks.data());
731 if (selectedLinks.size() > 0)
733 }
734
735 // Handle Delete key for nodes and links (only if canDelete)
736 if (ImGui::IsKeyPressed(ImGuiKey_Delete) && EditorContext::Get().CanDelete())
737 {
738 if (m_SelectedNodeId != -1)
739 {
740 // Delete selected node
741 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
742 auto cmd = std::make_unique<DeleteNodeCommand>(graphId, m_SelectedNodeId);
744 m_SelectedNodeId = -1;
745 }
746 else if (m_SelectedLinkId != -1)
747 {
748 // Delete selected link
749 // Extract the link index from the global link UID
750 int linkIndex = (m_SelectedLinkId - (graphID * LINK_ID_MULTIPLIER)) - 1;
751
752 auto links = graph->GetAllLinks();
753 if (linkIndex >= 0 && linkIndex < (int)links.size())
754 {
755 const GraphLink& link = links[linkIndex];
756 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
757 auto cmd = std::make_unique<UnlinkNodesCommand>(graphId, link.fromNode, link.toNode);
759 m_SelectedLinkId = -1;
760 }
761 }
762 }
763
764 // Check for node hover (for property panel selection)
765 int hoveredNodeUID = -1;
766 if (ImNodes::IsNodeHovered(&hoveredNodeUID))
767 {
768 // Node hover detection - used for selection
769 // (Legacy double-click modal removed - now use property panel instead)
770 }
771
772 // Right-click context menu on node
773 if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && hoveredNodeUID != -1)
774 {
775 // Convert global UID to local node ID
777 ImGui::OpenPopup("NodeContextMenu");
778 }
779
780 // Handle right-click on canvas for node creation menu (only if canCreate)
781 if (EditorContext::Get().CanCreate() &&
782 ImGui::IsMouseReleased(ImGuiMouseButton_Right) &&
783 ImNodes::IsEditorHovered() &&
784 !ImNodes::IsNodeHovered(&hoveredNodeUID))
785 {
786 ImGui::OpenPopup("NodeCreationMenu");
787 ImVec2 mousePos = ImGui::GetMousePos();
790 }
791
792 // Context menu on node
793 if (ImGui::BeginPopup("NodeContextMenu"))
794 {
796 if (selectedNode)
797 {
798 ImGui::Text("Node: %s (ID: %d)", selectedNode->name.c_str(), m_SelectedNodeId);
799 }
800 else
801 {
802 ImGui::Text("Node: %d", m_SelectedNodeId);
803 }
804 ImGui::Separator();
805
806 // Edit menu item removed - now use right-panel property editor instead
807 // if (ImGui::MenuItem("Edit Properties", "Double-click"))
808
809 // Duplicate only shown if allowed
810 if (EditorContext::Get().CanEdit() && EditorContext::Get().CanCreate())
811 {
812 if (ImGui::MenuItem("Duplicate", "Ctrl+D"))
813 {
815 int graphId = NodeGraphManager::Get().GetActiveGraphId();
817 adapter.DuplicateNode(m_SelectedNodeId);
818 ImGui::CloseCurrentPopup();
819 }
820 }
821
822 ImGui::Separator();
823
824 // Phase 38b: Delete only shown if allowed AND node is not the Root
825 if (EditorContext::Get().CanDelete())
826 {
827 bool isRootNode = (m_SelectedNodeId == graph->rootNodeId);
828
829 if (isRootNode)
830 {
831 // Root node cannot be deleted - show disabled item with tooltip
832 ImGui::BeginDisabled(true);
833 ImGui::MenuItem("Delete", "Del");
834 ImGui::EndDisabled();
835 if (ImGui::IsItemHovered())
836 {
837 ImGui::SetTooltip("Root node cannot be deleted");
838 }
839 }
840 else if (ImGui::MenuItem("Delete", "Del"))
841 {
842 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
843 auto cmd = std::make_unique<DeleteNodeCommand>(graphId, m_SelectedNodeId);
845 m_SelectedNodeId = -1;
846 ImGui::CloseCurrentPopup();
847 }
848 }
849
850 ImGui::EndPopup();
851 }
852
853 // Right-click context menu on link
854 int hoveredLinkUID = -1;
855 if (ImNodes::IsLinkHovered(&hoveredLinkUID))
856 {
857 if (ImGui::IsMouseReleased(ImGuiMouseButton_Right))
858 {
860 ImGui::OpenPopup("LinkContextMenu");
861 }
862 }
863
864 // Context menu on link
865 if (ImGui::BeginPopup("LinkContextMenu"))
866 {
867 // Extract link information
868 int linkIndex = (m_SelectedLinkId - (graphID * LINK_ID_MULTIPLIER)) - 1;
869 auto links = graph->GetAllLinks();
870
871 if (linkIndex >= 0 && linkIndex < (int)links.size())
872 {
873 const GraphLink& link = links[linkIndex];
874 GraphNode* fromNode = graph->GetNode(link.fromNode);
875 GraphNode* toNode = graph->GetNode(link.toNode);
876
877 if (fromNode && toNode)
878 {
879 ImGui::Text("Link: %s -> %s", fromNode->name.c_str(), toNode->name.c_str());
880 }
881 else
882 {
883 ImGui::Text("Link: %d -> %d", link.fromNode, link.toNode);
884 }
885 ImGui::Separator();
886
887 // Delete link
888 if (EditorContext::Get().CanDelete())
889 {
890 if (ImGui::MenuItem("Delete Link", "Del"))
891 {
892 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
893 auto cmd = std::make_unique<UnlinkNodesCommand>(graphId, link.fromNode, link.toNode);
895 m_SelectedLinkId = -1;
896 ImGui::CloseCurrentPopup();
897 }
898 }
899 }
900 else
901 {
902 ImGui::Text("Invalid link");
903 }
904
905 ImGui::EndPopup();
906 }
907
908 // Handle drag & drop from node palette
909 if (ImGui::BeginDragDropTarget())
910 {
911 // Handle BT node palette payload (NODE_TYPE = string-based type)
912 if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("NODE_TYPE"))
913 {
914 if (!payload->Data || payload->DataSize == 0)
915 {
916 std::cerr << "[NodeGraphPanel] ERROR: NODE_TYPE payload has null or empty data\n";
917 }
918 else
919 {
920 // Copy payload data to local string with bounds checking
921 size_t maxSize = 256; // Reasonable max for node type strings
922 size_t dataSize = (payload->DataSize < maxSize) ? payload->DataSize : maxSize;
923
924 std::string nodeTypeData;
925 nodeTypeData.resize(dataSize);
926 std::memcpy(&nodeTypeData[0], payload->Data, dataSize);
927
928 // Ensure NUL-termination
929 if (nodeTypeData.find('\0') == std::string::npos)
930 {
931 // Truncate at first non-printable or add terminator
932 size_t validLen = 0;
933 for (size_t i = 0; i < nodeTypeData.size(); ++i)
934 {
935 if (nodeTypeData[i] == '\0' || nodeTypeData[i] < 32)
936 break;
937 validLen = i + 1;
938 }
939 nodeTypeData.resize(validLen);
940 }
941
942 // Remove any trailing null bytes
943 while (!nodeTypeData.empty() && nodeTypeData.back() == '\0')
944 nodeTypeData.pop_back();
945
946 std::cout << "[NodeGraphPanel] Received NODE_TYPE payload: " << nodeTypeData << "\n";
947
948 // Convert screen space coordinates to grid space
949 ImVec2 mouseScreenPos = ImGui::GetMousePos();
950 ImVec2 canvasPos = ScreenSpaceToGridSpace(mouseScreenPos);
951
952 bool validNode = false;
953
954 // Parse the type and create appropriate node
955 if (nodeTypeData.find("Action:") == 0)
956 {
957 std::string actionType = nodeTypeData.substr(7);
958
959 // Validate action type exists in catalog
960 if (EnumCatalogManager::Get().IsValidActionType(actionType))
961 {
962 int nodeId = graph->CreateNode(NodeType::BT_Action, canvasPos.x, canvasPos.y, actionType);
963 GraphNode* node = graph->GetNode(nodeId);
964 if (node)
965 {
966 node->actionType = actionType;
967 validNode = true;
968 std::cout << "[NodeGraphPanel] Created Action node: " << actionType
969 << " at canvas pos (" << canvasPos.x << ", " << canvasPos.y << ")\n";
970 }
971 }
972 else
973 {
974 std::cerr << "[NodeGraphPanel] ERROR: Invalid ActionType: " << actionType << "\n";
975 ImGui::SetTooltip("Invalid ActionType: %s", actionType.c_str());
976 }
977 }
978 else if (nodeTypeData.find("Condition:") == 0)
979 {
980 std::string conditionType = nodeTypeData.substr(10);
981
982 // Validate condition type exists in catalog
983 if (EnumCatalogManager::Get().IsValidConditionType(conditionType))
984 {
985 int nodeId = graph->CreateNode(NodeType::BT_Condition, canvasPos.x, canvasPos.y, conditionType);
986 GraphNode* node = graph->GetNode(nodeId);
987 if (node)
988 {
989 node->conditionType = conditionType;
990 validNode = true;
991 std::cout << "[NodeGraphPanel] Created Condition node: " << conditionType
992 << " at canvas pos (" << canvasPos.x << ", " << canvasPos.y << ")\n";
993 }
994 }
995 else
996 {
997 std::cerr << "[NodeGraphPanel] ERROR: Invalid ConditionType: " << conditionType << "\n";
998 ImGui::SetTooltip("Invalid ConditionType: %s", conditionType.c_str());
999 }
1000 }
1001 else if (nodeTypeData.find("Decorator:") == 0)
1002 {
1003 std::string decoratorType = nodeTypeData.substr(10);
1004
1005 // Validate decorator type exists in catalog
1006 if (EnumCatalogManager::Get().IsValidDecoratorType(decoratorType))
1007 {
1008 int nodeId = graph->CreateNode(NodeType::BT_Decorator, canvasPos.x, canvasPos.y, decoratorType);
1009 GraphNode* node = graph->GetNode(nodeId);
1010 if (node)
1011 {
1012 node->decoratorType = decoratorType;
1013 validNode = true;
1014 std::cout << "[NodeGraphPanel] Created Decorator node: " << decoratorType
1015 << " at canvas pos (" << canvasPos.x << ", " << canvasPos.y << ")\n";
1016 }
1017 }
1018 else
1019 {
1020 std::cerr << "[NodeGraphPanel] ERROR: Invalid DecoratorType: " << decoratorType << "\n";
1021 ImGui::SetTooltip("Invalid DecoratorType: %s", decoratorType.c_str());
1022 }
1023 }
1024 else if (nodeTypeData == "Sequence" || nodeTypeData == "Selector")
1025 {
1027 int nodeId = graph->CreateNode(type, canvasPos.x, canvasPos.y, nodeTypeData);
1028 if (nodeId > 0)
1029 {
1030 validNode = true;
1031 std::cout << "[NodeGraphPanel] Created " << nodeTypeData << " node"
1032 << " at canvas pos (" << canvasPos.x << ", " << canvasPos.y << ")\n";
1033 }
1034 }
1035 else
1036 {
1037 std::cerr << "[NodeGraphPanel] ERROR: Unknown node type: " << nodeTypeData << "\n";
1038 ImGui::SetTooltip("Unknown node type: %s", nodeTypeData.c_str());
1039 }
1040
1041 if (!validNode)
1042 {
1043 std::cerr << "[NodeGraphPanel] Failed to create node from DnD payload\n";
1044 }
1045 }
1046 }
1047 // Handle VS node palette payload (VS_NODE_TYPE_ENUM = TaskNodeType enum as uint8_t)
1048 else if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("VS_NODE_TYPE_ENUM"))
1049 {
1050 if (payload->Data && payload->DataSize == sizeof(uint8_t))
1051 {
1052 uint8_t enumValue = *static_cast<const uint8_t*>(payload->Data);
1053 TaskNodeType nodeType = static_cast<TaskNodeType>(enumValue);
1054
1055 std::cout << "[NodeGraphPanel] Received VS_NODE_TYPE_ENUM payload: "
1056 << static_cast<int>(enumValue) << "\n";
1057
1058 std::string nodeTypeName;
1059 switch (nodeType)
1060 {
1061 case TaskNodeType::EntryPoint: nodeTypeName = "EntryPoint"; break;
1062 case TaskNodeType::Branch: nodeTypeName = "Branch"; break;
1063 case TaskNodeType::VSSequence: nodeTypeName = "Sequence"; break;
1064 case TaskNodeType::While: nodeTypeName = "While"; break;
1065 case TaskNodeType::ForEach: nodeTypeName = "ForEach"; break;
1066 case TaskNodeType::DoOnce: nodeTypeName = "DoOnce"; break;
1067 case TaskNodeType::Delay: nodeTypeName = "Delay"; break;
1068 case TaskNodeType::Switch: nodeTypeName = "Switch"; break;
1069 case TaskNodeType::AtomicTask: nodeTypeName = "AtomicTask"; break;
1070 case TaskNodeType::GetBBValue: nodeTypeName = "GetBBValue"; break;
1071 case TaskNodeType::SetBBValue: nodeTypeName = "SetBBValue"; break;
1072 case TaskNodeType::MathOp: nodeTypeName = "MathOp"; break;
1073 case TaskNodeType::SubGraph: nodeTypeName = "SubGraph"; break;
1074 default:
1075 std::cerr << "[NodeGraphPanel] ERROR: Unhandled TaskNodeType enum: "
1076 << static_cast<int>(enumValue) << "\n";
1077 break;
1078 }
1079
1080 if (!nodeTypeName.empty())
1081 {
1082 ImVec2 mousePos = ImGui::GetMousePos();
1083 // CreateNewNode expects screen coordinates; it converts to canvas space internally
1085 std::cout << "[NodeGraphPanel] Created VS node from palette: " << nodeTypeName << "\n";
1086 }
1087 else
1088 {
1089 std::cerr << "[NodeGraphPanel] Failed to create node from VS_NODE_TYPE_ENUM payload\n";
1090 }
1091 }
1092 else
1093 {
1094 std::cerr << "[NodeGraphPanel] ERROR: Invalid VS_NODE_TYPE_ENUM payload - ";
1095 if (!payload->Data)
1096 std::cerr << "null data\n";
1097 else
1098 std::cerr << "wrong size (expected " << sizeof(uint8_t)
1099 << ", got " << payload->DataSize << ")\n";
1100 }
1101 }
1102
1103 ImGui::EndDragDropTarget();
1104 }
1105
1106 // Update node positions using global UIDs
1107 for (GraphNode* node : nodes)
1108 {
1109 int globalNodeUID = (graphID * GRAPH_ID_MULTIPLIER) + node->id;
1110 ImVec2 pos = ImNodes::GetNodeGridSpacePos(globalNodeUID);
1111
1112 // Apply snap-to-grid when enabled.
1113 if (m_SnapToGrid && m_SnapGridSize > 0.0f)
1114 {
1115 pos.x = std::roundf(pos.x / m_SnapGridSize) * m_SnapGridSize;
1116 pos.y = std::roundf(pos.y / m_SnapGridSize) * m_SnapGridSize;
1117 // Push snapped position back so the node visually snaps.
1118 ImNodes::SetNodeGridSpacePos(globalNodeUID, pos);
1119 }
1120
1121 // Check if position changed
1122 if (node->posX != pos.x || node->posY != pos.y)
1123 {
1124 node->posX = pos.x;
1125 node->posY = pos.y;
1126
1127 // Mark graph as dirty when node is moved
1128 if (graph)
1129 graph->MarkDirty();
1130
1131 // Schedule an autosave. Serialization runs on the UI thread
1132 // inside Tick(); the background task only writes the file.
1133 {
1134 int capturedGid = graphID;
1135 std::string fp = (graph && graph->HasFilepath())
1136 ? graph->GetFilepath() : "";
1138 static_cast<double>(ImGui::GetTime()),
1139 [capturedGid]() -> std::string
1140 {
1142 if (g && g->IsDirty())
1143 return g->ToJson().dump(2);
1144 return std::string();
1145 },
1146 fp,
1147 "GameData/AI/autosave_");
1148 }
1149 }
1150 }
1151 }
1152
1154 {
1156 if (!graph)
1157 return;
1158
1159 std::vector<GraphNode*> nodes = graph->GetAllNodes();
1160
1161 // Update all node positions from ImNodes
1162 for (GraphNode* node : nodes)
1163 {
1164 int globalNodeUID = (graphID * GRAPH_ID_MULTIPLIER) + node->id;
1165 ImVec2 pos = ImNodes::GetNodeGridSpacePos(globalNodeUID);
1166
1167 // Update the node's position in the graph data
1168 if (node->posX != pos.x || node->posY != pos.y)
1169 {
1170 node->posX = pos.x;
1171 node->posY = pos.y;
1172 }
1173 }
1174 }
1175
1177 {
1179 if (!graph)
1180 return;
1181
1182 // Handle node selection
1183 int numSelected = ImNodes::NumSelectedNodes();
1184 if (numSelected > 0)
1185 {
1186 std::vector<int> selectedUIDs(numSelected);
1187 ImNodes::GetSelectedNodes(selectedUIDs.data());
1188
1189 // Convert the first global UID to local Node ID
1190 if (!selectedUIDs.empty())
1191 {
1194
1195 // NEW: If this is an AtomicTask node, display its parameters in the inspector
1198 {
1199 // Get the task registry to find the task display name
1201 std::string displayName = (taskSpec) ? taskSpec->displayName : selectedNode->actionType;
1202
1203 // Build empty parameters map (can be extended to read from node data)
1204 std::unordered_map<std::string, std::string> params;
1205
1206 // Display in the inspector panel via singleton
1208 if (inspector)
1209 {
1210 inspector->SetSelectedActionNode(selectedNode->actionType, displayName, params);
1211 }
1212 }
1213 else
1214 {
1215 // Not an action node - clear the action panel
1217 if (inspector)
1218 {
1219 inspector->ClearSelectedActionNode();
1220 }
1221 }
1222 }
1223 }
1224
1225 // Handle link creation (only if canLink)
1227 if (EditorContext::Get().CanLink() && ImNodes::IsLinkCreated(&startAttrUID, &endAttrUID))
1228 {
1229 // Extract the global UIDs of nodes
1230 int startNodeGlobalUID = startAttrUID / ATTR_ID_MULTIPLIER;
1231 int endNodeGlobalUID = endAttrUID / ATTR_ID_MULTIPLIER;
1232
1233 // Convert to local IDs
1236
1237 // Create the link with local IDs via adapter
1241 }
1242 }
1243
1244 // =========================================================================
1245 // RenderTypedPin
1246 // =========================================================================
1247
1248 void NodeGraphPanel::RenderTypedPin(int attrId, const char* label,
1249 bool isInput, bool isExec,
1250 const std::unordered_set<int>& connectedAttrIDs)
1251 {
1252 bool connected = connectedAttrIDs.count(attrId) > 0;
1253 // Filled when connected, outlined when not.
1257
1258 if (isInput)
1259 {
1260 ImNodes::BeginInputAttribute(attrId, shape);
1261 ImGui::TextUnformatted(label);
1262 ImNodes::EndInputAttribute();
1263 }
1264 else
1265 {
1266 ImNodes::BeginOutputAttribute(attrId, shape);
1267 ImGui::TextUnformatted(label);
1268 ImNodes::EndOutputAttribute();
1269 }
1270 }
1271
1272 // =========================================================================
1273 // RenderNodePinsAndContent
1274 // =========================================================================
1275
1277 int graphID,
1278 const std::unordered_set<int>& connectedAttrIDs)
1279 {
1280 // ----- Title bar (icon + name) --------------------------------------
1281 const NodeStyle& style = NodeStyleRegistry::Get().GetStyle(node->type);
1282
1283 ImNodes::BeginNodeTitleBar();
1284 if (style.icon[0] != '\0')
1285 ImGui::Text("[%s] %s", style.icon, node->name.c_str());
1286 else
1287 ImGui::TextUnformatted(node->name.c_str());
1288 ImNodes::EndNodeTitleBar();
1289
1290 // ---- Comment box: no pins, just an editable text area -------------
1291 if (node->type == NodeType::Comment)
1292 {
1293 // Provide a fixed-size text display; the text is stored in parameters["text"].
1294 auto it = node->parameters.find("text");
1295 std::string commentText = (it != node->parameters.end()) ? it->second : "";
1296 ImGui::SetNextItemWidth(180.0f);
1297 char commentBuf[1024];
1298 strncpy_s(commentBuf, commentText.c_str(), sizeof(commentBuf) - 1);
1299 commentBuf[sizeof(commentBuf) - 1] = '\0';
1300 std::string inputId = std::string("##comment") + std::to_string(node->id);
1301 if (ImGui::InputTextMultiline(inputId.c_str(), commentBuf, sizeof(commentBuf),
1302 ImVec2(180.0f, 60.0f)))
1303 {
1304 node->parameters["text"] = commentBuf;
1306 if (g) g->MarkDirty();
1307 }
1308 // Comment boxes have no exec/data pins.
1309 return;
1310 }
1311
1312 // ----- Exec pins: triangle shape -----------------------------------
1313 // Sequence and Selector are "composite" flow-control nodes -> exec pins.
1314 // All others use data (circle) pins.
1315 bool isExec = (node->type == NodeType::BT_Sequence ||
1316 node->type == NodeType::BT_Selector);
1317
1318 int inputAttrUID = globalNodeUID * ATTR_ID_MULTIPLIER + 1;
1319 int outputAttrUID = globalNodeUID * ATTR_ID_MULTIPLIER + 2;
1320
1321 // Root and OnEvent are entry points - they should NOT have input pins
1322 bool hasInputPin = (node->type != NodeType::BT_Root &&
1323 node->type != NodeType::BT_OnEvent);
1324
1325 // Use 2-column layout to align input pins (left) with output pins (right) on the same Y
1326 ImGui::Columns(3, "node_pins", false);
1327 ImGui::SetColumnWidth(0, 60.0f); // Left column: input pin
1328 ImGui::SetColumnWidth(1, 100.0f); // Center column: node content
1329
1330 // ---- LEFT COLUMN: Input Pin ----
1331 if (hasInputPin)
1333
1334 // ---- CENTER COLUMN: Node content ----
1335 ImGui::NextColumn();
1336 if (node->type == NodeType::BT_Action && !node->actionType.empty())
1337 ImGui::Text("%s", node->actionType.c_str());
1338 else if (node->type == NodeType::BT_Condition && !node->conditionType.empty())
1339 ImGui::Text("%s", node->conditionType.c_str());
1340 else if (node->type == NodeType::BT_Decorator && !node->decoratorType.empty())
1341 ImGui::Text("%s", node->decoratorType.c_str());
1342 else
1343 ImGui::Text("%s", NodeTypeToString(node->type));
1344
1345 // ---- RIGHT COLUMN: Output Pin ----
1346 ImGui::NextColumn();
1348
1349 ImGui::Columns(1); // End columns
1350 }
1351
1352 // =========================================================================
1353 // RenderActiveLinks
1354 // =========================================================================
1355
1357 {
1358 if (s_ActiveDebugNodeId < 0 || graph == nullptr)
1359 return;
1360
1361 ImDrawList* drawList = ImGui::GetWindowDrawList();
1362 if (drawList == nullptr)
1363 return;
1364
1365 // Pulsing amber/yellow: oscillate alpha and colour over time.
1366 float t = 0.5f + 0.5f * std::sin(static_cast<float>(ImGui::GetTime()) * 4.0f);
1367 float alpha = 0.6f + 0.4f * t;
1368
1369 ImU32 glowWide = IM_COL32(255, 200, 50, static_cast<int>(alpha * 80.0f));
1371 static_cast<int>(180.0f + t * 75.0f),
1372 static_cast<int>(140.0f + t * 115.0f),
1373 10,
1374 static_cast<int>(alpha * 255.0f));
1375
1376 auto links = graph->GetAllLinks();
1377 for (size_t i = 0; i < links.size(); ++i)
1378 {
1379 const GraphLink& link = links[i];
1380 bool isActive = (link.fromNode == s_ActiveDebugNodeId ||
1381 link.toNode == s_ActiveDebugNodeId);
1382 if (!isActive)
1383 continue;
1384
1385 int fromUID = graphID * GRAPH_ID_MULTIPLIER + link.fromNode;
1386 int toUID = graphID * GRAPH_ID_MULTIPLIER + link.toNode;
1387
1388 ImVec2 fromPos = ImNodes::GetNodeScreenSpacePos(fromUID);
1389 ImVec2 fromDim = ImNodes::GetNodeDimensions(fromUID);
1390 ImVec2 toPos = ImNodes::GetNodeScreenSpacePos(toUID);
1391 ImVec2 toDim = ImNodes::GetNodeDimensions(toUID);
1392
1393 // Pin-center anchors: output pin is at the right edge + PinOffset,
1394 // input pin is at the left edge - PinOffset. Both at centre height.
1395 const float po = ImNodes::GetStyle().PinOffset;
1396 ImVec2 p1 = ImVec2(fromPos.x + fromDim.x + po, fromPos.y + fromDim.y * 0.5f);
1397 ImVec2 p4 = ImVec2(toPos.x - po, toPos.y + toDim.y * 0.5f);
1398
1399 // Horizontal tangents give the classic node-graph S-curve shape.
1400 float curve = (p4.x - p1.x) * 0.4f;
1401 if (curve < 50.0f) curve = 50.0f;
1402 ImVec2 p2 = ImVec2(p1.x + curve, p1.y);
1403 ImVec2 p3 = ImVec2(p4.x - curve, p4.y);
1404
1405 // Wide transparent halo + narrow bright core.
1406 drawList->AddBezierCubic(p1, p2, p3, p4, glowWide, 6.0f);
1407 drawList->AddBezierCubic(p1, p2, p3, p4, glowCore, 2.0f);
1408 }
1409 }
1410
1411 // =========================================================================
1412 // RenderConnectionIndices - Phase 38: Y-Axis Positional Execution Order
1413 // =========================================================================
1414
1416 {
1417 if (graph == nullptr)
1418 return;
1419
1420 ImDrawList* drawList = ImGui::GetWindowDrawList();
1421 if (drawList == nullptr)
1422 return;
1423
1424 // Get all nodes (returns vector of const GraphNode*)
1425 auto graphNodes = graph->GetAllNodes();
1426
1427 // Iterate through all nodes, find Sequence/Selector nodes
1428 for (size_t i = 0; i < graphNodes.size(); ++i)
1429 {
1430 const GraphNode* parentNode = graphNodes[i];
1431 if (parentNode == nullptr)
1432 continue;
1433
1434 // Check if this node is a Sequence or Selector (NodeType enum comparison)
1436 continue;
1437
1438 // Get the parent node's screen position
1439 int parentUID = graphID * GRAPH_ID_MULTIPLIER + parentNode->id;
1440 ImVec2 parentPos = ImNodes::GetNodeScreenSpacePos(parentUID);
1441 ImVec2 parentDim = ImNodes::GetNodeDimensions(parentUID);
1442
1443 // Phase 38: Sort children by Y-position to determine execution order
1444 std::vector<std::pair<float, int>> childrenWithY;
1445 for (size_t j = 0; j < parentNode->childIds.size(); ++j)
1446 {
1447 int childId = parentNode->childIds[j];
1448
1449 // Find child node to get its Y position
1450 const GraphNode* childNode = graph->GetNode(childId);
1451 if (childNode != nullptr)
1452 {
1453 childrenWithY.push_back(std::make_pair(childNode->posY, childId));
1454 }
1455 }
1456
1457 // Sort by Y-coordinate (ascending: top node = first to execute)
1458 std::sort(childrenWithY.begin(), childrenWithY.end());
1459
1460 // Render index for each child connection
1461 for (size_t i = 0; i < childrenWithY.size(); ++i)
1462 {
1463 int childId = childrenWithY[i].second;
1464 uint32_t executionIndex = static_cast<uint32_t>(i + 1); // 1-based indexing
1465
1466 // Find the child node in the graph
1467 const GraphNode* childNode = graph->GetNode(childId);
1468 if (childNode == nullptr)
1469 continue;
1470
1471 // Get child node's screen position
1472 int childUID = graphID * GRAPH_ID_MULTIPLIER + childNode->id;
1473 ImVec2 childPos = ImNodes::GetNodeScreenSpacePos(childUID);
1474 ImVec2 childDim = ImNodes::GetNodeDimensions(childUID);
1475
1476 // Calculate connection curve (same as RenderActiveLinks)
1477 const float po = ImNodes::GetStyle().PinOffset;
1478 ImVec2 p1 = ImVec2(parentPos.x + parentDim.x + po, parentPos.y + parentDim.y * 0.5f);
1479 ImVec2 p4 = ImVec2(childPos.x - po, childPos.y + childDim.y * 0.5f);
1480
1481 // Horizontal tangents for S-curve
1482 float curve = (p4.x - p1.x) * 0.4f;
1483 if (curve < 50.0f) curve = 50.0f;
1484 ImVec2 p2 = ImVec2(p1.x + curve, p1.y);
1485 ImVec2 p3 = ImVec2(p4.x - curve, p4.y);
1486
1487 // Calculate midpoint of the Bezier curve for label placement
1488 // Use t=0.5 for cubic Bezier: B(0.5) = 0.125*p1 + 0.375*p2 + 0.375*p3 + 0.125*p4
1490 0.125f * p1.x + 0.375f * p2.x + 0.375f * p3.x + 0.125f * p4.x,
1491 0.125f * p1.y + 0.375f * p2.y + 0.375f * p3.y + 0.125f * p4.y
1492 );
1493
1494 // Format index text
1495 char indexText[16];
1496 snprintf(indexText, sizeof(indexText), "%u", executionIndex);
1497
1498 // Render index label with background
1499 ImVec2 textSize = ImGui::CalcTextSize(indexText);
1500 ImVec2 padding = ImVec2(4.0f, 2.0f);
1501 ImVec2 bgMin = ImVec2(midpoint.x - textSize.x * 0.5f - padding.x,
1502 midpoint.y - textSize.y * 0.5f - padding.y);
1503 ImVec2 bgMax = ImVec2(midpoint.x + textSize.x * 0.5f + padding.x,
1504 midpoint.y + textSize.y * 0.5f + padding.y);
1505
1506 // Background: Dark with border
1507 ImU32 bgColor = IM_COL32(40, 40, 50, 220);
1508 ImU32 borderColor = IM_COL32(100, 150, 200, 255);
1509 ImU32 textColor = IM_COL32(200, 220, 255, 255);
1510
1511 drawList->AddRectFilled(bgMin, bgMax, bgColor, 3.0f);
1512 drawList->AddRect(bgMin, bgMax, borderColor, 3.0f, 0, 1.0f);
1513
1514 // Text: White/light blue
1515 drawList->AddText(ImVec2(midpoint.x - textSize.x * 0.5f, midpoint.y - textSize.y * 0.5f),
1516 textColor, indexText);
1517 }
1518 }
1519 }
1520
1522 {
1523 if (ImGui::BeginPopup("NodeCreationMenu"))
1524 {
1525 // Reset search buffer when popup first opens
1526 ImGui::Text("Create Node");
1527 ImGui::Separator();
1528
1529 // Fuzzy search filter
1530 ImGui::SetNextItemWidth(-1.0f);
1531 ImGui::InputText("##search", m_ContextMenuSearch, sizeof(m_ContextMenuSearch));
1532 ImGui::SameLine(0.0f, 0.0f);
1533 ImGui::TextDisabled(" (search)");
1534 ImGui::Separator();
1535
1536 // Build lowercase search string for case-insensitive matching
1537 std::string searchLower(m_ContextMenuSearch);
1538 for (size_t i = 0; i < searchLower.size(); ++i)
1539 searchLower[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(searchLower[i])));
1540
1541 // Helper: returns true when item matches the search filter (empty = show all)
1542 auto matchesFilter = [&](const std::string& name) -> bool
1543 {
1544 if (searchLower.empty())
1545 return true;
1546 std::string nameLower = name;
1547 for (size_t i = 0; i < nameLower.size(); ++i)
1548 nameLower[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(nameLower[i])));
1549 return nameLower.find(searchLower) != std::string::npos;
1550 };
1551
1552 // ----- Built-in BT composite nodes ----------------------------
1553 if (searchLower.empty())
1554 {
1555 if (ImGui::BeginMenu("Composite"))
1556 {
1557 if (ImGui::MenuItem("Sequence"))
1559 if (ImGui::MenuItem("Selector"))
1561 ImGui::EndMenu();
1562 }
1563
1564 if (ImGui::BeginMenu("Action"))
1565 {
1567 for (const auto& actionType : actionTypes)
1568 {
1569 if (ImGui::MenuItem(actionType.c_str()))
1570 {
1573 if (graph)
1574 {
1575 auto allNodes = graph->GetAllNodes();
1576 if (!allNodes.empty())
1577 allNodes.back()->actionType = actionType;
1578 }
1579 }
1580 }
1581 ImGui::EndMenu();
1582 }
1583
1584 if (ImGui::BeginMenu("Condition"))
1585 {
1587 for (const auto& conditionType : conditionTypes)
1588 {
1589 if (ImGui::MenuItem(conditionType.c_str()))
1590 {
1593 if (graph)
1594 {
1595 auto allNodes = graph->GetAllNodes();
1596 if (!allNodes.empty())
1597 allNodes.back()->conditionType = conditionType;
1598 }
1599 }
1600 }
1601 ImGui::EndMenu();
1602 }
1603
1604 if (ImGui::BeginMenu("Decorator"))
1605 {
1607 for (const auto& decoratorType : decoratorTypes)
1608 {
1609 if (ImGui::MenuItem(decoratorType.c_str()))
1610 {
1613 if (graph)
1614 {
1615 auto allNodes = graph->GetAllNodes();
1616 if (!allNodes.empty())
1617 allNodes.back()->decoratorType = decoratorType;
1618 }
1619 }
1620 }
1621 ImGui::EndMenu();
1622 }
1623
1624 ImGui::Separator();
1625 ImGui::TextDisabled("-- Atomic Tasks --");
1626
1627 if (ImGui::MenuItem("Comment Box"))
1628 {
1630 if (graph)
1631 {
1632 ImVec2 canvasPos = ScreenSpaceToGridSpace(ImVec2(m_ContextMenuPosX, m_ContextMenuPosY));
1633 int nodeId = graph->CreateNode(NodeType::Comment, canvasPos.x, canvasPos.y, "Comment");
1634 GraphNode* cnode = graph->GetNode(nodeId);
1635 if (cnode)
1636 cnode->parameters["text"] = "// Enter comment here";
1637 }
1638 ImGui::CloseCurrentPopup();
1639 }
1640 }
1641
1642 // ----- AtomicTaskRegistry nodes (with fuzzy filter) -----------
1643 {
1644 std::vector<std::string> taskIds = AtomicTaskRegistry::Get().GetAllTaskIDs();
1645 // Sort for deterministic order
1646 std::sort(taskIds.begin(), taskIds.end());
1647
1648 bool anyShown = false;
1649 for (const auto& taskId : taskIds)
1650 {
1651 if (!matchesFilter(taskId))
1652 continue;
1653 anyShown = true;
1654 if (ImGui::MenuItem(taskId.c_str()))
1655 {
1658 if (graph)
1659 {
1660 auto allNodes = graph->GetAllNodes();
1661 if (!allNodes.empty())
1662 allNodes.back()->actionType = taskId;
1663 }
1664 }
1665 }
1666 if (!anyShown && !searchLower.empty())
1667 ImGui::TextDisabled("No results for \"%s\"", m_ContextMenuSearch);
1668 }
1669
1670 // Clear search when popup closes
1671 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows))
1672 m_ContextMenuSearch[0] = '\0';
1673
1674 ImGui::EndPopup();
1675 }
1676 }
1677
1678 void NodeGraphPanel::CreateNewNode(const char* nodeType, float screenX, float screenY)
1679 {
1681 if (!graph)
1682 {
1683 std::cerr << "[NodeGraphPanel] Cannot create node: No active graph\n";
1684 return;
1685 }
1686
1687 // Convert screen coordinates to canvas coordinates
1688 ImVec2 canvasPos = ScreenSpaceToGridSpace(ImVec2(screenX, screenY));
1689
1690 // Validate coordinates are finite (not NaN or infinity)
1691 if (!std::isfinite(canvasPos.x) || !std::isfinite(canvasPos.y))
1692 {
1693 std::cerr << "[NodeGraphPanel] Invalid coordinates for node creation\n";
1694 return;
1695 }
1696
1697 std::cout << "[NodeGraphPanel] Creating " << nodeType << " at canvas pos ("
1698 << canvasPos.x << ", " << canvasPos.y << ")\n";
1699
1700 // Use BlueprintAdapter to execute create node command through the editor stack
1702 int graphId = NodeGraphManager::Get().GetActiveGraphId();
1704 int createdId = adapter.CreateNode(nodeType, canvasPos.x, canvasPos.y, nodeType);
1705
1706 std::cout << "[NodeGraphPanel] Requested create node of type " << nodeType << " via command stack, id=" << createdId << "\n";
1707
1708 // If node created, select it in ImNodes and update internal selection state
1709 if (createdId > 0)
1710 {
1711 int globalUID = graphId * GRAPH_ID_MULTIPLIER + createdId;
1712 ImNodes::ClearNodeSelection();
1713 ImNodes::SelectNode(globalUID);
1715 // Move editor view to the newly created node so it is visible to the user
1716 ImNodes::EditorContextMoveToNode(globalUID);
1717 }
1718 }
1719
1721 {
1722 // This would show properties of the selected node
1723 // Can be integrated into inspector panel
1724 }
1725
1727 {
1729 if (!graph)
1730 return;
1731
1732 ImGuiIO& io = ImGui::GetIO();
1733
1734 // Ctrl+Z: Undo
1735 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Z) && !io.KeyShift)
1736 {
1738 }
1739
1740 // Ctrl+Y or Ctrl+Shift+Z: Redo
1741 if ((io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Y)) ||
1742 (io.KeyCtrl && io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_Z)))
1743 {
1745 }
1746
1747 // Ctrl+D: Duplicate selected node
1748 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_D))
1749 {
1750 int selectedNodeCount = ImNodes::NumSelectedNodes();
1751 if (selectedNodeCount > 0)
1752 {
1753 std::vector<int> selectedNodes(selectedNodeCount);
1754 ImNodes::GetSelectedNodes(selectedNodes.data());
1755 if (selectedNodes.size() > 0)
1756 {
1757 int nodeId = selectedNodes[0];
1758 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
1759 auto cmd = std::make_unique<Olympe::Blueprint::DuplicateNodeCommand>(graphId, nodeId);
1761 }
1762 }
1763 }
1764
1765 // Ctrl+C: Copy selected nodes to system clipboard
1766 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C))
1767 {
1770 if (g != nullptr)
1772 }
1773
1774 // Ctrl+V: Paste nodes from system clipboard under the mouse cursor
1775 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_V))
1776 {
1779 if (g != nullptr)
1780 {
1781 ImVec2 mousePos = ImGui::GetMousePos();
1782 ImVec2 gridPos = ScreenSpaceToGridSpace(mousePos);
1785 }
1786 }
1787
1788 // Ctrl+G: Toggle snap-to-grid
1789 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_G))
1790 {
1792 }
1793
1794 // Ctrl+M: Toggle minimap
1795 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_M))
1796 {
1798 }
1799
1800 // Ctrl+0: Reset panning to origin (fit view)
1801 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_0))
1802 {
1803 ImNodes::EditorContextResetPanning(ImVec2(0.0f, 0.0f));
1804 }
1805 }
1806
1807
1809 {
1811 return;
1812
1814 if (!graph)
1815 {
1816 m_ShowNodeEditModal = false;
1817 return;
1818 }
1819
1820 GraphNode* node = graph->GetNode(m_EditingNodeId);
1821 if (!node)
1822 {
1823 m_ShowNodeEditModal = false;
1824 return;
1825 }
1826
1827 ImGui::OpenPopup("Edit Node");
1828 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
1829 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1830
1831 if (ImGui::BeginPopupModal("Edit Node", &m_ShowNodeEditModal, ImGuiWindowFlags_AlwaysAutoResize))
1832 {
1833 // Node name
1834 if (ImGui::InputText("Name", m_NodeNameBuffer, sizeof(m_NodeNameBuffer)))
1835 {
1836 // Name will be saved on OK
1837 }
1838
1839 ImGui::Text("Type: %s", NodeTypeToString(node->type));
1840 ImGui::Text("ID: %d", node->id);
1841 ImGui::Separator();
1842
1843 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
1844
1845 // Type-specific parameters
1846 if (node->type == NodeType::BT_Action)
1847 {
1848 // Action type dropdown
1849 ImGui::Text("Action Type:");
1851 if (ImGui::BeginCombo("##actiontype", node->actionType.c_str()))
1852 {
1853 for (const auto& actionType : actionTypes)
1854 {
1855 bool isSelected = (node->actionType == actionType);
1856 if (ImGui::Selectable(actionType.c_str(), isSelected))
1857 {
1858 std::string oldType = node->actionType;
1859 node->actionType = actionType;
1860 // Could create EditNodeCommand here
1861 }
1862 if (isSelected)
1863 ImGui::SetItemDefaultFocus();
1864 }
1865 ImGui::EndCombo();
1866 }
1867
1868 // Show and edit parameters
1869 ImGui::Separator();
1870 ImGui::Text("Parameters:");
1871
1872 // Get parameter definitions from catalog
1874 if (actionDef)
1875 {
1876 for (const auto& paramDef : actionDef->parameters)
1877 {
1878 std::string currentValue = node->parameters[paramDef.name];
1879 if (currentValue.empty())
1880 currentValue = paramDef.defaultValue;
1881
1882 char buffer[256];
1883 strncpy_s(buffer, currentValue.c_str(), sizeof(buffer) - 1);
1884 buffer[sizeof(buffer) - 1] = '\0';
1885
1886 if (ImGui::InputText(paramDef.name.c_str(), buffer, sizeof(buffer)))
1887 {
1888 std::string oldValue = node->parameters[paramDef.name];
1889 node->parameters[paramDef.name] = buffer;
1890 // Could create SetParameterCommand here for undo support
1891 }
1892
1893 if (!actionDef->tooltip.empty() && ImGui::IsItemHovered())
1894 {
1895 ImGui::SetTooltip("%s", actionDef->tooltip.c_str());
1896 }
1897 }
1898 }
1899 }
1900 else if (node->type == NodeType::BT_Condition)
1901 {
1902 // Condition type dropdown
1903 ImGui::Text("Condition Type:");
1905 if (ImGui::BeginCombo("##conditiontype", node->conditionType.c_str()))
1906 {
1907 for (const auto& conditionType : conditionTypes)
1908 {
1909 bool isSelected = (node->conditionType == conditionType);
1910 if (ImGui::Selectable(conditionType.c_str(), isSelected))
1911 {
1912 node->conditionType = conditionType;
1913 }
1914 if (isSelected)
1915 ImGui::SetItemDefaultFocus();
1916 }
1917 ImGui::EndCombo();
1918 }
1919
1920 // Show and edit parameters
1921 ImGui::Separator();
1922 ImGui::Text("Parameters:");
1923
1925 if (conditionDef)
1926 {
1927 for (const auto& paramDef : conditionDef->parameters)
1928 {
1929 std::string currentValue = node->parameters[paramDef.name];
1930 if (currentValue.empty())
1931 currentValue = paramDef.defaultValue;
1932
1933 char buffer[256];
1934 strncpy_s(buffer, currentValue.c_str(), sizeof(buffer) - 1);
1935 buffer[sizeof(buffer) - 1] = '\0';
1936
1937 if (ImGui::InputText(paramDef.name.c_str(), buffer, sizeof(buffer)))
1938 {
1939 node->parameters[paramDef.name] = buffer;
1940 }
1941 }
1942 }
1943 }
1944 else if (node->type == NodeType::BT_Decorator)
1945 {
1946 // Decorator type dropdown
1947 ImGui::Text("Decorator Type:");
1949 if (ImGui::BeginCombo("##decoratortype", node->decoratorType.c_str()))
1950 {
1951 for (const auto& decoratorType : decoratorTypes)
1952 {
1953 bool isSelected = (node->decoratorType == decoratorType);
1954 if (ImGui::Selectable(decoratorType.c_str(), isSelected))
1955 {
1956 node->decoratorType = decoratorType;
1957 }
1958 if (isSelected)
1959 ImGui::SetItemDefaultFocus();
1960 }
1961 ImGui::EndCombo();
1962 }
1963 }
1964
1965 ImGui::Separator();
1966
1967 if (ImGui::Button("OK", ImVec2(120, 0)))
1968 {
1969 // Apply name change if different
1970 std::string newName(m_NodeNameBuffer);
1971 if (newName != node->name)
1972 {
1973 node->name = newName;
1974 }
1975
1976 // Mark graph as dirty since node was edited
1977 if (graph)
1978 graph->MarkDirty();
1979
1980 m_ShowNodeEditModal = false;
1981 m_EditingNodeId = -1;
1982 }
1983
1984 ImGui::SameLine();
1985
1986 if (ImGui::Button("Cancel", ImVec2(120, 0)))
1987 {
1988 m_ShowNodeEditModal = false;
1989 m_EditingNodeId = -1;
1990 }
1991
1992 ImGui::EndPopup();
1993 }
1994 }
1995
1996// ============================================================================
1997// Phase 8: Subgraph tab system
1998// ============================================================================
1999
2001 {
2002 if (ImGui::BeginTabBar("SubgraphTabs"))
2003 {
2004 for (int i = 0; i < (int)m_SubgraphTabs.size(); ++i)
2005 {
2007
2008 // Build label (add dirty marker when modified).
2009 std::string label = tab.displayName;
2010 if (tab.isDirty)
2011 label += " *";
2012
2016
2017 // Root tab cannot be closed; subgraph tabs show an X button.
2018 bool tabOpen = true;
2019 bool* pOpen = (i == 0) ? nullptr : &tabOpen;
2020
2021 if (ImGui::BeginTabItem(label.c_str(), pOpen, flags))
2022 {
2024 ImGui::EndTabItem();
2025 }
2026
2027 if (pOpen && !tabOpen)
2029 }
2030
2031 // "+ New SubGraph" trailing button.
2032 if (ImGui::TabItemButton("+ New SubGraph", ImGuiTabItemFlags_Trailing))
2033 ImGui::OpenPopup("NewSubgraphPopup");
2034
2035 ImGui::EndTabBar();
2036 }
2037
2038 // New SubGraph name popup.
2039 if (ImGui::BeginPopup("NewSubgraphPopup"))
2040 {
2041 ImGui::Text("SubGraph name:");
2042 ImGui::SetNextItemWidth(200.0f);
2043 ImGui::InputText("##newsgname", m_NewSubgraphNameBuffer,
2044 sizeof(m_NewSubgraphNameBuffer));
2045
2046 if (ImGui::Button("Create") ||
2047 (ImGui::IsItemFocused() &&
2048 ImGui::IsKeyPressed(ImGuiKey_Enter)))
2049 {
2050 std::string name(m_NewSubgraphNameBuffer);
2051 if (!name.empty())
2052 {
2053 CreateEmptySubgraph(name);
2054 m_NewSubgraphNameBuffer[0] = '\0';
2055 ImGui::CloseCurrentPopup();
2056 }
2057 }
2058 ImGui::SameLine();
2059 if (ImGui::Button("Cancel"))
2060 {
2061 m_NewSubgraphNameBuffer[0] = '\0';
2062 ImGui::CloseCurrentPopup();
2063 }
2064
2065 ImGui::EndPopup();
2066 }
2067 }
2068
2069 void NodeGraphPanel::OpenSubgraphTab(const std::string& subgraphUUID,
2070 const std::string& displayName)
2071 {
2072 // Check whether the tab is already open.
2073 for (int i = 0; i < (int)m_SubgraphTabs.size(); ++i)
2074 {
2075 if (m_SubgraphTabs[i].tabID == subgraphUUID)
2076 {
2078 return;
2079 }
2080 }
2081
2082 // Not open yet — create a new tab.
2083 std::string path = "subgraphs/" + subgraphUUID;
2084 m_SubgraphTabs.emplace_back(subgraphUUID, displayName, path);
2086
2087 std::cout << "[NodeGraphPanel] Opened subgraph tab: " << displayName
2088 << " (" << subgraphUUID << ")\n";
2089 }
2090
2092 {
2093 // Index 0 is the root graph — never close it.
2094 if (index <= 0 || index >= (int)m_SubgraphTabs.size())
2095 return;
2096 std::cout << "[NodeGraphPanel] Closed subgraph tab: "
2097 << m_SubgraphTabs[index].displayName << "\n";
2098
2099 m_SubgraphTabs.erase(m_SubgraphTabs.begin() + index);
2100
2101 // Clamp active index.
2102 if (m_ActiveSubgraphTabIndex >= (int)m_SubgraphTabs.size())
2106 }
2107
2108 void NodeGraphPanel::CreateEmptySubgraph(const std::string& name)
2109 {
2110 // Use a monotonically-increasing counter to ensure UUIDs remain unique
2111 // even when subgraph tabs are closed and new ones are created later.
2112 static int s_SubgraphCounter = 0;
2114
2115 std::string uuid = "sg_" + std::to_string(s_SubgraphCounter) + "_" + name;
2116 // Replace spaces with underscores for a valid key.
2117 std::replace(uuid.begin(), uuid.end(), ' ', '_');
2118
2119 std::cout << "[NodeGraphPanel] Created empty subgraph '" << name
2120 << "' with UUID: " << uuid << "\n";
2121
2122 // Open it in a new tab.
2123 OpenSubgraphTab(uuid, name);
2124 }
2125
2127 {
2128 if (m_ActiveSubgraphTabIndex >= 0 &&
2130 {
2132 }
2133 return nullptr;
2134 }
2135
2137 {
2138 const GraphTab* tab = GetActiveTab();
2139 if (tab && tab->tabID != "root")
2140 return tab->tabID;
2141 return "";
2142 }
2143
2144}
UI-side registry of available atomic tasks with display metadata.
Node-graph clipboard: copy/paste selected nodes via ImGui system clipboard.
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Editor mode and capabilities management.
Per-NodeType visual style registry (header colour, icon, pin colours).
std::vector< std::string > GetAllTaskIDs() const
Returns a vector of all registered task IDs.
static AtomicTaskRegistry & Get()
Returns the singleton instance.
static AtomicTaskUIRegistry & Get()
Returns the singleton instance.
const TaskSpec * GetTaskSpec(const std::string &id) const
Returns the TaskSpec for the given id, or nullptr if not found.
uint64_t GetSelectedEntity() const
Blueprint::CommandStack * GetCommandStack()
static BlueprintEditor & Get()
CommandStack - Manages undo/redo command history Maintains two stacks for undo and redo operations.
void ExecuteCommand(std::unique_ptr< EditorCommand > cmd)
void Init(std::function< void()> saveFn, float debounceSec=1.5f, float periodicIntervalSec=60.0f)
Set the timing parameters and an optional legacy save callback.
void Flush()
Block until any running async save finishes.
void ScheduleSave(double nowSec)
Notify the manager that a change occurred (legacy overload).
void Tick(double nowSec)
Must be called once per frame to advance timers and launch saves.
static EditorContext & Get()
static EntityInspectorManager & Get()
EntityInfo GetEntityInfo(EntityID entity) const
const CatalogType * FindActionType(const std::string &id) const
std::vector< std::string > GetDecoratorTypes() const
std::vector< std::string > GetActionTypes() const
static EnumCatalogManager & Get()
std::vector< std::string > GetConditionTypes() const
const CatalogType * FindConditionType(const std::string &id) const
InspectorPanel - Adaptive inspector panel Displays properties based on current selection context.
static InspectorPanel * GetInstance()
Get the active InspectorPanel instance (singleton access).
void PasteNodes(NodeGraph *graph, int graphID, float mousePosX, float mousePosY, bool snapToGrid=false, float snapGridSize=16.0f)
Read the system clipboard, deserialise nodes and create them in the active graph under the current mo...
static NodeGraphClipboard & Get()
Returns the singleton instance.
Definition Clipboard.cpp:60
void CopySelectedNodes(NodeGraph *graph, int graphID)
Serialise currently selected nodes to JSON and write to the system clipboard.
Definition Clipboard.cpp:70
std::vector< int > GetAllGraphIds() const
NodeGraph * GetGraph(int graphId)
std::string GetGraphName(int graphId) const
int CreateGraph(const std::string &name, const std::string &type)
static NodeGraphManager & Get()
void RenderSubgraphTabBar()
Renders the subgraph-aware tab bar above the graph canvas.
const GraphTab * GetActiveTab() const
Returns the GraphTab for the currently active tab.
void CreateNewNode(const char *nodeType, float x, float y)
ImNodesEditorContext * m_imnodesContext
Dedicated imnodes rendering context for this panel instance.
char m_NewSubgraphNameBuffer[128]
Buffer used by the "New SubGraph" name input popup.
int m_lastActiveGraphId
Graph ID that was active last frame; used to detect graph switches so m_positionedNodes can be cleare...
void CloseSubgraphTab(int index)
Closes the tab at index.
bool m_SnapToGrid
When true node positions are rounded to the nearest grid cell on move.
float m_SnapGridSize
Grid cell size in canvas units used when snap-to-grid is enabled.
std::vector< GraphTab > m_SubgraphTabs
Ordered list of open subgraph tabs. Index 0 is always the root graph.
void OpenSubgraphTab(const std::string &subgraphUUID, const std::string &displayName)
Opens (or focuses) the subgraph identified by subgraphUUID in a new tab.
void RenderActiveLinks(NodeGraph *graph, int graphID)
Overlay glow-coloured lines on links that connect to/from the active debug node, giving a visual "act...
static int s_ActiveDebugNodeId
Backing storage for SetActiveDebugNode: the local node ID currently executing (-1 = none).
bool m_SuppressGraphTabs
When true, RenderGraphTabs() is skipped (used by BehaviorTreeRenderer)
int m_ActiveSubgraphTabIndex
Index into m_SubgraphTabs of the currently visible tab.
bool m_ShowMinimap
When true the built-in ImNodes minimap is rendered in the bottom-right corner of the node editor canv...
std::unordered_set< int > m_positionedNodes
Tracks which global node UIDs have already had their ImNodes position initialised.
std::string GetActiveSubgraphUUID() const
Returns the subgraph UUID for the active tab, or empty string if the root graph is active.
void SyncNodePositionsFromImNodes(int graphID)
void RenderTypedPin(int attrId, const char *label, bool isInput, bool isExec, const std::unordered_set< int > &connectedAttrIDs={})
Render a single typed attribute pin using ImDrawList shapes.
int GlobalUIDToLocalNodeID(int globalUID, int graphID) const
std::unique_ptr< class ImNodesCanvasEditor > m_canvasEditor
Canvas editor adapter for minimap support (Phase 36) Abstracts imnodes minimap rendering through ICan...
void HandleNodeInteractions(int graphID)
EditorAutosaveManager m_autosave
Async autosave manager – persists node positions without blocking the UI.
void CreateEmptySubgraph(const std::string &name)
Creates an empty subgraph, inserts it into the active blueprint's data.subgraphs dict,...
void RenderNodePinsAndContent(GraphNode *node, int globalNodeUID, int graphID, const std::unordered_set< int > &connectedAttrIDs={})
Render a single node with a coloured title bar, icon, and typed pins.
void RenderConnectionIndices(NodeGraph *graph, int graphID)
Render execution indices (1, 2, 3, ...n) on connection lines for Sequence and Selector nodes based on...
static void SetActiveDebugNode(int localNodeId)
Set the local node ID that is currently executing.
nlohmann::json ToJson() const
std::vector< GraphNode * > GetAllNodes()
const NodeStyle & GetStyle(NodeType type) const
Returns the style for the given node type.
static NodeStyleRegistry & Get()
Returns the singleton instance.
constexpr uint32_t BT_ROOT_NODE_COLOR
Green color for Root node (entry point of behavior tree).
static uint32_t ToImU32_ABGR(uint32_t rgbaColor)
Helper function to extract ImU32 color (ImGui format).
< Provides AssetID and INVALID_ASSET_ID
@ BT_OnEvent
Phase 38b: Event-driven root (green, event-triggered)
@ BT_Root
Phase 38b: Root entry point (green, fixed position)
const char * NodeTypeToString(NodeType type)
TaskNodeType
Identifies the role of a node in the task graph.
@ AtomicTask
Leaf node that executes a single atomic task.
@ While
Conditional loop (Loop / Completed exec outputs)
@ SubGraph
Sub-graph call (SubTask)
@ DoOnce
Single-fire execution (reset via Reset pin)
@ Delay
Timer (Completed exec output after N seconds)
@ GetBBValue
Data node – reads a Blackboard key.
@ MathOp
Data node – arithmetic operation (+, -, *, /)
@ SetBBValue
Data node – writes a Blackboard key.
@ ForEach
Iterate over BB list (Loop Body / Completed exec outputs)
@ Switch
Multi-branch on value (N exec outputs)
@ EntryPoint
Unique entry node for VS graphs (replaces Root)
@ Branch
If/Else conditional (Then / Else exec outputs)
@ VSSequence
Execute N outputs in order ("VS" prefix avoids collision with BT Sequence=1)
ImVec2 ScreenSpaceToGridSpace(const ImVec2 &screenPos)
std::vector< CatalogParameter > parameters
std::map< std::string, std::string > parameters
Represents one open tab in the NodeGraphPanel tab bar.
std::string displayName
Label shown on the tab ("Root" or subgraph name)
Visual descriptor for a single node type.
ImU32 headerColor
Title-bar background colour (ImNodes TitleBar colour slot).
ImU32 headerSelectedColor
Title-bar colour when the node is selected.
const char * icon
Short ASCII icon displayed before the node title (no emoji/extended chars).
ImU32 headerHoveredColor
Title-bar colour when the node is hovered.
Display metadata for a single atomic task type.
std::string displayName
Human-readable name (e.g. "Move To Goal")