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 "EditorContext.h"
9#include "NodeGraphManager.h"
10#include "EnumCatalogManager.h"
11#include "CommandSystem.h"
12#include "../third_party/imgui/imgui.h"
13#include "../third_party/imnodes/imnodes.h"
14#include <iostream>
15#include <vector>
16#include <cstring>
17#include <cmath>
18
19namespace
20{
21 // UID generation constants for ImNodes
22 // These ensure unique IDs across multiple open graphs
23 constexpr int GRAPH_ID_MULTIPLIER = 10000; // Multiplier for graph ID in node UID calculation
24 constexpr int ATTR_ID_MULTIPLIER = 100; // Multiplier for node UID in attribute UID calculation
25 constexpr int LINK_ID_MULTIPLIER = 100000; // Multiplier for graph ID in link UID calculation
26
27 // Helper function to convert screen space coordinates to grid space coordinates
28 // Screen space: origin at upper-left corner of the window
29 // Grid space: origin at upper-left corner of the node editor, adjusted by panning
31 {
32 // Get the editor's screen space position
33 ImVec2 editorPos = ImGui::GetCursorScreenPos();
34
35 // Get the current panning offset
36 ImVec2 panning = ImNodes::EditorContextGetPanning();
37
38 // Convert: subtract editor position to get editor space, then subtract panning to get grid space
39 return ImVec2(screenPos.x - editorPos.x - panning.x,
40 screenPos.y - editorPos.y - panning.y);
41 }
42}
43
44namespace Olympe
45{
49
53
55 {
56 std::cout << "[NodeGraphPanel] Initialized\n";
57 }
58
60 {
61 std::cout << "[NodeGraphPanel] Shutdown\n";
62 }
63
65 {
66 ImGui::Begin("Node Graph Editor");
67
68 // Handle keyboard shortcuts
70
71 // Show currently selected entity at the top (informational only, doesn't block rendering)
73 if (selectedEntity != 0)
74 {
76 ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f),
77 "Editing for Entity: %s (ID: %llu)", info.name.c_str(), selectedEntity);
78 }
79 else
80 {
81 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.9f, 1.0f),
82 "Editing BehaviorTree Asset (no entity context)");
83 }
84 ImGui::Separator();
85
86 // Toolbar with Save/Save As buttons
88 if (activeGraph)
89 {
90 // Save button
91 bool canSave = activeGraph->HasFilepath();
92 if (!canSave)
93 ImGui::BeginDisabled();
94
95 if (ImGui::Button("Save"))
96 {
97 // Validate before saving
98 std::string validationError;
99 if (!activeGraph->ValidateGraph(validationError))
100 {
101 // Show validation error popup
102 ImGui::OpenPopup("ValidationError");
103 }
104 else
105 {
106 int graphId = NodeGraphManager::Get().GetActiveGraphId();
107 const std::string& filepath = activeGraph->GetFilepath();
108 if (NodeGraphManager::Get().SaveGraph(graphId, filepath))
109 {
110 std::cout << "[NodeGraphPanel] Saved graph to: " << filepath << std::endl;
111 }
112 else
113 {
114 std::cout << "[NodeGraphPanel] Failed to save graph!" << std::endl;
115 }
116 }
117 }
118
119 if (!canSave)
120 ImGui::EndDisabled();
121
122 if (!canSave && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
123 {
124 ImGui::SetTooltip("No filepath set. Use 'Save As...' first.");
125 }
126
127 ImGui::SameLine();
128
129 // Save As button
130 if (ImGui::Button("Save As..."))
131 {
132 // TODO: Open file dialog to select save location
133 // For now, show popup to enter filename
134 ImGui::OpenPopup("SaveAsPopup");
135 }
136
137 // Show dirty indicator
138 ImGui::SameLine();
139 if (activeGraph->IsDirty())
140 {
141 ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "*");
142 if (ImGui::IsItemHovered())
143 {
144 ImGui::SetTooltip("Unsaved changes");
145 }
146 }
147
148 // Save As popup (simple text input for now)
149 static bool saveAsPopupOpen = false;
150 static char filepathBuffer[512] = "";
151
152 if (ImGui::BeginPopup("SaveAsPopup"))
153 {
154 // Clear buffer when popup first opens
155 if (!saveAsPopupOpen)
156 {
157 filepathBuffer[0] = '\0';
158 saveAsPopupOpen = true;
159 }
160
161 ImGui::Text("Save graph as:");
162 ImGui::InputText("Filepath", filepathBuffer, sizeof(filepathBuffer));
163
164 if (ImGui::Button("Save", ImVec2(120, 0)))
165 {
166 std::string filepath(filepathBuffer);
167 if (!filepath.empty())
168 {
169 // Validate before saving
170 std::string validationError;
171 if (!activeGraph->ValidateGraph(validationError))
172 {
173 // Show validation error
174 saveAsPopupOpen = false;
175 ImGui::CloseCurrentPopup();
176 ImGui::OpenPopup("ValidationError");
177 }
178 else
179 {
180 // Ensure .json extension (check that it ends with .json)
181 if (filepath.size() < 5 || filepath.substr(filepath.size() - 5) != ".json")
182 filepath += ".json";
183
184 int graphId = NodeGraphManager::Get().GetActiveGraphId();
185 if (NodeGraphManager::Get().SaveGraph(graphId, filepath))
186 {
187 std::cout << "[NodeGraphPanel] Saved graph as: " << filepath << std::endl;
188 saveAsPopupOpen = false;
189 ImGui::CloseCurrentPopup();
190 }
191 else
192 {
193 std::cout << "[NodeGraphPanel] Failed to save graph!" << std::endl;
194 }
195 }
196 }
197 }
198 ImGui::SameLine();
199 if (ImGui::Button("Cancel", ImVec2(120, 0)))
200 {
201 saveAsPopupOpen = false;
202 ImGui::CloseCurrentPopup();
203 }
204 ImGui::EndPopup();
205 }
206 else
207 {
208 saveAsPopupOpen = false;
209 }
210
211 // Validation error popup
212 if (ImGui::BeginPopupModal("ValidationError", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
213 {
214 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Cannot save: Graph validation failed!");
215 ImGui::Separator();
216
217 std::string validationError;
218 if (!activeGraph->ValidateGraph(validationError))
219 {
220 ImGui::TextWrapped("%s", validationError.c_str());
221 }
222
223 ImGui::Separator();
224 if (ImGui::Button("OK", ImVec2(120, 0)))
225 {
226 ImGui::CloseCurrentPopup();
227 }
228 ImGui::EndPopup();
229 }
230
231 ImGui::Separator();
232 }
233
234 // Render graph tabs
236
237 ImGui::Separator();
238
239 // Render the active graph
241 if (activeGraph)
242 {
243 RenderGraph();
244 }
245 else
246 {
247 ImGui::Text("No graph open. Create or load a graph to begin.");
248 if (ImGui::Button("Create New Behavior Tree"))
249 {
250 NodeGraphManager::Get().CreateGraph("New Behavior Tree", "BehaviorTree");
251 }
252 ImGui::SameLine();
253 if (ImGui::Button("Create New HFSM"))
254 {
255 NodeGraphManager::Get().CreateGraph("New HFSM", "HFSM");
256 }
257 }
258
259 // Render node edit modal
261
262 ImGui::End();
263 }
264
266 {
269
270 // Track which graph was requested to close
271 // Using static but ensuring cleanup to prevent issues with multiple rapid closes
272 static int graphToClose = -1;
273 static bool confirmationOpen = false;
274
275 if (ImGui::BeginTabBar("GraphTabs"))
276 {
277 for (int graphId : graphIds)
278 {
279 std::string graphName = NodeGraphManager::Get().GetGraphName(graphId);
280
281 // Add dirty indicator to tab name
283 if (graph && graph->IsDirty())
284 graphName += " *";
285
286 // Only set ImGuiTabItemFlags_SetSelected if this is the active graph
287 // This ensures the tab is selected visually without forcing re-selection each frame
289 if (graphId == currentActiveId)
290 {
292 }
293
294 // Enable close button for tabs
295 bool tabOpen = true;
296 if (ImGui::BeginTabItem(graphName.c_str(), &tabOpen, flags))
297 {
298 // Only change active graph if user clicked this tab (and it's not already active)
299 // BeginTabItem returns true when the tab content should be shown
300 if (currentActiveId != graphId)
301 {
303 }
304 ImGui::EndTabItem();
305 }
306
307 // If tab was closed (X button clicked)
308 if (!tabOpen)
309 {
310 // Only process if no confirmation dialog is currently open
311 if (!confirmationOpen)
312 {
313 // Check if graph has unsaved changes
314 if (graph && graph->IsDirty())
315 {
316 graphToClose = graphId;
317 confirmationOpen = true;
318 ImGui::OpenPopup("ConfirmCloseUnsaved");
319 }
320 else
321 {
322 // Close immediately if no unsaved changes
324 }
325 }
326 }
327 }
328
329 // Add "+" button for new graph
330 if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing))
331 {
332 ImGui::OpenPopup("CreateGraphPopup");
333 }
334
335 ImGui::EndTabBar();
336 }
337
338 // Confirmation popup for closing unsaved graph
339 if (ImGui::BeginPopupModal("ConfirmCloseUnsaved", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
340 {
341 ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "Warning: Unsaved Changes!");
342 ImGui::Separator();
343
345 ImGui::TextWrapped("The graph '%s' has unsaved changes.", graphName.c_str());
346 ImGui::TextWrapped("Do you want to save before closing?");
347
348 ImGui::Separator();
349
350 // Save and Close button
351 if (ImGui::Button("Save and Close", ImVec2(120, 0)))
352 {
354 if (graph && graph->HasFilepath())
355 {
356 // Validate before saving
357 std::string validationError;
358 if (!graph->ValidateGraph(validationError))
359 {
360 // Show validation error
361 ImGui::CloseCurrentPopup();
362 ImGui::OpenPopup("ValidationError");
363 }
364 else
365 {
366 // Save and close
367 if (NodeGraphManager::Get().SaveGraph(graphToClose, graph->GetFilepath()))
368 {
370 graphToClose = -1;
371 confirmationOpen = false;
372 ImGui::CloseCurrentPopup();
373 }
374 }
375 }
376 else
377 {
378 // No filepath - need Save As
379 confirmationOpen = false;
380 ImGui::CloseCurrentPopup();
381 ImGui::OpenPopup("SaveAsPopup");
382 }
383 }
384
385 ImGui::SameLine();
386
387 // Close without saving button
388 if (ImGui::Button("Close Without Saving", ImVec2(150, 0)))
389 {
391 graphToClose = -1;
392 confirmationOpen = false;
393 ImGui::CloseCurrentPopup();
394 }
395
396 ImGui::SameLine();
397
398 // Cancel button
399 if (ImGui::Button("Cancel", ImVec2(120, 0)))
400 {
401 graphToClose = -1;
402 confirmationOpen = false;
403 ImGui::CloseCurrentPopup();
404 }
405
406 ImGui::EndPopup();
407 }
408 else
409 {
410 // Popup closed without action - reset state
411 if (confirmationOpen && graphToClose >= 0)
412 {
413 confirmationOpen = false;
414 graphToClose = -1;
415 }
416 }
417
418 // Create graph popup
419 if (ImGui::BeginPopup("CreateGraphPopup"))
420 {
421 if (ImGui::MenuItem("New Behavior Tree"))
422 {
423 NodeGraphManager::Get().CreateGraph("New Behavior Tree", "BehaviorTree");
424 }
425 if (ImGui::MenuItem("New HFSM"))
426 {
427 NodeGraphManager::Get().CreateGraph("New HFSM", "HFSM");
428 }
429 ImGui::EndPopup();
430 }
431 }
432
434 {
436 if (!graph)
437 return;
438
439 // Get the Graph ID for creating unique UIDs
441 if (graphID < 0)
442 {
443 std::cerr << "[NodeGraphPanel] Invalid graph ID" << std::endl;
444 return;
445 }
446
447 // Ensure canvas has valid size (minimum 1px to render)
448 constexpr float MIN_CANVAS_SIZE = 1.0f;
449 ImVec2 canvasSize = ImGui::GetContentRegionAvail();
451 {
452 ImGui::Text("Canvas too small to render graph");
453 return;
454 }
455
456 ImNodes::BeginNodeEditor();
457
458 // Render all nodes
459 auto nodes = graph->GetAllNodes();
460 for (GraphNode* node : nodes)
461 {
462 // Generate a global unique UID for ImNodes
463 // Format: graphID * GRAPH_ID_MULTIPLIER + nodeID
464 // This ensures no node from different graphs has the same UID
465 int globalNodeUID = (graphID * GRAPH_ID_MULTIPLIER) + node->id;
466
467 // Set node position BEFORE rendering (ImNodes requirement)
468 ImNodes::SetNodeGridSpacePos(globalNodeUID, ImVec2(node->posX, node->posY));
469
470 ImNodes::BeginNode(globalNodeUID);
471
472 // Title bar
473 ImNodes::BeginNodeTitleBar();
474 ImGui::TextUnformatted(node->name.c_str());
475 ImNodes::EndNodeTitleBar();
476
477 // Input attribute with UID based on globalNodeUID
478 int inputAttrUID = globalNodeUID * ATTR_ID_MULTIPLIER + 1;
479 ImNodes::BeginInputAttribute(inputAttrUID);
480 ImGui::Text("In");
481 ImNodes::EndInputAttribute();
482
483 // Node content based on type
484 ImGui::Text("Type: %s", NodeTypeToString(node->type));
485
486 if (node->type == NodeType::BT_Action && !node->actionType.empty())
487 {
488 ImGui::Text("Action: %s", node->actionType.c_str());
489 }
490 else if (node->type == NodeType::BT_Condition && !node->conditionType.empty())
491 {
492 ImGui::Text("Condition: %s", node->conditionType.c_str());
493 }
494 else if (node->type == NodeType::BT_Decorator && !node->decoratorType.empty())
495 {
496 ImGui::Text("Decorator: %s", node->decoratorType.c_str());
497 }
498
499 // Output attribute with UID based on globalNodeUID
500 int outputAttrUID = globalNodeUID * ATTR_ID_MULTIPLIER + 2;
501 ImNodes::BeginOutputAttribute(outputAttrUID);
502 ImGui::Text("Out");
503 ImNodes::EndOutputAttribute();
504
505 ImNodes::EndNode();
506 }
507
508 // Render all links with global UIDs
509 auto links = graph->GetAllLinks();
510 for (size_t i = 0; i < links.size(); ++i)
511 {
512 const GraphLink& link = links[i];
513
514 // Generate global UIDs for the attributes
515 int fromNodeUID = (graphID * GRAPH_ID_MULTIPLIER) + link.fromNode;
516 int toNodeUID = (graphID * GRAPH_ID_MULTIPLIER) + link.toNode;
517
518 int fromAttrUID = fromNodeUID * ATTR_ID_MULTIPLIER + 2; // Output attribute
519 int toAttrUID = toNodeUID * ATTR_ID_MULTIPLIER + 1; // Input attribute
520
521 // Link ID must also be unique globally
522 int globalLinkUID = (graphID * LINK_ID_MULTIPLIER) + (int)i + 1;
523
524 ImNodes::Link(globalLinkUID, fromAttrUID, toAttrUID);
525 }
526
527 ImNodes::EndNodeEditor();
528
529 // Handle node interactions with UID mapping
531
532 // Handle link selection
533 int numSelectedLinks = ImNodes::NumSelectedLinks();
534 if (numSelectedLinks > 0)
535 {
536 std::vector<int> selectedLinks(numSelectedLinks);
537 ImNodes::GetSelectedLinks(selectedLinks.data());
538 if (selectedLinks.size() > 0)
540 }
541
542 // Handle Delete key for nodes and links (only if canDelete)
543 if (ImGui::IsKeyPressed(ImGuiKey_Delete) && EditorContext::Get().CanDelete())
544 {
545 if (m_SelectedNodeId != -1)
546 {
547 // Delete selected node
548 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
549 auto cmd = std::make_unique<DeleteNodeCommand>(graphId, m_SelectedNodeId);
551 m_SelectedNodeId = -1;
552 }
553 else if (m_SelectedLinkId != -1)
554 {
555 // Delete selected link
556 // Extract the link index from the global link UID
557 int linkIndex = (m_SelectedLinkId - (graphID * LINK_ID_MULTIPLIER)) - 1;
558
559 auto links = graph->GetAllLinks();
560 if (linkIndex >= 0 && linkIndex < (int)links.size())
561 {
562 const GraphLink& link = links[linkIndex];
563 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
564 auto cmd = std::make_unique<UnlinkNodesCommand>(graphId, link.fromNode, link.toNode);
566 m_SelectedLinkId = -1;
567 }
568 }
569 }
570
571 // Check for double-click on node to open edit modal
572 int hoveredNodeUID = -1;
573 if (ImNodes::IsNodeHovered(&hoveredNodeUID))
574 {
575 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
576 {
577 // Convert global UID to local node ID
580 GraphNode* node = graph->GetNode(localNodeId);
581 if (node)
582 {
583 strncpy_s(m_NodeNameBuffer, node->name.c_str(), sizeof(m_NodeNameBuffer) - 1);
584 m_NodeNameBuffer[sizeof(m_NodeNameBuffer) - 1] = '\0';
585 m_ShowNodeEditModal = true;
586 }
587 }
588 }
589
590 // Right-click context menu on node
591 if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && hoveredNodeUID != -1)
592 {
593 // Convert global UID to local node ID
595 ImGui::OpenPopup("NodeContextMenu");
596 }
597
598 // Handle right-click on canvas for node creation menu (only if canCreate)
599 if (EditorContext::Get().CanCreate() &&
600 ImGui::IsMouseReleased(ImGuiMouseButton_Right) &&
601 ImNodes::IsEditorHovered() &&
602 !ImNodes::IsNodeHovered(&hoveredNodeUID))
603 {
604 ImGui::OpenPopup("NodeCreationMenu");
605 ImVec2 mousePos = ImGui::GetMousePos();
608 }
609
610 // Context menu on node
611 if (ImGui::BeginPopup("NodeContextMenu"))
612 {
613 ImGui::Text("Node: %d", m_SelectedNodeId);
614 ImGui::Separator();
615
616 // Edit is always available for viewing
617 if (ImGui::MenuItem("Edit", "Double-click"))
618 {
621 if (node)
622 {
623 strncpy_s(m_NodeNameBuffer, node->name.c_str(), sizeof(m_NodeNameBuffer) - 1);
624 m_NodeNameBuffer[sizeof(m_NodeNameBuffer) - 1] = '\0';
625 m_ShowNodeEditModal = true;
626 }
627 }
628
629 // Duplicate and Delete only shown if allowed
631 {
632 if (ImGui::MenuItem("Duplicate", "Ctrl+D"))
633 {
634 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
635 auto cmd = std::make_unique<DuplicateNodeCommand>(graphId, m_SelectedNodeId);
637 }
638 }
639
640 ImGui::Separator();
641
642 if (EditorContext::Get().CanDelete())
643 {
644 if (EditorContext::Get().CanDelete())
645 {
646 if (ImGui::MenuItem("Delete", "Del"))
647 {
648 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
649 auto cmd = std::make_unique<DeleteNodeCommand>(graphId, m_SelectedNodeId);
651 }
652 }
653
654 ImGui::EndPopup();
655 }
656
658
659 // Handle drag & drop from node palette
660 if (ImGui::BeginDragDropTarget())
661 {
662 const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("NODE_TYPE");
663
664 // Validate payload safety
665 if (payload && payload->Data && payload->DataSize > 0)
666 {
667 // Copy payload data to local string with bounds checking
668 size_t maxSize = 256; // Reasonable max for node type strings
669 size_t dataSize = (payload->DataSize < maxSize) ? payload->DataSize : maxSize;
670
671 std::string nodeTypeData;
672 nodeTypeData.resize(dataSize);
673 std::memcpy(&nodeTypeData[0], payload->Data, dataSize);
674
675 // Ensure NUL-termination
676 if (nodeTypeData.find('\0') == std::string::npos)
677 {
678 // Truncate at first non-printable or add terminator
679 size_t validLen = 0;
680 for (size_t i = 0; i < nodeTypeData.size(); ++i)
681 {
682 if (nodeTypeData[i] == '\0' || nodeTypeData[i] < 32)
683 break;
684 validLen = i + 1;
685 }
686 nodeTypeData.resize(validLen);
687 }
688
689 // Remove any trailing null bytes
690 while (!nodeTypeData.empty() && nodeTypeData.back() == '\0')
691 nodeTypeData.pop_back();
692
693 // Convert screen space coordinates to grid space
694 ImVec2 mouseScreenPos = ImGui::GetMousePos();
695 ImVec2 canvasPos = ScreenSpaceToGridSpace(mouseScreenPos);
696
697 bool validNode = false;
698
699 // Parse the type and create appropriate node
700 if (nodeTypeData.find("Action:") == 0)
701 {
702 std::string actionType = nodeTypeData.substr(7);
703
704 // Validate action type exists in catalog
705 if (EnumCatalogManager::Get().IsValidActionType(actionType))
706 {
707 int nodeId = graph->CreateNode(NodeType::BT_Action, canvasPos.x, canvasPos.y, actionType);
708 GraphNode* node = graph->GetNode(nodeId);
709 if (node)
710 {
711 node->actionType = actionType;
712 validNode = true;
713 std::cout << "[NodeGraphPanel] Created Action node: " << actionType
714 << " at canvas pos (" << canvasPos.x << ", " << canvasPos.y << ")\n";
715 }
716 }
717 else
718 {
719 std::cerr << "[NodeGraphPanel] ERROR: Invalid ActionType: " << actionType << "\n";
720 ImGui::SetTooltip("Invalid ActionType: %s", actionType.c_str());
721 }
722 }
723 else if (nodeTypeData.find("Condition:") == 0)
724 {
725 std::string conditionType = nodeTypeData.substr(10);
726
727 // Validate condition type exists in catalog
728 if (EnumCatalogManager::Get().IsValidConditionType(conditionType))
729 {
730 int nodeId = graph->CreateNode(NodeType::BT_Condition, canvasPos.x, canvasPos.y, conditionType);
731 GraphNode* node = graph->GetNode(nodeId);
732 if (node)
733 {
734 node->conditionType = conditionType;
735 validNode = true;
736 std::cout << "[NodeGraphPanel] Created Condition node: " << conditionType
737 << " at canvas pos (" << canvasPos.x << ", " << canvasPos.y << ")\n";
738 }
739 }
740 else
741 {
742 std::cerr << "[NodeGraphPanel] ERROR: Invalid ConditionType: " << conditionType << "\n";
743 ImGui::SetTooltip("Invalid ConditionType: %s", conditionType.c_str());
744 }
745 }
746 else if (nodeTypeData.find("Decorator:") == 0)
747 {
748 std::string decoratorType = nodeTypeData.substr(10);
749
750 // Validate decorator type exists in catalog
751 if (EnumCatalogManager::Get().IsValidDecoratorType(decoratorType))
752 {
753 int nodeId = graph->CreateNode(NodeType::BT_Decorator, canvasPos.x, canvasPos.y, decoratorType);
754 GraphNode* node = graph->GetNode(nodeId);
755 if (node)
756 {
757 node->decoratorType = decoratorType;
758 validNode = true;
759 std::cout << "[NodeGraphPanel] Created Decorator node: " << decoratorType
760 << " at canvas pos (" << canvasPos.x << ", " << canvasPos.y << ")\n";
761 }
762 }
763 else
764 {
765 std::cerr << "[NodeGraphPanel] ERROR: Invalid DecoratorType: " << decoratorType << "\n";
766 ImGui::SetTooltip("Invalid DecoratorType: %s", decoratorType.c_str());
767 }
768 }
769 else if (nodeTypeData == "Sequence" || nodeTypeData == "Selector")
770 {
772 int nodeId = graph->CreateNode(type, canvasPos.x, canvasPos.y, nodeTypeData);
773 if (nodeId > 0)
774 {
775 validNode = true;
776 std::cout << "[NodeGraphPanel] Created " << nodeTypeData << " node"
777 << " at canvas pos (" << canvasPos.x << ", " << canvasPos.y << ")\n";
778 }
779 }
780 else
781 {
782 std::cerr << "[NodeGraphPanel] ERROR: Unknown node type: " << nodeTypeData << "\n";
783 ImGui::SetTooltip("Unknown node type: %s", nodeTypeData.c_str());
784 }
785
786 if (!validNode)
787 {
788 std::cerr << "[NodeGraphPanel] Failed to create node from DnD payload\n";
789 }
790 }
791 else
792 {
793 std::cerr << "[NodeGraphPanel] Invalid DnD payload received (null or empty)\n";
794 }
795
796 ImGui::EndDragDropTarget();
797 }
798
799 // Update node positions using global UIDs
800 for (GraphNode* node : nodes)
801 {
802 int globalNodeUID = (graphID * GRAPH_ID_MULTIPLIER) + node->id;
803 ImVec2 pos = ImNodes::GetNodeGridSpacePos(globalNodeUID);
804
805 // Check if position changed
806 if (node->posX != pos.x || node->posY != pos.y)
807 {
808 node->posX = pos.x;
809 node->posY = pos.y;
810
811 // Mark graph as dirty when node is moved
812 if (graph)
813 graph->MarkDirty();
814 }
815 }
816 }
817 }
818
820 {
822 if (!graph)
823 return;
824
825 // Handle node selection
826 int numSelected = ImNodes::NumSelectedNodes();
827 if (numSelected > 0)
828 {
829 std::vector<int> selectedUIDs(numSelected);
830 ImNodes::GetSelectedNodes(selectedUIDs.data());
831
832 // Convert the first global UID to local Node ID
833 if (!selectedUIDs.empty())
834 {
837 }
838 }
839
840 // Handle link creation (only if canLink)
842 if (EditorContext::Get().CanLink() && ImNodes::IsLinkCreated(&startAttrUID, &endAttrUID))
843 {
844 // Extract the global UIDs of nodes
845 int startNodeGlobalUID = startAttrUID / ATTR_ID_MULTIPLIER;
846 int endNodeGlobalUID = endAttrUID / ATTR_ID_MULTIPLIER;
847
848 // Convert to local IDs
851
852 // Create the link with local IDs
853 std::string graphId = std::to_string(graphID);
854 auto cmd = std::make_unique<LinkNodesCommand>(graphId, startNodeLocalID, endNodeLocalID);
856 }
857 }
858
860 {
861 if (ImGui::BeginPopup("NodeCreationMenu"))
862 {
863 ImGui::Text("Create Node");
864 ImGui::Separator();
865
866 if (ImGui::BeginMenu("Composite"))
867 {
868 if (ImGui::MenuItem("Sequence"))
870 if (ImGui::MenuItem("Selector"))
872 ImGui::EndMenu();
873 }
874
875 if (ImGui::BeginMenu("Action"))
876 {
878 for (const auto& actionType : actionTypes)
879 {
880 if (ImGui::MenuItem(actionType.c_str()))
881 {
883 // Set action type on the created node
885 if (graph)
886 {
887 auto nodes = graph->GetAllNodes();
888 if (!nodes.empty())
889 {
890 nodes.back()->actionType = actionType;
891 }
892 }
893 }
894 }
895 ImGui::EndMenu();
896 }
897
898 if (ImGui::BeginMenu("Condition"))
899 {
901 for (const auto& conditionType : conditionTypes)
902 {
903 if (ImGui::MenuItem(conditionType.c_str()))
904 {
906 // Set condition type on the created node
908 if (graph)
909 {
910 auto nodes = graph->GetAllNodes();
911 if (!nodes.empty())
912 {
913 nodes.back()->conditionType = conditionType;
914 }
915 }
916 }
917 }
918 ImGui::EndMenu();
919 }
920
921 if (ImGui::BeginMenu("Decorator"))
922 {
924 for (const auto& decoratorType : decoratorTypes)
925 {
926 if (ImGui::MenuItem(decoratorType.c_str()))
927 {
929 // Set decorator type on the created node
931 if (graph)
932 {
933 auto nodes = graph->GetAllNodes();
934 if (!nodes.empty())
935 {
936 nodes.back()->decoratorType = decoratorType;
937 }
938 }
939 }
940 }
941 ImGui::EndMenu();
942 }
943
944 ImGui::EndPopup();
945 }
946 }
947
949 {
951 if (!graph)
952 {
953 std::cerr << "[NodeGraphPanel] Cannot create node: No active graph\n";
954 return;
955 }
956
957 // Convert screen coordinates to canvas coordinates
958 ImVec2 canvasPos = ScreenSpaceToGridSpace(ImVec2(screenX, screenY));
959
960 // Validate coordinates are finite (not NaN or infinity)
961 if (!std::isfinite(canvasPos.x) || !std::isfinite(canvasPos.y))
962 {
963 std::cerr << "[NodeGraphPanel] Invalid coordinates for node creation\n";
964 return;
965 }
966
967 std::cout << "[NodeGraphPanel] Creating " << nodeType << " at canvas pos ("
968 << canvasPos.x << ", " << canvasPos.y << ")\n";
969
971 int nodeId = graph->CreateNode(type, canvasPos.x, canvasPos.y, nodeType);
972
973 std::cout << "[NodeGraphPanel] Created node " << nodeId << " of type " << nodeType << "\n";
974 }
975
977 {
978 // This would show properties of the selected node
979 // Can be integrated into inspector panel
980 }
981
983 {
985 if (!graph)
986 return;
987
988 ImGuiIO& io = ImGui::GetIO();
989
990 // Ctrl+Z: Undo
991 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Z) && !io.KeyShift)
992 {
994 }
995
996 // Ctrl+Y or Ctrl+Shift+Z: Redo
997 if ((io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Y)) ||
998 (io.KeyCtrl && io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_Z)))
999 {
1001 }
1002
1003 // Ctrl+D: Duplicate selected node
1004 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_D))
1005 {
1006 int selectedNodeCount = ImNodes::NumSelectedNodes();
1007 if (selectedNodeCount > 0)
1008 {
1009 std::vector<int> selectedNodes(selectedNodeCount);
1010 ImNodes::GetSelectedNodes(selectedNodes.data());
1011 if (selectedNodes.size() > 0)
1012 {
1013 int nodeId = selectedNodes[0];
1014 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
1015 auto cmd = std::make_unique<DuplicateNodeCommand>(graphId, nodeId);
1017 }
1018 }
1019 }
1020 }
1021
1023 {
1025 return;
1026
1028 if (!graph)
1029 {
1030 m_ShowNodeEditModal = false;
1031 return;
1032 }
1033
1034 GraphNode* node = graph->GetNode(m_EditingNodeId);
1035 if (!node)
1036 {
1037 m_ShowNodeEditModal = false;
1038 return;
1039 }
1040
1041 ImGui::OpenPopup("Edit Node");
1042 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
1043 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1044
1045 if (ImGui::BeginPopupModal("Edit Node", &m_ShowNodeEditModal, ImGuiWindowFlags_AlwaysAutoResize))
1046 {
1047 // Node name
1048 if (ImGui::InputText("Name", m_NodeNameBuffer, sizeof(m_NodeNameBuffer)))
1049 {
1050 // Name will be saved on OK
1051 }
1052
1053 ImGui::Text("Type: %s", NodeTypeToString(node->type));
1054 ImGui::Text("ID: %d", node->id);
1055 ImGui::Separator();
1056
1057 std::string graphId = std::to_string(NodeGraphManager::Get().GetActiveGraphId());
1058
1059 // Type-specific parameters
1060 if (node->type == NodeType::BT_Action)
1061 {
1062 // Action type dropdown
1063 ImGui::Text("Action Type:");
1065 if (ImGui::BeginCombo("##actiontype", node->actionType.c_str()))
1066 {
1067 for (const auto& actionType : actionTypes)
1068 {
1069 bool isSelected = (node->actionType == actionType);
1070 if (ImGui::Selectable(actionType.c_str(), isSelected))
1071 {
1072 std::string oldType = node->actionType;
1073 node->actionType = actionType;
1074 // Could create EditNodeCommand here
1075 }
1076 if (isSelected)
1077 ImGui::SetItemDefaultFocus();
1078 }
1079 ImGui::EndCombo();
1080 }
1081
1082 // Show and edit parameters
1083 ImGui::Separator();
1084 ImGui::Text("Parameters:");
1085
1086 // Get parameter definitions from catalog
1088 if (actionDef)
1089 {
1090 for (const auto& paramDef : actionDef->parameters)
1091 {
1092 std::string currentValue = node->parameters[paramDef.name];
1093 if (currentValue.empty())
1094 currentValue = paramDef.defaultValue;
1095
1096 char buffer[256];
1097 strncpy_s(buffer, currentValue.c_str(), sizeof(buffer) - 1);
1098 buffer[sizeof(buffer) - 1] = '\0';
1099
1100 if (ImGui::InputText(paramDef.name.c_str(), buffer, sizeof(buffer)))
1101 {
1102 std::string oldValue = node->parameters[paramDef.name];
1103 node->parameters[paramDef.name] = buffer;
1104 // Could create SetParameterCommand here for undo support
1105 }
1106
1107 if (!actionDef->tooltip.empty() && ImGui::IsItemHovered())
1108 {
1109 ImGui::SetTooltip("%s", actionDef->tooltip.c_str());
1110 }
1111 }
1112 }
1113 }
1114 else if (node->type == NodeType::BT_Condition)
1115 {
1116 // Condition type dropdown
1117 ImGui::Text("Condition Type:");
1119 if (ImGui::BeginCombo("##conditiontype", node->conditionType.c_str()))
1120 {
1121 for (const auto& conditionType : conditionTypes)
1122 {
1123 bool isSelected = (node->conditionType == conditionType);
1124 if (ImGui::Selectable(conditionType.c_str(), isSelected))
1125 {
1126 node->conditionType = conditionType;
1127 }
1128 if (isSelected)
1129 ImGui::SetItemDefaultFocus();
1130 }
1131 ImGui::EndCombo();
1132 }
1133
1134 // Show and edit parameters
1135 ImGui::Separator();
1136 ImGui::Text("Parameters:");
1137
1139 if (conditionDef)
1140 {
1141 for (const auto& paramDef : conditionDef->parameters)
1142 {
1143 std::string currentValue = node->parameters[paramDef.name];
1144 if (currentValue.empty())
1145 currentValue = paramDef.defaultValue;
1146
1147 char buffer[256];
1148 strncpy_s(buffer, currentValue.c_str(), sizeof(buffer) - 1);
1149 buffer[sizeof(buffer) - 1] = '\0';
1150
1151 if (ImGui::InputText(paramDef.name.c_str(), buffer, sizeof(buffer)))
1152 {
1153 node->parameters[paramDef.name] = buffer;
1154 }
1155 }
1156 }
1157 }
1158 else if (node->type == NodeType::BT_Decorator)
1159 {
1160 // Decorator type dropdown
1161 ImGui::Text("Decorator Type:");
1163 if (ImGui::BeginCombo("##decoratortype", node->decoratorType.c_str()))
1164 {
1165 for (const auto& decoratorType : decoratorTypes)
1166 {
1167 bool isSelected = (node->decoratorType == decoratorType);
1168 if (ImGui::Selectable(decoratorType.c_str(), isSelected))
1169 {
1170 node->decoratorType = decoratorType;
1171 }
1172 if (isSelected)
1173 ImGui::SetItemDefaultFocus();
1174 }
1175 ImGui::EndCombo();
1176 }
1177 }
1178
1179 ImGui::Separator();
1180
1181 if (ImGui::Button("OK", ImVec2(120, 0)))
1182 {
1183 // Apply name change if different
1184 std::string newName(m_NodeNameBuffer);
1185 if (newName != node->name)
1186 {
1187 node->name = newName;
1188 }
1189
1190 // Mark graph as dirty since node was edited
1191 if (graph)
1192 graph->MarkDirty();
1193
1194 m_ShowNodeEditModal = false;
1195 m_EditingNodeId = -1;
1196 }
1197
1198 ImGui::SameLine();
1199
1200 if (ImGui::Button("Cancel", ImVec2(120, 0)))
1201 {
1202 m_ShowNodeEditModal = false;
1203 m_EditingNodeId = -1;
1204 }
1205
1206 ImGui::EndPopup();
1207 }
1208 }
1209}
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
uint64_t GetSelectedEntity() const
class CommandStack * GetCommandStack()
static BlueprintEditor & Get()
void ExecuteCommand(std::unique_ptr< EditorCommand > cmd)
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
std::vector< int > GetAllGraphIds() const
void SetActiveGraph(int graphId)
NodeGraph * GetGraph(int graphId)
std::string GetGraphName(int graphId) const
int CreateGraph(const std::string &name, const std::string &type)
static NodeGraphManager & Get()
void CreateNewNode(const char *nodeType, float x, float y)
int GlobalUIDToLocalNodeID(int globalUID, int graphID) const
void HandleNodeInteractions(int graphID)
bool HasFilepath() const
std::vector< GraphNode * > GetAllNodes()
const char * NodeTypeToString(NodeType type)
NodeType StringToNodeType(const std::string &str)
ImVec2 ScreenSpaceToGridSpace(const ImVec2 &screenPos)
std::vector< CatalogParameter > parameters
std::string conditionType
std::string decoratorType