Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
VisualScriptEditorPanel_Canvas.cpp
Go to the documentation of this file.
1/**
2 * @file VisualScriptEditorPanel_Canvas.cpp
3 * @brief Canvas rendering, node palette, and context menus for VisualScriptEditorPanel.
4 * @author Olympe Engine
5 * @date 2026-03-09
6 *
7 * @details Extracted from VisualScriptEditorPanel.cpp (Phase 9 refactoring).
8 * Contains: RenderCanvas, RenderNodePalette, RenderContextMenus.
9 * C++14 compliant — no std::optional, structured bindings, std::filesystem.
10 */
11
13#include "DebugController.h"
15#include "ConditionRegistry.h"
16#include "OperatorRegistry.h"
17#include "BBVariableRegistry.h"
18#include "MathOpOperand.h"
19#include "TabManager.h"
20#include "../system/system_utils.h"
21#include "../system/system_consts.h"
22#include "../NodeGraphCore/GlobalTemplateBlackboard.h"
23
24#include "../third_party/imgui/imgui.h"
25#include "../third_party/imnodes/imnodes.h"
26#include "../json_helper.h"
27#include "../TaskSystem/TaskGraphLoader.h"
28
29#include <fstream>
30#include <iostream>
31#include <algorithm>
32#include <cmath>
33#include <cstring>
34#include <sstream>
35#include <iomanip>
36#include <cstdlib>
37#include <unordered_set>
38
39namespace Olympe {
40
41// ============================================================================
42// Canvas Rendering
43// ============================================================================
44
46{
47 // Switch to this panel's dedicated ImNodes context so that node positions
48 // and canvas panning are preserved independently for each open tab.
50 ImNodes::EditorContextSet(m_imnodesContext);
51
52 // On the first render after LoadTemplate(), push the stored (posX, posY)
53 // of each node into ImNodes so the canvas matches the saved layout.
54 // BUG-003 Fix: positions are stored in grid space; use SetNodeGridSpacePos
55 // to restore them pan-independently (avoids double-offset with viewport pan).
57 {
58 for (size_t i = 0; i < m_editorNodes.size(); ++i)
59 {
60 ImNodes::SetNodeGridSpacePos(
61 m_editorNodes[i].nodeID,
62 ImVec2(m_editorNodes[i].posX, m_editorNodes[i].posY));
63 }
64 m_needsPositionSync = false;
65 }
66
67 // Phase 21-B: focus/scroll to a node requested from the verification panel
68 if (m_focusNodeID >= 0)
69 {
70 for (size_t i = 0; i < m_editorNodes.size(); ++i)
71 {
72 if (m_editorNodes[i].nodeID == m_focusNodeID)
73 {
74 // BUG-003 Fix: restore in grid space for pan-independent positioning.
75 ImNodes::SetNodeGridSpacePos(
77 ImVec2(m_editorNodes[i].posX, m_editorNodes[i].posY));
78 break;
79 }
80 }
81 m_focusNodeID = -1;
82 }
83
84 ImNodes::BeginNodeEditor();
85
86 // NOTE: Right-click context menu detection is deferred until after
87 // ImNodes::EndNodeEditor() below so that IsNodeHovered() / IsLinkHovered()
88 // (which require ImNodesScope_None) can be used to determine what was clicked.
89
91
92 // Build connected attribute IDs set from current editor links.
93 // Pins whose attribute ID is in this set will be rendered filled;
94 // unconnected pins are rendered outlined (empty).
95 std::unordered_set<int> connectedAttrIDs;
96 for (size_t li = 0; li < m_editorLinks.size(); ++li)
97 {
98 connectedAttrIDs.insert(m_editorLinks[li].srcAttrID);
99 connectedAttrIDs.insert(m_editorLinks[li].dstAttrID);
100 }
101
102 // Render all nodes
104
105 for (size_t i = 0; i < m_editorNodes.size(); ++i)
106 {
107 VSEditorNode& eNode = m_editorNodes[i];
108
109 bool hasBreakpoint = DebugController::Get().HasBreakpoint(
110 0 /* graphID placeholder */, eNode.nodeID);
111 bool isActive = (eNode.nodeID == activeNodeID &&
113
114 // Phase 21-B: highlight nodes that have Error issues in the verification result
115 bool hasVerifError = false;
117 {
118 for (size_t vi = 0; vi < m_verificationResult.issues.size(); ++vi)
119 {
120 if (m_verificationResult.issues[vi].nodeID == eNode.nodeID &&
122 {
123 hasVerifError = true;
124 break;
125 }
126 }
127 }
128 if (hasVerifError)
129 {
130 ImNodes::PushColorStyle(ImNodesCol_NodeBackground,
131 IM_COL32(120, 30, 30, 230));
132 ImNodes::PushColorStyle(ImNodesCol_NodeBackgroundHovered,
133 IM_COL32(120, 30, 30, 230));
134 ImNodes::PushColorStyle(ImNodesCol_NodeBackgroundSelected,
135 IM_COL32(120, 30, 30, 230));
136 }
137
138 auto execIn = GetExecInputPins(eNode.def.Type);
140
141 // Phase 24.2 FIX: Ensure data-pure nodes have DataPins initialized
142 // This handles both newly created nodes AND nodes loaded from blueprints
143
144 // Initialize DataPins for GetBBValue (Variable) nodes
145 if (eNode.def.Type == TaskNodeType::GetBBValue && eNode.def.DataPins.empty())
146 {
147 DataPinDefinition pinOut;
148 pinOut.PinName = "Value";
150 pinOut.PinType = VariableType::Float; // Will be resolved at runtime based on actual variable
151 eNode.def.DataPins.push_back(pinOut);
152 std::cerr << "[VSEditor] Initialized DataPins for GetBBValue (Variable) node #" << eNode.nodeID << "\n";
153 }
154
155 // Initialize DataPins for MathOp nodes
156 if (eNode.def.Type == TaskNodeType::MathOp && eNode.def.DataPins.empty())
157 {
158 // Initialize DataPins for this MathOp node if not already present
159 DataPinDefinition pinA;
160 pinA.PinName = "A";
162 pinA.PinType = VariableType::Float;
163 eNode.def.DataPins.push_back(pinA);
164
165 DataPinDefinition pinB;
166 pinB.PinName = "B";
168 pinB.PinType = VariableType::Float;
169 eNode.def.DataPins.push_back(pinB);
170
171 DataPinDefinition pinResult;
172 pinResult.PinName = "Result";
175 eNode.def.DataPins.push_back(pinResult);
176
177 std::cerr << "[VSEditor] Initialized DataPins for MathOp node #" << eNode.nodeID << "\n";
178 }
179
180 // Initialize DataPins for SetBBValue nodes
181 if (eNode.def.Type == TaskNodeType::SetBBValue && eNode.def.DataPins.empty())
182 {
183 DataPinDefinition pinIn;
184 pinIn.PinName = "Value";
186 pinIn.PinType = VariableType::Float; // Will be resolved at runtime based on target variable
187 eNode.def.DataPins.push_back(pinIn);
188 std::cerr << "[VSEditor] Initialized DataPins for SetBBValue node #" << eNode.nodeID << "\n";
189 }
190
191 std::vector<std::pair<std::string, VariableType>> dataIn, dataOut;
192 for (size_t p = 0; p < eNode.def.DataPins.size(); ++p)
193 {
194 const DataPinDefinition& pin = eNode.def.DataPins[p];
195 if (pin.Dir == DataPinDir::Input)
196 dataIn.push_back({pin.PinName, pin.PinType});
197 else
198 dataOut.push_back({pin.PinName, pin.PinType});
199 }
200
201 // Phase 24 — Dispatcher: Branch nodes use specialized renderer
203 {
204 // Convert TaskNodeDefinition to NodeBranchData for specialized rendering
205 NodeBranchData branchData;
206 branchData.nodeID = eNode.nodeID; // int nodeID for ImNodes attribute UIDs
207 branchData.nodeName = eNode.def.NodeName;
208 branchData.conditionRefs = eNode.def.conditionRefs;
209 branchData.dynamicPins = eNode.def.dynamicPins;
210 branchData.breakpoint = hasBreakpoint;
211
212 // Render via NodeBranchRenderer (4-section layout)
213 // Must be wrapped with ImNodes::BeginNode/EndNode just like generic renderer
214 ImNodes::BeginNode(eNode.nodeID);
216 ImNodes::EndNode();
217 }
218 else
219 {
220 // Use generic renderer for all other node types
222 eNode.nodeID,
223 eNode.nodeID,
224 0 /* graphID placeholder */,
225 eNode.def,
226 hasBreakpoint,
227 isActive,
230 [](int nid, void* ud) {
231 VisualScriptEditorPanel* panel =
232 static_cast<VisualScriptEditorPanel*>(ud);
233 panel->m_pendingAddPin = true;
234 panel->m_pendingAddPinNodeID = nid;
235 },
236 this,
237 [](int nid, int dynIdx, void* ud) {
238 VisualScriptEditorPanel* panel =
239 static_cast<VisualScriptEditorPanel*>(ud);
240 panel->m_pendingRemovePin = true;
241 panel->m_pendingRemovePinNodeID = nid;
242 panel->m_pendingRemovePinDynIdx = dynIdx;
243 },
244 this,
246 }
247
248 if (hasVerifError)
249 {
250 ImNodes::PopColorStyle();
251 ImNodes::PopColorStyle();
252 ImNodes::PopColorStyle();
253 }
254
255 // Breakpoint / active overlays
256 if (hasBreakpoint)
258 if (isActive)
260
261 // Phase 33 — Selection glow (MUST BE INSIDE BeginNodeEditor/EndNodeEditor scope)
262 // Draw selection glow for this node if it's selected
263 // Glow color adapts to the node's visual style
264 if (ImNodes::IsNodeSelected(eNode.nodeID))
265 {
266 ImVec2 nodeScreenPos = ImNodes::GetNodeScreenSpacePos(eNode.nodeID);
267 ImVec2 nodeSize = ImNodes::GetNodeDimensions(eNode.nodeID);
268
271
272 // Adapt glow color to node type
274 unsigned int glowColor = GetNodeTitleColor(nodeStyle);
275
276 // Use SelectionEffectRenderer with node-type-specific color
278 screenMin,
279 screenMax,
280 glowColor, // Glow color matches node type
281 2.0f, // baseWidth
282 1.0f, // zoom (imnodes fixed 1.0x)
283 1.0f, // nodeScale
284 5.0f // cornerRadius
285 );
286 }
287
288 // Mark this node as rendered so position sync is safe
289 m_positionedNodes.insert(eNode.nodeID);
290 }
291
292 // Render links
293 for (size_t i = 0; i < m_editorLinks.size(); ++i)
294 {
295 const VSEditorLink& link = m_editorLinks[i];
296 if (link.isData)
298 else
300 ImNodes::Link(link.linkID, link.srcAttrID, link.dstAttrID);
301 ImNodes::PopColorStyle();
302 }
303
304 // Phase 37 — Render minimap overlay on canvas
305 if (m_canvasEditor)
306 m_canvasEditor->RenderMinimap();
307
308 ImNodes::EndNodeEditor();
309
310 // FIX 4: Skip position sync if undo/redo just executed.
311 // SyncEditorNodesFromTemplate() has already written the correct undo-target
312 // positions into m_editorNodes and SetNodeEditorSpacePos() has pushed them
313 // to ImNodes. Reading them back here (before ImNodes has rendered the new
314 // positions once) would overwrite the correct values with stale ImNodes state.
316 {
318 }
319 else
320 {
322 }
323
324 // ========================================================================
325 // Context menu dispatch (requires ImNodesScope_None, i.e. after EndNodeEditor)
326 // Priority: node hover > link hover > canvas background.
327 // ========================================================================
328 {
329 int hoveredNode = -1;
330 int hoveredLink = -1;
331 bool nodeHovered = ImNodes::IsNodeHovered(&hoveredNode);
332 bool linkHovered = ImNodes::IsLinkHovered(&hoveredLink);
333
334 // PHASE 1: Detect right-click and open the appropriate popup.
335 // Use ImNodes::IsEditorHovered() for canvas background detection so
336 // that the check works even when ImNodes has captured mouse focus.
337 if (ImGui::IsMouseClicked(ImGuiMouseButton_Right))
338 {
339 if (nodeHovered)
340 {
342 ImGui::OpenPopup("VSNodeContextMenu");
343 SYSTEM_LOG << "[VSEditor] Opened context menu on NODE #" << hoveredNode << "\n";
344 }
345 else if (linkHovered)
346 {
348 ImGui::OpenPopup("VSLinkContextMenu");
349 SYSTEM_LOG << "[VSEditor] Opened context menu on LINK #" << hoveredLink << "\n";
350 }
351 else if (ImNodes::IsEditorHovered())
352 {
353 // Convert screen-space mouse position to canvas-space by
354 // subtracting the ImNodes canvas panning offset.
355 // Note: ImNodes 0.4 does not expose a zoom accessor, so zoom=1.0f.
356 ImVec2 mp = ImGui::GetMousePos();
357 ImVec2 canvasOrigin = ImNodes::EditorContextGetPanning();
358 float zoom = 1.0f;
359 m_contextMenuX = (mp.x - canvasOrigin.x) / zoom;
360 m_contextMenuY = (mp.y - canvasOrigin.y) / zoom;
361 ImGui::OpenPopup("VSNodePalette");
362 SYSTEM_LOG << "[VSEditor] Opened context menu on CANVAS at ("
363 << m_contextMenuX << ", " << m_contextMenuY << ")\n";
364 }
365 }
366
367 // PHASE 1.5: Detect left-click (for double-click tracking).
368 // Used to detect when user double-clicks on a node (e.g., SubGraph).
369 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
370 {
371 if (nodeHovered)
372 {
373 float currentTime = static_cast<float>(ImGui::GetTime());
374
375 // Check if this is a double-click (same node + within threshold)
378 {
379 // Double-click detected!
381 m_lastClickNodeID = -1; // Reset to prevent triple-click
382 }
383 else
384 {
385 // First click (or click on different node)
388 }
389 }
390 else
391 {
392 // Clicked on empty canvas, reset double-click state
394 }
395 }
396
397 // PHASE 2: Render popups in the same ImGui window scope.
399 }
400
401 // ========================================================================
402 // PHASE 2: Detect drag & drop (store pending node creation).
403 // AddNode() must NOT be called here — ImNodes' internal state is still
404 // being finalised at this point and SetNodeEditorSpacePos would assert.
405 // ========================================================================
406 if (ImGui::BeginDragDropTarget())
407 {
408 const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("VS_NODE_TYPE_ENUM");
409 if (payload && payload->Data && payload->DataSize == sizeof(uint8_t))
410 {
411 uint8_t enumValue = *static_cast<const uint8_t*>(payload->Data);
412 TaskNodeType nodeType = static_cast<TaskNodeType>(enumValue);
413
414 // Get mouse position in canvas space
415 ImVec2 mousePos = ImGui::GetMousePos();
416 ImVec2 canvasPos = ImNodes::EditorContextGetPanning();
417 float zoom = 1.0f; // ImNodes doesn't expose zoom yet
418
419 // Convert screen space to canvas space
420 ImVec2 windowPos = ImGui::GetWindowPos();
421 float canvasX = (mousePos.x - windowPos.x - canvasPos.x) / zoom;
422 float canvasY = (mousePos.y - windowPos.y - canvasPos.y) / zoom;
423
424 // CRITICAL: Don't call AddNode() here — just store the request.
425 // The node will be created in Phase 2 below, safely outside the
426 // ImNodes editor scope.
427 m_pendingNodeDrop = true;
428 m_pendingNodeType = nodeType;
431 }
432 ImGui::EndDragDropTarget();
433 }
434
435 // ========================================================================
436 // PHASE 2: Process pending node creation (outside editor scope).
437 // AddNode() and SetNodeEditorSpacePos() are both safe here — the editor
438 // context is fully closed (ImNodesScope_None).
439 // ========================================================================
441 {
442 // Ensure positions are not garbage values (defend against FLT_MAX or corrupted memory)
443 float safeX = m_pendingNodeX;
444 float safeY = m_pendingNodeY;
445 if (!std::isfinite(safeX) || !std::isfinite(safeY) ||
448 {
449 safeX = 0.0f;
450 safeY = 0.0f;
451 SYSTEM_LOG << "[VSEditor] Warning: pending node position was garbage; reset to (0, 0)\n";
452 }
453
455
456 // Pre-register the position so ImNodes places the node correctly
457 // on the very first frame it is rendered (next frame).
458 ImNodes::SetNodeEditorSpacePos(newNodeID, ImVec2(safeX, safeY));
459
460 m_dirty = true;
461 m_pendingNodeDrop = false;
462
463 std::cout << "[VisualScriptEditorPanel] Node created: ID=" << newNodeID
464 << " type=" << static_cast<int>(m_pendingNodeType)
465 << " at (" << safeX << ", " << safeY << ")"
466 << std::endl;
467 }
468
469 // ========================================================================
470 // PHASE 2: Process pending dynamic pin addition (outside editor scope).
471 // The [+] button callback on VSSequence/Switch stores the request here; we
472 // process it after EndNodeEditor so that AddDynamicPinCommand can safely
473 // modify the template and trigger RebuildLinks().
474 //
475 // Phase 3 FIX (Switch nodes): For Switch nodes, instead of direct
476 // DynamicExecOutputPins modification, open the modal for safe editing.
477 // ========================================================================
478 if (m_pendingAddPin)
479 {
480 m_pendingAddPin = false;
481
482 VSEditorNode* eNode = nullptr;
483 for (size_t i = 0; i < m_editorNodes.size(); ++i)
484 {
486 {
488 break;
489 }
490 }
491
492 if (eNode != nullptr)
493 {
494 // Phase 3 FIX: For Switch nodes, open modal instead of direct modification
495 if (eNode->def.Type == TaskNodeType::Switch)
496 {
498 m_switchCaseModal = std::make_unique<SwitchCaseEditorModal>();
499 m_switchCaseModal->Open(eNode->def.switchCases);
500 m_selectedNodeID = m_pendingAddPinNodeID; // Ensure UI knows which node to edit
501 SYSTEM_LOG << "[VSEditor] Switch node #" << m_pendingAddPinNodeID
502 << ": opened modal for safe case editing (Phase 3 FIX)\n";
503 }
504 else if (eNode->def.Type == TaskNodeType::VSSequence)
505 {
506 // VSSequence: proceed with direct pin addition (unchanged behavior)
507 int pinIdx = static_cast<int>(eNode->def.DynamicExecOutputPins.size()) + 1;
508 std::string pinName = "Out_" + std::to_string(pinIdx);
509
510 // Update editor-side def immediately
511 eNode->def.DynamicExecOutputPins.push_back(pinName);
512
513 // Push undo command (also updates template)
515 std::unique_ptr<ICommand>(
516 new AddDynamicPinCommand(m_pendingAddPinNodeID, pinName)),
517 m_template);
518
519 RebuildLinks();
520 m_dirty = true;
521 SYSTEM_LOG << "[VSEditor] AddDynamicPin: VSSequence node #" << m_pendingAddPinNodeID
522 << " added pin '" << pinName << "'\n";
523 }
524 }
525 }
526
527 // ========================================================================
528 // PHASE 2: Process pending dynamic pin removal (outside editor scope).
529 // The [-] button callback on dynamic pins stores the request here; we
530 // process it after EndNodeEditor so that RemoveExecPinCommand can safely
531 // modify the template and trigger RebuildLinks().
532 //
533 // Phase 3 FIX (Switch nodes): For Switch nodes, instead of direct
534 // DynamicExecOutputPins modification, open the modal for safe editing.
535 // ========================================================================
537 {
538 m_pendingRemovePin = false;
539
540 VSEditorNode* eNode = nullptr;
541 for (size_t i = 0; i < m_editorNodes.size(); ++i)
542 {
544 {
546 break;
547 }
548 }
549
550 if (eNode != nullptr)
551 {
552 // Phase 3 FIX: For Switch nodes, open modal instead of direct removal
553 if (eNode->def.Type == TaskNodeType::Switch)
554 {
556 m_switchCaseModal = std::make_unique<SwitchCaseEditorModal>();
557 m_switchCaseModal->Open(eNode->def.switchCases);
558 m_selectedNodeID = m_pendingRemovePinNodeID; // Ensure UI knows which node to edit
559 SYSTEM_LOG << "[VSEditor] Switch node #" << m_pendingRemovePinNodeID
560 << ": opened modal for safe case removal (Phase 3 FIX)\n";
561 }
562 else if (eNode->def.Type == TaskNodeType::VSSequence &&
564 m_pendingRemovePinDynIdx < static_cast<int>(eNode->def.DynamicExecOutputPins.size()))
565 {
566 // VSSequence: proceed with direct pin removal (unchanged behavior)
567 const std::string pinName =
568 eNode->def.DynamicExecOutputPins[static_cast<size_t>(m_pendingRemovePinDynIdx)];
569
570 // Find any outgoing link from this pin in the template
572 std::string linkedTargetPinName;
573 for (size_t c = 0; c < m_template.ExecConnections.size(); ++c)
574 {
575 const ExecPinConnection& ec = m_template.ExecConnections[c];
576 if (ec.SourceNodeID == m_pendingRemovePinNodeID &&
577 ec.SourcePinName == pinName)
578 {
579 linkedTargetNodeID = ec.TargetNodeID;
580 linkedTargetPinName = ec.TargetPinName;
581 break;
582 }
583 }
584
585 // Update editor-side def immediately
586 eNode->def.DynamicExecOutputPins.erase(
587 eNode->def.DynamicExecOutputPins.begin() + m_pendingRemovePinDynIdx);
588
589 // Push undo command (also updates template)
591 std::unique_ptr<ICommand>(
592 new RemoveExecPinCommand(m_pendingRemovePinNodeID,
593 pinName,
597 m_template);
598
599 RebuildLinks();
600 m_dirty = true;
601 SYSTEM_LOG << "[VSEditor] RemoveDynamicPin: VSSequence node #" << m_pendingRemovePinNodeID
602 << " removed pin '" << pinName << "'\n";
603 }
604 }
605 }
606
607 // Track node moves for undo/redo using MoveNodeCommand.
608 // Phase 19 — snapshot-at-click approach:
609 // Step 1 (MouseClicked) : snapshot current eNode.posX/Y for all positioned nodes.
610 // Step 2 (MouseDown) : keep eNode.posX/Y in sync with ImNodes live positions.
611 // Step 3 (MouseReleased) : for each snapshotted node, push MoveNodeCommand if
612 // final position differs from snapshot by more than 1px.
613 //
614 // Only query nodes that have been rendered at least once (present in
615 // m_positionedNodes) to avoid ImNodes assertions for brand-new nodes.
616 {
617 // Skip movement detection for one frame immediately after an undo/redo
618 // so that stale ImNodes positions (not yet updated by the new render
619 // cycle) are not mistaken for user-initiated drag-start positions.
621 {
623 }
624 else
625 {
626 // Step 1: on the initial click, snapshot positions of all positioned nodes.
627 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
628 {
630 for (size_t i = 0; i < m_editorNodes.size(); ++i)
631 {
632 const VSEditorNode& eNode = m_editorNodes[i];
633 if (m_positionedNodes.count(eNode.nodeID) == 0)
634 continue;
636 std::make_pair(eNode.posX, eNode.posY);
637 }
638 //SYSTEM_LOG << "[VSEditor] Mouse clicked: snapshot " << static_cast<size_t>(m_nodeDragStartPositions.size()) << " node positions\n";
639 }
640
641 // Step 2: while mouse is held, keep eNode.posX/Y current (live Save support).
642 if (ImGui::IsMouseDown(ImGuiMouseButton_Left))
643 {
644 for (size_t i = 0; i < m_editorNodes.size(); ++i)
645 {
646 VSEditorNode& eNode = m_editorNodes[i];
647 if (m_positionedNodes.count(eNode.nodeID) == 0)
648 continue;
649 const ImVec2 pos = ImNodes::GetNodeEditorSpacePos(eNode.nodeID);
650 eNode.posX = pos.x;
651 eNode.posY = pos.y;
652 }
653 }
654
655 // Step 3: on release, push MoveNodeCommand for any node that moved > 1px.
656 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left))
657 {
658 for (const auto& entry : m_nodeDragStartPositions)
659 {
660 const int nodeID = entry.first;
661 const float startX = entry.second.first;
662 const float startY = entry.second.second;
663
664 // CRITICAL FIX: Check if node still exists before querying ImNodes
665 // The node could have been deleted or the canvas reloaded between mouse click and release.
666 // Without this check, GetNodeEditorSpacePos() will assert on a non-existent node.
667 if (m_positionedNodes.count(nodeID) == 0)
668 continue; // Skip this node, it was deleted or canvas state changed
669
670 const ImVec2 finalPos = ImNodes::GetNodeEditorSpacePos(nodeID);
671
672 // Update eNode with final position
673 for (size_t i = 0; i < m_editorNodes.size(); ++i)
674 {
675 if (m_editorNodes[i].nodeID == nodeID)
676 {
677 m_editorNodes[i].posX = finalPos.x;
678 m_editorNodes[i].posY = finalPos.y;
679 break;
680 }
681 }
682
683 if (std::abs(finalPos.x - startX) > 1.0f ||
684 std::abs(finalPos.y - startY) > 1.0f)
685 {
687 std::unique_ptr<ICommand>(
688 new MoveNodeCommand(nodeID,
689 startX, startY,
690 finalPos.x, finalPos.y)),
691 m_template);
692 //SYSTEM_LOG << "[VSEditor] MoveNodeCommand pushed node #" << nodeID
693 // << " (" << startX << "," << startY
694 // << ") -> (" << finalPos.x << "," << finalPos.y
695 // << ") [UNDOABLE]\n";
696 m_dirty = true;
697 }
698 //else
699 //{
700 // SYSTEM_LOG << "[VSEditor] Node #" << nodeID
701 // << " not moved (delta < 1px), skipping\n";
702 //}
703 }
705 }
706 } // end !m_justPerformedUndoRedo
707 }
708
709 // Hover tooltip — ImNodes::IsNodeHovered() requires ImNodesScope_None,
710 // so it must be called here (after EndNodeEditor), never inside the
711 // BeginNodeEditor/EndNodeEditor block.
712 {
713 int hoveredNode = -1;
714 if (ImNodes::IsNodeHovered(&hoveredNode))
715 {
716 auto it = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
717 [hoveredNode](const VSEditorNode& n) {
718 return n.nodeID == hoveredNode;
719 });
720 if (it != m_editorNodes.end())
721 {
722 const char* tip = GetNodeTypeLabel(it->def.Type);
723 if (tip && tip[0] != '\0')
724 {
725 ImGui::BeginTooltip();
726 ImGui::TextUnformatted(tip);
727 ImGui::EndTooltip();
728 }
729 }
730 }
731 }
732
733 // Handle new link creation
734 int startAttr = -1, endAttr = -1;
735 if (ImNodes::IsLinkCreated(&startAttr, &endAttr))
736 {
737 int startOffset = startAttr % 10000;
738 int endOffset = endAttr % 10000;
739
740 // Classify pin directions by offset range:
741 // 0 -> exec-in (Input)
742 // 100–199 -> exec-out (Output)
743 // 200–299 -> data-in (Input)
744 // 300–399 -> data-out (Output)
745 bool startIsOutput = (startOffset >= 100 && startOffset < 200) ||
746 (startOffset >= 300 && startOffset < 400);
747 bool endIsInput = (endOffset == 0) ||
748 (endOffset >= 200 && endOffset < 300);
749
750 // Auto-swap if user dragged backwards (Input -> Output).
751 // ImNodes normalises the direction automatically (Output pin is always
752 // returned as startAttr), so this branch fires only in edge cases where
753 // the pin type could not be determined by ImNodes.
755 {
756 std::swap(startAttr, endAttr);
757 startOffset = startAttr % 10000;
758 endOffset = endAttr % 10000;
759 // Recalculate flags from the new offsets after swap.
760 startIsOutput = (startOffset >= 100 && startOffset < 200) ||
761 (startOffset >= 300 && startOffset < 400);
762 endIsInput = (endOffset == 0) ||
763 (endOffset >= 200 && endOffset < 300);
764 }
765
767 {
768 const bool isExecLink = (startOffset >= 100 && startOffset < 200);
769 const bool isDataLink = (startOffset >= 300 && startOffset < 400);
770 const int srcNodeID = startAttr / 10000;
771 const int dstNodeID = endAttr / 10000;
772
773 // Phase 24: CRITICAL - Prevent data links from connecting to exec-in pin (offset 0)
774 // If this is a data link and destination is exec-in, find the first available data-in pin instead
775 if (isDataLink && endOffset == 0)
776 {
777 // Data-to-exec mismatch detected. Try to find the first available data-in pin.
778 auto dstIt = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
779 [dstNodeID](const VSEditorNode& n) {
780 return n.nodeID == dstNodeID;
781 });
782
783 if (dstIt != m_editorNodes.end() &&
784 dstIt->def.Type == TaskNodeType::Branch &&
785 !dstIt->def.dynamicPins.empty())
786 {
787 // Force endAttr to point to the first dynamic data-in pin (offset 200)
788 endAttr = dstNodeID * 10000 + 200;
789 endOffset = 200;
790 }
791 else
792 {
793 // No valid data-in pins available, reject this link
794 m_dirty = false;
795 // Skip link creation
796 startIsOutput = false;
797 endIsInput = false;
798 }
799 }
800
801 // Only proceed if we still have valid pins to connect
802 if (!startIsOutput || !endIsInput)
803 return;
804
805 if (isExecLink)
806 {
807 // Resolve source exec-out pin name from its index
808 const int srcPinIndex = startOffset - 100;
809 std::string srcPinName = "Out";
810
811 auto srcIt = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
812 [srcNodeID](const VSEditorNode& n) {
813 return n.nodeID == srcNodeID;
814 });
815 if (srcIt != m_editorNodes.end())
816 {
818 if (srcPinIndex < static_cast<int>(outPins.size()))
820 }
821
823 {
825 SYSTEM_LOG << "[VSEditor] Created exec link: node #" << srcNodeID
826 << "." << srcPinName << " -> node #" << dstNodeID << ".In\n";
827 m_dirty = true;
828 }
829 else
830 {
831 SYSTEM_LOG << "[VSEditor] Exec link validation failed: node #" << srcNodeID
832 << "." << srcPinName << " -> node #" << dstNodeID << ".In\n";
833 }
834 }
835 else if (isDataLink)
836 {
837 // Resolve source data-out and destination data-in pin names
838 int srcPinIndex = startOffset - 300;
839 int dstPinIndex = endOffset - 200;
840 std::string srcPinName = "Value";
841 std::string dstPinName = "Value";
842
843 auto srcIt = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
844 [srcNodeID](const VSEditorNode& n) {
845 return n.nodeID == srcNodeID;
846 });
847 auto dstIt = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
848 [dstNodeID](const VSEditorNode& n) {
849 return n.nodeID == dstNodeID;
850 });
851
852 if (srcIt != m_editorNodes.end())
853 {
854 // Try static pin list first, then fall back to DataPins vector
855 auto outPins = GetDataOutputPins(srcIt->def.Type);
856 if (srcPinIndex < static_cast<int>(outPins.size()))
857 {
859 }
860 else
861 {
862 int outIdx = 0;
863 for (size_t p = 0; p < srcIt->def.DataPins.size(); ++p)
864 {
865 if (srcIt->def.DataPins[p].Dir == DataPinDir::Output)
866 {
867 if (outIdx == srcPinIndex)
868 {
869 srcPinName = srcIt->def.DataPins[p].PinName;
870 break;
871 }
872 ++outIdx;
873 }
874 }
875 }
876 }
877
878 if (dstIt != m_editorNodes.end())
879 {
880 // Phase 24: Check if destination is a Branch node with dynamic pins
881 if (dstIt->def.Type == TaskNodeType::Branch)
882 {
883 // For Branch nodes, data-in pins start at offset 200
884 // If dstPinIndex is negative or out of range, force it to 0 (first pin)
885 if (dstPinIndex < 0 || dstPinIndex >= static_cast<int>(dstIt->def.dynamicPins.size()))
886 {
887 if (!dstIt->def.dynamicPins.empty())
888 {
889 dstPinIndex = 0; // Force first available data-in pin
890 std::cerr << "[VSEditor] Data-in pin index corrected to 0 (first available)\n";
891 }
892 }
893
894 if (dstPinIndex >= 0 && dstPinIndex < static_cast<int>(dstIt->def.dynamicPins.size()))
895 {
896 // Use the dynamic pin's ID as the target pin name
897 dstPinName = dstIt->def.dynamicPins[dstPinIndex].id;
898 }
899 else
900 {
901 std::cerr << "[VSEditor] Cannot find valid data-in pin on Branch node\n";
902 return; // Skip this link
903 }
904 }
905 else
906 {
907 // Fall back to static data pins
908 auto inPins = GetDataInputPins(dstIt->def.Type);
909 if (dstPinIndex < static_cast<int>(inPins.size()))
910 {
912 }
913 else
914 {
915 int inIdx = 0;
916 for (size_t p = 0; p < dstIt->def.DataPins.size(); ++p)
917 {
918 if (dstIt->def.DataPins[p].Dir == DataPinDir::Input)
919 {
920 if (inIdx == dstPinIndex)
921 {
922 dstPinName = dstIt->def.DataPins[p].PinName;
923 break;
924 }
925 ++inIdx;
926 }
927 }
928 }
929 }
930 }
931
933 std::cout << "[VisualScriptEditorPanel] Created data link: node"
934 << srcNodeID << "." << srcPinName
935 << " -> node" << dstNodeID << "." << dstPinName << "\n";
936 m_dirty = true;
937 }
938 else
939 {
940 std::cerr << "[VisualScriptEditorPanel] Cannot create link"
941 " — incompatible pin types (exec/data mismatch)\n";
942 }
943 }
944 else
945 {
946 std::cerr << "[VisualScriptEditorPanel] Cannot create link"
947 " — incompatible pin types (both inputs or both outputs)\n";
948 }
949 }
950
951 // Handle link deletion (triggered when the user Ctrl+clicks a link in ImNodes)
952 int destroyedLink = -1;
953 if (ImNodes::IsLinkDestroyed(&destroyedLink))
954 {
955 // Delegate to RemoveLink() so that:
956 // 1. The underlying template connection is removed (not just the
957 // visual m_editorLinks entry). Without this the connection would
958 // reappear as a "ghost" link the next time RebuildLinks() is called
959 // (e.g. after any undo/redo).
960 // 2. A DeleteLinkCommand is pushed onto the undo stack, making the
961 // deletion reversible via Ctrl+Z.
963 }
964
965 // Handle node selection
966 if (ImNodes::NumSelectedNodes() == 1)
967 {
968 int selNodes[1] = {-1};
969 ImNodes::GetSelectedNodes(selNodes);
971 }
972 else if (ImNodes::NumSelectedNodes() == 0)
973 {
974 m_selectedNodeID = -1;
975 }
976
977 // F9 = toggle breakpoint on selected node
978 if (ImGui::IsKeyPressed(ImGuiKey_F9) && m_selectedNodeID >= 0)
979 {
982 "Node " + std::to_string(m_selectedNodeID));
983 }
984
985 // Delete key = remove all selected nodes and links
986 if (ImGui::IsKeyPressed(ImGuiKey_Delete) && ImGui::IsWindowFocused())
987 {
988 int numSelectedNodes = ImNodes::NumSelectedNodes();
989 if (numSelectedNodes > 0)
990 {
991 if (numSelectedNodes > 5)
992 {
993 std::cout << "[VSEditor] Warning: Deleting " << numSelectedNodes
994 << " nodes" << std::endl;
995 }
996
997 std::vector<int> selectedNodes(static_cast<size_t>(numSelectedNodes));
998 ImNodes::GetSelectedNodes(selectedNodes.data());
999
1000 for (int nodeID : selectedNodes)
1001 {
1002 if (m_selectedNodeID == nodeID)
1003 m_selectedNodeID = -1;
1004 RemoveNode(nodeID);
1005 std::cout << "[VSEditor] Deleted node " << nodeID << std::endl;
1006 }
1007
1008 m_dirty = true;
1009 }
1010
1011 int numSelectedLinks = ImNodes::NumSelectedLinks();
1012 if (numSelectedLinks > 0)
1013 {
1014 std::vector<int> selectedLinks(static_cast<size_t>(numSelectedLinks));
1015 ImNodes::GetSelectedLinks(selectedLinks.data());
1016
1017 for (int linkID : selectedLinks)
1018 {
1019 RemoveLink(linkID);
1020 std::cout << "[VSEditor] Deleted link " << linkID << std::endl;
1021 }
1022
1023 m_dirty = true;
1024 }
1025 }
1026
1028}
1029
1030
1031// ============================================================================
1032// Node Palette
1033// ============================================================================
1034
1036{
1037 if (!ImGui::BeginPopup("VSNodePalette"))
1038 return;
1039
1040 ImGui::TextDisabled("Add Node");
1041 ImGui::Separator();
1042
1043 // Flow Control
1044 if (ImGui::BeginMenu("Flow Control"))
1045 {
1046 auto addFlowNode = [&](TaskNodeType type, const char* label) {
1047 if (ImGui::MenuItem(label))
1048 {
1050 ImGui::CloseCurrentPopup();
1051 }
1052 };
1060 ImGui::EndMenu();
1061 }
1062
1063 if (ImGui::BeginMenu("Actions"))
1064 {
1065 if (ImGui::MenuItem("AtomicTask"))
1066 {
1068 ImGui::CloseCurrentPopup();
1069 }
1070 ImGui::EndMenu();
1071 }
1072
1073 if (ImGui::BeginMenu("Data"))
1074 {
1075 if (ImGui::MenuItem("GetBBValue"))
1076 {
1078 ImGui::CloseCurrentPopup();
1079 }
1080 if (ImGui::MenuItem("SetBBValue"))
1081 {
1083 ImGui::CloseCurrentPopup();
1084 }
1085 if (ImGui::MenuItem("MathOp"))
1086 {
1088 ImGui::CloseCurrentPopup();
1089 }
1090 ImGui::EndMenu();
1091 }
1092
1093 if (ImGui::BeginMenu("SubGraph"))
1094 {
1095 if (ImGui::MenuItem("SubGraph"))
1096 {
1098 ImGui::CloseCurrentPopup();
1099 }
1100 ImGui::EndMenu();
1101 }
1102
1103 ImGui::EndPopup();
1104}
1105
1106// ============================================================================
1107// Context Menus
1108// ============================================================================
1109
1111{
1112 // ========================================================================
1113 // Node context menu
1114 // ========================================================================
1115 if (ImGui::BeginPopup("VSNodeContextMenu"))
1116 {
1117 if (ImGui::MenuItem("Edit Properties"))
1118 {
1120 SYSTEM_LOG << "[VSEditor] Selected node #" << m_contextNodeID
1121 << " for editing\n";
1122 }
1123
1124 ImGui::Separator();
1125
1126 if (ImGui::MenuItem("Delete Node"))
1127 {
1130 m_selectedNodeID = -1;
1131 m_dirty = true;
1132 SYSTEM_LOG << "[VSEditor] Deleted node #" << m_contextNodeID
1133 << " via context menu\n";
1134 }
1135
1136 ImGui::Separator();
1137
1138 {
1140 if (ImGui::MenuItem(hasBP ? "Remove Breakpoint (F9)" : "Add Breakpoint (F9)"))
1141 {
1144 "Node " + std::to_string(m_contextNodeID));
1145 SYSTEM_LOG << "[VSEditor] Toggled breakpoint on node #"
1146 << m_contextNodeID << " -> "
1147 << (hasBP ? "OFF" : "ON") << "\n";
1148 }
1149 }
1150
1151 ImGui::Separator();
1152
1153 if (ImGui::MenuItem("Duplicate"))
1154 {
1155 auto it = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
1156 [this](const VSEditorNode& n) { return n.nodeID == m_contextNodeID; });
1157 if (it != m_editorNodes.end())
1158 {
1159 TaskNodeDefinition newDef = it->def;
1160 newDef.NodeID = AllocNodeID();
1161 newDef.NodeName += " (Copy)";
1162 newDef.EditorPosX = it->posX + 50.0f;
1163 newDef.EditorPosY = it->posY + 50.0f;
1164 newDef.HasEditorPos = true;
1165
1166 VSEditorNode eNew;
1167 eNew.nodeID = newDef.NodeID;
1168 eNew.posX = newDef.EditorPosX;
1169 eNew.posY = newDef.EditorPosY;
1170 eNew.def = newDef;
1171 m_editorNodes.push_back(eNew);
1172
1174 std::unique_ptr<ICommand>(new AddNodeCommand(newDef)),
1175 m_template);
1176 m_dirty = true;
1177 SYSTEM_LOG << "[VSEditor] Node " << m_contextNodeID
1178 << " duplicated as #" << newDef.NodeID << "\n";
1179 }
1180 }
1181
1182 ImGui::EndPopup();
1183 }
1184
1185 // ========================================================================
1186 // Link context menu
1187 // ========================================================================
1188 if (ImGui::BeginPopup("VSLinkContextMenu"))
1189 {
1190 if (ImGui::MenuItem("Delete Connection"))
1191 {
1193 m_dirty = true;
1194 SYSTEM_LOG << "[VSEditor] Deleted link #" << m_contextLinkID
1195 << " via context menu\n";
1196 }
1197 ImGui::EndPopup();
1198 }
1199}
1200
1201} // namespace Olympe
UI-side registry of available atomic tasks with display metadata.
Wrapper around the graph blackboard entries for dropdown editors.
Registry of available condition types for Branch/While node dropdowns.
Runtime debug controller for ATS Visual Scripting (Phase 5).
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Defines MathOpOperand — operand references for MathOp nodes.
Hardcoded lists of math and comparison operators for dropdown editors.
Central manager for the multi-graph tab system.
ImNodes-based graph editor for ATS Visual Script graphs (Phase 5).
int GetCurrentNodeID() const
Returns the node being executed (-1 if none).
void ToggleBreakpoint(int graphID, int nodeID, const std::string &graphName="", const std::string &nodeName="")
Toggles the breakpoint at (graphID, nodeID).
static DebugController & Get()
Returns the singleton instance (Meyers pattern).
bool IsDebugging() const
Returns true when a debug session is active.
bool HasBreakpoint(int graphID, int nodeID) const
Returns true if an enabled breakpoint exists at (graphID, nodeID).
void RenderCompleteSelection(const ImVec2 &minScreen, const ImVec2 &maxScreen, ImU32 borderColor, float baseWidth, float canvasZoom=1.0f, float nodeScale=1.0f, float cornerRadius=5.0f) const
Rend l'ensemble de l'effet de sélection (glow + bordure)
std::vector< ExecPinConnection > ExecConnections
Explicit exec connections (ATS VS only)
std::string Name
Friendly name of this template (e.g. "PatrolBehaviour")
void PushCommand(std::unique_ptr< ICommand > cmd, TaskGraphTemplate &graph)
Executes the command on graph, then pushes it onto the undo stack.
static bool IsExecConnectionValid(const TaskGraphTemplate &graph, int srcNodeID, const std::string &srcPinName, int dstNodeID)
Returns true if adding an exec connection from srcNodeID/srcPinName to dstNodeID would be valid (no s...
UndoRedoStack m_undoStack
Undo/Redo command stack for reversible graph editing operations.
float m_contextMenuX
Right-click paste position.
void OnNodeDoubleClicked(int nodeID)
Handles double-click on a node (opens SubGraph, etc).
bool m_pendingRemovePin
Pending dynamic pin removal (from [-] button clicked in canvas)
std::vector< VSEditorLink > m_editorLinks
Editor links (exec + data)
TaskGraphTemplate m_template
The template currently being edited.
int m_lastClickNodeID
Node ID of the last left-click (for double-click detection)
void RemoveNode(int nodeID)
Removes a node from the canvas.
std::unique_ptr< SwitchCaseEditorModal > m_switchCaseModal
Phase 26 — Switch Case Editor Modal.
void RemoveLink(int linkID)
Removes an ImNodes link (and its underlying template connection) by link ID.
bool m_skipPositionSyncNextFrame
Set to true by Undo/Redo; causes next frame to skip SyncNodePositionsFromImNodes() so that the positi...
void ConnectData(int srcNodeID, const std::string &srcPinName, int dstNodeID, const std::string &dstPinName)
Creates a data connection between two nodes.
static std::vector< std::string > GetExecInputPins(TaskNodeType type)
Returns the exec-in pin names for a node type.
static constexpr float DOUBLE_CLICK_THRESHOLD
Threshold for detecting double-click (300ms)
std::unique_ptr< NodeBranchRenderer > m_branchRenderer
Specialized renderer for Branch nodes (4-section layout with conditions).
int m_contextLinkID
Link ID captured at the moment a right-click context menu was opened on a link.
void ConnectExec(int srcNodeID, const std::string &srcPinName, int dstNodeID, const std::string &dstPinName)
Creates an exec connection between two nodes.
static std::vector< std::string > GetDataOutputPins(TaskNodeType type)
Returns the data-out pin names for a node type.
bool m_pendingNodeDrop
True when a node drop is pending processing this frame.
void RenderContextMenus()
Render node/link context menus opened by right-click detection.
int AddNode(TaskNodeType type, float x, float y)
Creates a new node on the canvas.
VSVerificationResult m_verificationResult
Latest verification result (produced by RunVerification())
int m_pendingRemovePinDynIdx
0-based index in DynamicExecOutputPins
static std::vector< std::string > GetDataInputPins(TaskNodeType type)
Returns the data-in pin names for a node type.
std::unordered_set< int > m_positionedNodes
Nodes for which ImNodes has been given a position.
SelectionEffectRenderer m_selectionRenderer
Renders glow effect for selected nodes (cyan halo + thickened border).
std::unique_ptr< ImNodesCanvasEditor > m_canvasEditor
Canvas editor adapter for minimap support (Phase 37) Abstracts imnodes minimap rendering through ICan...
void RebuildLinks()
Rebuilds ImNodes exec/data link arrays from the template.
int m_focusNodeID
Node ID to focus/scroll to on next RenderCanvas() frame (-1 = none)
void SyncNodePositionsFromImNodes()
Pulls the current node positions from ImNodes into m_editorNodes.
int m_contextNodeID
Node ID captured at the moment a right-click context menu was opened on a node.
bool m_justPerformedUndoRedo
Set to true immediately after Undo/Redo; blocks node movement tracking for 1 frame to allow ImNodes t...
std::unordered_map< int, std::pair< float, float > > m_nodeDragStartPositions
Per-node drag-start positions used to record a single MoveNodeCommand per drag gesture instead of one...
float m_lastClickTime
Frame time of the last left-click (seconds, from ImGui::GetTime())
std::vector< std::string > GetExecOutputPinsForNode(const TaskNodeDefinition &def) const
Returns exec-out pin names for a node definition, including any dynamically-added pins (VSSequence).
bool m_verificationDone
True once RunVerification() has been called at least once for the current graph.
bool m_pendingAddPin
Pending dynamic pin addition (from [+] button clicked in canvas)
std::vector< VSEditorNode > m_editorNodes
Editor nodes (mirrors m_template.Nodes + position/selection state)
int m_selectedNodeID
Currently selected node (for properties panel)
static void RenderNode(int nodeUID, int nodeID, int graphID, const std::string &nodeName, TaskNodeType type, bool hasBreakpoint, bool isActive, const std::vector< std::string > &execInputPins, const std::vector< std::string > &execOutputPins, const std::vector< std::pair< std::string, VariableType > > &dataInputPins, const std::vector< std::pair< std::string, VariableType > > &dataOutputPins, const std::unordered_set< int > &connectedAttrIDs={})
Renders a complete VS node (title + exec pins + data pins).
static void RenderBreakpointIndicator(int nodeUID)
Renders a breakpoint indicator (red circle) next to a node.
static void RenderActiveNodeGlow(int nodeUID)
Renders a "currently executing" glow overlay around a node.
constexpr uint32_t EXEC_CONNECTION_COLOR
Alternative white color for exec connections (Bezier curves).
constexpr uint32_t DATA_CONNECTION_COLOR
Alternative violet color for data connections (Bezier curves).
< Provides AssetID and INVALID_ASSET_ID
const char * GetNodeTypeLabel(TaskNodeType type)
Returns a human-readable label for a TaskNodeType.
@ Float
Single-precision float.
VSNodeStyle
Visual style category for a VS node.
VSNodeStyle GetNodeStyle(TaskNodeType type)
Returns the VSNodeStyle appropriate for a given node 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)
@ Output
Value produced by the node.
@ Input
Value consumed by the node.
unsigned int GetNodeTitleColor(VSNodeStyle style)
Returns the title-bar RGBA colour for a given style.
std::vector< VSVerificationIssue > issues
#define SYSTEM_LOG