Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
VisualScriptEditorPanel - backup.cpp
Go to the documentation of this file.
1/**
2 * @file VisualScriptEditorPanel.cpp
3 * @brief ImNodes graph editor implementation for ATS VS graphs (Phase 5).
4 * @author Olympe Engine
5 * @date 2026-03-09
6 *
7 * @details C++14 compliant — no std::optional, structured bindings, std::filesystem.
8 */
9
11#include "DebugController.h"
13#include "ConditionRegistry.h"
14#include "OperatorRegistry.h"
15#include "BBVariableRegistry.h"
16#include "MathOpOperand.h"
17#include "../system/system_utils.h"
18#include "../system/system_consts.h"
19#include "../NodeGraphCore/GlobalTemplateBlackboard.h"
20
21#include "../third_party/imgui/imgui.h"
22#include "../third_party/imnodes/imnodes.h"
23#include "../json_helper.h"
24#include "../TaskSystem/TaskGraphLoader.h"
25
26#include <fstream>
27#include <iostream>
28#include <algorithm>
29#include <cmath>
30#include <cstring>
31#include <sstream>
32#include <iomanip>
33#include <cstdlib>
34#include <unordered_set>
35
36namespace Olympe {
37
38// ============================================================================
39// Constructor / Destructor
40// ============================================================================
41
47
51
53{
54 // Create a dedicated ImNodes editor context for this panel instance.
55 // This ensures that node positions and canvas panning are tracked
56 // independently for each open tab (switching tabs preserves layout).
57 m_imnodesContext = ImNodes::EditorContextCreate();
58
59 // Phase 24 — Condition Preset UI: create helpers bound to m_presetRegistry.
60 m_pinManager = std::unique_ptr<DynamicDataPinManager>(
62 m_branchRenderer = std::unique_ptr<NodeBranchRenderer>(
64 m_conditionsPanel = std::unique_ptr<NodeConditionsPanel>(
66 m_mathOpPanel = std::unique_ptr<MathOpPropertyPanel>(
68 m_getBBPanel = std::unique_ptr<GetBBValuePropertyPanel>(
70 m_setBBPanel = std::unique_ptr<SetBBValuePropertyPanel>(
72 m_variablePanel = std::unique_ptr<VariablePropertyPanel>(
74 m_libraryPanel = std::unique_ptr<ConditionPresetLibraryPanel>(
76
77 // Phase 24 Global Blackboard Integration: Create EntityBlackboard for managing
78 // both local and global variables in the editor context (entity ID 0)
79 m_entityBlackboard = std::unique_ptr<EntityBlackboard>(
80 new EntityBlackboard(0)); // 0 = editor context entity
81
82 // Wire the pin-regeneration callback so the Edit-Conditions modal can
83 // trigger a canvas update when the user confirms changes.
84 m_conditionsPanel->OnDynamicPinsNeedRegeneration = [this]()
85 {
86 if (m_selectedNodeID < 0)
87 return;
88 for (size_t ni = 0; ni < m_editorNodes.size(); ++ni)
89 {
91 if (eNode.nodeID != m_selectedNodeID)
92 continue;
93
94 // Phase 24: Get FRESH condition data from panel (not stale data from eNode)
95 // This ensures that edits via RenderConditionList dropdown are picked up
96 std::vector<NodeConditionRef> freshConditionRefs = m_conditionsPanel->GetConditionRefs();
97 std::vector<ConditionRef> freshOperandRefs = m_conditionsPanel->GetConditionOperandRefs();
98
99 // Sync fresh data to eNode
101 eNode.def.conditionOperandRefs = freshOperandRefs;
102
103 // Regenerate pins with FRESH operand data
104 m_pinManager->RegeneratePinsFromConditions(freshConditionRefs, freshOperandRefs);
105 eNode.def.dynamicPins = m_pinManager->GetAllPins();
106
107 // Keep m_template in sync for serialization.
108 for (size_t ti = 0; ti < m_template.Nodes.size(); ++ti)
109 {
110 if (m_template.Nodes[ti].NodeID == m_selectedNodeID)
111 {
112 m_template.Nodes[ti].conditionRefs = eNode.def.conditionRefs;
113 m_template.Nodes[ti].conditionOperandRefs = eNode.def.conditionOperandRefs;
114 m_template.Nodes[ti].dynamicPins = eNode.def.dynamicPins;
115 break;
116 }
117 }
118 m_conditionsPanel->SetDynamicPins(eNode.def.dynamicPins);
119 m_dirty = true;
120 break;
121 }
122 };
123
124 // Phase 24 — Load presets from the graph (now embedded in blueprint JSON)
125 // instead of from an external file. This makes each blueprint self-contained.
126 // If the graph has presets, populate the registry; otherwise leave empty.
127 if (!m_template.Presets.empty())
128 {
130 SYSTEM_LOG << "[VSEditor] Initialize: loaded " << m_template.Presets.size()
131 << " presets from graph '" << m_template.Name << "'\n";
132 }
133 else
134 {
135 SYSTEM_LOG << "[VSEditor] Initialize: graph '" << m_template.Name
136 << "' has no embedded presets\n";
137 }
138}
139
141{
143 {
144 ImNodes::EditorContextFree(m_imnodesContext);
145 m_imnodesContext = nullptr;
146 }
147 m_editorNodes.clear();
148 m_editorLinks.clear();
149 m_positionedNodes.clear();
150
151 // Phase 24 — release helpers before registry is destroyed.
152 m_conditionsPanel.reset();
153 m_mathOpPanel.reset();
154 m_getBBPanel.reset();
155 m_setBBPanel.reset();
156 m_variablePanel.reset();
157 m_libraryPanel.reset();
158 m_branchRenderer.reset();
159 m_pinManager.reset();
161}
162
163// ============================================================================
164// UID helpers
165// ============================================================================
166
171
176
177// Attribute UIDs are built as:
178// nodeID * 10000 + offset
179// The offsets are:
180// 0 -> exec-in "In"
181// 100–199 -> exec-out pins (index 0-99)
182// 200–299 -> data-in pins (index 0-99)
183// 300–399 -> data-out pins (index 0-99)
184
186{
187 return nodeID * 10000 + 0;
188}
189
191{
192 return nodeID * 10000 + 100 + pinIndex;
193}
194
196{
197 return nodeID * 10000 + 200 + pinIndex;
198}
199
201{
202 return nodeID * 10000 + 300 + pinIndex;
203}
204
205// ============================================================================
206// Pin name helpers
207// ============================================================================
208
210{
211 switch (type)
212 {
214 return {}; // No exec-in on EntryPoint
216 return {}; // Phase 24.2: Variable (GetBBValue) is data-pure (no execution pins)
218 return {}; // Phase 24.2: MathOp is data-pure (no execution pins)
219 default:
220 return {"In"};
221 }
222}
223
225{
226 switch (type)
227 {
228 case TaskNodeType::EntryPoint: return {"Out"};
229 case TaskNodeType::Branch: return {"Then", "Else"};
230 case TaskNodeType::While: return {"Loop", "Completed"};
231 case TaskNodeType::ForEach: return {"Loop Body", "Completed"};
232 case TaskNodeType::DoOnce: return {"Out"};
233 case TaskNodeType::Delay: return {"Completed"};
234 case TaskNodeType::SubGraph: return {"Completed"};
235 case TaskNodeType::VSSequence: return {"Out"};
236 case TaskNodeType::Switch: return {"Case_0"};
237 case TaskNodeType::AtomicTask: return {"Completed"};
238 case TaskNodeType::GetBBValue: return {}; // Phase 24.2: Variable (GetBBValue) is data-pure (no execution pins)
239 case TaskNodeType::SetBBValue: return {"Completed"}; // SetBBValue needs exec-out for control flow
240 case TaskNodeType::MathOp: return {}; // Phase 24.2: MathOp is data-pure (no execution pins)
241 default: return {"Out"};
242 }
243}
244
246 const TaskNodeDefinition& def) const
247{
248 std::vector<std::string> pins = GetExecOutputPins(def.Type);
250 {
251 for (size_t i = 0; i < def.DynamicExecOutputPins.size(); ++i)
252 pins.push_back(def.DynamicExecOutputPins[i]);
253 }
254 return pins;
255}
256
258{
259 switch (type)
260 {
261 case TaskNodeType::SetBBValue: return {"Value"};
262 case TaskNodeType::MathOp: return {"A", "B"};
263 // Phase 24: Branch nodes use ONLY dynamic data-in pins (Pin-in)
264 // No static "Condition" pin to avoid conflicts with dynamic pins
265 case TaskNodeType::Branch: return {};
266 default: return {};
267 }
268}
269
271{
272 switch (type)
273 {
274 case TaskNodeType::GetBBValue: return {"Value"};
275 case TaskNodeType::MathOp: return {"Result"};
276 default: return {};
277 }
278}
279
280// ============================================================================
281// Node management
282// ============================================================================
283
285{
286 // Validate incoming position parameters to prevent garbage values
287 if (!std::isfinite(x) || !std::isfinite(y))
288 {
289 SYSTEM_LOG << "[VSEditor] AddNode: warning - non-finite position provided (x="
290 << x << ", y=" << y << "), resetting to (0, 0)\n";
291 x = 0.0f;
292 y = 0.0f;
293 }
294
295 // Clamp to a reasonable range to prevent extreme coordinate values
296 if (x < -100000.0f || x > 100000.0f) x = 0.0f;
297 if (y < -100000.0f || y > 100000.0f) y = 0.0f;
298
299 int newID = AllocNodeID();
300
302 def.NodeID = newID;
303 def.Type = type;
304 def.NodeName = GetNodeTypeLabel(type);
305
306 // EntryPoint is special
308 {
311 }
312
313 // Phase 24 FIX: Initialize DataPins for MathOp, GetBBValue, SetBBValue nodes
314 // Ensures data pins are rendered correctly with proper offsets
315 if (type == TaskNodeType::MathOp)
316 {
317 // Add input pins A and B
319 pinA.PinName = "A";
321 pinA.PinType = VariableType::Float;
322 def.DataPins.push_back(pinA);
323
325 pinB.PinName = "B";
327 pinB.PinType = VariableType::Float;
328 def.DataPins.push_back(pinB);
329
330 // Add output pin Result
332 pinResult.PinName = "Result";
335 def.DataPins.push_back(pinResult);
336
337 // Phase 24 Milestone 2: Initialize MathOpRef with default operands
338 // left = Const "0", operator = "+", right = Const "0"
341 def.mathOpRef.mathOperator = "+";
344 }
345 else if (type == TaskNodeType::GetBBValue)
346 {
347 // Phase 24 Milestone 3: GetBBValue outputs a data pin (Value)
349 pinValue.PinName = "Value";
351 pinValue.PinType = VariableType::None; // Type determined by selected variable
352 def.DataPins.push_back(pinValue);
353 }
354 else if (type == TaskNodeType::SetBBValue)
355 {
356 // Phase 24 Milestone 3: SetBBValue inputs a data pin (Value)
358 pinValue.PinName = "Value";
360 pinValue.PinType = VariableType::None; // Type determined by target variable
361 def.DataPins.push_back(pinValue);
362 }
363
364 // Persist the spawn position in Parameters so that redo (re-executing
365 // AddNodeCommand) restores the node at its original position rather than
366 // falling back to the default grid layout.
367 {
370 bx.LiteralValue = TaskValue(x);
372 by.LiteralValue = TaskValue(y);
373 def.Parameters["__posX"] = bx;
374 def.Parameters["__posY"] = by;
375 }
376
377 // Editor-side node (tracks canvas position independently of the template)
380 eNode.posX = x;
381 eNode.posY = y;
382 eNode.def = def;
383 m_editorNodes.push_back(eNode);
384
385 // Command adds the node to m_template.Nodes and rebuilds the lookup cache
387 std::unique_ptr<ICommand>(new AddNodeCommand(def)),
388 m_template);
389
390 m_dirty = true;
391 m_verificationDone = false;
392 return newID;
393}
394
396{
397 // Remove from editor nodes (canvas-side)
398 m_editorNodes.erase(
399 std::remove_if(m_editorNodes.begin(), m_editorNodes.end(),
400 [nodeID](const VSEditorNode& n) { return n.nodeID == nodeID; }),
401 m_editorNodes.end());
402
403 // Command removes the node + all associated connections from m_template
405 std::unique_ptr<ICommand>(new DeleteNodeCommand(nodeID)),
406 m_template);
407
408 RebuildLinks();
409 m_dirty = true;
410 m_verificationDone = false;
411}
412
414 const std::string& srcPinName,
415 int dstNodeID,
416 const std::string& dstPinName)
417{
420 conn.SourcePinName = srcPinName;
421 conn.TargetNodeID = dstNodeID;
422 conn.TargetPinName = dstPinName;
423 // Push to undo stack so link creation can be reversed via Ctrl+Z.
424 // AddConnectionCommand::Execute() calls graph.ExecConnections.push_back().
426 std::unique_ptr<ICommand>(new AddConnectionCommand(conn)),
427 m_template);
428 RebuildLinks();
429 m_dirty = true;
430 m_verificationDone = false;
431}
432
434 const std::string& srcPinName,
435 int dstNodeID,
436 const std::string& dstPinName)
437{
440 conn.SourcePinName = srcPinName;
441 conn.TargetNodeID = dstNodeID;
442 conn.TargetPinName = dstPinName;
443 // Push to undo stack so data link creation can be reversed via Ctrl+Z.
444 // AddDataConnectionCommand::Execute() calls graph.DataConnections.push_back().
446 std::unique_ptr<ICommand>(new AddDataConnectionCommand(conn)),
447 m_template);
448 RebuildLinks();
449 m_dirty = true;
450 m_verificationDone = false;
451}
452
453// ============================================================================
454// Template / canvas sync
455// ============================================================================
456
458{
459 m_editorNodes.clear();
460 m_positionedNodes.clear();
461 m_nextNodeID = 1;
462
463 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
464 {
465 const TaskNodeDefinition& def = m_template.Nodes[i];
466
468 eNode.nodeID = def.NodeID;
469 eNode.def = def;
470
471 // Use position loaded from JSON if available; otherwise fall back to auto-layout.
472 if (def.HasEditorPos)
473 {
474 // Validate loaded position to prevent garbage values from corrupted JSON
475 if (std::isfinite(def.EditorPosX) && std::isfinite(def.EditorPosY) &&
476 def.EditorPosX >= -100000.0f && def.EditorPosX <= 100000.0f &&
477 def.EditorPosY >= -100000.0f && def.EditorPosY <= 100000.0f)
478 {
479 eNode.posX = def.EditorPosX;
480 eNode.posY = def.EditorPosY;
481 }
482 else
483 {
484 SYSTEM_LOG << "[VSEditor] SyncCanvasFromTemplate: node #" << def.NodeID
485 << " had garbage position (" << def.EditorPosX << ", " << def.EditorPosY
486 << "), using auto-layout\n";
487 eNode.posX = 200.0f * static_cast<float>(i);
488 eNode.posY = 100.0f;
489 }
490 }
491 else
492 {
493 eNode.posX = 200.0f * static_cast<float>(i); // Default auto-layout
494 eNode.posY = 100.0f;
495 }
496
497 if (def.NodeID >= m_nextNodeID)
498 m_nextNodeID = def.NodeID + 1;
499
500 // Phase 24: Regenerate dynamic pins for Branch nodes after load
501 // This ensures Pin-in connectors are available even if they weren't saved
502 // (they are derived from conditionRefs/conditionOperandRefs)
503 if (def.Type == TaskNodeType::Branch && (!def.conditionRefs.empty() || !def.conditionOperandRefs.empty()))
504 {
505 m_pinManager->RegeneratePinsFromConditions(eNode.def.conditionRefs,
506 eNode.def.conditionOperandRefs);
507 eNode.def.dynamicPins = m_pinManager->GetAllPins();
508
509 // Also update template for consistency
510 for (size_t ti = 0; ti < m_template.Nodes.size(); ++ti)
511 {
512 if (m_template.Nodes[ti].NodeID == eNode.nodeID)
513 {
514 m_template.Nodes[ti].dynamicPins = eNode.def.dynamicPins;
515 break;
516 }
517 }
518 }
519
520 m_editorNodes.push_back(eNode);
521 }
522
523 RebuildLinks();
524 // Request position restore on the next RenderCanvas() call so that
525 // ImNodes places each node at its stored (posX, posY) coordinates.
526 m_needsPositionSync = true;
527}
528
530{
531 // Update template nodes from editor nodes
532 m_template.Nodes.clear();
533 for (size_t i = 0; i < m_editorNodes.size(); ++i)
534 {
535 m_template.Nodes.push_back(m_editorNodes[i].def);
536 }
538}
539
541{
542 m_editorLinks.clear();
543
544 // Exec links
545 for (size_t i = 0; i < m_template.ExecConnections.size(); ++i)
546 {
548
549 // Determine pin index for source
550 std::vector<std::string> outPins;
551 const TaskNodeDefinition* srcNode = m_template.GetNode(conn.SourceNodeID);
552 if (srcNode != nullptr)
553 {
555 if (srcNode->Type == TaskNodeType::VSSequence ||
557 {
558 for (size_t d = 0; d < srcNode->DynamicExecOutputPins.size(); ++d)
559 outPins.push_back(srcNode->DynamicExecOutputPins[d]);
560 }
561 }
562
563 int pinIdx = 0;
564 for (size_t p = 0; p < outPins.size(); ++p)
565 {
566 if (outPins[p] == conn.SourcePinName)
567 {
568 pinIdx = static_cast<int>(p);
569 break;
570 }
571 }
572
575 link.srcAttrID = ExecOutAttrUID(conn.SourceNodeID, pinIdx);
576 link.dstAttrID = ExecInAttrUID(conn.TargetNodeID);
577 link.isData = false;
578 m_editorLinks.push_back(link);
579 }
580
581 // Data links — resolve pin indices from stored pin names
582 for (size_t i = 0; i < m_template.DataConnections.size(); ++i)
583 {
585
586 // Determine data-out pin index for source
587 int srcPinIdx = 0;
588 const TaskNodeDefinition* srcNode = m_template.GetNode(conn.SourceNodeID);
589 if (srcNode != nullptr)
590 {
591 // Try static list first
592 auto outPins = GetDataOutputPins(srcNode->Type);
593 bool found = false;
594 for (size_t p = 0; p < outPins.size(); ++p)
595 {
596 if (outPins[p] == conn.SourcePinName)
597 {
598 srcPinIdx = static_cast<int>(p);
599 found = true;
600 break;
601 }
602 }
603 if (!found)
604 {
605 // Fall back to DataPins vector
606 int outIdx = 0;
607 for (size_t p = 0; p < srcNode->DataPins.size(); ++p)
608 {
609 if (srcNode->DataPins[p].Dir == DataPinDir::Output)
610 {
611 if (srcNode->DataPins[p].PinName == conn.SourcePinName)
612 {
614 break;
615 }
616 ++outIdx;
617 }
618 }
619 }
620 }
621
622 // Determine data-in pin index for destination
623 int dstPinIdx = 0;
624 const TaskNodeDefinition* dstNode = m_template.GetNode(conn.TargetNodeID);
625 if (dstNode != nullptr)
626 {
627 // Phase 24: Check if destination is a Branch node with dynamic pins
628 bool foundDynamicPin = false;
629 if (dstNode->Type == TaskNodeType::Branch && !dstNode->dynamicPins.empty())
630 {
631 // Try to find a matching dynamic pin ID
632 for (size_t p = 0; p < dstNode->dynamicPins.size(); ++p)
633 {
634 if (dstNode->dynamicPins[p].id == conn.TargetPinName)
635 {
636 dstPinIdx = static_cast<int>(p);
637 foundDynamicPin = true;
638 break;
639 }
640 }
641 }
642
643 if (!foundDynamicPin)
644 {
645 // Fall back to static data pins
646 auto inPins = GetDataInputPins(dstNode->Type);
647 bool found = false;
648 for (size_t p = 0; p < inPins.size(); ++p)
649 {
650 if (inPins[p] == conn.TargetPinName)
651 {
652 dstPinIdx = static_cast<int>(p);
653 found = true;
654 break;
655 }
656 }
657 if (!found)
658 {
659 int inIdx = 0;
660 for (size_t p = 0; p < dstNode->DataPins.size(); ++p)
661 {
662 if (dstNode->DataPins[p].Dir == DataPinDir::Input)
663 {
664 if (dstNode->DataPins[p].PinName == conn.TargetPinName)
665 {
667 break;
668 }
669 ++inIdx;
670 }
671 }
672 }
673 }
674 }
675
678 link.srcAttrID = DataOutAttrUID(conn.SourceNodeID, srcPinIdx);
679 link.dstAttrID = DataInAttrUID(conn.TargetNodeID, dstPinIdx);
680 link.isData = true;
681 m_editorLinks.push_back(link);
682 }
683}
684
685// ============================================================================
686// Undo/Redo helpers
687// ============================================================================
688
690{
691 // Default grid spacing used when a node has no recorded canvas position
692 // (e.g. after a Redo restores a previously-deleted node).
693 static const float DEFAULT_NODE_X_OFFSET = 50.0f;
694 static const float DEFAULT_NODE_X_SPACING = 200.0f;
695 static const float DEFAULT_NODE_Y = 100.0f;
696
697 // Preserve canvas positions for nodes that still exist
698 std::unordered_map<int, std::pair<float, float> > savedPos;
699 for (size_t i = 0; i < m_editorNodes.size(); ++i)
700 {
701 float posX = m_editorNodes[i].posX;
702 float posY = m_editorNodes[i].posY;
703
704 // Validate saved positions before preserving them
705 if (!std::isfinite(posX) || !std::isfinite(posY) ||
706 posX < -100000.0f || posX > 100000.0f ||
708 {
710 posY = DEFAULT_NODE_Y;
711 SYSTEM_LOG << "[VSEditor] SyncEditorNodesFromTemplate: node #" << m_editorNodes[i].nodeID
712 << " had garbage position, reset to defaults\n";
713 }
714
715 savedPos[m_editorNodes[i].nodeID] = std::make_pair(posX, posY);
716 }
717
718 m_editorNodes.clear();
719 m_positionedNodes.clear();
720 // Clear drag-start positions on undo/redo: any in-progress drag is
721 // invalidated when the graph state changes underneath it. The user must
722 // re-drag after undo/redo; the old drag-start is no longer meaningful.
724
725 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
726 {
727 const TaskNodeDefinition& def = m_template.Nodes[i];
728
730 eNode.nodeID = def.NodeID;
731 eNode.def = def;
732
733 // Prefer position stored in template Parameters by MoveNodeCommand
734 // (reflects the undo/redo target state for this node).
735 auto posXIt = def.Parameters.find("__posX");
736 auto posYIt = def.Parameters.find("__posY");
737 if (posXIt != def.Parameters.end() &&
738 posYIt != def.Parameters.end() &&
739 posXIt->second.Type == ParameterBindingType::Literal &&
741 {
742 float paramX = posXIt->second.LiteralValue.AsFloat();
743 float paramY = posYIt->second.LiteralValue.AsFloat();
744
745 // Validate parameter positions
746 if (std::isfinite(paramX) && std::isfinite(paramY) &&
747 paramX >= -100000.0f && paramX <= 100000.0f &&
748 paramY >= -100000.0f && paramY <= 100000.0f)
749 {
750 eNode.posX = paramX;
751 eNode.posY = paramY;
752 }
753 else
754 {
755 SYSTEM_LOG << "[VSEditor] SyncEditorNodesFromTemplate: node #" << def.NodeID
756 << " had garbage params (" << paramX << ", " << paramY
757 << "), falling back\n";
758 auto it = savedPos.find(def.NodeID);
759 if (it != savedPos.end())
760 {
761 eNode.posX = it->second.first;
762 eNode.posY = it->second.second;
763 }
764 else if (def.HasEditorPos)
765 {
766 eNode.posX = def.EditorPosX;
767 eNode.posY = def.EditorPosY;
768 }
769 else
770 {
771 eNode.posX = DEFAULT_NODE_X_OFFSET + DEFAULT_NODE_X_SPACING * static_cast<float>(i);
772 eNode.posY = DEFAULT_NODE_Y;
773 }
774 }
775 }
776 else
777 {
778 auto it = savedPos.find(def.NodeID);
779 if (it != savedPos.end())
780 {
781 // Restore previously known position
782 eNode.posX = it->second.first;
783 eNode.posY = it->second.second;
784 }
785 else if (def.HasEditorPos)
786 {
787 // Validate file-loaded position
788 if (std::isfinite(def.EditorPosX) && std::isfinite(def.EditorPosY) &&
789 def.EditorPosX >= -100000.0f && def.EditorPosX <= 100000.0f &&
790 def.EditorPosY >= -100000.0f && def.EditorPosY <= 100000.0f)
791 {
792 eNode.posX = def.EditorPosX;
793 eNode.posY = def.EditorPosY;
794 }
795 else
796 {
797 eNode.posX = DEFAULT_NODE_X_OFFSET + DEFAULT_NODE_X_SPACING * static_cast<float>(i);
798 eNode.posY = DEFAULT_NODE_Y;
799 }
800 }
801 else
802 {
803 // New node (e.g. restored by Redo) – use a default spread position
804 eNode.posX = DEFAULT_NODE_X_OFFSET + DEFAULT_NODE_X_SPACING * static_cast<float>(i);
805 eNode.posY = DEFAULT_NODE_Y;
806 }
807 }
808
809 if (def.NodeID >= m_nextNodeID)
810 m_nextNodeID = def.NodeID + 1;
811
812 SYSTEM_LOG << "[VSEditor] SyncEditorNodesFromTemplate: node #" << eNode.nodeID
813 << " restored to (" << eNode.posX << "," << eNode.posY << ")\n";
814
815 m_editorNodes.push_back(eNode);
816 }
817
818 // FIX 1: Rebuild links from template so that ghost links (links that
819 // belong to a deleted node) are removed from m_editorLinks after undo/redo.
820 RebuildLinks();
821
822 // Request a position-restore pass on the next RenderCanvas() call
823 m_needsPositionSync = true;
824}
825
827{
828 // Find the link descriptor
829 VSEditorLink* link = nullptr;
830 for (size_t i = 0; i < m_editorLinks.size(); ++i)
831 {
832 if (m_editorLinks[i].linkID == linkID)
833 {
834 link = &m_editorLinks[i];
835 break;
836 }
837 }
838 if (!link)
839 return;
840
841 if (link->isData)
842 {
843 // Decode data-out -> data-in
844 int srcNodeID = link->srcAttrID / 10000;
845 int srcPinIdx = link->srcAttrID % 10000 - 300; // data-out range 300-399
846 int dstNodeID = link->dstAttrID / 10000;
847 int dstPinIdx = link->dstAttrID % 10000 - 200; // data-in range 200-299
848
849 std::string srcPinName = "Value";
850 std::string dstPinName = "Value";
851
854
855 if (srcNode)
856 {
857 auto pins = GetDataOutputPins(srcNode->Type);
858 if (srcPinIdx >= 0 && srcPinIdx < static_cast<int>(pins.size()))
859 srcPinName = pins[static_cast<size_t>(srcPinIdx)];
860 }
861 if (dstNode)
862 {
863 auto pins = GetDataInputPins(dstNode->Type);
864 if (dstPinIdx >= 0 && dstPinIdx < static_cast<int>(pins.size()))
865 dstPinName = pins[static_cast<size_t>(dstPinIdx)];
866 }
867
870 conn.SourcePinName = srcPinName;
871 conn.TargetNodeID = dstNodeID;
872 conn.TargetPinName = dstPinName;
874 std::unique_ptr<ICommand>(new DeleteLinkCommand(conn)),
875 m_template);
876 }
877 else
878 {
879 // Decode exec-out -> exec-in
880 int srcNodeID = link->srcAttrID / 10000;
881 int srcPinIdx = link->srcAttrID % 10000 - 100; // exec-out range 100-199
882 int dstNodeID = link->dstAttrID / 10000;
883 int dstPinIdx = link->dstAttrID % 10000; // exec-in range 0-99
884
885 std::string srcPinName = "Out";
886 std::string dstPinName = "In";
887
889 if (srcNode)
890 {
891 auto pins = GetExecOutputPins(srcNode->Type);
892 if (srcNode->Type == TaskNodeType::VSSequence ||
894 {
895 for (size_t d = 0; d < srcNode->DynamicExecOutputPins.size(); ++d)
896 pins.push_back(srcNode->DynamicExecOutputPins[d]);
897 }
898 if (srcPinIdx >= 0 && srcPinIdx < static_cast<int>(pins.size()))
899 srcPinName = pins[static_cast<size_t>(srcPinIdx)];
900 }
901
903 if (dstNode)
904 {
905 auto pins = GetExecInputPins(dstNode->Type);
906 if (dstPinIdx >= 0 && dstPinIdx < static_cast<int>(pins.size()))
907 dstPinName = pins[static_cast<size_t>(dstPinIdx)];
908 }
909
912 conn.SourcePinName = srcPinName;
913 conn.TargetNodeID = dstNodeID;
914 conn.TargetPinName = dstPinName;
916 std::unique_ptr<ICommand>(new DeleteLinkCommand(conn)),
917 m_template);
918 }
919
920 RebuildLinks();
921 m_dirty = true;
922 m_verificationDone = false;
923}
924
925// ============================================================================
926// Load / Save
927// ============================================================================
928
930 const std::string& path)
931{
932 if (tmpl == nullptr)
933 return;
934
935 m_template = *tmpl;
936 m_currentPath = path;
937 m_dirty = false;
938
939 // Rebuild lookup cache after copy (pointers from old template are now invalid)
941
942 // Phase 24 — Load embedded presets from the graph
943 // This replaces the old file-based approach with graph-embedded storage
944 if (!m_template.Presets.empty())
945 {
947 SYSTEM_LOG << "[VSEditor] LoadTemplate: loaded " << m_template.Presets.size()
948 << " presets from graph '" << m_template.Name << "'\n";
949 }
950 else
951 {
952 // Clear registry if graph has no presets (fresh start)
954 SYSTEM_LOG << "[VSEditor] LoadTemplate: graph '" << m_template.Name
955 << "' has no embedded presets - starting with empty bank\n";
956 }
957
958 // Phase 24 Global Blackboard Integration: Initialize EntityBlackboard
959 // This merges local (from m_template.Blackboard) + global variables (from registry)
960
961 // Reload global variables from registry (in case they were modified outside this editor instance)
963
965 {
966 m_entityBlackboard->Initialize(m_template);
967 SYSTEM_LOG << "[VSEditor] LoadTemplate: initialized EntityBlackboard with "
968 << m_entityBlackboard->GetLocalVariableCount() << " local + "
969 << m_entityBlackboard->GetGlobalVariableCount() << " global variables\n";
970
971 // Phase 24 Global Blackboard Integration: Restore entity-specific global variable values
972 // If the graph has stored global variable overrides, restore them now
974 {
975 m_entityBlackboard->ImportGlobalsFromJson(m_template.GlobalVariableValues);
976 SYSTEM_LOG << "[VSEditor] LoadTemplate: restored global variable overrides from graph\n";
977 }
978 }
979
980 // NOTE: Do NOT clear the undo stack here. Each VisualScriptEditorPanel
981 // instance owns its own stack (one per tab), so there is no cross-tab
982 // contamination. Preserving the stack lets the user undo edits made
983 // before saving and reloading, and is required for undo to function
984 // correctly after opening a file from the Blueprint Files browser.
985
987
988 // Phase 18: Do NOT pre-populate m_nodeDragStartPositions here.
989 // The former "FIX 2" block pre-populated every node's drag-start position
990 // with its loaded position. Because the guard in the drag-tracking loop is
991 // "insert only if key is absent", the pre-populated value was never
992 // overwritten. On the first drag after load the key already existed, so no
993 // new start position was recorded — eNode.posX/Y (kept current each frame
994 // while mouseDown) serves as the correct "position before this drag" and is
995 // used when the key is absent. Keeping m_nodeDragStartPositions empty here
996 // allows the tracking loop to record the true pre-drag position.
998 m_verificationDone = false;
999}
1000
1002{
1003 SYSTEM_LOG << "[VisualScriptEditorPanel] Save() called. m_currentPath='"
1004 << m_currentPath << "'\n";
1005
1006 if (m_currentPath.empty())
1007 {
1008 SYSTEM_LOG << "[VisualScriptEditorPanel] Save() aborted: m_currentPath is empty\n";
1009 return false;
1010 }
1011
1012 // BUG-003 Fix: Reset viewport panning BEFORE syncing positions so that
1013 // any residual editor-space offset from navigation is neutralised.
1014 // Positions are stored in grid space (GetNodeGridSpacePos), so this is
1015 // belt-and-suspenders safety; panning is restored by AfterSave().
1017
1018 // Fix #1: Commit any deferred key-name edits before save
1020
1021 // Fix #1: Remove invalid blackboard entries before save
1023
1024 // Phase 24: CRITICAL - Sync conditions from panel to template BEFORE serialization
1025 // This ensures conditionRefs and conditionOperandRefs are up-to-date before save
1026 if (m_selectedNodeID >= 0)
1027 {
1028 for (size_t ni = 0; ni < m_editorNodes.size(); ++ni)
1029 {
1030 if (m_editorNodes[ni].nodeID == m_selectedNodeID &&
1032 {
1033 m_editorNodes[ni].def.conditionRefs = m_conditionsPanel->GetConditionRefs();
1034 m_editorNodes[ni].def.conditionOperandRefs = m_conditionsPanel->GetConditionOperandRefs();
1035
1036 // Also sync to template
1037 for (size_t ti = 0; ti < m_template.Nodes.size(); ++ti)
1038 {
1039 if (m_template.Nodes[ti].NodeID == m_selectedNodeID)
1040 {
1041 m_template.Nodes[ti].conditionRefs = m_editorNodes[ni].def.conditionRefs;
1042 m_template.Nodes[ti].conditionOperandRefs = m_editorNodes[ni].def.conditionOperandRefs;
1043 break;
1044 }
1045 }
1046 break;
1047 }
1048 }
1049 }
1050
1051 // Phase 24: CRITICAL - Sync presets from registry to template BEFORE serialization
1052 // This ensures all presets (newly created, modified, duplicated) are included in save
1054
1055 // Phase 24 Global Blackboard Integration: Sync global variable values from EntityBlackboard to template
1056 // This ensures entity-specific global variable overrides are included in save
1058 {
1059 m_template.GlobalVariableValues = m_entityBlackboard->ExportGlobalsToJson();
1060 }
1061
1062 // CRITICAL FIX: Sync node positions from ImNodes BEFORE serialization.
1063 // RenderToolbar() (which calls Save) executes before RenderCanvas() syncs
1064 // positions, so we must pull fresh positions here to avoid stale data.
1066
1068
1069 // BUG-003 Fix #5: Restore viewport so the canvas does not visually jump.
1070 AfterSave();
1071
1072 SYSTEM_LOG << "[VisualScriptEditorPanel] Save() "
1073 << (ok ? "succeeded" : "FAILED") << ": '" << m_currentPath << "'\n";
1074 return ok;
1075}
1076
1077bool VisualScriptEditorPanel::SaveAs(const std::string& path)
1078{
1079 SYSTEM_LOG << "[VisualScriptEditorPanel] SaveAs() called. path='" << path << "'\n";
1080
1081 if (path.empty())
1082 return false;
1083
1084 // BUG-003 Fix: Reset viewport before position sync (same as Save()).
1086
1087 // Fix #1: Commit and validate before save
1090
1091 // Phase 24: CRITICAL - Sync presets from registry to template BEFORE serialization
1092 // This ensures all presets (newly created, modified, duplicated) are included in save
1094
1095 // Phase 24 Global Blackboard Integration: Sync global variable values from EntityBlackboard to template
1096 // This ensures entity-specific global variable overrides are included in save
1098 {
1099 m_template.GlobalVariableValues = m_entityBlackboard->ExportGlobalsToJson();
1100 }
1101
1102 // CRITICAL FIX: Same position sync as Save() — ensure fresh positions
1103 // before serialization regardless of when in the frame SaveAs is called.
1105
1106 bool ok = SerializeAndWrite(path);
1107
1108 // BUG-003 Fix #5: Restore viewport.
1109 AfterSave();
1110
1111 if (ok)
1112 {
1113 m_currentPath = path;
1114 m_dirty = false;
1115 SYSTEM_LOG << "[VisualScriptEditorPanel] SaveAs() succeeded: '" << path << "'\n";
1116 }
1117 else
1118 {
1119 SYSTEM_LOG << "[VisualScriptEditorPanel] SaveAs() FAILED: '" << path << "'\n";
1120 }
1121 return ok;
1122}
1123
1125{
1126 for (size_t i = 0; i < m_editorNodes.size(); ++i)
1127 {
1129 // Only query nodes that have been rendered at least once to avoid
1130 // an ImNodes assertion for nodes that have not yet gone through
1131 // BeginNode()/EndNode() this session.
1132 if (m_positionedNodes.count(eNode.nodeID) > 0)
1133 {
1134 // BUG-003 Fix: use GetNodeGridSpacePos() (pan-independent grid
1135 // coordinates) instead of GetNodeEditorSpacePos() which returns
1136 // Origin + Panning. Storing grid-space positions means the saved
1137 // values are never corrupted by the current viewport pan offset.
1138 ImVec2 pos = ImNodes::GetNodeGridSpacePos(eNode.nodeID);
1139 eNode.posX = pos.x;
1140 eNode.posY = pos.y;
1141
1142 // Keep the template's Parameters in sync so that
1143 // SyncEditorNodesFromTemplate() (called on undo/redo) can always
1144 // find the live canvas position in Parameters["__posX/__posY"],
1145 // even for nodes that were loaded from file and have never been
1146 // moved via an explicit MoveNodeCommand.
1147 for (size_t j = 0; j < m_template.Nodes.size(); ++j)
1148 {
1149 if (m_template.Nodes[j].NodeID == eNode.nodeID)
1150 {
1153 bx.LiteralValue = TaskValue(pos.x);
1155 by.LiteralValue = TaskValue(pos.y);
1156 m_template.Nodes[j].Parameters["__posX"] = bx;
1157 m_template.Nodes[j].Parameters["__posY"] = by;
1158 break;
1159 }
1160 }
1161 }
1162 }
1163}
1164
1166{
1167 // Phase 24 FIX: Sync ALL presets from the registry to the template
1168 // This ensures that presets created/modified via UI are included in the save
1169 // Previously, only modified presets were synced, missing newly created ones
1170
1171 // Get all presets from the registry
1172 std::vector<std::string> allPresetIDs = m_presetRegistry.GetAllPresetIDs();
1173
1174 // Clear template presets and rebuild from registry
1175 m_template.Presets.clear();
1176
1177 for (const auto& presetID : allPresetIDs)
1178 {
1180 if (preset)
1181 {
1182 m_template.Presets.push_back(*preset);
1183 }
1184 }
1185
1186 SYSTEM_LOG << "[VisualScriptEditorPanel] SyncPresetsFromRegistryToTemplate: synced "
1187 << m_template.Presets.size() << " presets from registry to template\n";
1188}
1189
1191{
1192 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: writing to '" << path << "'\n";
1193
1195
1196 json root;
1197 root["schema_version"] = 4;
1198 root["name"] = m_template.Name;
1199 root["graphType"] = "VisualScript";
1200
1201 // Blackboard
1202 // BUG-001 Hotfix: skip invalid entries (empty key or VariableType::None)
1203 // to prevent save crash caused by unhandled None type during serialization.
1204 int bbSkipped = 0;
1205 json bbArray = json::array();
1206 for (size_t i = 0; i < m_template.Blackboard.size(); ++i)
1207 {
1209
1210 if (entry.Key.empty() || entry.Type == VariableType::None)
1211 {
1212 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: skipping invalid blackboard entry"
1213 << " (key='" << entry.Key << "', type=None)\n";
1214 ++bbSkipped;
1215 continue;
1216 }
1217
1218 json e;
1219 e["key"] = entry.Key;
1220 e["isGlobal"] = entry.IsGlobal;
1221
1222 // Guard each accessor against type mismatch: if Default was not
1223 // initialised to the right type (e.g. loaded as Int when Float was
1224 // expected), fall back to a zero-value rather than throwing.
1225 switch (entry.Type)
1226 {
1227 case VariableType::Bool:
1228 e["type"] = "Bool";
1229 e["value"] = (entry.Default.GetType() == VariableType::Bool)
1230 ? entry.Default.AsBool() : false;
1231 break;
1232 case VariableType::Int:
1233 e["type"] = "Int";
1234 e["value"] = (entry.Default.GetType() == VariableType::Int)
1235 ? entry.Default.AsInt() : 0;
1236 break;
1238 e["type"] = "Float";
1239 e["value"] = (entry.Default.GetType() == VariableType::Float)
1240 ? entry.Default.AsFloat() : 0.0f;
1241 break;
1243 e["type"] = "String";
1244 e["value"] = (entry.Default.GetType() == VariableType::String)
1245 ? entry.Default.AsString() : std::string("");
1246 break;
1248 e["type"] = "EntityID";
1249 e["value"] = std::to_string(
1250 (entry.Default.GetType() == VariableType::EntityID)
1251 ? entry.Default.AsEntityID() : 0);
1252 break;
1254 {
1255 // Vector default is auto-assigned at runtime from entity position.
1256 // Persist as a zero-initialised object so the type tag is preserved
1257 // across save/load and does not degrade to "None".
1258 const ::Vector v = (entry.Default.GetType() == VariableType::Vector)
1259 ? entry.Default.AsVector()
1260 : ::Vector{0.f, 0.f, 0.f};
1261 json vec;
1262 vec["x"] = v.x;
1263 vec["y"] = v.y;
1264 vec["z"] = v.z;
1265 e["type"] = "Vector";
1266 e["value"] = vec;
1267 break;
1268 }
1269 default:
1270 e["type"] = "None";
1271 e["value"] = nullptr;
1272 break;
1273 }
1274 bbArray.push_back(e);
1275 }
1276 if (bbSkipped > 0)
1277 {
1278 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: " << bbSkipped
1279 << " invalid blackboard entries skipped (BUG-001)\n";
1280 }
1281 root["blackboard"] = bbArray;
1282
1283 // Nodes
1284 json nodesArray = json::array();
1285 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1286 {
1287 const TaskNodeDefinition& def = m_template.Nodes[i];
1288 json n;
1289 n["id"] = def.NodeID;
1290 n["label"] = def.NodeName;
1291 n["type"] = GetNodeTypeLabel(def.Type);
1292
1293 if (def.Type == TaskNodeType::AtomicTask)
1294 n["taskType"] = def.AtomicTaskID;
1295 if (def.Type == TaskNodeType::Delay)
1296 n["delaySeconds"] = def.DelaySeconds;
1297 if (!def.BBKey.empty())
1298 n["bbKey"] = def.BBKey;
1299 if (!def.SubGraphPath.empty())
1300 n["subGraphPath"] = def.SubGraphPath;
1301 if (!def.ConditionID.empty())
1302 n["conditionKey"] = def.ConditionID;
1303 if (!def.MathOperator.empty())
1304 n["mathOp"] = def.MathOperator;
1305
1306 // Serialize parameters (AtomicTask and other node types with parameters)
1307 if (!def.Parameters.empty())
1308 {
1309 json paramsObj = json::object();
1310 for (const auto& paramPair : def.Parameters)
1311 {
1312 const std::string& paramName = paramPair.first;
1313 const ParameterBinding& binding = paramPair.second;
1314
1315 json bindingObj = json::object();
1316
1317 switch (binding.Type)
1318 {
1320 bindingObj["Type"] = "Literal";
1321 // Serialize the literal value based on its type
1322 if (!binding.LiteralValue.IsNone())
1323 {
1324 switch (binding.LiteralValue.GetType())
1325 {
1326 case VariableType::Bool:
1327 bindingObj["LiteralValue"] = binding.LiteralValue.AsBool();
1328 break;
1329 case VariableType::Int:
1330 bindingObj["LiteralValue"] = binding.LiteralValue.AsInt();
1331 break;
1333 bindingObj["LiteralValue"] = binding.LiteralValue.AsFloat();
1334 break;
1336 bindingObj["LiteralValue"] = binding.LiteralValue.AsString();
1337 break;
1339 {
1340 const ::Vector v = binding.LiteralValue.AsVector();
1341 json vec;
1342 vec["x"] = v.x;
1343 vec["y"] = v.y;
1344 vec["z"] = v.z;
1345 bindingObj["LiteralValue"] = vec;
1346 break;
1347 }
1349 bindingObj["LiteralValue"] = std::to_string(binding.LiteralValue.AsEntityID());
1350 break;
1351 default:
1352 break;
1353 }
1354 }
1355 break;
1356
1358 bindingObj["Type"] = "LocalVariable";
1359 bindingObj["VariableName"] = binding.VariableName;
1360 break;
1361
1363 bindingObj["Type"] = "AtomicTaskID";
1364 bindingObj["value"] = binding.VariableName;
1365 break;
1366
1368 bindingObj["Type"] = "ConditionID";
1369 bindingObj["value"] = binding.VariableName;
1370 break;
1371
1373 bindingObj["Type"] = "MathOperator";
1374 bindingObj["value"] = binding.VariableName;
1375 break;
1376
1378 bindingObj["Type"] = "ComparisonOp";
1379 bindingObj["value"] = binding.VariableName;
1380 break;
1381
1383 bindingObj["Type"] = "SubGraphPath";
1384 bindingObj["value"] = binding.VariableName;
1385 break;
1386
1387 default:
1388 bindingObj["Type"] = "Literal";
1389 break;
1390 }
1391
1393 }
1394 n["params"] = paramsObj;
1395 }
1396
1397 // Switch enhancements (Phase 22-A)
1398 if (def.Type == TaskNodeType::Switch)
1399 {
1400 if (!def.switchVariable.empty())
1401 n["switchVariable"] = def.switchVariable;
1402
1403 if (!def.switchCases.empty())
1404 {
1405 json casesArray = json::array();
1406 for (size_t c = 0; c < def.switchCases.size(); ++c)
1407 {
1408 const SwitchCaseDefinition& sc = def.switchCases[c];
1409 json caseObj;
1410 caseObj["value"] = sc.value;
1411 caseObj["pin"] = sc.pinName;
1412 if (!sc.customLabel.empty())
1413 caseObj["label"] = sc.customLabel;
1414 casesArray.push_back(caseObj);
1415 }
1416 n["switchCases"] = casesArray;
1417 }
1418 }
1419
1420 // Phase 24 Milestone 2 — MathOp operand serialization
1421 // Serialize the complete MathOpRef (left operand, operator, right operand)
1422 if (def.Type == TaskNodeType::MathOp && !def.mathOpRef.mathOperator.empty())
1423 {
1424 n["mathOpRef"] = def.mathOpRef.ToJson();
1425 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: serialized mathOpRef for MathOp node "
1426 << def.NodeID << "\n";
1427 }
1428
1429 // Structured conditions (Phase 23-B.4 — Branch/While)
1430 if ((def.Type == TaskNodeType::Branch || def.Type == TaskNodeType::While) &&
1431 !def.conditions.empty())
1432 {
1433 json condArray = json::array();
1434 for (size_t ci = 0; ci < def.conditions.size(); ++ci)
1435 {
1436 const Condition& cond = def.conditions[ci];
1437 json cj;
1438
1439 // Left side
1440 cj["leftMode"] = cond.leftMode;
1441 if (!cond.leftPin.empty())
1442 cj["leftPin"] = cond.leftPin;
1443 if (!cond.leftVariable.empty())
1444 cj["leftVariable"] = cond.leftVariable;
1445 if (cond.leftMode == "Const" && !cond.leftConstValue.IsNone())
1446 {
1447 const TaskValue& lv = cond.leftConstValue;
1448 switch (lv.GetType()) {
1449 case VariableType::Bool: cj["leftConstValue"] = lv.AsBool(); break;
1450 case VariableType::Int: cj["leftConstValue"] = lv.AsInt(); break;
1451 case VariableType::Float: cj["leftConstValue"] = lv.AsFloat(); break;
1452 case VariableType::String: cj["leftConstValue"] = lv.AsString();break;
1453 default: break;
1454 }
1455 }
1456
1457 // Operator
1458 cj["operator"] = cond.operatorStr;
1459
1460 // Right side
1461 cj["rightMode"] = cond.rightMode;
1462 if (!cond.rightPin.empty())
1463 cj["rightPin"] = cond.rightPin;
1464 if (!cond.rightVariable.empty())
1465 cj["rightVariable"] = cond.rightVariable;
1466 if (cond.rightMode == "Const" && !cond.rightConstValue.IsNone())
1467 {
1468 const TaskValue& rv = cond.rightConstValue;
1469 switch (rv.GetType()) {
1470 case VariableType::Bool: cj["rightConstValue"] = rv.AsBool(); break;
1471 case VariableType::Int: cj["rightConstValue"] = rv.AsInt(); break;
1472 case VariableType::Float: cj["rightConstValue"] = rv.AsFloat(); break;
1473 case VariableType::String: cj["rightConstValue"] = rv.AsString();break;
1474 default: break;
1475 }
1476 }
1477
1478 // Type hint
1479 if (cond.compareType != VariableType::None)
1480 cj["compareType"] = VariableTypeToString(cond.compareType);
1481
1482 condArray.push_back(cj);
1483 }
1484 n["conditions"] = condArray;
1485 }
1486
1487 // Phase 24 Milestone 2.2 — conditionRefs serialization (new inline system)
1488 // Saves OperandRef data including dynamicPinID for Pin-mode operands.
1489 // Coexists with legacy def.conditions[] during transition.
1490 if ((def.Type == TaskNodeType::Branch || def.Type == TaskNodeType::While) &&
1491 !def.conditionOperandRefs.empty())
1492 {
1493 json condRefsArray = json::array();
1494
1495 for (size_t i = 0; i < def.conditionOperandRefs.size(); ++i)
1496 {
1497 const ConditionRef& ref = def.conditionOperandRefs[i];
1498 json refObj;
1499 refObj["conditionIndex"] = static_cast<int>(i);
1500
1501 // Left operand
1502 {
1503 json lj;
1504 switch (ref.leftOperand.mode)
1505 {
1507 lj["mode"] = "Variable";
1508 lj["variableName"] = ref.leftOperand.variableName;
1509 break;
1511 lj["mode"] = "Const";
1512 lj["constValue"] = ref.leftOperand.constValue;
1513 break;
1515 lj["mode"] = "Pin";
1516 lj["dynamicPinID"] = ref.leftOperand.dynamicPinID;
1517 break;
1518 default:
1519 lj["mode"] = "Const";
1520 break;
1521 }
1522 refObj["leftOperand"] = lj;
1523 }
1524
1525 refObj["operator"] = ref.operatorStr;
1526
1527 // Right operand
1528 {
1529 json rj;
1530 switch (ref.rightOperand.mode)
1531 {
1533 rj["mode"] = "Variable";
1534 rj["variableName"] = ref.rightOperand.variableName;
1535 break;
1537 rj["mode"] = "Const";
1538 rj["constValue"] = ref.rightOperand.constValue;
1539 break;
1541 rj["mode"] = "Pin";
1542 rj["dynamicPinID"] = ref.rightOperand.dynamicPinID;
1543 break;
1544 default:
1545 rj["mode"] = "Const";
1546 break;
1547 }
1548 refObj["rightOperand"] = rj;
1549 }
1550
1551 if (ref.compareType != VariableType::None)
1552 refObj["compareType"] = VariableTypeToString(ref.compareType);
1553
1554 condRefsArray.push_back(refObj);
1555 }
1556
1557 n["conditionRefs"] = condRefsArray;
1558
1559 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: Phase 24: serialized "
1560 << def.conditionOperandRefs.size() << " conditionRefs for node "
1561 << def.NodeID << "\n";
1562 }
1563
1564 // Phase 24 Milestone 2.3 — Node condition references (preset IDs + logical operators)
1565 // Save which presets are used and their logical operator chain
1566 if ((def.Type == TaskNodeType::Branch || def.Type == TaskNodeType::While) &&
1567 !def.conditionRefs.empty())
1568 {
1569 json nodeCondRefsArray = json::array();
1570 for (const auto& ncref : def.conditionRefs)
1571 {
1572 json nobj = ncref.ToJson();
1573 nodeCondRefsArray.push_back(nobj);
1574 }
1575 n["nodeConditionRefs"] = nodeCondRefsArray;
1576
1577 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: Phase 24: serialized "
1578 << def.conditionRefs.size() << " nodeConditionRefs for node "
1579 << def.NodeID << "\n";
1580 }
1581
1582 // Dynamic exec-out pins (VSSequence and Switch)
1583 if ((def.Type == TaskNodeType::VSSequence || def.Type == TaskNodeType::Switch) &&
1584 !def.DynamicExecOutputPins.empty())
1585 {
1586 json dynPins = json::array();
1587 for (size_t p = 0; p < def.DynamicExecOutputPins.size(); ++p)
1588 dynPins.push_back(def.DynamicExecOutputPins[p]);
1589 n["dynamicExecPins"] = dynPins;
1590 }
1591
1592 // SubGraph input and output parameters (Phase 3)
1593 if (def.Type == TaskNodeType::SubGraph)
1594 {
1595 // Input parameters: map of name -> ParameterBinding
1596 if (!def.InputParams.empty())
1597 {
1598 json inputParamsObj = json::object();
1599 for (const auto& paramPair : def.InputParams)
1600 {
1601 const std::string& paramName = paramPair.first;
1602 const ParameterBinding& binding = paramPair.second;
1603
1604 json bindingObj = json::object();
1605
1606 switch (binding.Type)
1607 {
1609 bindingObj["Type"] = "Literal";
1610 if (!binding.LiteralValue.IsNone())
1611 {
1612 switch (binding.LiteralValue.GetType())
1613 {
1614 case VariableType::Bool:
1615 bindingObj["LiteralValue"] = binding.LiteralValue.AsBool();
1616 break;
1617 case VariableType::Int:
1618 bindingObj["LiteralValue"] = binding.LiteralValue.AsInt();
1619 break;
1621 bindingObj["LiteralValue"] = binding.LiteralValue.AsFloat();
1622 break;
1624 bindingObj["LiteralValue"] = binding.LiteralValue.AsString();
1625 break;
1627 {
1628 const ::Vector v = binding.LiteralValue.AsVector();
1629 json vec;
1630 vec["x"] = v.x;
1631 vec["y"] = v.y;
1632 vec["z"] = v.z;
1633 bindingObj["LiteralValue"] = vec;
1634 break;
1635 }
1637 bindingObj["LiteralValue"] = std::to_string(binding.LiteralValue.AsEntityID());
1638 break;
1639 default:
1640 break;
1641 }
1642 }
1643 break;
1644
1646 bindingObj["Type"] = "LocalVariable";
1647 bindingObj["VariableName"] = binding.VariableName;
1648 break;
1649
1651 bindingObj["Type"] = "AtomicTaskID";
1652 bindingObj["value"] = binding.VariableName;
1653 break;
1654
1656 bindingObj["Type"] = "ConditionID";
1657 bindingObj["value"] = binding.VariableName;
1658 break;
1659
1661 bindingObj["Type"] = "MathOperator";
1662 bindingObj["value"] = binding.VariableName;
1663 break;
1664
1666 bindingObj["Type"] = "ComparisonOp";
1667 bindingObj["value"] = binding.VariableName;
1668 break;
1669
1671 bindingObj["Type"] = "SubGraphPath";
1672 bindingObj["value"] = binding.VariableName;
1673 break;
1674
1675 default:
1676 bindingObj["Type"] = "Literal";
1677 break;
1678 }
1679
1681 }
1682 n["InputParams"] = inputParamsObj;
1683 }
1684
1685 // Output parameters: map of name -> blackboard key
1686 if (!def.OutputParams.empty())
1687 {
1688 json outputParamsObj = json::object();
1689 for (const auto& paramPair : def.OutputParams)
1690 {
1691 outputParamsObj[paramPair.first] = paramPair.second;
1692 }
1693 n["OutputParams"] = outputParamsObj;
1694 }
1695 }
1696
1697 // Position from editor node
1698 for (size_t j = 0; j < m_editorNodes.size(); ++j)
1699 {
1700 if (m_editorNodes[j].nodeID == def.NodeID)
1701 {
1702 json pos;
1703 pos["x"] = m_editorNodes[j].posX;
1704 pos["y"] = m_editorNodes[j].posY;
1705 n["position"] = pos;
1706 break;
1707 }
1708 }
1709
1710 nodesArray.push_back(n);
1711 }
1712 root["nodes"] = nodesArray;
1713
1714 // Exec connections
1715 json execArray = json::array();
1716 for (size_t i = 0; i < m_template.ExecConnections.size(); ++i)
1717 {
1719 json c;
1720 c["fromNode"] = conn.SourceNodeID;
1721 c["fromPin"] = conn.SourcePinName;
1722 c["toNode"] = conn.TargetNodeID;
1723 c["toPin"] = conn.TargetPinName;
1724 execArray.push_back(c);
1725 }
1726 root["execConnections"] = execArray;
1727
1728 // Data connections
1729 json dataArray = json::array();
1730 for (size_t i = 0; i < m_template.DataConnections.size(); ++i)
1731 {
1733 json c;
1734 c["fromNode"] = conn.SourceNodeID;
1735 c["fromPin"] = conn.SourcePinName;
1736 c["toNode"] = conn.TargetNodeID;
1737 c["toPin"] = conn.TargetPinName;
1738 dataArray.push_back(c);
1739 }
1740 root["dataConnections"] = dataArray;
1741
1742 // Phase 24 Global Blackboard Integration: Serialize global variable values
1743 // These are entity-specific values stored in the template before serialization
1745 {
1746 root["globalVariableValues"] = m_template.GlobalVariableValues;
1747 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: Phase 24 - serialized "
1748 << "global variable values\n";
1749 }
1750
1751 // Phase 24 — Condition Preset Bank (embedded in graph JSON)
1752 // Presets are now serialized as part of the graph, making blueprints self-contained.
1753 if (!m_template.Presets.empty())
1754 {
1755 json presetsArray = json::array();
1756 for (size_t i = 0; i < m_template.Presets.size(); ++i)
1757 {
1759 json presetObj = preset.ToJson(); // Delegate serialization to preset's own method
1760 presetsArray.push_back(presetObj);
1761 }
1762 root["presets"] = presetsArray;
1763
1764 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: Phase 24 - serialized "
1765 << m_template.Presets.size() << " embedded presets\n";
1766 }
1767
1768 // Write file
1769 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: opening '"
1770 << path << "' for writing\n";
1771 std::ofstream ofs(path);
1772 if (!ofs.is_open())
1773 {
1774 std::cerr << "[VisualScriptEditorPanel] Cannot open file for write: " << path << std::endl;
1775 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite FAILED: cannot open '"
1776 << path << "'\n";
1777 return false;
1778 }
1779 ofs << root.dump(2);
1780 ofs.close();
1781 m_dirty = false;
1782 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite succeeded: '" << path << "'\n";
1783 return true;
1784}
1785
1786// ============================================================================
1787// Blackboard validation helpers (BUG-002 Fix #1)
1788// ============================================================================
1789
1791{
1792 std::vector<BlackboardEntry>& entries = m_template.Blackboard;
1793 size_t before = entries.size();
1794
1795 entries.erase(
1796 std::remove_if(entries.begin(), entries.end(),
1797 [](const BlackboardEntry& e) {
1798 if (e.Key.empty()) {
1799 SYSTEM_LOG << "[VSEditor] ValidateAndClean: removing entry with empty key\n";
1800 return true;
1801 }
1802 if (e.Type == VariableType::None) {
1803 SYSTEM_LOG << "[VSEditor] ValidateAndClean: removing entry '"
1804 << e.Key << "' with VariableType::None\n";
1805 return true;
1806 }
1807 return false;
1808 }),
1809 entries.end());
1810
1811 size_t removed = before - entries.size();
1812 if (removed > 0)
1813 {
1814 SYSTEM_LOG << "[VSEditor] ValidateAndClean: removed " << removed
1815 << " invalid blackboard entries\n";
1816 m_dirty = true;
1817 }
1818}
1819
1820void VisualScriptEditorPanel::CommitPendingBlackboardEdits()
1821{
1822 for (std::unordered_map<int, std::string>::iterator it = m_pendingBlackboardEdits.begin();
1823 it != m_pendingBlackboardEdits.end(); ++it)
1824 {
1825 int idx = it->first;
1826 if (idx >= 0 && idx < static_cast<int>(m_template.Blackboard.size()))
1827 {
1828 m_template.Blackboard[static_cast<size_t>(idx)].Key = it->second;
1829 }
1830 }
1831 m_pendingBlackboardEdits.clear();
1832}
1833
1834// ============================================================================
1835// BUG-003 Viewport helpers
1836// ============================================================================
1837
1838void VisualScriptEditorPanel::ResetViewportBeforeSave()
1839{
1840 SYSTEM_LOG << "[VSEditor] ResetViewportBeforeSave: saving current panning\n";
1841 m_lastViewportPanning = Vector::FromImVec2(ImNodes::EditorContextGetPanning());
1842 m_viewportResetDone = true;
1843
1844 // Reset panning to (0, 0) so that any residual editor-space offset from
1845 // user navigation is zeroed out before SyncNodePositionsFromImNodes reads
1846 // GetNodeGridSpacePos (which is already pan-independent, but this ensures
1847 // no subtle ImNodes internal state leaks into the saved positions).
1848 ImNodes::EditorContextResetPanning(ImVec2(0.0f, 0.0f));
1849 SYSTEM_LOG << "[VSEditor] ResetViewportBeforeSave: panning reset to (0,0) "
1850 << "(was " << m_lastViewportPanning.x << "," << m_lastViewportPanning.y << ")\n";
1851}
1852
1853void VisualScriptEditorPanel::AfterSave()
1854{
1855 if (!m_viewportResetDone)
1856 return;
1857
1858 // Restore the viewport so the canvas does not visually jump for the user.
1859 ImNodes::EditorContextResetPanning(m_lastViewportPanning.ToImVec2());
1860 m_viewportResetDone = false;
1861 SYSTEM_LOG << "[VSEditor] AfterSave: viewport panning restored to ("
1862 << m_lastViewportPanning.x << "," << m_lastViewportPanning.y << ")\n";
1863}
1864
1865ImVec2 VisualScriptEditorPanel::ScreenToCanvasPos(ImVec2 screenPos) const
1866{
1867 // Convert absolute screen-space position to ImNodes editor (canvas) space.
1868 // Editor space = grid space + panning, so:
1869 // editorX = screenX - canvasOrigin.x - windowPos.x
1870 // ImNodes 0.4 has no zoom API; zoom is implicitly 1.0f.
1871 ImVec2 canvasPanning = ImNodes::EditorContextGetPanning();
1872 ImVec2 windowPos = ImGui::GetWindowPos();
1873 return ImVec2(
1876}
1877
1878// ============================================================================
1879// UX Enhancement #3 — Type-filtered variable utility
1880// ============================================================================
1881
1882/*static*/
1883std::vector<BlackboardEntry> VisualScriptEditorPanel::GetVariablesByType(
1884 const std::vector<BlackboardEntry>& allVars,
1885 VariableType expectedType)
1886{
1887 std::vector<BlackboardEntry> filtered;
1888 for (size_t i = 0; i < allVars.size(); ++i)
1889 {
1890 if (allVars[i].Type == expectedType)
1891 filtered.push_back(allVars[i]);
1892 }
1893 return filtered;
1894}
1895
1896// ============================================================================
1897// Rendering
1898// ============================================================================
1899
1900// ============================================================================
1901// Undo/Redo wrappers
1902// ============================================================================
1903
1904void VisualScriptEditorPanel::PerformUndo()
1905{
1906 if (!m_undoStack.CanUndo())
1907 return;
1908
1909 std::string desc = m_undoStack.PeekUndoDescription();
1910 SYSTEM_LOG << "[VSEditor] UNDO: " << desc << "\n";
1911 m_undoStack.Undo(m_template);
1912 SyncEditorNodesFromTemplate();
1913 RebuildLinks();
1914
1915 // Force-push the restored positions into ImNodes so that the next
1916 // BeginNode()/EndNode() cycle renders them at the correct location.
1917 // BUG-003 Fix: positions stored in m_editorNodes are grid-space
1918 // (written by SyncNodePositionsFromImNodes via GetNodeGridSpacePos),
1919 // so use SetNodeGridSpacePos to restore them pan-independently.
1920 for (size_t i = 0; i < m_editorNodes.size(); ++i)
1921 {
1922 ImNodes::SetNodeGridSpacePos(
1923 m_editorNodes[i].nodeID,
1924 ImVec2(m_editorNodes[i].posX, m_editorNodes[i].posY));
1925 }
1926
1927 // Block position sync and movement tracking for 1 frame so that stale
1928 // ImNodes state cannot overwrite the correct undo-target positions before
1929 // ImNodes has rendered the new layout at least once.
1930 m_justPerformedUndoRedo = true;
1931 m_skipPositionSyncNextFrame = true;
1932 m_nodeDragStartPositions.clear();
1933 m_dirty = true;
1934 m_verificationDone = false;
1935 SYSTEM_LOG << "[VSEditor] Undo complete. Template now has "
1936 << m_template.Nodes.size() << " nodes, "
1937 << m_template.ExecConnections.size() << " exec connections\n";
1938}
1939
1940void VisualScriptEditorPanel::PerformRedo()
1941{
1942 if (!m_undoStack.CanRedo())
1943 return;
1944
1945 std::string desc = m_undoStack.PeekRedoDescription();
1946 SYSTEM_LOG << "[VSEditor] REDO: " << desc << "\n";
1947 m_undoStack.Redo(m_template);
1948 SyncEditorNodesFromTemplate();
1949 RebuildLinks();
1950
1951 // Same treatment as PerformUndo().
1952 // BUG-003 Fix: use SetNodeGridSpacePos (grid-space) for pan-independent restore.
1953 for (size_t i = 0; i < m_editorNodes.size(); ++i)
1954 {
1955 ImNodes::SetNodeGridSpacePos(
1956 m_editorNodes[i].nodeID,
1957 ImVec2(m_editorNodes[i].posX, m_editorNodes[i].posY));
1958 }
1959
1960 m_justPerformedUndoRedo = true;
1961 m_skipPositionSyncNextFrame = true;
1962 m_nodeDragStartPositions.clear();
1963 m_dirty = true;
1964 m_verificationDone = false;
1965 SYSTEM_LOG << "[VSEditor] Redo complete. Template now has "
1966 << m_template.Nodes.size() << " nodes, "
1967 << m_template.ExecConnections.size() << " exec connections\n";
1968}
1969
1970void VisualScriptEditorPanel::Render()
1971{
1972 if (!m_visible)
1973 return;
1974
1975 ImGui::Begin("VS Graph Editor", &m_visible);
1976 RenderContent();
1977 ImGui::End();
1978
1979 // Render the condition preset library panel (Phase 24 UI integration)
1980 m_libraryPanel->Render();
1981}
1982
1983void VisualScriptEditorPanel::RenderContent()
1984{
1985 RenderToolbar();
1986 RenderSaveAsDialog();
1987 ImGui::Separator();
1988
1989 // Two-column layout: canvas (left) | resize handle | properties panel (right, 3 sub-panels)
1990 float totalWidth = ImGui::GetContentRegionAvail().x;
1991
1992 // Initialize panel width to default 28% on first use
1993 if (m_propertiesPanelWidth <= 0.0f)
1994 m_propertiesPanelWidth = totalWidth * 0.28f;
1995
1996 // Clamp to a sensible range
1997 if (m_propertiesPanelWidth < 200.0f) m_propertiesPanelWidth = 200.0f;
1998 if (m_propertiesPanelWidth > totalWidth * 0.60f) m_propertiesPanelWidth = totalWidth * 0.60f;
1999
2000 float handleWidth = 6.0f;
2001 float canvasWidth = totalWidth - m_propertiesPanelWidth - handleWidth;
2002
2003 ImGui::BeginChild("VSCanvas", ImVec2(canvasWidth, 0), false,
2005 RenderCanvas();
2006 ImGui::EndChild();
2007
2008 ImGui::SameLine();
2009
2010 // UX Fix #3: Drag-to-resize handle between canvas and properties panel
2011 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.35f, 0.35f, 0.5f));
2012 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.55f, 0.55f, 0.55f, 0.8f));
2013 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.70f, 0.70f, 0.70f, 1.0f));
2014 ImGui::Button("##vsresize", ImVec2(handleWidth, -1.0f));
2015 if (ImGui::IsItemHovered())
2016 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
2017 if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left))
2018 {
2019 m_propertiesPanelWidth -= ImGui::GetIO().MouseDelta.x;
2020 if (m_propertiesPanelWidth < 200.0f) m_propertiesPanelWidth = 200.0f;
2021 if (m_propertiesPanelWidth > totalWidth * 0.60f) m_propertiesPanelWidth = totalWidth * 0.60f;
2022 }
2023 ImGui::PopStyleColor(3);
2024
2025 ImGui::SameLine();
2026
2027 // Right panel container with 3 vertical sub-panels (A: Node Props | B: Preset Bank | C: Local Vars)
2028 ImGui::BeginChild("VSRightPanel", ImVec2(m_propertiesPanelWidth, 0), true);
2029
2030 float rightPanelHeight = ImGui::GetContentRegionAvail().y;
2031 float splitterHeight = 4.0f;
2032
2033 // Initialize sub-panel heights on first use (equal thirds for 3 panels)
2034 if (m_nodePropertiesPanelHeight <= 0.0f)
2035 {
2036 m_nodePropertiesPanelHeight = (rightPanelHeight - splitterHeight * 2) / 3.0f;
2037 m_presetBankPanelHeight = (rightPanelHeight - splitterHeight * 2) / 3.0f;
2038 }
2039
2040 // Clamp heights to reasonable ranges
2041 float minPanelHeight = 50.0f;
2042 if (m_nodePropertiesPanelHeight < minPanelHeight) m_nodePropertiesPanelHeight = minPanelHeight;
2043 if (m_presetBankPanelHeight < minPanelHeight) m_presetBankPanelHeight = minPanelHeight;
2044
2045 float localVarHeight = rightPanelHeight - m_nodePropertiesPanelHeight - m_presetBankPanelHeight - splitterHeight * 2;
2047
2048 // ---- Part A: Node Properties Panel ----
2049 ImGui::BeginChild("Part_A_NodeProps", ImVec2(0, m_nodePropertiesPanelHeight), false,
2051 RenderNodePropertiesPanel();
2052 ImGui::EndChild();
2053
2054 // ---- Splitter 1 (between Part A and Part B) ----
2055 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.35f, 0.35f, 0.5f));
2056 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.55f, 0.55f, 0.55f, 0.8f));
2057 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.70f, 0.70f, 0.70f, 1.0f));
2058 ImGui::Button("##splitter1", ImVec2(-1.0f, splitterHeight));
2059 if (ImGui::IsItemHovered())
2060 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
2061 if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left))
2062 {
2063 m_nodePropertiesPanelHeight += ImGui::GetIO().MouseDelta.y;
2064 if (m_nodePropertiesPanelHeight < minPanelHeight) m_nodePropertiesPanelHeight = minPanelHeight;
2065 }
2066 ImGui::PopStyleColor(3);
2067
2068 // ---- Part B: Preset Bank Panel ----
2069 ImGui::BeginChild("Part_B_PresetBank", ImVec2(0, m_presetBankPanelHeight), false,
2071 RenderPresetBankPanel();
2072 ImGui::EndChild();
2073
2074 // ---- Splitter 2 (between Part B and Part C) ----
2075 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.35f, 0.35f, 0.5f));
2076 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.55f, 0.55f, 0.55f, 0.8f));
2077 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.70f, 0.70f, 0.70f, 1.0f));
2078 ImGui::Button("##splitter2", ImVec2(-1.0f, splitterHeight));
2079 if (ImGui::IsItemHovered())
2080 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
2081 if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left))
2082 {
2083 m_presetBankPanelHeight += ImGui::GetIO().MouseDelta.y;
2084 if (m_presetBankPanelHeight < minPanelHeight) m_presetBankPanelHeight = minPanelHeight;
2085 }
2086 ImGui::PopStyleColor(3);
2087
2088 // ---- Part C: Local/Global Variables Panel (with tab selection) ----
2089 ImGui::BeginChild("Part_C_Blackboard", ImVec2(0, localVarHeight), false,
2091
2092 // Tab selector for Local vs Global variables
2093 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 4.0f));
2094 ImGui::RadioButton("Local Variables", &m_blackboardTabSelection, 0);
2095 ImGui::SameLine(150.0f);
2096 ImGui::RadioButton("Global Variables", &m_blackboardTabSelection, 1);
2097 ImGui::PopStyleVar();
2098 ImGui::Separator();
2099
2100 // Render appropriate panel based on tab selection
2101 if (m_blackboardTabSelection == 0)
2102 RenderLocalVariablesPanel();
2103 else
2104 RenderGlobalVariablesPanel();
2105
2106 ImGui::EndChild();
2107
2108 ImGui::EndChild(); // End VSRightPanel
2109}
2110
2111void VisualScriptEditorPanel::RenderToolbar()
2112{
2113 // Title
2114 const char* title = m_currentPath.empty()
2115 ? "Untitled VS Graph"
2116 : m_currentPath.c_str();
2117 ImGui::TextDisabled("%s%s", title, m_dirty ? " *" : "");
2118
2119 ImGui::SameLine();
2120 if (ImGui::Button("Save"))
2121 {
2122 SYSTEM_LOG << "[VisualScriptEditorPanel] Save button clicked. m_currentPath='"
2123 << m_currentPath << "'\n";
2124 if (m_currentPath.empty())
2125 {
2126 m_showSaveAsDialog = true;
2127 }
2128 else if (!Save())
2129 {
2130 ImGui::OpenPopup("SaveError");
2131 }
2132 }
2133 ImGui::SameLine();
2134 if (ImGui::Button("Save As"))
2135 {
2136 m_showSaveAsDialog = true;
2137 }
2138 ImGui::SameLine();
2139 // Phase 24.3 — Removed "New Graph" button as requested
2140 // Users must create new graphs through the file browser instead
2141
2142 if (ImGui::Button("Verify##gvs"))
2143 {
2144 RunVerification();
2145 }
2146 ImGui::SameLine();
2147 if (ImGui::Button("Condition Presets"))
2148 {
2149 m_libraryPanel->Open();
2150 }
2151 ImGui::SameLine();
2152 if (m_verificationDone)
2153 {
2154 if (m_verificationResult.HasErrors())
2155 {
2156 int errorCount = 0;
2157 for (size_t i = 0; i < m_verificationResult.issues.size(); ++i)
2158 {
2159 if (m_verificationResult.issues[i].severity == VSVerificationSeverity::Error)
2160 ++errorCount;
2161 }
2162 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
2163 "[%d error(s)]", errorCount);
2164 }
2165 else if (m_verificationResult.HasWarnings())
2166 {
2167 ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "[OK - warnings]");
2168 }
2169 else
2170 {
2171 ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "[OK]");
2172 }
2173 }
2174
2175 // Keyboard shortcuts
2176 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows))
2177 {
2178 if (ImGui::IsKeyPressed(ImGuiKey_S) &&
2179 ImGui::GetIO().KeyCtrl)
2180 {
2181 SYSTEM_LOG << "[VisualScriptEditorPanel] Ctrl+S pressed. m_currentPath='"
2182 << m_currentPath << "'\n";
2183 if (m_currentPath.empty())
2184 {
2185 m_showSaveAsDialog = true;
2186 }
2187 else if (!Save())
2188 {
2189 ImGui::OpenPopup("SaveError");
2190 }
2191 }
2192
2193 // Undo (Ctrl+Z)
2194 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Z) &&
2195 m_undoStack.CanUndo())
2196 {
2197 PerformUndo();
2198 }
2199
2200 // Redo (Ctrl+Y)
2201 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Y) &&
2202 m_undoStack.CanRedo())
2203 {
2204 PerformRedo();
2205 }
2206 }
2207
2208 if (ImGui::BeginPopup("SaveError"))
2209 {
2210 ImGui::TextColored(ImVec4(1,0,0,1), "Save failed — check file path.");
2211 if (ImGui::Button("OK")) ImGui::CloseCurrentPopup();
2212 ImGui::EndPopup();
2213 }
2214}
2215
2216void VisualScriptEditorPanel::RenderSaveAsDialog()
2217{
2218 if (m_showSaveAsDialog)
2219 {
2220 ImGui::OpenPopup("SaveAsDialog");
2221 m_showSaveAsDialog = false;
2222
2223 // Derive the save extension from the currently loaded file so we
2224 // preserve .json files as .json (instead of silently renaming to .ats).
2225 // Fall back to .ats for new/untitled graphs.
2226 m_saveAsExtension = ".ats";
2227 if (!m_currentPath.empty())
2228 {
2229 size_t dotPos = m_currentPath.rfind('.');
2230 if (dotPos != std::string::npos)
2231 m_saveAsExtension = m_currentPath.substr(dotPos);
2232 }
2233
2234 // Pre-fill the filename from the current path so the user doesn't have
2235 // to retype a name they already gave the graph.
2236 if (!m_currentPath.empty())
2237 {
2238 size_t lastSlash = m_currentPath.find_last_of("/\\");
2239 std::string fname = (lastSlash != std::string::npos)
2240 ? m_currentPath.substr(lastSlash + 1)
2241 : m_currentPath;
2242 // Strip extension
2243 size_t dotPos = fname.rfind('.');
2244 if (dotPos != std::string::npos)
2245 fname = fname.substr(0, dotPos);
2246
2247 strncpy_s(m_saveAsFilename, sizeof(m_saveAsFilename), fname.c_str(), _TRUNCATE);
2248 }
2249 // else: keep whatever is already in the buffer (set in constructor or
2250 // carried over from a previous dialog invocation).
2251 }
2252
2253 if (ImGui::BeginPopupModal("SaveAsDialog", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
2254 {
2255 ImGui::Text("Save Visual Script As");
2256 ImGui::Separator();
2257
2258 // Directory dropdown
2259 ImGui::Text("Directory:");
2260 ImGui::SameLine();
2261 if (ImGui::BeginCombo("##SaveDir", m_saveAsDirectory.c_str()))
2262 {
2263 static const char* dirs[] = {
2264 "Blueprints/AI",
2265 "Blueprints/AI/Tests",
2266 "Gamedata/TaskGraph/Examples",
2267 "Gamedata/TaskGraph/Templates"
2268 };
2269 for (int i = 0; i < static_cast<int>(sizeof(dirs) / sizeof(dirs[0])); ++i)
2270 {
2271 bool selected = (m_saveAsDirectory == dirs[i]);
2272 if (ImGui::Selectable(dirs[i], selected))
2273 m_saveAsDirectory = dirs[i];
2274 if (selected)
2275 ImGui::SetItemDefaultFocus();
2276 }
2277 ImGui::EndCombo();
2278 }
2279
2280 // Filename input
2281 ImGui::Text("Filename:");
2282 ImGui::SameLine();
2283 ImGui::InputText("##FileName", m_saveAsFilename, sizeof(m_saveAsFilename));
2284
2285 // Full path preview
2286 ImGui::TextDisabled("Full path: %s/%s%s",
2287 m_saveAsDirectory.c_str(),
2288 m_saveAsFilename,
2289 m_saveAsExtension.c_str());
2290
2291 ImGui::Separator();
2292
2293 // Save / Cancel buttons
2294 bool filenameEmpty = (std::strlen(m_saveAsFilename) == 0);
2295 if (filenameEmpty)
2296 ImGui::BeginDisabled();
2297 if (ImGui::Button("Save", ImVec2(120, 0)))
2298 {
2299 std::string fullPath = m_saveAsDirectory + "/" +
2300 std::string(m_saveAsFilename) + m_saveAsExtension;
2301 SYSTEM_LOG << "[VisualScriptEditorPanel] SaveAs dialog confirmed. fullPath='"
2302 << fullPath << "'\n";
2303 if (SaveAs(fullPath))
2304 {
2305 std::cout << "[VisualScriptEditorPanel] Saved to: " << fullPath << std::endl;
2306 ImGui::CloseCurrentPopup();
2307 }
2308 else
2309 {
2310 ImGui::OpenPopup("SaveAsError");
2311 }
2312 }
2313 if (filenameEmpty)
2314 ImGui::EndDisabled();
2315
2316 ImGui::SameLine();
2317 if (ImGui::Button("Cancel", ImVec2(120, 0)))
2318 {
2319 ImGui::CloseCurrentPopup();
2320 }
2321
2322 // Nested error popup
2323 if (ImGui::BeginPopupModal("SaveAsError", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
2324 {
2325 ImGui::TextColored(ImVec4(1, 0, 0, 1), "Save failed — check directory and permissions.");
2326 if (ImGui::Button("OK")) ImGui::CloseCurrentPopup();
2327 ImGui::EndPopup();
2328 }
2329
2330 ImGui::EndPopup();
2331 }
2332}
2333
2334void VisualScriptEditorPanel::RenderCanvas()
2335{
2336 // Switch to this panel's dedicated ImNodes context so that node positions
2337 // and canvas panning are preserved independently for each open tab.
2338 if (m_imnodesContext)
2339 ImNodes::EditorContextSet(m_imnodesContext);
2340
2341 // On the first render after LoadTemplate(), push the stored (posX, posY)
2342 // of each node into ImNodes so the canvas matches the saved layout.
2343 // BUG-003 Fix: positions are stored in grid space; use SetNodeGridSpacePos
2344 // to restore them pan-independently (avoids double-offset with viewport pan).
2345 if (m_needsPositionSync)
2346 {
2347 for (size_t i = 0; i < m_editorNodes.size(); ++i)
2348 {
2349 ImNodes::SetNodeGridSpacePos(
2350 m_editorNodes[i].nodeID,
2351 ImVec2(m_editorNodes[i].posX, m_editorNodes[i].posY));
2352 }
2353 m_needsPositionSync = false;
2354 }
2355
2356 // Phase 21-B: focus/scroll to a node requested from the verification panel
2357 if (m_focusNodeID >= 0)
2358 {
2359 for (size_t i = 0; i < m_editorNodes.size(); ++i)
2360 {
2361 if (m_editorNodes[i].nodeID == m_focusNodeID)
2362 {
2363 // BUG-003 Fix: restore in grid space for pan-independent positioning.
2364 ImNodes::SetNodeGridSpacePos(
2365 m_focusNodeID,
2366 ImVec2(m_editorNodes[i].posX, m_editorNodes[i].posY));
2367 break;
2368 }
2369 }
2370 m_focusNodeID = -1;
2371 }
2372
2373 ImNodes::BeginNodeEditor();
2374
2375 // NOTE: Right-click context menu detection is deferred until after
2376 // ImNodes::EndNodeEditor() below so that IsNodeHovered() / IsLinkHovered()
2377 // (which require ImNodesScope_None) can be used to determine what was clicked.
2378
2379 RenderNodePalette();
2380
2381 // Build connected attribute IDs set from current editor links.
2382 // Pins whose attribute ID is in this set will be rendered filled;
2383 // unconnected pins are rendered outlined (empty).
2384 std::unordered_set<int> connectedAttrIDs;
2385 for (size_t li = 0; li < m_editorLinks.size(); ++li)
2386 {
2387 connectedAttrIDs.insert(m_editorLinks[li].srcAttrID);
2388 connectedAttrIDs.insert(m_editorLinks[li].dstAttrID);
2389 }
2390
2391 // Render all nodes
2392 int activeNodeID = DebugController::Get().GetCurrentNodeID();
2393
2394 for (size_t i = 0; i < m_editorNodes.size(); ++i)
2395 {
2396 VSEditorNode& eNode = m_editorNodes[i];
2397
2398 bool hasBreakpoint = DebugController::Get().HasBreakpoint(
2399 0 /* graphID placeholder */, eNode.nodeID);
2400 bool isActive = (eNode.nodeID == activeNodeID &&
2401 DebugController::Get().IsDebugging());
2402
2403 // Phase 21-B: highlight nodes that have Error issues in the verification result
2404 bool hasVerifError = false;
2405 if (m_verificationDone)
2406 {
2407 for (size_t vi = 0; vi < m_verificationResult.issues.size(); ++vi)
2408 {
2409 if (m_verificationResult.issues[vi].nodeID == eNode.nodeID &&
2410 m_verificationResult.issues[vi].severity == VSVerificationSeverity::Error)
2411 {
2412 hasVerifError = true;
2413 break;
2414 }
2415 }
2416 }
2417 if (hasVerifError)
2418 {
2419 ImNodes::PushColorStyle(ImNodesCol_NodeBackground,
2420 IM_COL32(120, 30, 30, 230));
2421 ImNodes::PushColorStyle(ImNodesCol_NodeBackgroundHovered,
2422 IM_COL32(120, 30, 30, 230));
2423 ImNodes::PushColorStyle(ImNodesCol_NodeBackgroundSelected,
2424 IM_COL32(120, 30, 30, 230));
2425 }
2426
2427 auto execIn = GetExecInputPins(eNode.def.Type);
2428 auto execOut = GetExecOutputPinsForNode(eNode.def);
2429
2430 // Phase 24.2 FIX: Ensure data-pure nodes have DataPins initialized
2431 // This handles both newly created nodes AND nodes loaded from blueprints
2432
2433 // Initialize DataPins for GetBBValue (Variable) nodes
2434 if (eNode.def.Type == TaskNodeType::GetBBValue && eNode.def.DataPins.empty())
2435 {
2437 pinOut.PinName = "Value";
2438 pinOut.Dir = DataPinDir::Output;
2439 pinOut.PinType = VariableType::Float; // Will be resolved at runtime based on actual variable
2440 eNode.def.DataPins.push_back(pinOut);
2441 std::cerr << "[VSEditor] Initialized DataPins for GetBBValue (Variable) node #" << eNode.nodeID << "\n";
2442 }
2443
2444 // Initialize DataPins for MathOp nodes
2445 if (eNode.def.Type == TaskNodeType::MathOp && eNode.def.DataPins.empty())
2446 {
2447 // Initialize DataPins for this MathOp node if not already present
2449 pinA.PinName = "A";
2450 pinA.Dir = DataPinDir::Input;
2451 pinA.PinType = VariableType::Float;
2452 eNode.def.DataPins.push_back(pinA);
2453
2455 pinB.PinName = "B";
2456 pinB.Dir = DataPinDir::Input;
2457 pinB.PinType = VariableType::Float;
2458 eNode.def.DataPins.push_back(pinB);
2459
2461 pinResult.PinName = "Result";
2462 pinResult.Dir = DataPinDir::Output;
2463 pinResult.PinType = VariableType::Float;
2464 eNode.def.DataPins.push_back(pinResult);
2465
2466 std::cerr << "[VSEditor] Initialized DataPins for MathOp node #" << eNode.nodeID << "\n";
2467 }
2468
2469 // Initialize DataPins for SetBBValue nodes
2470 if (eNode.def.Type == TaskNodeType::SetBBValue && eNode.def.DataPins.empty())
2471 {
2473 pinIn.PinName = "Value";
2474 pinIn.Dir = DataPinDir::Input;
2475 pinIn.PinType = VariableType::Float; // Will be resolved at runtime based on target variable
2476 eNode.def.DataPins.push_back(pinIn);
2477 std::cerr << "[VSEditor] Initialized DataPins for SetBBValue node #" << eNode.nodeID << "\n";
2478 }
2479
2480 std::vector<std::pair<std::string, VariableType>> dataIn, dataOut;
2481 for (size_t p = 0; p < eNode.def.DataPins.size(); ++p)
2482 {
2483 const DataPinDefinition& pin = eNode.def.DataPins[p];
2484 if (pin.Dir == DataPinDir::Input)
2485 dataIn.push_back({pin.PinName, pin.PinType});
2486 else
2487 dataOut.push_back({pin.PinName, pin.PinType});
2488 }
2489
2490 // Phase 24 — Dispatcher: Branch nodes use specialized renderer
2491 if (eNode.def.Type == TaskNodeType::Branch && m_branchRenderer)
2492 {
2493 // Convert TaskNodeDefinition to NodeBranchData for specialized rendering
2495 branchData.nodeID = eNode.nodeID; // int nodeID for ImNodes attribute UIDs
2496 branchData.nodeName = eNode.def.NodeName;
2497 branchData.conditionRefs = eNode.def.conditionRefs;
2498 branchData.dynamicPins = eNode.def.dynamicPins;
2499 branchData.breakpoint = hasBreakpoint;
2500
2501 // Render via NodeBranchRenderer (4-section layout)
2502 // Must be wrapped with ImNodes::BeginNode/EndNode just like generic renderer
2503 ImNodes::BeginNode(eNode.nodeID);
2504 m_branchRenderer->RenderNode(branchData, connectedAttrIDs);
2505 ImNodes::EndNode();
2506 }
2507 else
2508 {
2509 // Use generic renderer for all other node types
2510 VisualScriptNodeRenderer::RenderNode(
2511 eNode.nodeID,
2512 eNode.nodeID,
2513 0 /* graphID placeholder */,
2514 eNode.def,
2515 hasBreakpoint,
2516 isActive,
2517 execIn, execOut,
2518 dataIn, dataOut,
2519 [](int nid, void* ud) {
2520 VisualScriptEditorPanel* panel =
2521 static_cast<VisualScriptEditorPanel*>(ud);
2522 panel->m_pendingAddPin = true;
2523 panel->m_pendingAddPinNodeID = nid;
2524 },
2525 this,
2526 [](int nid, int dynIdx, void* ud) {
2527 VisualScriptEditorPanel* panel =
2528 static_cast<VisualScriptEditorPanel*>(ud);
2529 panel->m_pendingRemovePin = true;
2530 panel->m_pendingRemovePinNodeID = nid;
2531 panel->m_pendingRemovePinDynIdx = dynIdx;
2532 },
2533 this,
2535 }
2536
2537 if (hasVerifError)
2538 {
2539 ImNodes::PopColorStyle();
2540 ImNodes::PopColorStyle();
2541 ImNodes::PopColorStyle();
2542 }
2543
2544 // Breakpoint / active overlays
2545 if (hasBreakpoint)
2546 VisualScriptNodeRenderer::RenderBreakpointIndicator(eNode.nodeID);
2547 if (isActive)
2548 VisualScriptNodeRenderer::RenderActiveNodeGlow(eNode.nodeID);
2549
2550 // Mark this node as rendered so position sync is safe
2551 m_positionedNodes.insert(eNode.nodeID);
2552 }
2553
2554 // Render links
2555 for (size_t i = 0; i < m_editorLinks.size(); ++i)
2556 {
2557 const VSEditorLink& link = m_editorLinks[i];
2558 if (link.isData)
2559 ImNodes::PushColorStyle(ImNodesCol_Link, SystemColors::DATA_CONNECTION_COLOR);
2560 else
2561 ImNodes::PushColorStyle(ImNodesCol_Link, SystemColors::EXEC_CONNECTION_COLOR);
2562 ImNodes::Link(link.linkID, link.srcAttrID, link.dstAttrID);
2563 ImNodes::PopColorStyle();
2564 }
2565
2566 ImNodes::EndNodeEditor();
2567
2568 // FIX 4: Skip position sync if undo/redo just executed.
2569 // SyncEditorNodesFromTemplate() has already written the correct undo-target
2570 // positions into m_editorNodes and SetNodeEditorSpacePos() has pushed them
2571 // to ImNodes. Reading them back here (before ImNodes has rendered the new
2572 // positions once) would overwrite the correct values with stale ImNodes state.
2573 if (m_skipPositionSyncNextFrame)
2574 {
2575 m_skipPositionSyncNextFrame = false;
2576 }
2577 else
2578 {
2579 SyncNodePositionsFromImNodes();
2580 }
2581
2582 // ========================================================================
2583 // Context menu dispatch (requires ImNodesScope_None, i.e. after EndNodeEditor)
2584 // Priority: node hover > link hover > canvas background.
2585 // ========================================================================
2586 {
2587 int hoveredNode = -1;
2588 int hoveredLink = -1;
2589 bool nodeHovered = ImNodes::IsNodeHovered(&hoveredNode);
2590 bool linkHovered = ImNodes::IsLinkHovered(&hoveredLink);
2591
2592 // PHASE 1: Detect right-click and open the appropriate popup.
2593 // Use ImNodes::IsEditorHovered() for canvas background detection so
2594 // that the check works even when ImNodes has captured mouse focus.
2595 if (ImGui::IsMouseClicked(ImGuiMouseButton_Right))
2596 {
2597 if (nodeHovered)
2598 {
2599 m_contextNodeID = hoveredNode;
2600 ImGui::OpenPopup("VSNodeContextMenu");
2601 SYSTEM_LOG << "[VSEditor] Opened context menu on NODE #" << hoveredNode << "\n";
2602 }
2603 else if (linkHovered)
2604 {
2605 m_contextLinkID = hoveredLink;
2606 ImGui::OpenPopup("VSLinkContextMenu");
2607 SYSTEM_LOG << "[VSEditor] Opened context menu on LINK #" << hoveredLink << "\n";
2608 }
2609 else if (ImNodes::IsEditorHovered())
2610 {
2611 // Convert screen-space mouse position to canvas-space by
2612 // subtracting the ImNodes canvas panning offset.
2613 // Note: ImNodes 0.4 does not expose a zoom accessor, so zoom=1.0f.
2614 ImVec2 mp = ImGui::GetMousePos();
2615 ImVec2 canvasOrigin = ImNodes::EditorContextGetPanning();
2616 float zoom = 1.0f;
2617 m_contextMenuX = (mp.x - canvasOrigin.x) / zoom;
2618 m_contextMenuY = (mp.y - canvasOrigin.y) / zoom;
2619 ImGui::OpenPopup("VSNodePalette");
2620 SYSTEM_LOG << "[VSEditor] Opened context menu on CANVAS at ("
2621 << m_contextMenuX << ", " << m_contextMenuY << ")\n";
2622 }
2623 }
2624
2625 // PHASE 2: Render popups in the same ImGui window scope.
2626 RenderContextMenus();
2627 }
2628
2629 // ========================================================================
2630 // PHASE 1: Detect drag & drop (store pending node creation).
2631 // AddNode() must NOT be called here — ImNodes' internal state is still
2632 // being finalised at this point and SetNodeEditorSpacePos would assert.
2633 // ========================================================================
2634 if (ImGui::BeginDragDropTarget())
2635 {
2636 const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("VS_NODE_TYPE_ENUM");
2637 if (payload && payload->Data && payload->DataSize == sizeof(uint8_t))
2638 {
2639 uint8_t enumValue = *static_cast<const uint8_t*>(payload->Data);
2640 TaskNodeType nodeType = static_cast<TaskNodeType>(enumValue);
2641
2642 // Get mouse position in canvas space
2643 ImVec2 mousePos = ImGui::GetMousePos();
2644 ImVec2 canvasPos = ImNodes::EditorContextGetPanning();
2645 float zoom = 1.0f; // ImNodes doesn't expose zoom yet
2646
2647 // Convert screen space to canvas space
2648 ImVec2 windowPos = ImGui::GetWindowPos();
2649 float canvasX = (mousePos.x - windowPos.x - canvasPos.x) / zoom;
2650 float canvasY = (mousePos.y - windowPos.y - canvasPos.y) / zoom;
2651
2652 // CRITICAL: Don't call AddNode() here — just store the request.
2653 // The node will be created in Phase 2 below, safely outside the
2654 // ImNodes editor scope.
2655 m_pendingNodeDrop = true;
2656 m_pendingNodeType = nodeType;
2657 m_pendingNodeX = canvasX;
2658 m_pendingNodeY = canvasY;
2659 }
2660 ImGui::EndDragDropTarget();
2661 }
2662
2663 // ========================================================================
2664 // PHASE 2: Process pending node creation (outside editor scope).
2665 // AddNode() and SetNodeEditorSpacePos() are both safe here — the editor
2666 // context is fully closed (ImNodesScope_None).
2667 // ========================================================================
2668 if (m_pendingNodeDrop)
2669 {
2670 // Ensure positions are not garbage values (defend against FLT_MAX or corrupted memory)
2671 float safeX = m_pendingNodeX;
2672 float safeY = m_pendingNodeY;
2673 if (!std::isfinite(safeX) || !std::isfinite(safeY) ||
2674 safeX < -100000.0f || safeX > 100000.0f ||
2676 {
2677 safeX = 0.0f;
2678 safeY = 0.0f;
2679 SYSTEM_LOG << "[VSEditor] Warning: pending node position was garbage; reset to (0, 0)\n";
2680 }
2681
2682 int newNodeID = AddNode(m_pendingNodeType, safeX, safeY);
2683
2684 // Pre-register the position so ImNodes places the node correctly
2685 // on the very first frame it is rendered (next frame).
2686 ImNodes::SetNodeEditorSpacePos(newNodeID, ImVec2(safeX, safeY));
2687
2688 m_dirty = true;
2689 m_pendingNodeDrop = false;
2690
2691 std::cout << "[VisualScriptEditorPanel] Node created: ID=" << newNodeID
2692 << " type=" << static_cast<int>(m_pendingNodeType)
2693 << " at (" << safeX << ", " << safeY << ")"
2694 << std::endl;
2695 }
2696
2697 // ========================================================================
2698 // PHASE 2: Process pending dynamic pin addition (outside editor scope).
2699 // The [+] button callback on VSSequence/Switch stores the request here; we
2700 // process it after EndNodeEditor so that AddDynamicPinCommand can safely
2701 // modify the template and trigger RebuildLinks().
2702 // ========================================================================
2703 if (m_pendingAddPin)
2704 {
2705 m_pendingAddPin = false;
2706
2707 VSEditorNode* eNode = nullptr;
2708 for (size_t i = 0; i < m_editorNodes.size(); ++i)
2709 {
2710 if (m_editorNodes[i].nodeID == m_pendingAddPinNodeID)
2711 {
2712 eNode = &m_editorNodes[i];
2713 break;
2714 }
2715 }
2716 if (eNode != nullptr &&
2717 (eNode->def.Type == TaskNodeType::VSSequence ||
2718 eNode->def.Type == TaskNodeType::Switch))
2719 {
2720 int pinIdx = static_cast<int>(eNode->def.DynamicExecOutputPins.size()) + 1;
2721 std::string pinName;
2722 if (eNode->def.Type == TaskNodeType::VSSequence)
2723 pinName = "Out_" + std::to_string(pinIdx);
2724 else
2725 pinName = "Case_" + std::to_string(pinIdx);
2726
2727 // Update editor-side def immediately
2728 eNode->def.DynamicExecOutputPins.push_back(pinName);
2729
2730 // Push undo command (also updates template)
2731 m_undoStack.PushCommand(
2732 std::unique_ptr<ICommand>(
2733 new AddDynamicPinCommand(m_pendingAddPinNodeID, pinName)),
2734 m_template);
2735
2736 RebuildLinks();
2737 m_dirty = true;
2738 SYSTEM_LOG << "[VSEditor] AddDynamicPin: node #" << m_pendingAddPinNodeID
2739 << " added pin '" << pinName << "'\n";
2740 }
2741 }
2742
2743 // ========================================================================
2744 // PHASE 2: Process pending dynamic pin removal (outside editor scope).
2745 // The [-] button callback on dynamic pins stores the request here; we
2746 // process it after EndNodeEditor so that RemoveExecPinCommand can safely
2747 // modify the template and trigger RebuildLinks().
2748 // ========================================================================
2749 if (m_pendingRemovePin)
2750 {
2751 m_pendingRemovePin = false;
2752
2753 VSEditorNode* eNode = nullptr;
2754 for (size_t i = 0; i < m_editorNodes.size(); ++i)
2755 {
2756 if (m_editorNodes[i].nodeID == m_pendingRemovePinNodeID)
2757 {
2758 eNode = &m_editorNodes[i];
2759 break;
2760 }
2761 }
2762 if (eNode != nullptr &&
2763 m_pendingRemovePinDynIdx >= 0 &&
2764 m_pendingRemovePinDynIdx < static_cast<int>(eNode->def.DynamicExecOutputPins.size()))
2765 {
2766 const std::string pinName =
2767 eNode->def.DynamicExecOutputPins[static_cast<size_t>(m_pendingRemovePinDynIdx)];
2768
2769 // Find any outgoing link from this pin in the template
2771 std::string linkedTargetPinName;
2772 for (size_t c = 0; c < m_template.ExecConnections.size(); ++c)
2773 {
2774 const ExecPinConnection& ec = m_template.ExecConnections[c];
2775 if (ec.SourceNodeID == m_pendingRemovePinNodeID &&
2776 ec.SourcePinName == pinName)
2777 {
2779 linkedTargetPinName = ec.TargetPinName;
2780 break;
2781 }
2782 }
2783
2784 // Update editor-side def immediately
2785 eNode->def.DynamicExecOutputPins.erase(
2786 eNode->def.DynamicExecOutputPins.begin() + m_pendingRemovePinDynIdx);
2787
2788 // Push undo command (also updates template)
2789 m_undoStack.PushCommand(
2790 std::unique_ptr<ICommand>(
2791 new RemoveExecPinCommand(m_pendingRemovePinNodeID,
2792 pinName,
2793 m_pendingRemovePinDynIdx,
2796 m_template);
2797
2798 RebuildLinks();
2799 m_dirty = true;
2800 SYSTEM_LOG << "[VSEditor] RemoveDynamicPin: node #" << m_pendingRemovePinNodeID
2801 << " removed pin '" << pinName << "'\n";
2802 }
2803 }
2804
2805 // Track node moves for undo/redo using MoveNodeCommand.
2806 // Phase 19 — snapshot-at-click approach:
2807 // Step 1 (MouseClicked) : snapshot current eNode.posX/Y for all positioned nodes.
2808 // Step 2 (MouseDown) : keep eNode.posX/Y in sync with ImNodes live positions.
2809 // Step 3 (MouseReleased) : for each snapshotted node, push MoveNodeCommand if
2810 // final position differs from snapshot by more than 1px.
2811 //
2812 // Only query nodes that have been rendered at least once (present in
2813 // m_positionedNodes) to avoid ImNodes assertions for brand-new nodes.
2814 {
2815 // Skip movement detection for one frame immediately after an undo/redo
2816 // so that stale ImNodes positions (not yet updated by the new render
2817 // cycle) are not mistaken for user-initiated drag-start positions.
2818 if (m_justPerformedUndoRedo)
2819 {
2820 m_justPerformedUndoRedo = false;
2821 }
2822 else
2823 {
2824 // Step 1: on the initial click, snapshot positions of all positioned nodes.
2825 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
2826 {
2827 m_nodeDragStartPositions.clear();
2828 for (size_t i = 0; i < m_editorNodes.size(); ++i)
2829 {
2830 const VSEditorNode& eNode = m_editorNodes[i];
2831 if (m_positionedNodes.count(eNode.nodeID) == 0)
2832 continue;
2833 m_nodeDragStartPositions[eNode.nodeID] =
2834 std::make_pair(eNode.posX, eNode.posY);
2835 }
2836 //SYSTEM_LOG << "[VSEditor] Mouse clicked: snapshot " << static_cast<size_t>(m_nodeDragStartPositions.size()) << " node positions\n";
2837 }
2838
2839 // Step 2: while mouse is held, keep eNode.posX/Y current (live Save support).
2840 if (ImGui::IsMouseDown(ImGuiMouseButton_Left))
2841 {
2842 for (size_t i = 0; i < m_editorNodes.size(); ++i)
2843 {
2844 VSEditorNode& eNode = m_editorNodes[i];
2845 if (m_positionedNodes.count(eNode.nodeID) == 0)
2846 continue;
2847 const ImVec2 pos = ImNodes::GetNodeEditorSpacePos(eNode.nodeID);
2848 eNode.posX = pos.x;
2849 eNode.posY = pos.y;
2850 }
2851 }
2852
2853 // Step 3: on release, push MoveNodeCommand for any node that moved > 1px.
2854 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left))
2855 {
2856 for (const auto& entry : m_nodeDragStartPositions)
2857 {
2858 const int nodeID = entry.first;
2859 const float startX = entry.second.first;
2860 const float startY = entry.second.second;
2861
2862 // CRITICAL FIX: Check if node still exists before querying ImNodes
2863 // The node could have been deleted or the canvas reloaded between mouse click and release.
2864 // Without this check, GetNodeEditorSpacePos() will assert on a non-existent node.
2865 if (m_positionedNodes.count(nodeID) == 0)
2866 continue; // Skip this node, it was deleted or canvas state changed
2867
2868 const ImVec2 finalPos = ImNodes::GetNodeEditorSpacePos(nodeID);
2869
2870 // Update eNode with final position
2871 for (size_t i = 0; i < m_editorNodes.size(); ++i)
2872 {
2873 if (m_editorNodes[i].nodeID == nodeID)
2874 {
2875 m_editorNodes[i].posX = finalPos.x;
2876 m_editorNodes[i].posY = finalPos.y;
2877 break;
2878 }
2879 }
2880
2881 if (std::abs(finalPos.x - startX) > 1.0f ||
2882 std::abs(finalPos.y - startY) > 1.0f)
2883 {
2884 m_undoStack.PushCommand(
2885 std::unique_ptr<ICommand>(
2886 new MoveNodeCommand(nodeID,
2887 startX, startY,
2888 finalPos.x, finalPos.y)),
2889 m_template);
2890 //SYSTEM_LOG << "[VSEditor] MoveNodeCommand pushed node #" << nodeID
2891 // << " (" << startX << "," << startY
2892 // << ") -> (" << finalPos.x << "," << finalPos.y
2893 // << ") [UNDOABLE]\n";
2894 m_dirty = true;
2895 }
2896 //else
2897 //{
2898 // SYSTEM_LOG << "[VSEditor] Node #" << nodeID
2899 // << " not moved (delta < 1px), skipping\n";
2900 //}
2901 }
2902 m_nodeDragStartPositions.clear();
2903 }
2904 } // end !m_justPerformedUndoRedo
2905 }
2906
2907 // Hover tooltip — ImNodes::IsNodeHovered() requires ImNodesScope_None,
2908 // so it must be called here (after EndNodeEditor), never inside the
2909 // BeginNodeEditor/EndNodeEditor block.
2910 {
2911 int hoveredNode = -1;
2912 if (ImNodes::IsNodeHovered(&hoveredNode))
2913 {
2914 auto it = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
2915 [hoveredNode](const VSEditorNode& n) {
2916 return n.nodeID == hoveredNode;
2917 });
2918 if (it != m_editorNodes.end())
2919 {
2920 const char* tip = GetNodeTypeLabel(it->def.Type);
2921 if (tip && tip[0] != '\0')
2922 {
2923 ImGui::BeginTooltip();
2924 ImGui::TextUnformatted(tip);
2925 ImGui::EndTooltip();
2926 }
2927 }
2928 }
2929 }
2930
2931 // Handle new link creation
2932 int startAttr = -1, endAttr = -1;
2933 if (ImNodes::IsLinkCreated(&startAttr, &endAttr))
2934 {
2935 int startOffset = startAttr % 10000;
2936 int endOffset = endAttr % 10000;
2937
2938 // Classify pin directions by offset range:
2939 // 0 -> exec-in (Input)
2940 // 100–199 -> exec-out (Output)
2941 // 200–299 -> data-in (Input)
2942 // 300–399 -> data-out (Output)
2943 bool startIsOutput = (startOffset >= 100 && startOffset < 200) ||
2944 (startOffset >= 300 && startOffset < 400);
2945 bool endIsInput = (endOffset == 0) ||
2946 (endOffset >= 200 && endOffset < 300);
2947
2948 // Auto-swap if user dragged backwards (Input -> Output).
2949 // ImNodes normalises the direction automatically (Output pin is always
2950 // returned as startAttr), so this branch fires only in edge cases where
2951 // the pin type could not be determined by ImNodes.
2952 if (!startIsOutput && endIsInput)
2953 {
2954 std::swap(startAttr, endAttr);
2955 startOffset = startAttr % 10000;
2956 endOffset = endAttr % 10000;
2957 // Recalculate flags from the new offsets after swap.
2958 startIsOutput = (startOffset >= 100 && startOffset < 200) ||
2959 (startOffset >= 300 && startOffset < 400);
2960 endIsInput = (endOffset == 0) ||
2961 (endOffset >= 200 && endOffset < 300);
2962 }
2963
2965 {
2966 const bool isExecLink = (startOffset >= 100 && startOffset < 200);
2967 const bool isDataLink = (startOffset >= 300 && startOffset < 400);
2968 const int srcNodeID = startAttr / 10000;
2969 const int dstNodeID = endAttr / 10000;
2970
2971 // Phase 24: CRITICAL - Prevent data links from connecting to exec-in pin (offset 0)
2972 // If this is a data link and destination is exec-in, find the first available data-in pin instead
2973 if (isDataLink && endOffset == 0)
2974 {
2975 // Data-to-exec mismatch detected. Try to find the first available data-in pin.
2976 auto dstIt = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
2977 [dstNodeID](const VSEditorNode& n) {
2978 return n.nodeID == dstNodeID;
2979 });
2980
2981 if (dstIt != m_editorNodes.end() &&
2982 dstIt->def.Type == TaskNodeType::Branch &&
2983 !dstIt->def.dynamicPins.empty())
2984 {
2985 // Force endAttr to point to the first dynamic data-in pin (offset 200)
2986 endAttr = dstNodeID * 10000 + 200;
2987 endOffset = 200;
2988 }
2989 else
2990 {
2991 // No valid data-in pins available, reject this link
2992 m_dirty = false;
2993 // Skip link creation
2994 startIsOutput = false;
2995 endIsInput = false;
2996 }
2997 }
2998
2999 // Only proceed if we still have valid pins to connect
3000 if (!startIsOutput || !endIsInput)
3001 return;
3002
3003 if (isExecLink)
3004 {
3005 // Resolve source exec-out pin name from its index
3006 const int srcPinIndex = startOffset - 100;
3007 std::string srcPinName = "Out";
3008
3009 auto srcIt = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
3010 [srcNodeID](const VSEditorNode& n) {
3011 return n.nodeID == srcNodeID;
3012 });
3013 if (srcIt != m_editorNodes.end())
3014 {
3015 auto outPins = GetExecOutputPinsForNode(srcIt->def);
3016 if (srcPinIndex < static_cast<int>(outPins.size()))
3018 }
3019
3020 if (VSConnectionValidator::IsExecConnectionValid(m_template, srcNodeID, srcPinName, dstNodeID))
3021 {
3022 ConnectExec(srcNodeID, srcPinName, dstNodeID, "In");
3023 SYSTEM_LOG << "[VSEditor] Created exec link: node #" << srcNodeID
3024 << "." << srcPinName << " -> node #" << dstNodeID << ".In\n";
3025 m_dirty = true;
3026 }
3027 else
3028 {
3029 SYSTEM_LOG << "[VSEditor] Exec link validation failed: node #" << srcNodeID
3030 << "." << srcPinName << " -> node #" << dstNodeID << ".In\n";
3031 }
3032 }
3033 else if (isDataLink)
3034 {
3035 // Resolve source data-out and destination data-in pin names
3036 int srcPinIndex = startOffset - 300;
3037 int dstPinIndex = endOffset - 200;
3038 std::string srcPinName = "Value";
3039 std::string dstPinName = "Value";
3040
3041 auto srcIt = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
3042 [srcNodeID](const VSEditorNode& n) {
3043 return n.nodeID == srcNodeID;
3044 });
3045 auto dstIt = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
3046 [dstNodeID](const VSEditorNode& n) {
3047 return n.nodeID == dstNodeID;
3048 });
3049
3050 if (srcIt != m_editorNodes.end())
3051 {
3052 // Try static pin list first, then fall back to DataPins vector
3053 auto outPins = GetDataOutputPins(srcIt->def.Type);
3054 if (srcPinIndex < static_cast<int>(outPins.size()))
3055 {
3057 }
3058 else
3059 {
3060 int outIdx = 0;
3061 for (size_t p = 0; p < srcIt->def.DataPins.size(); ++p)
3062 {
3063 if (srcIt->def.DataPins[p].Dir == DataPinDir::Output)
3064 {
3065 if (outIdx == srcPinIndex)
3066 {
3067 srcPinName = srcIt->def.DataPins[p].PinName;
3068 break;
3069 }
3070 ++outIdx;
3071 }
3072 }
3073 }
3074 }
3075
3076 if (dstIt != m_editorNodes.end())
3077 {
3078 // Phase 24: Check if destination is a Branch node with dynamic pins
3079 if (dstIt->def.Type == TaskNodeType::Branch)
3080 {
3081 // For Branch nodes, data-in pins start at offset 200
3082 // If dstPinIndex is negative or out of range, force it to 0 (first pin)
3083 if (dstPinIndex < 0 || dstPinIndex >= static_cast<int>(dstIt->def.dynamicPins.size()))
3084 {
3085 if (!dstIt->def.dynamicPins.empty())
3086 {
3087 dstPinIndex = 0; // Force first available data-in pin
3088 std::cerr << "[VSEditor] Data-in pin index corrected to 0 (first available)\n";
3089 }
3090 }
3091
3092 if (dstPinIndex >= 0 && dstPinIndex < static_cast<int>(dstIt->def.dynamicPins.size()))
3093 {
3094 // Use the dynamic pin's ID as the target pin name
3095 dstPinName = dstIt->def.dynamicPins[dstPinIndex].id;
3096 }
3097 else
3098 {
3099 std::cerr << "[VSEditor] Cannot find valid data-in pin on Branch node\n";
3100 return; // Skip this link
3101 }
3102 }
3103 else
3104 {
3105 // Fall back to static data pins
3106 auto inPins = GetDataInputPins(dstIt->def.Type);
3107 if (dstPinIndex < static_cast<int>(inPins.size()))
3108 {
3110 }
3111 else
3112 {
3113 int inIdx = 0;
3114 for (size_t p = 0; p < dstIt->def.DataPins.size(); ++p)
3115 {
3116 if (dstIt->def.DataPins[p].Dir == DataPinDir::Input)
3117 {
3118 if (inIdx == dstPinIndex)
3119 {
3120 dstPinName = dstIt->def.DataPins[p].PinName;
3121 break;
3122 }
3123 ++inIdx;
3124 }
3125 }
3126 }
3127 }
3128 }
3129
3130 ConnectData(srcNodeID, srcPinName, dstNodeID, dstPinName);
3131 std::cout << "[VisualScriptEditorPanel] Created data link: node"
3132 << srcNodeID << "." << srcPinName
3133 << " -> node" << dstNodeID << "." << dstPinName << "\n";
3134 m_dirty = true;
3135 }
3136 else
3137 {
3138 std::cerr << "[VisualScriptEditorPanel] Cannot create link"
3139 " — incompatible pin types (exec/data mismatch)\n";
3140 }
3141 }
3142 else
3143 {
3144 std::cerr << "[VisualScriptEditorPanel] Cannot create link"
3145 " — incompatible pin types (both inputs or both outputs)\n";
3146 }
3147 }
3148
3149 // Handle link deletion (triggered when the user Ctrl+clicks a link in ImNodes)
3150 int destroyedLink = -1;
3151 if (ImNodes::IsLinkDestroyed(&destroyedLink))
3152 {
3153 // Delegate to RemoveLink() so that:
3154 // 1. The underlying template connection is removed (not just the
3155 // visual m_editorLinks entry). Without this the connection would
3156 // reappear as a "ghost" link the next time RebuildLinks() is called
3157 // (e.g. after any undo/redo).
3158 // 2. A DeleteLinkCommand is pushed onto the undo stack, making the
3159 // deletion reversible via Ctrl+Z.
3160 RemoveLink(destroyedLink);
3161 }
3162
3163 // Handle node selection
3164 if (ImNodes::NumSelectedNodes() == 1)
3165 {
3166 int selNodes[1] = {-1};
3167 ImNodes::GetSelectedNodes(selNodes);
3168 m_selectedNodeID = selNodes[0];
3169 }
3170 else if (ImNodes::NumSelectedNodes() == 0)
3171 {
3172 m_selectedNodeID = -1;
3173 }
3174
3175 // F9 = toggle breakpoint on selected node
3176 if (ImGui::IsKeyPressed(ImGuiKey_F9) && m_selectedNodeID >= 0)
3177 {
3178 DebugController::Get().ToggleBreakpoint(0, m_selectedNodeID,
3179 m_template.Name,
3180 "Node " + std::to_string(m_selectedNodeID));
3181 }
3182
3183 // Delete key = remove all selected nodes and links
3184 if (ImGui::IsKeyPressed(ImGuiKey_Delete) && ImGui::IsWindowFocused())
3185 {
3186 int numSelectedNodes = ImNodes::NumSelectedNodes();
3187 if (numSelectedNodes > 0)
3188 {
3189 if (numSelectedNodes > 5)
3190 {
3191 std::cout << "[VSEditor] Warning: Deleting " << numSelectedNodes
3192 << " nodes" << std::endl;
3193 }
3194
3195 std::vector<int> selectedNodes(static_cast<size_t>(numSelectedNodes));
3196 ImNodes::GetSelectedNodes(selectedNodes.data());
3197
3198 for (int nodeID : selectedNodes)
3199 {
3200 if (m_selectedNodeID == nodeID)
3201 m_selectedNodeID = -1;
3202 RemoveNode(nodeID);
3203 std::cout << "[VSEditor] Deleted node " << nodeID << std::endl;
3204 }
3205
3206 m_dirty = true;
3207 }
3208
3209 int numSelectedLinks = ImNodes::NumSelectedLinks();
3210 if (numSelectedLinks > 0)
3211 {
3212 std::vector<int> selectedLinks(static_cast<size_t>(numSelectedLinks));
3213 ImNodes::GetSelectedLinks(selectedLinks.data());
3214
3215 for (int linkID : selectedLinks)
3216 {
3217 RemoveLink(linkID);
3218 std::cout << "[VSEditor] Deleted link " << linkID << std::endl;
3219 }
3220
3221 m_dirty = true;
3222 }
3223 }
3224
3225 RenderValidationOverlay();
3226}
3227
3228void VisualScriptEditorPanel::RenderNodePalette()
3229{
3230 if (!ImGui::BeginPopup("VSNodePalette"))
3231 return;
3232
3233 ImGui::TextDisabled("Add Node");
3234 ImGui::Separator();
3235
3236 // Flow Control
3237 if (ImGui::BeginMenu("Flow Control"))
3238 {
3239 auto addFlowNode = [&](TaskNodeType type, const char* label) {
3240 if (ImGui::MenuItem(label))
3241 {
3242 AddNode(type, m_contextMenuX, m_contextMenuY);
3243 ImGui::CloseCurrentPopup();
3244 }
3245 };
3246 addFlowNode(TaskNodeType::EntryPoint, "EntryPoint");
3247 addFlowNode(TaskNodeType::Branch, "Branch");
3248 addFlowNode(TaskNodeType::VSSequence, "Sequence");
3249 addFlowNode(TaskNodeType::While, "While");
3250 addFlowNode(TaskNodeType::ForEach, "ForEach");
3251 addFlowNode(TaskNodeType::DoOnce, "DoOnce");
3252 addFlowNode(TaskNodeType::Delay, "Delay");
3253 ImGui::EndMenu();
3254 }
3255
3256 if (ImGui::BeginMenu("Actions"))
3257 {
3258 if (ImGui::MenuItem("AtomicTask"))
3259 {
3260 AddNode(TaskNodeType::AtomicTask, m_contextMenuX, m_contextMenuY);
3261 ImGui::CloseCurrentPopup();
3262 }
3263 ImGui::EndMenu();
3264 }
3265
3266 if (ImGui::BeginMenu("Data"))
3267 {
3268 if (ImGui::MenuItem("GetBBValue"))
3269 {
3270 AddNode(TaskNodeType::GetBBValue, m_contextMenuX, m_contextMenuY);
3271 ImGui::CloseCurrentPopup();
3272 }
3273 if (ImGui::MenuItem("SetBBValue"))
3274 {
3275 AddNode(TaskNodeType::SetBBValue, m_contextMenuX, m_contextMenuY);
3276 ImGui::CloseCurrentPopup();
3277 }
3278 if (ImGui::MenuItem("MathOp"))
3279 {
3280 AddNode(TaskNodeType::MathOp, m_contextMenuX, m_contextMenuY);
3281 ImGui::CloseCurrentPopup();
3282 }
3283 ImGui::EndMenu();
3284 }
3285
3286 if (ImGui::BeginMenu("SubGraph"))
3287 {
3288 if (ImGui::MenuItem("SubGraph"))
3289 {
3290 AddNode(TaskNodeType::SubGraph, m_contextMenuX, m_contextMenuY);
3291 ImGui::CloseCurrentPopup();
3292 }
3293 ImGui::EndMenu();
3294 }
3295
3296 ImGui::EndPopup();
3297}
3298
3299void VisualScriptEditorPanel::RenderContextMenus()
3300{
3301 // ========================================================================
3302 // Node context menu
3303 // ========================================================================
3304 if (ImGui::BeginPopup("VSNodeContextMenu"))
3305 {
3306 if (ImGui::MenuItem("Edit Properties"))
3307 {
3308 m_selectedNodeID = m_contextNodeID;
3309 SYSTEM_LOG << "[VSEditor] Selected node #" << m_contextNodeID
3310 << " for editing\n";
3311 }
3312
3313 ImGui::Separator();
3314
3315 if (ImGui::MenuItem("Delete Node"))
3316 {
3317 RemoveNode(m_contextNodeID);
3318 if (m_selectedNodeID == m_contextNodeID)
3319 m_selectedNodeID = -1;
3320 m_dirty = true;
3321 SYSTEM_LOG << "[VSEditor] Deleted node #" << m_contextNodeID
3322 << " via context menu\n";
3323 }
3324
3325 ImGui::Separator();
3326
3327 {
3328 bool hasBP = DebugController::Get().HasBreakpoint(0, m_contextNodeID);
3329 if (ImGui::MenuItem(hasBP ? "Remove Breakpoint (F9)" : "Add Breakpoint (F9)"))
3330 {
3331 DebugController::Get().ToggleBreakpoint(0, m_contextNodeID,
3332 m_template.Name,
3333 "Node " + std::to_string(m_contextNodeID));
3334 SYSTEM_LOG << "[VSEditor] Toggled breakpoint on node #"
3335 << m_contextNodeID << " -> "
3336 << (hasBP ? "OFF" : "ON") << "\n";
3337 }
3338 }
3339
3340 ImGui::Separator();
3341
3342 if (ImGui::MenuItem("Duplicate"))
3343 {
3344 auto it = std::find_if(m_editorNodes.begin(), m_editorNodes.end(),
3345 [this](const VSEditorNode& n) { return n.nodeID == m_contextNodeID; });
3346 if (it != m_editorNodes.end())
3347 {
3349 newDef.NodeID = AllocNodeID();
3350 newDef.NodeName += " (Copy)";
3351 newDef.EditorPosX = it->posX + 50.0f;
3352 newDef.EditorPosY = it->posY + 50.0f;
3353 newDef.HasEditorPos = true;
3354
3356 eNew.nodeID = newDef.NodeID;
3357 eNew.posX = newDef.EditorPosX;
3358 eNew.posY = newDef.EditorPosY;
3359 eNew.def = newDef;
3360 m_editorNodes.push_back(eNew);
3361
3362 m_undoStack.PushCommand(
3363 std::unique_ptr<ICommand>(new AddNodeCommand(newDef)),
3364 m_template);
3365 m_dirty = true;
3366 SYSTEM_LOG << "[VSEditor] Node " << m_contextNodeID
3367 << " duplicated as #" << newDef.NodeID << "\n";
3368 }
3369 }
3370
3371 ImGui::EndPopup();
3372 }
3373
3374 // ========================================================================
3375 // Link context menu
3376 // ========================================================================
3377 if (ImGui::BeginPopup("VSLinkContextMenu"))
3378 {
3379 if (ImGui::MenuItem("Delete Connection"))
3380 {
3381 RemoveLink(m_contextLinkID);
3382 m_dirty = true;
3383 SYSTEM_LOG << "[VSEditor] Deleted link #" << m_contextLinkID
3384 << " via context menu\n";
3385 }
3386 ImGui::EndPopup();
3387 }
3388}
3389
3390// ============================================================================
3391// Branch / While node — dedicated Properties panel renderer
3392// ============================================================================
3393
3394void VisualScriptEditorPanel::RenderBranchNodeProperties(VSEditorNode& eNode,
3395 TaskNodeDefinition& def)
3396{
3397 // ── Blue header: node name (matches canvas Section 1 title bar) ──────────
3398 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.0f, 0.4f, 0.8f, 1.0f));
3399 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.0f, 0.5f, 0.9f, 1.0f));
3400 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.0f, 0.3f, 0.7f, 1.0f));
3401 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
3402 ImGui::Selectable(def.NodeName.c_str(), true,
3403 ImGuiSelectableFlags_None, ImVec2(0.f, 28.f));
3404 ImGui::PopStyleColor(4);
3405
3406 ImGui::Separator();
3407 ImGui::Spacing();
3408
3409 // ── Structured Conditions (Phase 24 — NodeConditionsPanel) ───────────────
3410 if (m_conditionsPanel)
3411 {
3412 // Reload the panel when the selected node changes.
3413 if (m_condPanelNodeID != eNode.nodeID)
3414 {
3415 m_condPanelNodeID = eNode.nodeID;
3416 m_conditionsPanel->SetNodeName(def.NodeName);
3417 m_conditionsPanel->SetConditionRefs(def.conditionRefs);
3418 m_conditionsPanel->SetDynamicPins(def.dynamicPins);
3419 m_conditionsPanel->ClearDirty();
3420 }
3421 else
3422 {
3423 // Keep node name in sync with any in-frame name edits.
3424 m_conditionsPanel->SetNodeName(def.NodeName);
3425 }
3426
3427 m_conditionsPanel->Render();
3428
3429 if (m_conditionsPanel->IsDirty())
3430 {
3431 def.conditionRefs = m_conditionsPanel->GetConditionRefs();
3432
3433 // Keep m_template in sync for serialization.
3434 for (size_t ti = 0; ti < m_template.Nodes.size(); ++ti)
3435 {
3436 if (m_template.Nodes[ti].NodeID == m_selectedNodeID)
3437 {
3438 m_template.Nodes[ti].conditionRefs = def.conditionRefs;
3439 break;
3440 }
3441 }
3442 m_conditionsPanel->ClearDirty();
3443 m_dirty = true;
3444 }
3445 }
3446
3447 ImGui::Separator();
3448 ImGui::Spacing();
3449
3450 // ── Breakpoint checkbox (F9) ─────────────────────────────────────────────
3451 bool hasBP = DebugController::Get().HasBreakpoint(0, m_selectedNodeID);
3452 if (ImGui::Checkbox("Breakpoint (F9)##vsbp_branch", &hasBP))
3453 {
3454 DebugController::Get().ToggleBreakpoint(0, m_selectedNodeID,
3455 m_template.Name,
3456 def.NodeName);
3457 }
3458
3459 RenderVerificationPanel();
3460
3461 (void)eNode; // suppress unused-warning when branches have no eNode-specific fields
3462}
3463
3464// ============================================================================
3465// MathOp node — dedicated Properties panel renderer
3466// ============================================================================
3467
3468void VisualScriptEditorPanel::RenderMathOpNodeProperties(VSEditorNode& eNode,
3469 TaskNodeDefinition& def)
3470{
3471 // ── Blue header: node name (matches canvas Section 1 title bar) ──────────
3472 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.0f, 0.4f, 0.8f, 1.0f));
3473 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.0f, 0.5f, 0.9f, 1.0f));
3474 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.0f, 0.3f, 0.7f, 1.0f));
3475 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
3476 ImGui::Selectable(def.NodeName.c_str(), true,
3477 ImGuiSelectableFlags_None, ImVec2(0.f, 28.f));
3478 ImGui::PopStyleColor(4);
3479
3480 ImGui::Separator();
3481 ImGui::Spacing();
3482
3483 // ── Operand Editor (Phase 24 Milestone 2 — MathOpPropertyPanel) ───────────
3484 if (m_mathOpPanel)
3485 {
3486 // Lazy-initialize the panel when node changes
3487 if (!m_mathOpPanel)
3488 {
3489 m_mathOpPanel = std::unique_ptr<MathOpPropertyPanel>(
3490 new MathOpPropertyPanel(m_presetRegistry, *m_pinManager));
3491 }
3492
3493 m_mathOpPanel->SetNodeName(def.NodeName);
3494 m_mathOpPanel->SetMathOpRef(def.mathOpRef);
3495 m_mathOpPanel->SetDynamicPins(def.dynamicPins);
3496
3497 m_mathOpPanel->SetOnOperandChange([this]() {
3498 // Callback when operands change: regenerate dynamic pins
3499 if (m_pinManager && m_selectedNodeID >= 0)
3500 {
3501 for (size_t i = 0; i < m_editorNodes.size(); ++i)
3502 {
3503 if (m_editorNodes[i].nodeID == m_selectedNodeID)
3504 {
3505 m_editorNodes[i].def.mathOpRef = m_mathOpPanel->GetMathOpRef();
3506 break;
3507 }
3508 }
3509 m_dirty = true;
3510 }
3511 });
3512
3513 m_mathOpPanel->Render();
3514
3515 if (m_mathOpPanel->IsDirty())
3516 {
3517 def.mathOpRef = m_mathOpPanel->GetMathOpRef();
3518
3519 // Keep m_template in sync for serialization
3520 for (size_t ti = 0; ti < m_template.Nodes.size(); ++ti)
3521 {
3522 if (m_template.Nodes[ti].NodeID == m_selectedNodeID)
3523 {
3524 m_template.Nodes[ti].mathOpRef = def.mathOpRef;
3525 break;
3526 }
3527 }
3528 m_mathOpPanel->ClearDirty();
3529 m_dirty = true;
3530 }
3531 }
3532
3533 ImGui::Separator();
3534 ImGui::Spacing();
3535
3536 RenderVerificationPanel();
3537
3538 (void)eNode; // suppress unused-warning
3539}
3540
3541void VisualScriptEditorPanel::RenderNodeDataParameters(TaskNodeDefinition& def)
3542{
3543 // Phase 24 — Generic parameter editor for data nodes (GetBBValue, SetBBValue, MathOp)
3544 // Allows storing and serializing additional parameters on data nodes
3545
3546 // Filter out system parameters (those starting with __)
3547 std::vector<std::string> userParams;
3548 for (const auto& paramPair : def.Parameters)
3549 {
3550 const std::string& paramName = paramPair.first;
3551 // Skip system parameters
3552 if (paramName.length() >= 2 && paramName[0] == '_' && paramName[1] == '_')
3553 continue;
3554 userParams.push_back(paramName);
3555 }
3556
3557 ImGui::Separator();
3558 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Node Parameters:");
3559
3560 if (userParams.empty())
3561 {
3562 ImGui::TextDisabled("(no user parameters - add one below)");
3563 }
3564
3565 // Display user parameters
3566 for (const auto& paramName : userParams)
3567 {
3568 auto paramIt = def.Parameters.find(paramName);
3569 if (paramIt == def.Parameters.end())
3570 continue;
3571
3572 ParameterBinding& binding = paramIt->second;
3573
3574 ImGui::PushID(paramName.c_str());
3575
3576 // Display parameter name
3577 ImGui::TextColored(ImVec4(0.8f, 0.95f, 1.0f, 1.0f), "%s", paramName.c_str());
3578
3579 // Build a label showing the binding type
3580 const char* typeLabel = "?";
3581 switch (binding.Type)
3582 {
3583 case ParameterBindingType::Literal: typeLabel = "Literal"; break;
3584 case ParameterBindingType::LocalVariable: typeLabel = "Variable"; break;
3585 case ParameterBindingType::AtomicTaskID: typeLabel = "AtomicTaskID"; break;
3586 case ParameterBindingType::ConditionID: typeLabel = "ConditionID"; break;
3587 case ParameterBindingType::MathOperator: typeLabel = "MathOp"; break;
3588 case ParameterBindingType::ComparisonOp: typeLabel = "CompOp"; break;
3589 case ParameterBindingType::SubGraphPath: typeLabel = "SubGraph"; break;
3590 }
3591 ImGui::SameLine();
3592 ImGui::TextDisabled("(%s)", typeLabel);
3593
3594 // Input field for editing parameter value
3595 if (binding.Type == ParameterBindingType::Literal)
3596 {
3597 // For literal values, show an input field
3598 std::string currentValue;
3599 if (!binding.LiteralValue.IsNone())
3600 {
3601 currentValue = binding.LiteralValue.AsString();
3602 }
3603
3604 char buf[256];
3605 strncpy_s(buf, sizeof(buf), currentValue.c_str(), _TRUNCATE);
3606 ImGui::SetNextItemWidth(-1.0f);
3607 if (ImGui::InputText(("##" + paramName + "_val").c_str(), buf, sizeof(buf)))
3608 {
3609 // Parse and store the value
3610 std::string strVal(buf);
3611 binding.LiteralValue = TaskValue(strVal);
3612
3613 // Keep template in sync
3614 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
3615 {
3616 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
3617 {
3618 m_template.Nodes[i].Parameters[paramName] = binding;
3619 break;
3620 }
3621 }
3622 m_dirty = true;
3623 }
3624 }
3625 else if (binding.Type == ParameterBindingType::LocalVariable)
3626 {
3627 // For local variables, show a dropdown of available variables
3629 bbReg.LoadFromTemplate(m_template);
3630 const std::vector<VarSpec> vars = bbReg.GetAllVariables();
3631
3632 const char* preview = binding.VariableName.empty() ? "(select...)" : binding.VariableName.c_str();
3633 ImGui::SetNextItemWidth(-1.0f);
3634 if (ImGui::BeginCombo(("##" + paramName + "_var").c_str(), preview))
3635 {
3636 for (const auto& var : vars)
3637 {
3638 bool selected = (var.name == binding.VariableName);
3639 if (ImGui::Selectable(var.displayLabel.c_str(), selected))
3640 {
3641 binding.VariableName = var.name;
3642
3643 // Keep template in sync
3644 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
3645 {
3646 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
3647 {
3648 m_template.Nodes[i].Parameters[paramName] = binding;
3649 break;
3650 }
3651 }
3652 m_dirty = true;
3653 }
3654 if (selected)
3655 ImGui::SetItemDefaultFocus();
3656 }
3657 ImGui::EndCombo();
3658 }
3659 }
3660 else
3661 {
3662 // For other types, show a text field for the identifier
3663 char buf[256];
3664 strncpy_s(buf, sizeof(buf), binding.VariableName.c_str(), _TRUNCATE);
3665 ImGui::SetNextItemWidth(-1.0f);
3666 if (ImGui::InputText(("##" + paramName + "_id").c_str(), buf, sizeof(buf)))
3667 {
3668 binding.VariableName = buf;
3669
3670 // Keep template in sync
3671 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
3672 {
3673 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
3674 {
3675 m_template.Nodes[i].Parameters[paramName] = binding;
3676 break;
3677 }
3678 }
3679 m_dirty = true;
3680 }
3681 }
3682
3683 ImGui::PopID();
3684 }
3685
3686 // Add parameter section
3687 ImGui::Separator();
3688 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Add Parameter:");
3689
3690 static char paramNameBuf[256] = "";
3691 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 80.0f);
3692 ImGui::InputText("##new_param_name", paramNameBuf, sizeof(paramNameBuf), ImGuiInputTextFlags_CharsNoBlank);
3693 ImGui::SameLine();
3694 if (ImGui::Button("Add", ImVec2(70.0f, 0.0f)))
3695 {
3696 std::string newParamName(paramNameBuf);
3697 if (!newParamName.empty() && def.Parameters.find(newParamName) == def.Parameters.end())
3698 {
3699 // Create new parameter with default Literal binding
3701 newBinding.Type = ParameterBindingType::Literal;
3702 newBinding.LiteralValue = TaskValue("");
3703
3705
3706 // Keep template in sync
3707 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
3708 {
3709 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
3710 {
3711 m_template.Nodes[i].Parameters[newParamName] = newBinding;
3712 break;
3713 }
3714 }
3715
3716 m_dirty = true;
3717 paramNameBuf[0] = '\0'; // Clear the input field
3718 }
3719 }
3720}
3721
3722void VisualScriptEditorPanel::RenderProperties()
3723{
3724 ImGui::TextDisabled("Properties");
3725
3726 if (m_selectedNodeID < 0)
3727 {
3728 ImGui::TextDisabled("(select a node)");
3729 return;
3730 }
3731
3732 // Find the editor node
3733 VSEditorNode* eNode = nullptr;
3734 for (size_t i = 0; i < m_editorNodes.size(); ++i)
3735 {
3736 if (m_editorNodes[i].nodeID == m_selectedNodeID)
3737 {
3738 eNode = &m_editorNodes[i];
3739 break;
3740 }
3741 }
3742 if (eNode == nullptr)
3743 return;
3744
3745 TaskNodeDefinition& def = eNode->def;
3746
3747 // Reset focus-node tracking when the selected node changes.
3748 // Old-value snapshots do NOT need explicit resetting here — they are
3749 // naturally overwritten by the next IsItemActivated() event.
3750 m_propEditNodeIDOnFocus = m_selectedNodeID;
3751
3752 // ---- NodeName (present for all node types) ----
3753 {
3754 char nameBuf[128];
3755 strncpy_s(nameBuf, sizeof(nameBuf), def.NodeName.c_str(), _TRUNCATE);
3756 if (ImGui::InputText("Name##vsname", nameBuf, sizeof(nameBuf)))
3757 {
3758 def.NodeName = nameBuf;
3759 // Sync live to template for immediate canvas display and serialization
3760 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
3761 {
3762 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
3763 {
3764 m_template.Nodes[i].NodeName = def.NodeName;
3765 break;
3766 }
3767 }
3768 m_dirty = true;
3769 }
3770 if (ImGui::IsItemActivated())
3771 {
3772 m_propEditOldName = def.NodeName;
3773 m_propEditNodeIDOnFocus = m_selectedNodeID;
3774 }
3775 if (ImGui::IsItemDeactivatedAfterEdit() &&
3776 m_propEditNodeIDOnFocus == m_selectedNodeID &&
3777 def.NodeName != m_propEditOldName)
3778 {
3779 m_undoStack.PushCommand(
3780 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
3781 m_selectedNodeID, "NodeName",
3782 PropertyValue::FromString(m_propEditOldName),
3783 PropertyValue::FromString(def.NodeName))),
3784 m_template);
3785 }
3786 }
3787
3788 // ---- Type-specific fields — all buffers are local (non-static) to avoid
3789 // stale data when switching between selected nodes. ----
3790 switch (def.Type)
3791 {
3792 case TaskNodeType::AtomicTask:
3793 {
3794 // --- AtomicTaskID dropdown ---
3795 const std::vector<TaskSpec> tasks = AtomicTaskUIRegistry::Get().GetSortedForUI();
3796 const std::string& currentTask = def.AtomicTaskID;
3797 const char* previewLabel = currentTask.empty() ? "(select task...)" : currentTask.c_str();
3798
3799 if (ImGui::IsItemActivated())
3800 {
3801 m_propEditOldTaskID = def.AtomicTaskID;
3802 m_propEditNodeIDOnFocus = m_selectedNodeID;
3803 }
3804
3805 if (ImGui::BeginCombo("TaskType##vstask", previewLabel))
3806 {
3807 if (m_propEditOldTaskID != def.AtomicTaskID)
3808 {
3809 m_propEditOldTaskID = def.AtomicTaskID;
3810 m_propEditNodeIDOnFocus = m_selectedNodeID;
3811 }
3812 std::string lastCat;
3813 for (size_t ti = 0; ti < tasks.size(); ++ti)
3814 {
3815 const TaskSpec& spec = tasks[ti];
3816 // Show category header separator when category changes
3817 if (spec.category != lastCat)
3818 {
3819 if (!lastCat.empty())
3820 ImGui::Separator();
3821 ImGui::TextDisabled("%s", spec.category.c_str());
3822 lastCat = spec.category;
3823 }
3824 bool selected = (spec.id == currentTask);
3825 std::string label = " " + spec.displayName + "##" + spec.id;
3826 if (ImGui::Selectable(label.c_str(), selected))
3827 {
3828 const std::string oldTaskID = def.AtomicTaskID;
3829 def.AtomicTaskID = spec.id;
3830 // Auto-fill node name with the action's display name
3831 def.NodeName = spec.displayName;
3832 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
3833 {
3834 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
3835 {
3836 m_template.Nodes[i].AtomicTaskID = def.AtomicTaskID;
3837 m_template.Nodes[i].NodeName = def.NodeName;
3838 break;
3839 }
3840 }
3841 if (def.AtomicTaskID != oldTaskID)
3842 {
3843 m_undoStack.PushCommand(
3844 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
3845 m_selectedNodeID, "AtomicTaskID",
3846 PropertyValue::FromString(oldTaskID),
3847 PropertyValue::FromString(def.AtomicTaskID))),
3848 m_template);
3849 }
3850 m_dirty = true;
3851 }
3852 if (selected)
3853 ImGui::SetItemDefaultFocus();
3854 // Tooltip with description
3855 if (ImGui::IsItemHovered() && !spec.description.empty())
3856 {
3857 ImGui::BeginTooltip();
3858 ImGui::TextUnformatted(spec.description.c_str());
3859 ImGui::EndTooltip();
3860 }
3861 }
3862 ImGui::EndCombo();
3863 }
3864 break;
3865 }
3866 case TaskNodeType::Delay:
3867 {
3868 float delay = def.DelaySeconds;
3869 if (ImGui::InputFloat("Delay (s)##vsdelay", &delay, 0.1f, 1.0f))
3870 {
3871 def.DelaySeconds = delay;
3872 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
3873 {
3874 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
3875 {
3876 m_template.Nodes[i].DelaySeconds = def.DelaySeconds;
3877 break;
3878 }
3879 }
3880 m_dirty = true;
3881 }
3882 if (ImGui::IsItemActivated())
3883 {
3884 m_propEditOldDelay = def.DelaySeconds;
3885 m_propEditNodeIDOnFocus = m_selectedNodeID;
3886 }
3887 if (ImGui::IsItemDeactivatedAfterEdit() &&
3888 m_propEditNodeIDOnFocus == m_selectedNodeID &&
3889 def.DelaySeconds != m_propEditOldDelay)
3890 {
3891 m_undoStack.PushCommand(
3892 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
3893 m_selectedNodeID, "DelaySeconds",
3894 PropertyValue::FromFloat(m_propEditOldDelay),
3895 PropertyValue::FromFloat(def.DelaySeconds))),
3896 m_template);
3897 }
3898 break;
3899 }
3900 case TaskNodeType::GetBBValue:
3901 {
3902 // Phase 24.2: Variable node (data pure read node) - use dedicated renderer
3903 // Variables are rendered with m_variablePanel instead of m_getBBPanel
3904 if (m_variablePanel)
3905 {
3906 m_variablePanel->SetNodeName(def.NodeName);
3907 m_variablePanel->SetTemplate(&m_template);
3908 m_variablePanel->SetBBKey(def.BBKey);
3909
3910 m_variablePanel->Render();
3911
3912 if (m_variablePanel->IsDirty())
3913 {
3914 const std::string oldKey = def.BBKey;
3915 def.BBKey = m_variablePanel->GetBBKey();
3916
3917 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
3918 {
3919 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
3920 {
3921 m_template.Nodes[i].BBKey = def.BBKey;
3922 break;
3923 }
3924 }
3925
3926 if (def.BBKey != oldKey)
3927 {
3928 m_undoStack.PushCommand(
3929 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
3930 m_selectedNodeID, "BBKey",
3931 PropertyValue::FromString(oldKey),
3932 PropertyValue::FromString(def.BBKey))),
3933 m_template);
3934 }
3935 m_variablePanel->ClearDirty();
3936 m_dirty = true;
3937 }
3938 }
3939
3940 // Render node parameters (Phase 24 — node data serialization)
3941 RenderNodeDataParameters(def);
3942 break;
3943 }
3944 case TaskNodeType::SetBBValue:
3945 {
3946 // Phase 24 Milestone 3: Delegate to dedicated SetBBValue properties renderer
3947 if (m_setBBPanel)
3948 {
3949 m_setBBPanel->SetNodeName(def.NodeName);
3950 m_setBBPanel->SetTemplate(&m_template);
3951 m_setBBPanel->SetBBKey(def.BBKey);
3952
3953 m_setBBPanel->Render();
3954
3955 if (m_setBBPanel->IsDirty())
3956 {
3957 const std::string oldKey = def.BBKey;
3958 def.BBKey = m_setBBPanel->GetBBKey();
3959
3960 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
3961 {
3962 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
3963 {
3964 m_template.Nodes[i].BBKey = def.BBKey;
3965 break;
3966 }
3967 }
3968
3969 if (def.BBKey != oldKey)
3970 {
3971 m_undoStack.PushCommand(
3972 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
3973 m_selectedNodeID, "BBKey",
3974 PropertyValue::FromString(oldKey),
3975 PropertyValue::FromString(def.BBKey))),
3976 m_template);
3977 }
3978 m_setBBPanel->ClearDirty();
3979 m_dirty = true;
3980 }
3981 }
3982
3983 // Render node parameters (Phase 24 — node data serialization)
3984 RenderNodeDataParameters(def);
3985 break;
3986 }
3987 case TaskNodeType::Branch:
3988 case TaskNodeType::While:
3989 {
3990 // Delegate to the dedicated Phase 24-Rendering branch properties renderer.
3991 // This shows: blue header -> NodeConditionsPanel -> Breakpoint checkbox.
3992 // The return prevents any legacy condition UI from also rendering.
3993 RenderBranchNodeProperties(*eNode, def);
3994 return;
3995 }
3996 case TaskNodeType::SubGraph:
3997 {
3998 char sgPathBuf[256];
3999 strncpy_s(sgPathBuf, sizeof(sgPathBuf), def.SubGraphPath.c_str(), _TRUNCATE);
4000 if (ImGui::InputText("SubGraph Path##vssg", sgPathBuf, sizeof(sgPathBuf)))
4001 {
4002 def.SubGraphPath = sgPathBuf;
4003 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
4004 {
4005 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
4006 {
4007 m_template.Nodes[i].SubGraphPath = def.SubGraphPath;
4008 break;
4009 }
4010 }
4011 m_dirty = true;
4012 }
4013 if (ImGui::IsItemActivated())
4014 {
4015 m_propEditOldSubGraphPath = def.SubGraphPath;
4016 m_propEditNodeIDOnFocus = m_selectedNodeID;
4017 }
4018 if (ImGui::IsItemDeactivatedAfterEdit() &&
4019 m_propEditNodeIDOnFocus == m_selectedNodeID &&
4020 def.SubGraphPath != m_propEditOldSubGraphPath)
4021 {
4022 m_undoStack.PushCommand(
4023 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
4024 m_selectedNodeID, "SubGraphPath",
4025 PropertyValue::FromString(m_propEditOldSubGraphPath),
4026 PropertyValue::FromString(def.SubGraphPath))),
4027 m_template);
4028 }
4029 break;
4030 }
4031 case TaskNodeType::MathOp:
4032 {
4033 // Phase 24 Milestone 2: Delegate to the dedicated MathOp properties renderer.
4034 // This shows: blue header -> MathOpPropertyPanel -> operand editors.
4035 RenderMathOpNodeProperties(*eNode, def);
4036
4037 // Render node parameters (Phase 24 — node data serialization)
4038 RenderNodeDataParameters(def);
4039 return;
4040 }
4041 case TaskNodeType::Switch:
4042 {
4043 // Sync m_propEditSwitchCases with the node's switchCases when node changes
4044 if (m_propEditNodeIDOnFocus != m_selectedNodeID)
4045 m_propEditSwitchCases = def.switchCases;
4046
4047 // Find the corresponding template node once for all edits below
4048 TaskNodeDefinition* tmplNode = nullptr;
4049 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
4050 {
4051 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
4052 {
4053 tmplNode = &m_template.Nodes[i];
4054 break;
4055 }
4056 }
4057
4058 // ---- Switch Variable ----
4059 {
4060 // Dropdown populated from Int-typed blackboard variables only
4061 // (Switch node evaluates an Int variable against integer case values)
4063 bbReg.LoadFromTemplate(m_template);
4064 const std::vector<VarSpec> vars = bbReg.GetVariablesByType(VariableType::Int);
4065 const std::string& curVar = def.switchVariable;
4066 const char* previewVar = curVar.empty() ? "(select variable...)" : curVar.c_str();
4067
4068 if (ImGui::BeginCombo("Switch Var##vsswitchvar", previewVar))
4069 {
4070 for (size_t vi = 0; vi < vars.size(); ++vi)
4071 {
4072 const VarSpec& v = vars[vi];
4073 bool selected = (v.name == curVar);
4074 if (ImGui::Selectable(v.displayLabel.c_str(), selected))
4075 {
4076 def.switchVariable = v.name;
4077 if (tmplNode)
4078 tmplNode->switchVariable = def.switchVariable;
4079 m_dirty = true;
4080 }
4081 if (selected)
4082 ImGui::SetItemDefaultFocus();
4083 }
4084 ImGui::EndCombo();
4085 }
4086 }
4087
4088 // ---- Case Labels ----
4089 if (!def.switchCases.empty())
4090 {
4091 ImGui::Separator();
4092 ImGui::TextDisabled("Case Labels");
4093 }
4094
4095 // Ensure our edit buffer stays in sync
4096 if (m_propEditSwitchCases.size() != def.switchCases.size())
4097 m_propEditSwitchCases = def.switchCases;
4098
4099 for (size_t ci = 0; ci < def.switchCases.size(); ++ci)
4100 {
4101 // Show pin name as read-only, allow editing the custom label
4102 const std::string pinLabel = def.switchCases[ci].pinName
4103 + " (val=" + def.switchCases[ci].value + ")";
4104 ImGui::TextUnformatted(pinLabel.c_str());
4105 ImGui::SameLine();
4106
4107 char labelBuf[64];
4108 const std::string& curLabel = m_propEditSwitchCases[ci].customLabel;
4109 strncpy_s(labelBuf, sizeof(labelBuf), curLabel.c_str(), _TRUNCATE);
4110
4111 // Unique widget ID per case index
4112 std::string widgetID = "##vscaselabel" + std::to_string(ci);
4113 if (ImGui::InputText(widgetID.c_str(), labelBuf, sizeof(labelBuf)))
4114 {
4115 m_propEditSwitchCases[ci].customLabel = labelBuf;
4116 // Apply to the live def and template immediately
4117 def.switchCases[ci].customLabel = labelBuf;
4118 if (tmplNode && ci < tmplNode->switchCases.size())
4119 tmplNode->switchCases[ci].customLabel = labelBuf;
4120 m_dirty = true;
4121 }
4122 }
4123 break;
4124 }
4125 default:
4126 break;
4127 }
4128
4129 // Breakpoint toggle button
4130 bool hasBP = DebugController::Get().HasBreakpoint(0, m_selectedNodeID);
4131 if (ImGui::Checkbox("Breakpoint (F9)##vsbp", &hasBP))
4132 {
4133 DebugController::Get().ToggleBreakpoint(0, m_selectedNodeID,
4134 m_template.Name,
4135 def.NodeName);
4136 }
4137
4138 RenderVerificationPanel();
4139}
4140
4141void VisualScriptEditorPanel::RenderBlackboard()
4142{
4143 ImGui::TextDisabled("Local Blackboard");
4144 ImGui::Separator();
4145
4146 // BUG-001 Hotfix: warn user if invalid entries exist (key empty or type None)
4147 // to prevent save crash caused by unhandled None type during serialization.
4148 bool hasInvalid = false;
4149 for (size_t i = 0; i < m_template.Blackboard.size(); ++i)
4150 {
4151 const BlackboardEntry& entry = m_template.Blackboard[static_cast<size_t>(i)];
4152 if (entry.Key.empty() || entry.Type == VariableType::None)
4153 {
4154 hasInvalid = true;
4155 break;
4156 }
4157 }
4158 if (hasInvalid)
4159 {
4160 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
4161 ImGui::TextUnformatted("[!] Invalid entries will be skipped on save");
4162 ImGui::PopStyleColor();
4163 }
4164
4165 // Add entry button — BUG-001 Hotfix: init with safe defaults (non-empty key, Int type)
4166 if (ImGui::Button("+##vsbbAdd"))
4167 {
4169 entry.Key = "NewVariable";
4170 entry.Type = VariableType::Int;
4171 entry.Default = GetDefaultValueForType(VariableType::Int); // UX Fix #1
4172 entry.IsGlobal = false;
4173 m_template.Blackboard.push_back(entry);
4174 m_dirty = true;
4175 }
4176 ImGui::SameLine();
4177 ImGui::TextDisabled("Add key");
4178
4179 // List existing entries
4180 for (int idx = static_cast<int>(m_template.Blackboard.size()) - 1; idx >= 0; --idx)
4181 {
4182 BlackboardEntry& entry = m_template.Blackboard[static_cast<size_t>(idx)];
4183
4184 ImGui::PushID(idx);
4185
4186 // Use a local (non-static) buffer per iteration to avoid sharing across entries
4187 char keyBuf[64];
4188 strncpy_s(keyBuf, sizeof(keyBuf), entry.Key.c_str(), _TRUNCATE);
4189 ImGui::SetNextItemWidth(120.0f);
4190 if (ImGui::InputText("##bbkey", keyBuf, sizeof(keyBuf)))
4191 {
4192 entry.Key = keyBuf;
4193 m_dirty = true;
4194 }
4195 ImGui::SameLine();
4196
4197 // Fix #2: Type selector — "None" is excluded to prevent invalid entries.
4198 // Enum layout: None=0, Bool=1, Int=2, Float=3, Vector=4, EntityID=5, String=6.
4199 // typeIdx maps to enum value minus 1 (offset by 1 to skip None).
4200 const char* typeLabels[] = {"Bool","Int","Float","Vector","EntityID","String"};
4201 int typeIdx = static_cast<int>(entry.Type) - 1; // offset: Bool->0, Int->1, ...
4203 {
4204 typeIdx = 1; // default to "Int" (array index 1; maps to VariableType::Int via typeIdx+1)
4205 entry.Type = VariableType::Int;
4206 }
4207 ImGui::SetNextItemWidth(80.0f);
4208 if (ImGui::Combo("##bbtype", &typeIdx, typeLabels, 6))
4209 {
4210 VariableType newType = static_cast<VariableType>(typeIdx + 1); // +1 to skip None
4211 entry.Type = newType;
4212 entry.Default = GetDefaultValueForType(newType); // UX Fix #1: sync default
4213 m_dirty = true;
4214 }
4215 ImGui::SameLine();
4216
4217 // IsGlobal checkbox
4218 ImGui::Checkbox("G##bbglob", &entry.IsGlobal);
4219 ImGui::SameLine();
4220
4221 // Remove button
4222 if (ImGui::SmallButton("x##bbdel"))
4223 {
4224 m_template.Blackboard.erase(m_template.Blackboard.begin() + idx);
4225 m_pendingBlackboardEdits.erase(idx);
4226 m_dirty = true;
4227 ImGui::PopID();
4228 continue;
4229 }
4230
4231 // UX Fix #2: Default value editor (type-specific input field)
4232 ImGui::TextDisabled("Default:");
4233 ImGui::SameLine();
4234 switch (entry.Type)
4235 {
4236 case VariableType::Bool:
4237 {
4238 bool bVal = entry.Default.IsNone() ? false : entry.Default.AsBool();
4239 if (ImGui::Checkbox("##bbval", &bVal))
4240 {
4241 entry.Default = TaskValue(bVal);
4242 m_dirty = true;
4243 }
4244 break;
4245 }
4246 case VariableType::Int:
4247 {
4248 int iVal = entry.Default.IsNone() ? 0 : entry.Default.AsInt();
4249 ImGui::SetNextItemWidth(70.0f);
4250 if (ImGui::InputInt("##bbval", &iVal))
4251 {
4252 entry.Default = TaskValue(iVal);
4253 m_dirty = true;
4254 }
4255 break;
4256 }
4257 case VariableType::Float:
4258 {
4259 float fVal = entry.Default.IsNone() ? 0.0f : entry.Default.AsFloat();
4260 ImGui::SetNextItemWidth(70.0f);
4261 if (ImGui::InputFloat("##bbval", &fVal, 0.0f, 0.0f, "%.3f"))
4262 {
4263 entry.Default = TaskValue(fVal);
4264 m_dirty = true;
4265 }
4266 break;
4267 }
4268 case VariableType::String:
4269 {
4270 std::string sVal = entry.Default.IsNone() ? "" : entry.Default.AsString();
4271 char sBuf[128];
4272 strncpy_s(sBuf, sizeof(sBuf), sVal.c_str(), _TRUNCATE);
4273 ImGui::SetNextItemWidth(100.0f);
4274 if (ImGui::InputText("##bbval", sBuf, sizeof(sBuf)))
4275 {
4276 entry.Default = TaskValue(std::string(sBuf));
4277 m_dirty = true;
4278 }
4279 break;
4280 }
4281 case VariableType::Vector:
4282 {
4283 // UX Enhancement #1: Vector is auto-sourced from entity position at runtime.
4284 // Display as read-only to prevent user from entering a value that will be
4285 // overwritten anyway.
4286 ImGui::BeginDisabled(true);
4287 float vecVal[3] = { 0.0f, 0.0f, 0.0f };
4288 ImGui::SetNextItemWidth(140.0f);
4289 ImGui::DragFloat3("##bbval", vecVal, 0.1f);
4290 ImGui::EndDisabled();
4291 ImGui::SameLine();
4292 ImGui::TextDisabled("(auto from entity position)");
4293 break;
4294 }
4295 case VariableType::EntityID:
4296 {
4297 // UX Enhancement #2: EntityID is assigned at runtime; read-only display.
4298 ImGui::BeginDisabled(true);
4299 int entityId = 0;
4300 ImGui::SetNextItemWidth(70.0f);
4301 ImGui::InputInt("##bbval", &entityId);
4302 ImGui::EndDisabled();
4303 ImGui::SameLine();
4304 ImGui::TextDisabled("(assigned at runtime)");
4305 break;
4306 }
4307 default:
4308 ImGui::TextDisabled("(n/a)");
4309 break;
4310 }
4311
4312 ImGui::PopID();
4313 }
4314}
4315
4316void VisualScriptEditorPanel::RenderValidationOverlay()
4317{
4318 m_validationWarnings.clear();
4319 m_validationErrors.clear();
4320
4321 // Check: every non-EntryPoint node should have at least one exec-in connection
4322 for (size_t i = 0; i < m_editorNodes.size(); ++i)
4323 {
4324 const VSEditorNode& eNode = m_editorNodes[i];
4325 if (eNode.def.Type == TaskNodeType::EntryPoint)
4326 continue;
4327
4328 bool hasExecIn = false;
4329 for (size_t c = 0; c < m_template.ExecConnections.size(); ++c)
4330 {
4331 if (m_template.ExecConnections[c].TargetNodeID == eNode.nodeID)
4332 {
4333 hasExecIn = true;
4334 break;
4335 }
4336 }
4337 if (!hasExecIn)
4338 {
4339 m_validationErrors.push_back(
4340 "Node " + std::to_string(eNode.nodeID) + " (" +
4341 eNode.def.NodeName + "): no exec-in connection");
4342 }
4343
4344 // SubGraph path validation
4345 if (eNode.def.Type == TaskNodeType::SubGraph &&
4346 eNode.def.SubGraphPath.empty())
4347 {
4348 m_validationWarnings.push_back(
4349 "Node " + std::to_string(eNode.nodeID) +
4350 " (SubGraph): SubGraphPath is empty");
4351 }
4352 }
4353}
4354
4355// ============================================================================
4356// Phase 21-B — Graph Verification
4357// ============================================================================
4358
4359void VisualScriptEditorPanel::RunVerification()
4360{
4361 SYSTEM_LOG << "[VisualScriptEditorPanel] RunVerification() called for graph '"
4362 << m_template.Name << "'\n";
4363 m_verificationResult = VSGraphVerifier::Verify(m_template);
4364 m_verificationDone = true;
4365
4366 // Phase 24.3 — Populate verification logs for display in the output panel
4367 m_verificationLogs.clear();
4368 for (size_t i = 0; i < m_verificationResult.issues.size(); ++i)
4369 {
4370 const VSVerificationIssue& issue = m_verificationResult.issues[i];
4371 std::string logEntry;
4372
4373 // Format: "[SEVERITY] message (Node: nodeID)"
4374 if (issue.severity == VSVerificationSeverity::Error)
4375 logEntry = "[ERROR] ";
4376 else if (issue.severity == VSVerificationSeverity::Warning)
4377 logEntry = "[WARN] ";
4378 else
4379 logEntry = "[INFO] ";
4380
4382 if (issue.nodeID >= 0)
4383 logEntry += " (Node: " + std::to_string(issue.nodeID) + ")";
4384
4385 m_verificationLogs.push_back(logEntry);
4386 }
4387
4388 SYSTEM_LOG << "[VisualScriptEditorPanel] RunVerification() done: "
4389 << m_verificationResult.issues.size() << " issue(s), "
4390 << "errors=" << (m_verificationResult.HasErrors() ? "yes" : "no") << ", "
4391 << "warnings=" << (m_verificationResult.HasWarnings() ? "yes" : "no") << "\n";
4392}
4393
4394void VisualScriptEditorPanel::RenderVerificationPanel()
4395{
4396 ImGui::Separator();
4397 ImGui::TextDisabled("Graph Verification");
4398
4399 if (!m_verificationDone)
4400 {
4401 ImGui::TextDisabled("Click 'Verify' in toolbar to run verification.");
4402 return;
4403 }
4404
4405 // Global status line
4406 if (m_verificationResult.HasErrors())
4407 {
4408 int errorCount = 0;
4409 for (size_t i = 0; i < m_verificationResult.issues.size(); ++i)
4410 {
4411 if (m_verificationResult.issues[i].severity == VSVerificationSeverity::Error)
4412 ++errorCount;
4413 }
4414 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
4415 "Errors found: %d", errorCount);
4416 }
4417 else if (m_verificationResult.HasWarnings())
4418 {
4419 ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "OK — warnings present");
4420 }
4421 else
4422 {
4423 ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "OK — no issues");
4424 }
4425
4426 if (m_verificationResult.issues.empty())
4427 return;
4428
4429 // List issues grouped: Errors first, then Warnings, then Info
4431 VSVerificationSeverity::Error,
4432 VSVerificationSeverity::Warning,
4433 VSVerificationSeverity::Info
4434 };
4435
4436 for (int s = 0; s < 3; ++s)
4437 {
4439 for (size_t i = 0; i < m_verificationResult.issues.size(); ++i)
4440 {
4441 const VSVerificationIssue& issue = m_verificationResult.issues[i];
4442 if (issue.severity != sev)
4443 continue;
4444
4445 ImGui::PushID(static_cast<int>(i));
4446
4447 if (sev == VSVerificationSeverity::Error)
4448 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "[E]");
4449 else if (sev == VSVerificationSeverity::Warning)
4450 ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "[W]");
4451 else
4452 ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "[I]");
4453
4454 ImGui::SameLine();
4455 ImGui::Text("%s: %s", issue.ruleID.c_str(), issue.message.c_str());
4456
4457 if (issue.nodeID >= 0)
4458 {
4459 ImGui::SameLine();
4460 std::string btnLabel = "Go##go" + std::to_string(i);
4461 if (ImGui::SmallButton(btnLabel.c_str()))
4462 {
4463 m_focusNodeID = issue.nodeID;
4464 m_selectedNodeID = issue.nodeID;
4465 }
4466 }
4467
4468 ImGui::PopID();
4469 }
4470 }
4471}
4472
4473// ============================================================================
4474// Phase 24.3 — Verification Logs Panel
4475// ============================================================================
4476
4477void VisualScriptEditorPanel::RenderVerificationLogsPanel()
4478{
4479 // Note: The header "Verification Output" is rendered by the container (BlueprintEditorGUI),
4480 // so we only render the content here (status + logs).
4481
4482 if (!m_verificationDone)
4483 {
4484 ImGui::TextDisabled("(Click 'Verify' button to run verification)");
4485 return;
4486 }
4487
4488 // Display verification result summary
4489 ImGui::Spacing();
4490
4491 // Debug: Show issue count
4492 ImGui::TextDisabled("Issues found: %zu", m_verificationResult.issues.size());
4493
4494 // Status line with color coding
4495 if (m_verificationResult.HasErrors())
4496 {
4497 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
4498 "[ERROR] Graph has %d error(s)",
4499 (int)m_verificationResult.issues.size());
4500 }
4501 else if (m_verificationResult.HasWarnings())
4502 {
4503 ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f),
4504 "[WARNING] Graph is valid but has warnings");
4505 }
4506 else
4507 {
4508 ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f),
4509 "[OK] Graph is valid - no issues found");
4510 }
4511
4512 ImGui::Separator();
4513
4514 // Display issues grouped by severity
4515 ImGui::BeginChild("VerificationLogsChild", ImVec2(0, 0), true);
4516
4518 VSVerificationSeverity::Error,
4519 VSVerificationSeverity::Warning,
4520 VSVerificationSeverity::Info
4521 };
4522
4523 const char* sevLabels[3] = { "[ERROR]", "[WARN]", "[INFO]" };
4524 ImVec4 sevColors[3] = {
4525 ImVec4(1.0f, 0.3f, 0.3f, 1.0f), // Error: red
4526 ImVec4(1.0f, 0.85f, 0.0f, 1.0f), // Warning: yellow
4527 ImVec4(0.5f, 0.8f, 1.0f, 1.0f) // Info: light blue
4528 };
4529
4530 for (int s = 0; s < 3; ++s)
4531 {
4533 bool hasThisSeverity = false;
4534
4535 // Count issues of this severity
4536 for (size_t i = 0; i < m_verificationResult.issues.size(); ++i)
4537 {
4538 if (m_verificationResult.issues[i].severity == sev)
4539 {
4540 hasThisSeverity = true;
4541 break;
4542 }
4543 }
4544
4545 if (!hasThisSeverity)
4546 continue;
4547
4548 // Display header for this severity level
4549 ImGui::TextColored(sevColors[s], "%s", sevLabels[s]);
4550
4551 // Display all issues with this severity
4552 for (size_t i = 0; i < m_verificationResult.issues.size(); ++i)
4553 {
4554 const VSVerificationIssue& issue = m_verificationResult.issues[i];
4555 if (issue.severity != sev)
4556 continue;
4557
4558 // Format message: "[SEVERITY] message (NodeID: xxx)"
4559 std::string message = issue.message;
4560 if (issue.nodeID >= 0)
4561 {
4562 message += " (Node: " + std::to_string(issue.nodeID) + ")";
4563 }
4564
4565 ImGui::BulletText("%s", message.c_str());
4566 }
4567
4568 ImGui::Spacing();
4569 }
4570
4571 ImGui::EndChild();
4572}
4573
4574// ============================================================================
4575// Phase 23-B.4 — Condition Editor UI helpers
4576// ============================================================================
4577
4578void VisualScriptEditorPanel::RenderConditionEditor(
4580 int conditionIndex,
4581 const std::vector<BlackboardEntry>& allVars,
4582 const std::vector<std::string>& availablePins)
4583{
4584 ImGui::PushID(conditionIndex);
4585 ImGui::Separator();
4586 ImGui::Text("Condition #%d", conditionIndex + 1);
4587
4588 // -- LEFT SIDE --
4589 ImGui::Text("Left:");
4590 ImGui::SameLine();
4591
4592 const bool isLeftPin = (condition.leftMode == "Pin");
4593 const bool isLeftVar = (condition.leftMode == "Variable");
4594 const bool isLeftConst = (condition.leftMode == "Const");
4595
4596 if (ImGui::Button(isLeftPin ? "[PIN]" : "Pin", ImVec2(55, 0)))
4597 {
4598 condition.leftMode = "Pin";
4599 condition.leftPin = "";
4600 m_dirty = true;
4601 }
4602 ImGui::SameLine();
4603 if (ImGui::Button(isLeftVar ? "[VAR]" : "Var", ImVec2(55, 0)))
4604 {
4605 condition.leftMode = "Variable";
4606 condition.leftVariable = "";
4607 m_dirty = true;
4608 }
4609 ImGui::SameLine();
4610 if (ImGui::Button(isLeftConst ? "[CST]" : "Const", ImVec2(55, 0)))
4611 {
4612 condition.leftMode = "Const";
4613 m_dirty = true;
4614 }
4615
4616 ImGui::Indent();
4617 if (condition.leftMode == "Pin")
4618 RenderPinSelector(condition.leftPin, availablePins, "##leftpin");
4619 else if (condition.leftMode == "Variable")
4620 RenderVariableSelector(condition.leftVariable, allVars,
4621 condition.compareType, "##leftvar");
4622 else
4623 RenderConstValueInput(condition.leftConstValue,
4624 condition.compareType, "##leftconst");
4625 ImGui::Unindent();
4626
4627 // -- OPERATOR --
4628 ImGui::Text("Op:");
4629 ImGui::SameLine();
4630 const char* operators[] = { "==", "!=", "<", ">", "<=", ">=" };
4631 int opIdx = 0;
4632 for (int i = 0; i < 6; ++i)
4633 {
4634 if (condition.operatorStr == operators[i])
4635 {
4636 opIdx = i;
4637 break;
4638 }
4639 }
4640 ImGui::SetNextItemWidth(70.0f);
4641 if (ImGui::Combo("##op", &opIdx, operators, 6))
4642 {
4643 condition.operatorStr = operators[opIdx];
4644 m_dirty = true;
4645 }
4646
4647 // -- RIGHT SIDE --
4648 ImGui::Text("Right:");
4649 ImGui::SameLine();
4650
4651 const bool isRightPin = (condition.rightMode == "Pin");
4652 const bool isRightVar = (condition.rightMode == "Variable");
4653 const bool isRightConst = (condition.rightMode == "Const");
4654
4655 if (ImGui::Button(isRightPin ? "[PIN]##r" : "Pin##r", ImVec2(55, 0)))
4656 {
4657 condition.rightMode = "Pin";
4658 condition.rightPin = "";
4659 m_dirty = true;
4660 }
4661 ImGui::SameLine();
4662 if (ImGui::Button(isRightVar ? "[VAR]##r" : "Var##r", ImVec2(55, 0)))
4663 {
4664 condition.rightMode = "Variable";
4665 condition.rightVariable = "";
4666 m_dirty = true;
4667 }
4668 ImGui::SameLine();
4669 if (ImGui::Button(isRightConst ? "[CST]##r" : "Const##r", ImVec2(55, 0)))
4670 {
4671 condition.rightMode = "Const";
4672 m_dirty = true;
4673 }
4674
4675 ImGui::Indent();
4676 if (condition.rightMode == "Pin")
4677 RenderPinSelector(condition.rightPin, availablePins, "##rightpin");
4678 else if (condition.rightMode == "Variable")
4679 RenderVariableSelector(condition.rightVariable, allVars,
4680 condition.compareType, "##rightvar");
4681 else
4682 RenderConstValueInput(condition.rightConstValue,
4683 condition.compareType, "##rightconst");
4684 ImGui::Unindent();
4685
4686 // -- TYPE HINT --
4687 ImGui::Text("Type:");
4688 ImGui::SameLine();
4689 const char* types[] = { "None", "Bool", "Int", "Float", "String", "Vector" };
4690 const VariableType typeValues[] = {
4691 VariableType::None, VariableType::Bool, VariableType::Int,
4692 VariableType::Float, VariableType::String, VariableType::Vector
4693 };
4694 int typeIdx = 0;
4695 for (int i = 0; i < 6; ++i)
4696 {
4697 if (condition.compareType == typeValues[i])
4698 {
4699 typeIdx = i;
4700 break;
4701 }
4702 }
4703 ImGui::SetNextItemWidth(80.0f);
4704 if (ImGui::Combo("##cmptype", &typeIdx, types, 6))
4705 {
4706 condition.compareType = typeValues[typeIdx];
4707 m_dirty = true;
4708 }
4709
4710 // -- PREVIEW --
4711 const std::string preview = BuildConditionPreview(condition);
4712 ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f),
4713 "Preview: %s", preview.c_str());
4714
4715 ImGui::PopID();
4716}
4717
4718// ----------------------------------------------------------------------------
4719
4720void VisualScriptEditorPanel::RenderVariableSelector(
4721 std::string& selectedVar,
4722 const std::vector<BlackboardEntry>& allVars,
4723 VariableType expectedType,
4724 const char* label)
4725{
4726 // Filter by type (if a type is specified)
4727 std::vector<std::string> names;
4728 for (size_t i = 0; i < allVars.size(); ++i)
4729 {
4730 if (expectedType == VariableType::None || allVars[i].Type == expectedType)
4731 {
4732 if (!allVars[i].Key.empty())
4733 names.push_back(allVars[i].Key);
4734 }
4735 }
4736
4737 if (names.empty())
4738 {
4739 ImGui::TextDisabled("(no variables)");
4740 return;
4741 }
4742
4743 // BUG-029 Fix: auto-initialise to the first available variable when the
4744 // selection is empty (e.g. right after switching to Variable mode).
4745 // Without this the combo visually shows the first item but selectedVar
4746 // remains "" so BuildConditionPreview displays "[Var: ?]".
4747 if (selectedVar.empty())
4748 {
4749 selectedVar = names[0];
4750 m_dirty = true;
4751 }
4752
4753 int selected = 0;
4754 for (int i = 0; i < static_cast<int>(names.size()); ++i)
4755 {
4756 if (names[static_cast<size_t>(i)] == selectedVar)
4757 {
4758 selected = i;
4759 break;
4760 }
4761 }
4762
4763 std::vector<const char*> cstrs;
4764 cstrs.reserve(names.size());
4765 for (size_t i = 0; i < names.size(); ++i)
4766 cstrs.push_back(names[i].c_str());
4767
4768 ImGui::SetNextItemWidth(120.0f);
4769 if (ImGui::Combo(label, &selected, cstrs.data(), static_cast<int>(cstrs.size())))
4770 {
4771 selectedVar = names[static_cast<size_t>(selected)];
4772 m_dirty = true;
4773 }
4774}
4775
4776// ----------------------------------------------------------------------------
4777
4778void VisualScriptEditorPanel::RenderConstValueInput(
4779 TaskValue& value,
4781 const char* label)
4782{
4783 // BUG-029 Fix: auto-initialise to a typed default when value is None and
4784 // a type is known. Without this the preview always shows "[Const: ?]"
4785 // until the user explicitly edits the field, because BuildConditionPreview
4786 // only formats the value when !IsNone().
4787 if (value.IsNone() && varType != VariableType::None)
4788 {
4789 switch (varType)
4790 {
4791 case VariableType::Bool: value = TaskValue(false); break;
4792 case VariableType::Int: value = TaskValue(0); break;
4793 case VariableType::Float: value = TaskValue(0.0f); break;
4794 case VariableType::String: value = TaskValue(std::string("")); break;
4795 case VariableType::Vector: value = TaskValue(::Vector{0.f, 0.f, 0.f}); break;
4796 default: break;
4797 }
4798 if (!value.IsNone())
4799 m_dirty = true;
4800 }
4801
4802 switch (varType)
4803 {
4804 case VariableType::Bool:
4805 {
4806 bool bVal = value.IsNone() ? false : value.AsBool();
4807 if (ImGui::Checkbox(label, &bVal))
4808 {
4809 value = TaskValue(bVal);
4810 m_dirty = true;
4811 }
4812 break;
4813 }
4814 case VariableType::Int:
4815 {
4816 int iVal = value.IsNone() ? 0 : value.AsInt();
4817 ImGui::SetNextItemWidth(80.0f);
4818 if (ImGui::InputInt(label, &iVal))
4819 {
4820 value = TaskValue(iVal);
4821 m_dirty = true;
4822 }
4823 break;
4824 }
4825 case VariableType::Float:
4826 {
4827 float fVal = value.IsNone() ? 0.0f : value.AsFloat();
4828 ImGui::SetNextItemWidth(80.0f);
4829 if (ImGui::InputFloat(label, &fVal, 0.0f, 0.0f, "%.3f"))
4830 {
4831 value = TaskValue(fVal);
4832 m_dirty = true;
4833 }
4834 break;
4835 }
4836 case VariableType::String:
4837 {
4838 const std::string sVal = value.IsNone() ? "" : value.AsString();
4839 char sBuf[256];
4840 strncpy_s(sBuf, sizeof(sBuf), sVal.c_str(), _TRUNCATE);
4841 ImGui::SetNextItemWidth(120.0f);
4842 if (ImGui::InputText(label, sBuf, sizeof(sBuf)))
4843 {
4844 value = TaskValue(std::string(sBuf));
4845 m_dirty = true;
4846 }
4847 break;
4848 }
4849 case VariableType::Vector:
4850 {
4851 ::Vector vVal = value.IsNone() ? ::Vector{0.f, 0.f, 0.f} : value.AsVector();
4852 float v[3] = { vVal.x, vVal.y, vVal.z };
4853 ImGui::SetNextItemWidth(160.0f);
4854 if (ImGui::InputFloat3(label, v))
4855 {
4856 value = TaskValue(::Vector{ v[0], v[1], v[2] });
4857 m_dirty = true;
4858 }
4859 break;
4860 }
4861 default:
4862 {
4863 // No type set yet — show a hint
4864 ImGui::TextDisabled("(set Type first)");
4865 break;
4866 }
4867 }
4868}
4869
4870// ----------------------------------------------------------------------------
4871
4872void VisualScriptEditorPanel::RenderPinSelector(
4873 std::string& selectedPin,
4874 const std::vector<std::string>& availablePins,
4875 const char* label)
4876{
4877 if (availablePins.empty())
4878 {
4879 ImGui::TextDisabled("(no data-output pins in graph)");
4880 return;
4881 }
4882
4883 ImGui::SetNextItemWidth(160.0f);
4884 if (ImGui::BeginCombo(label, selectedPin.empty() ? "(select pin)" : selectedPin.c_str()))
4885 {
4886 for (size_t i = 0; i < availablePins.size(); ++i)
4887 {
4888 const bool isSelected = (selectedPin == availablePins[i]);
4889 if (ImGui::Selectable(availablePins[i].c_str(), isSelected))
4891 if (isSelected)
4892 ImGui::SetItemDefaultFocus();
4893 }
4894 ImGui::EndCombo();
4895 }
4896}
4897
4898// ----------------------------------------------------------------------------
4899
4900/*static*/
4901std::string VisualScriptEditorPanel::BuildConditionPreview(const Condition& cond)
4902{
4903 auto descSide = [](const std::string& mode,
4904 const std::string& pin,
4905 const std::string& var,
4906 const TaskValue& constValue) -> std::string
4907 {
4908 if (mode == "Pin")
4909 return "[Pin: " + (pin.empty() ? "?" : pin) + "]";
4910 if (mode == "Variable")
4911 return "[Var: " + (var.empty() ? "?" : var) + "]";
4912
4913 // Const — try to format value
4914 if (!constValue.IsNone())
4915 {
4916 std::ostringstream oss;
4917 switch (constValue.GetType())
4918 {
4919 case VariableType::Bool: oss << (constValue.AsBool() ? "true" : "false"); break;
4920 case VariableType::Int: oss << constValue.AsInt(); break;
4921 case VariableType::Float: oss << constValue.AsFloat(); break;
4922 case VariableType::String: oss << '"' << constValue.AsString() << '"'; break;
4923 case VariableType::Vector:
4924 {
4925 const ::Vector v = constValue.AsVector();
4926 oss << "(" << v.x << "," << v.y << "," << v.z << ")";
4927 break;
4928 }
4929 default: oss << "?"; break;
4930 }
4931 return "[Const: " + oss.str() + "]";
4932 }
4933 return "[Const: ?]";
4934 };
4935
4936 const std::string left = descSide(cond.leftMode, cond.leftPin, cond.leftVariable, cond.leftConstValue);
4937 const std::string right = descSide(cond.rightMode, cond.rightPin, cond.rightVariable, cond.rightConstValue);
4938 const std::string op = cond.operatorStr.empty() ? "?" : cond.operatorStr;
4939
4940 return left + " " + op + " " + right;
4941}
4942
4943// ============================================================================
4944// PHASE 24 Panel Integration — Part A: Node Properties
4945// ============================================================================
4946
4947void VisualScriptEditorPanel::RenderNodePropertiesPanel()
4948{
4949 ImGui::TextDisabled("Node Properties");
4950
4951 if (m_selectedNodeID < 0)
4952 {
4953 ImGui::TextDisabled("(select a node)");
4954 return;
4955 }
4956
4957 // Find the editor node
4958 VSEditorNode* eNode = nullptr;
4959 for (size_t i = 0; i < m_editorNodes.size(); ++i)
4960 {
4961 if (m_editorNodes[i].nodeID == m_selectedNodeID)
4962 {
4963 eNode = &m_editorNodes[i];
4964 break;
4965 }
4966 }
4967 if (eNode == nullptr)
4968 return;
4969
4970 TaskNodeDefinition& def = eNode->def;
4971
4972 // ---- ALL node types: standard fields ----
4973 {
4974 // Node Name
4975 char nameBuf[128];
4976 strncpy_s(nameBuf, sizeof(nameBuf), def.NodeName.c_str(), _TRUNCATE);
4977 if (ImGui::InputText("Name##nodeprops_name", nameBuf, sizeof(nameBuf)))
4978 {
4979 def.NodeName = nameBuf;
4980 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
4981 {
4982 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
4983 {
4984 m_template.Nodes[i].NodeName = def.NodeName;
4985 break;
4986 }
4987 }
4988 m_dirty = true;
4989 }
4990
4991 ImGui::Separator();
4992 }
4993
4994 // ---- Type-specific fields (for non-Branch nodes) ----
4995 // For Branch nodes, the specialized renderer is handled separately
4996 if (def.Type != TaskNodeType::Branch)
4997 {
4998 // Call RenderProperties() which already handles all type-specific fields
4999 // BUT we need to inline it here to avoid infinite recursion / double-rendering
5000 // So instead, render just the critical type-specific parts:
5001
5002 switch (def.Type)
5003 {
5004 case TaskNodeType::AtomicTask:
5005 {
5006 const std::vector<TaskSpec> tasks = AtomicTaskUIRegistry::Get().GetSortedForUI();
5007 const std::string& currentTask = def.AtomicTaskID;
5008 const char* previewLabel = currentTask.empty() ? "(select task...)" : currentTask.c_str();
5009
5010 ImGui::SetNextItemWidth(-1.0f);
5011 if (ImGui::BeginCombo("Task##nodeprops_task", previewLabel))
5012 {
5013 for (const auto& spec : tasks)
5014 {
5015 bool selected = (spec.id == currentTask);
5016 if (ImGui::Selectable(spec.displayName.c_str(), selected))
5017 {
5018 def.AtomicTaskID = spec.id;
5019 // Auto-fill node name with the action's display name
5020 def.NodeName = spec.displayName;
5021 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
5022 {
5023 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
5024 {
5025 m_template.Nodes[i].AtomicTaskID = def.AtomicTaskID;
5026 m_template.Nodes[i].NodeName = def.NodeName;
5027 break;
5028 }
5029 }
5030 m_dirty = true;
5031 }
5032 if (selected)
5033 ImGui::SetItemDefaultFocus();
5034 }
5035 ImGui::EndCombo();
5036 }
5037
5038 // Display task parameters
5039 if (!currentTask.empty())
5040 {
5041 const TaskSpec* taskSpec = AtomicTaskUIRegistry::Get().GetTaskSpec(currentTask);
5042 if (taskSpec && !taskSpec->parameters.empty())
5043 {
5044 ImGui::Separator();
5045 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Parameters:");
5046
5047 for (const auto& param : taskSpec->parameters)
5048 {
5049 ImGui::PushID(param.name.c_str());
5050
5051 // Build label: parameter name + type hint
5052 std::string label = param.name + " (" + param.type + ")";
5053
5054 // Get current value from def.Parameters if it exists
5055 std::string currentValue = param.defaultValue;
5056 auto paramIt = def.Parameters.find(param.name);
5057 if (paramIt != def.Parameters.end() && paramIt->second.Type == ParameterBindingType::Literal)
5058 {
5059 currentValue = paramIt->second.LiteralValue.AsString();
5060 }
5061
5062 // Display parameter name with description as label
5063 ImGui::TextColored(ImVec4(0.8f, 0.95f, 1.0f, 1.0f), "%s", param.name.c_str());
5064 ImGui::SameLine();
5065
5066 // Add help icon (?) next to parameter name for discoverability
5067 ImGui::TextDisabled("(?)");
5068
5069 // Add tooltip with description if available (on parameter name or help icon)
5070 if (ImGui::IsItemHovered() && !param.description.empty())
5071 {
5072 ImGui::BeginTooltip();
5073 ImGui::TextWrapped("%s", param.description.c_str());
5074 ImGui::Separator();
5075 ImGui::TextDisabled("Type: %s", param.type.c_str());
5076 ImGui::TextDisabled("Default: %s", param.defaultValue.c_str());
5077 ImGui::EndTooltip();
5078 }
5079
5080 // Add description text below the parameter name (smaller, grayed out) for immediate clarity
5081 if (!param.description.empty())
5082 {
5083 ImGui::TextDisabled("%s", param.description.c_str());
5084 }
5085
5086 if (param.type == "Bool")
5087 {
5088 bool value = (currentValue == "true" || currentValue == "1");
5089 ImGui::SetNextItemWidth(-1.0f);
5090 if (ImGui::Checkbox(("##" + param.name + "_input").c_str(), &value))
5091 {
5093 binding.Type = ParameterBindingType::Literal;
5094 binding.LiteralValue = TaskValue(value);
5095 def.Parameters[param.name] = binding;
5096
5097 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
5098 {
5099 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
5100 {
5101 m_template.Nodes[i].Parameters[param.name] = binding;
5102 break;
5103 }
5104 }
5105 m_dirty = true;
5106 }
5107 }
5108 else if (param.type == "Int")
5109 {
5110 int value = 0;
5111 try { value = std::stoi(currentValue); } catch (...) {}
5112 ImGui::SetNextItemWidth(-1.0f);
5113 if (ImGui::InputInt(("##" + param.name + "_input").c_str(), &value))
5114 {
5116 binding.Type = ParameterBindingType::Literal;
5117 binding.LiteralValue = TaskValue(value);
5118 def.Parameters[param.name] = binding;
5119
5120 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
5121 {
5122 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
5123 {
5124 m_template.Nodes[i].Parameters[param.name] = binding;
5125 break;
5126 }
5127 }
5128 m_dirty = true;
5129 }
5130 }
5131 else if (param.type == "Float")
5132 {
5133 float value = 0.0f;
5134 try { value = std::stof(currentValue); } catch (...) {}
5135 ImGui::SetNextItemWidth(-1.0f);
5136 if (ImGui::InputFloat(("##" + param.name + "_input").c_str(), &value, 0.1f))
5137 {
5139 binding.Type = ParameterBindingType::Literal;
5140 binding.LiteralValue = TaskValue(value);
5141 def.Parameters[param.name] = binding;
5142
5143 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
5144 {
5145 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
5146 {
5147 m_template.Nodes[i].Parameters[param.name] = binding;
5148 break;
5149 }
5150 }
5151 m_dirty = true;
5152 }
5153 }
5154 else if (param.type == "String")
5155 {
5156 static char buffer[512] = {0};
5157 strncpy_s(buffer, currentValue.c_str(), sizeof(buffer) - 1);
5158 buffer[sizeof(buffer) - 1] = '\0';
5159
5160 ImGui::SetNextItemWidth(-1.0f);
5161 if (ImGui::InputText(("##" + param.name + "_input").c_str(), buffer, sizeof(buffer)))
5162 {
5164 binding.Type = ParameterBindingType::Literal;
5165 binding.LiteralValue = TaskValue(std::string(buffer));
5166 def.Parameters[param.name] = binding;
5167
5168 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
5169 {
5170 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
5171 {
5172 m_template.Nodes[i].Parameters[param.name] = binding;
5173 break;
5174 }
5175 }
5176 m_dirty = true;
5177 }
5178 }
5179
5180 ImGui::Spacing();
5181 ImGui::PopID();
5182 }
5183 }
5184 }
5185 break;
5186 }
5187
5188 case TaskNodeType::Delay:
5189 {
5190 float delay = def.DelaySeconds;
5191 if (ImGui::InputFloat("Delay (s)##nodeprops_delay", &delay, 0.1f, 1.0f))
5192 {
5193 def.DelaySeconds = delay;
5194 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
5195 {
5196 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
5197 {
5198 m_template.Nodes[i].DelaySeconds = def.DelaySeconds;
5199 break;
5200 }
5201 }
5202 m_dirty = true;
5203 }
5204 break;
5205 }
5206
5207 case TaskNodeType::Switch:
5208 {
5209 ImGui::TextDisabled("Switch node - edit via modal");
5210 if (ImGui::Button("Edit Switch Cases"))
5211 {
5212 // Open switch case editor if available
5213 }
5214 break;
5215 }
5216
5217 case TaskNodeType::GetBBValue:
5218 case TaskNodeType::SetBBValue:
5219 {
5220 const char* nodeType = (def.Type == TaskNodeType::GetBBValue) ? "Get" : "Set";
5221 ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "%s Blackboard Value", nodeType);
5222 ImGui::Separator();
5223
5224 // BBKey dropdown selector from local blackboard variables
5226 bbReg.LoadFromTemplate(m_template);
5227 const std::vector<VarSpec> allVars = bbReg.GetAllVariables();
5228
5229 const char* previewLabel = def.BBKey.empty() ? "(select variable...)" : def.BBKey.c_str();
5230
5231 ImGui::SetNextItemWidth(-1.0f);
5232 if (ImGui::BeginCombo("Blackboard Variable##bbkey_combo", previewLabel))
5233 {
5234 for (const auto& var : allVars)
5235 {
5236 bool selected = (var.name == def.BBKey);
5237 if (ImGui::Selectable(var.displayLabel.c_str(), selected))
5238 {
5239 const std::string oldBBKey = def.BBKey;
5240 def.BBKey = var.name;
5241
5242 // Sync to template
5243 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
5244 {
5245 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
5246 {
5247 m_template.Nodes[i].BBKey = def.BBKey;
5248 break;
5249 }
5250 }
5251
5252 // Push undo command if changed
5253 if (def.BBKey != oldBBKey)
5254 {
5255 m_undoStack.PushCommand(
5256 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
5257 m_selectedNodeID, "BBKey",
5258 PropertyValue::FromString(oldBBKey),
5259 PropertyValue::FromString(def.BBKey))),
5260 m_template);
5261 }
5262 m_dirty = true;
5263 }
5264 if (selected)
5265 ImGui::SetItemDefaultFocus();
5266 }
5267 ImGui::EndCombo();
5268 }
5269
5270 // Display node parameters (common for data nodes)
5271 if (!def.Parameters.empty())
5272 {
5273 ImGui::Separator();
5274 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Parameters:");
5275 RenderNodeDataParameters(def);
5276 }
5277
5278 break;
5279 }
5280
5281 case TaskNodeType::MathOp:
5282 {
5283 ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Math Operation");
5284 ImGui::Separator();
5285
5286 // MathOpRef operator editor
5287 static const char* operators[] = { "+", "-", "*", "/", "%", "^" };
5288 static int operatorIdx = 0;
5289
5290 if (!def.mathOpRef.mathOperator.empty())
5291 {
5292 for (int i = 0; i < 6; ++i)
5293 {
5294 if (def.mathOpRef.mathOperator == operators[i])
5295 {
5296 operatorIdx = i;
5297 break;
5298 }
5299 }
5300 }
5301
5302 ImGui::SetNextItemWidth(-1.0f);
5303 if (ImGui::Combo("Operator##mathop", &operatorIdx, operators, 6))
5304 {
5306 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
5307 {
5308 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
5309 {
5310 m_template.Nodes[i].mathOpRef.mathOperator = operators[operatorIdx];
5311 break;
5312 }
5313 }
5314 m_dirty = true;
5315 }
5316
5317 // Display operation preview with actual operand values
5318 ImGui::Separator();
5319 ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), "Operation:");
5320 ImGui::SameLine();
5321
5322 // Build left operand display string
5323 std::string leftStr = "A";
5324 if (def.mathOpRef.leftOperand.mode == MathOpOperand::Mode::Const)
5326 else if (def.mathOpRef.leftOperand.mode == MathOpOperand::Mode::Variable)
5327 leftStr = "[" + def.mathOpRef.leftOperand.variableName + "]";
5328 else if (def.mathOpRef.leftOperand.mode == MathOpOperand::Mode::Pin)
5329 leftStr = "[Pin]";
5330
5331 // Build right operand display string
5332 std::string rightStr = "B";
5333 if (def.mathOpRef.rightOperand.mode == MathOpOperand::Mode::Const)
5335 else if (def.mathOpRef.rightOperand.mode == MathOpOperand::Mode::Variable)
5336 rightStr = "[" + def.mathOpRef.rightOperand.variableName + "]";
5337 else if (def.mathOpRef.rightOperand.mode == MathOpOperand::Mode::Pin)
5338 rightStr = "[Pin]";
5339
5340 // Display final operation string in bright green
5341 ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.5f, 1.0f),
5342 "%s %s %s",
5343 leftStr.c_str(),
5344 def.mathOpRef.mathOperator.empty() ? "?" : def.mathOpRef.mathOperator.c_str(),
5345 rightStr.c_str());
5346
5347 // Display node parameters
5348 if (!def.Parameters.empty())
5349 {
5350 ImGui::Separator();
5351 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Custom Parameters:");
5352 RenderNodeDataParameters(def);
5353 }
5354
5355 break;
5356 }
5357
5358 case TaskNodeType::SubGraph:
5359 {
5360 ImGui::TextDisabled("SubGraph");
5361 ImGui::TextDisabled("Path: %s", def.SubGraphPath.c_str());
5362 break;
5363 }
5364
5365 case TaskNodeType::Sequence:
5366 case TaskNodeType::Selector:
5367 case TaskNodeType::Parallel:
5368 {
5369 ImGui::TextDisabled("Control flow node");
5370 break;
5371 }
5372
5373 default:
5374 ImGui::TextDisabled("(type-specific properties)");
5375 break;
5376 }
5377
5378 ImGui::Separator();
5379 }
5380
5381 // ---- Branch-specific: Conditions panel ----
5382 if (def.Type == TaskNodeType::Branch)
5383 {
5384 // Update condition panel with current node's data
5385 if (m_condPanelNodeID != m_selectedNodeID)
5386 {
5387 m_conditionsPanel->SetConditionRefs(def.conditionRefs);
5388 m_conditionsPanel->SetConditionOperandRefs(def.conditionOperandRefs);
5389 m_conditionsPanel->SetDynamicPins(def.dynamicPins);
5390 m_conditionsPanel->SetNodeName(def.NodeName);
5391 m_condPanelNodeID = m_selectedNodeID;
5392 }
5393
5394 // Render the conditions panel
5395 m_conditionsPanel->Render();
5396
5397 // Check if dirty and sync back to node
5398 if (m_conditionsPanel->IsDirty())
5399 {
5400 def.conditionRefs = m_conditionsPanel->GetConditionRefs();
5401 def.conditionOperandRefs = m_conditionsPanel->GetConditionOperandRefs();
5402 m_conditionsPanel->ClearDirty();
5403 m_dirty = true;
5404
5405 // Sync to template
5406 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
5407 {
5408 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
5409 {
5410 m_template.Nodes[i].conditionRefs = def.conditionRefs;
5411 m_template.Nodes[i].conditionOperandRefs = def.conditionOperandRefs;
5412 break;
5413 }
5414 }
5415 }
5416
5417 ImGui::Separator();
5418 }
5419
5420 // ---- ALL nodes: Breakpoint ----
5421 bool hasBP = DebugController::Get().HasBreakpoint(0, m_selectedNodeID);
5422 if (ImGui::Checkbox("Breakpoint (F9)##nodeprops_bp", &hasBP))
5423 {
5424 DebugController::Get().ToggleBreakpoint(0, m_selectedNodeID,
5425 m_template.Name,
5426 def.NodeName);
5427 }
5428}
5429
5430// ============================================================================
5431// PHASE 24 Panel Integration — Part B: Preset Bank
5432// ============================================================================
5433
5434void VisualScriptEditorPanel::RenderPresetBankPanel()
5435{
5436 ImGui::TextDisabled("Preset Bank (Global)");
5437 ImGui::Separator();
5438
5439 if (!m_libraryPanel)
5440 return;
5441
5442 size_t presetCount = m_presetRegistry.GetPresetCount();
5443
5444 // Toolbar: Add preset button
5445 if (ImGui::Button("+##addpreset", ImVec2(25, 0)))
5446 {
5447 m_libraryPanel->OnAddPresetClicked();
5448 }
5449 ImGui::SameLine();
5450 ImGui::TextDisabled("New Preset");
5451
5452 ImGui::Separator();
5453 ImGui::TextDisabled("Total: %zu preset(s)", presetCount);
5454 ImGui::Separator();
5455
5456 // List all presets in compact horizontal format
5457 std::vector<ConditionPreset> allPresets = m_presetRegistry.GetFilteredPresets("");
5458
5459 if (allPresets.empty())
5460 {
5461 ImGui::TextDisabled("(no presets - create one to get started)");
5462 }
5463
5464 for (size_t i = 0; i < allPresets.size(); ++i)
5465 {
5467 ImGui::PushID(preset.id.c_str());
5468 RenderPresetItemCompact(preset, i + 1); // 1-indexed for display
5469 ImGui::PopID();
5470 }
5471}
5472
5473void VisualScriptEditorPanel::RenderPresetItemCompact(const ConditionPreset& preset, size_t index)
5474{
5475#ifndef OLYMPE_HEADLESS
5476 // Single-line horizontal layout matching mockup:
5477 // [Index: Name (yellow)] [Left▼ mode] [value] [Op▼] [Right▼ mode] [value] [Edit] [Dup] [X]
5478
5479 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 1.0f));
5480 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 1.0f));
5481 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
5482
5483 // Get a mutable copy of the preset for editing
5485 bool presetModified = false;
5486
5487 // Condition name display with index (yellow)
5488 // Use PushID for unique identification, don't add UUID to visible text
5489 ImGui::PushID(editablePreset.id.c_str());
5490 ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), "Condition #%zu", index);
5491 ImGui::PopID();
5492 ImGui::SameLine(0.0f, 12.0f);
5493
5494 // Left operand with unified dropdown (mode + value combined)
5495 if (RenderOperandEditor(editablePreset.left, "##left_op"))
5496 {
5497 presetModified = true;
5498 }
5499 ImGui::SameLine(0.0f, 6.0f);
5500
5501 // Operator dropdown
5502 std::string opStr;
5503 switch (editablePreset.op)
5504 {
5505 case ComparisonOp::Equal: opStr = "=="; break;
5506 case ComparisonOp::NotEqual: opStr = "!="; break;
5507 case ComparisonOp::Less: opStr = "<"; break;
5508 case ComparisonOp::LessEqual: opStr = "<="; break;
5509 case ComparisonOp::Greater: opStr = ">"; break;
5510 case ComparisonOp::GreaterEqual: opStr = ">="; break;
5511 default: opStr = "?"; break;
5512 }
5513
5514 const char* opNames[] = { "==", "!=", "<", "<=", ">", ">=" };
5515 const ComparisonOp opValues[] = {
5516 ComparisonOp::Equal, ComparisonOp::NotEqual,
5517 ComparisonOp::Less, ComparisonOp::LessEqual,
5518 ComparisonOp::Greater, ComparisonOp::GreaterEqual
5519 };
5520 int curOpIdx = 0;
5521 for (int i = 0; i < 6; ++i)
5522 {
5523 if (editablePreset.op == opValues[i])
5524 {
5525 curOpIdx = i;
5526 break;
5527 }
5528 }
5529
5530 ImGui::SetNextItemWidth(50.0f);
5531 if (ImGui::Combo("##op_type", &curOpIdx, opNames, 6))
5532 {
5534 presetModified = true;
5535 }
5536 ImGui::SameLine(0.0f, 6.0f);
5537
5538 // Right operand with unified dropdown (mode + value combined)
5539 if (RenderOperandEditor(editablePreset.right, "##right_op"))
5540 {
5541 presetModified = true;
5542 }
5543 ImGui::SameLine(0.0f, 12.0f);
5544
5545 // Save modified preset if changed
5546 if (presetModified)
5547 {
5548 m_presetRegistry.UpdatePreset(editablePreset.id, editablePreset);
5549
5550 // Phase 24 — Sync to template presets for graph serialization
5551 // Update the preset in m_template.Presets so it gets saved with the graph
5552 for (size_t pi = 0; pi < m_template.Presets.size(); ++pi)
5553 {
5554 if (m_template.Presets[pi].id == editablePreset.id)
5555 {
5556 m_template.Presets[pi] = editablePreset;
5557 break;
5558 }
5559 }
5560
5561 m_dirty = true;
5562 }
5563
5564 // Duplicate button
5565 if (ImGui::Button("Dup##dup_preset", ImVec2(40, 0)))
5566 {
5567 std::string newPresetID = m_presetRegistry.DuplicatePreset(editablePreset.id);
5568
5569 // Phase 24 — Add the duplicate to template presets as well
5570 if (!newPresetID.empty())
5571 {
5572 const ConditionPreset* newPreset = m_presetRegistry.GetPreset(newPresetID);
5573 if (newPreset)
5574 {
5575 m_template.Presets.push_back(*newPreset);
5576 }
5577 }
5578
5579 m_dirty = true;
5580 }
5581 ImGui::SameLine(0.0f, 4.0f);
5582
5583 // Delete button
5584 if (ImGui::Button("X##del_preset", ImVec2(25, 0)))
5585 {
5586 m_presetRegistry.DeletePreset(editablePreset.id);
5587 m_pinManager->InvalidatePreset(editablePreset.id);
5588
5589 // Phase 24 — Remove from template presets as well
5590 for (size_t pi = 0; pi < m_template.Presets.size(); ++pi)
5591 {
5592 if (m_template.Presets[pi].id == editablePreset.id)
5593 {
5594 m_template.Presets.erase(m_template.Presets.begin() + pi);
5595 break;
5596 }
5597 }
5598 // Persist the deletion to disk
5599 m_presetRegistry.Save("Blueprints/Presets/condition_presets.json");
5600 }
5601
5602 ImGui::PopStyleColor(3);
5603
5604 // Add visual separator between presets
5605 ImGui::Separator();
5606#endif
5607}
5608
5609bool VisualScriptEditorPanel::RenderOperandEditor(Operand& operand, const char* labelSuffix)
5610{
5611#ifndef OLYMPE_HEADLESS
5612 bool modified = false;
5613
5614 // Build a unified dropdown list with this ORDER:
5615 // 1. [Pin-in #1], [Pin-in #2], ...
5616 // 2. [Const] <value>
5617 // 3. Variables (sorted alphabetically)
5618
5619 std::vector<std::string> allOptions;
5620 std::vector<int> optionTypes; // 0=Variable, 1=Const, 2=Pin
5621 std::vector<std::string> optionValues; // Store the actual value for each option
5622
5623 int currentSelectionIdx = -1;
5624
5625 // ── Add all available pins FIRST ─────────────────────────────────────
5626 {
5627 std::vector<DynamicDataPin> allPins = m_pinManager->GetAllPins();
5628 for (const auto& pin : allPins)
5629 {
5630 allOptions.push_back("[Pin-in] " + pin.label);
5631 optionTypes.push_back(2); // Pin
5632 optionValues.push_back(pin.label);
5633
5634 if (operand.mode == OperandMode::Pin &&
5635 operand.stringValue == pin.label)
5636 {
5637 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
5638 }
5639 }
5640
5641 // If no pins are available, still show the [Pin-in] option as a category
5642 if (allPins.empty())
5643 {
5644 allOptions.push_back("[Pin-in] (none available)");
5645 optionTypes.push_back(2); // Pin
5646 optionValues.push_back(""); // Empty value for unavailable pin
5647 }
5648 }
5649
5650 // ── Add [Const] option SECOND ────────────────────────────────────────
5651 {
5652 std::string constLabel = "[Const] ";
5653 std::ostringstream oss;
5654 oss << std::fixed << std::setprecision(3) << operand.constValue;
5655 std::string constVal = oss.str();
5656 // Trim trailing zeros
5657 size_t dot = constVal.find('.');
5658 if (dot != std::string::npos)
5659 {
5660 size_t last = constVal.find_last_not_of('0');
5661 if (last != std::string::npos && last > dot)
5662 constVal = constVal.substr(0, last + 1);
5663 else if (last == dot)
5664 constVal = constVal.substr(0, dot);
5665 }
5667
5668 allOptions.push_back(constLabel);
5669 optionTypes.push_back(1); // Const
5670 optionValues.push_back(constVal);
5671
5672 if (operand.mode == OperandMode::Const)
5673 {
5674 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
5675 }
5676 }
5677
5678 // ── Add all local variables LAST (sorted alphabetically) ──────────────
5679 {
5680 std::vector<std::string> sortedVarNames;
5681 for (const auto& entry : m_template.Blackboard)
5682 {
5683 if (entry.Type != VariableType::None && !entry.Key.empty())
5684 {
5685 sortedVarNames.push_back(entry.Key);
5686 }
5687 }
5688 // Sort alphabetically
5689 std::sort(sortedVarNames.begin(), sortedVarNames.end());
5690
5691 for (const auto& varName : sortedVarNames)
5692 {
5693 allOptions.push_back(varName);
5694 optionTypes.push_back(0); // Variable
5695 optionValues.push_back(varName);
5696
5697 // Check if this is the currently selected variable
5698 if (operand.mode == OperandMode::Variable &&
5699 operand.stringValue == varName)
5700 {
5701 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
5702 }
5703 }
5704 }
5705
5706 // ── Add all global variables (Phase 24) ───────────────────────────────
5707 {
5708 GlobalTemplateBlackboard& gtb = GlobalTemplateBlackboard::Get();
5709 const std::vector<GlobalEntryDefinition>& globalVars = gtb.GetAllVariables();
5710
5711 // Add separator if we have both local and global
5712 if (!m_template.Blackboard.empty() && !globalVars.empty())
5713 {
5714 allOptions.push_back("--- Global Variables ---");
5715 optionTypes.push_back(-1); // Separator (no type)
5716 optionValues.push_back("");
5717 }
5718
5719 // Add global variables
5720 for (const auto& globalVar : globalVars)
5721 {
5722 allOptions.push_back(globalVar.Key);
5723 optionTypes.push_back(0); // Variable
5724 optionValues.push_back(globalVar.Key);
5725
5726 // Check if this is the currently selected global variable
5727 if (operand.mode == OperandMode::Variable &&
5728 operand.stringValue == globalVar.Key)
5729 {
5730 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
5731 }
5732 }
5733 }
5734
5735 // ── Render unified dropdown ──────────────────────────────────────────
5736 ImGui::SetNextItemWidth(120.0f);
5737
5738 const char* displayText = (currentSelectionIdx >= 0) ? allOptions[currentSelectionIdx].c_str() : "(none)";
5739
5740 if (ImGui::BeginCombo(labelSuffix, displayText))
5741 {
5742 // Create mutable array of C strings for ImGui
5743 std::vector<const char*> optionsCStr;
5744 for (const auto& opt : allOptions)
5745 optionsCStr.push_back(opt.c_str());
5746
5747 for (int i = 0; i < static_cast<int>(allOptions.size()); ++i)
5748 {
5749 bool selected = (i == currentSelectionIdx);
5750
5751 // Skip rendering separator as selectable
5752 if (optionTypes[i] == -1)
5753 {
5754 ImGui::Separator();
5755 continue;
5756 }
5757
5758 if (ImGui::Selectable(optionsCStr[i], selected))
5759 {
5760 // Update operand based on selected type
5761 switch (optionTypes[i])
5762 {
5763 case 0: // Variable
5764 operand.mode = OperandMode::Variable;
5765 operand.stringValue = optionValues[i];
5766 break;
5767 case 1: // Const
5768 operand.mode = OperandMode::Const;
5769 try {
5770 operand.constValue = std::stod(optionValues[i]);
5771 } catch (...) {
5772 operand.constValue = 0.0;
5773 }
5774 break;
5775 case 2: // Pin
5776 operand.mode = OperandMode::Pin;
5777 // For pins, store the pin label. If no specific pin selected (empty),
5778 // use a placeholder value that indicates "any available pin"
5779 operand.stringValue = optionValues[i].empty() ? "[Pin-in]" : optionValues[i];
5780 break;
5781 }
5782 modified = true;
5783 }
5784 }
5785 ImGui::EndCombo();
5786 }
5787
5788 // ── Add numeric input field for Const mode ──────────────────────────────
5789 if (operand.mode == OperandMode::Const)
5790 {
5791 ImGui::SameLine(0.0f, 4.0f);
5792 ImGui::SetNextItemWidth(60.0f);
5793 if (ImGui::InputDouble("##const_value", &operand.constValue, 0.0, 0.0, "%.3f"))
5794 {
5795 modified = true;
5796 }
5797 }
5798
5799 return modified;
5800#else
5801 return false;
5802#endif
5803}
5804
5805// ============================================================================
5806// PHASE 24 Panel Integration — Part C: Local Variables Reference
5807// ============================================================================
5808
5809void VisualScriptEditorPanel::RenderLocalVariablesPanel()
5810{
5811 ImGui::TextDisabled("Local Blackboard");
5812 ImGui::Separator();
5813
5814 // BUG-001 Hotfix: warn user if invalid entries exist (key empty or type None)
5815 // to prevent save crash caused by unhandled None type during serialization.
5816 bool hasInvalid = false;
5817 for (size_t i = 0; i < m_template.Blackboard.size(); ++i)
5818 {
5819 const BlackboardEntry& entry = m_template.Blackboard[static_cast<size_t>(i)];
5820 if (entry.Key.empty() || entry.Type == VariableType::None)
5821 {
5822 hasInvalid = true;
5823 break;
5824 }
5825 }
5826 if (hasInvalid)
5827 {
5828 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
5829 ImGui::TextUnformatted("[!] Invalid entries will be skipped on save");
5830 ImGui::PopStyleColor();
5831 }
5832
5833 // Add entry button — BUG-001 Hotfix: init with safe defaults (non-empty key, Int type)
5834 if (ImGui::Button("+##vsbbAdd"))
5835 {
5837 entry.Key = "NewVariable";
5838 entry.Type = VariableType::Int;
5839 entry.Default = GetDefaultValueForType(VariableType::Int); // UX Fix #1
5840 entry.IsGlobal = false;
5841 m_template.Blackboard.push_back(entry);
5842 m_dirty = true;
5843 }
5844 ImGui::SameLine();
5845 ImGui::TextDisabled("Add key");
5846
5847 // List existing entries
5848 for (int idx = static_cast<int>(m_template.Blackboard.size()) - 1; idx >= 0; --idx)
5849 {
5850 BlackboardEntry& entry = m_template.Blackboard[static_cast<size_t>(idx)];
5851
5852 ImGui::PushID(idx);
5853
5854 // Use a local (non-static) buffer per iteration to avoid sharing across entries
5855 char keyBuf[64];
5856 strncpy_s(keyBuf, sizeof(keyBuf), entry.Key.c_str(), _TRUNCATE);
5857
5858 // ── Name (editable text field) ──
5859 ImGui::SetNextItemWidth(140.0f);
5860 if (ImGui::InputText("##bbkey", keyBuf, sizeof(keyBuf)))
5861 {
5862 entry.Key = keyBuf;
5863 m_pendingBlackboardEdits[idx] = keyBuf;
5864 m_dirty = true;
5865 }
5866
5867 ImGui::SameLine();
5868
5869 // ── Type dropdown ──
5870 const char* typeNames[] = { "None", "Bool", "Int", "Float", "String", "Vector" };
5871 const VariableType typeValues[] = {
5872 VariableType::None, VariableType::Bool, VariableType::Int,
5873 VariableType::Float, VariableType::String, VariableType::Vector
5874 };
5875 int curTypeIdx = 0;
5876 for (int ti = 0; ti < 6; ++ti)
5877 if (entry.Type == typeValues[ti])
5878 { curTypeIdx = ti; break; }
5879
5880 ImGui::SetNextItemWidth(80.0f);
5881 if (ImGui::Combo("##bbtype", &curTypeIdx, typeNames, 6))
5882 {
5883 entry.Type = typeValues[curTypeIdx];
5884 entry.Default = GetDefaultValueForType(entry.Type);
5885 m_dirty = true;
5886 }
5887
5888 // ── Default value (type-aware editor) ──
5889 if (entry.Type != VariableType::None)
5890 {
5891 ImGui::SameLine();
5892 ImGui::TextDisabled("Default:");
5893 ImGui::SameLine();
5894 RenderConstValueInput(entry.Default, entry.Type, "##bbdefault");
5895 }
5896
5897 // ── Global toggle ──
5898 ImGui::SameLine();
5899 bool isGlobal = entry.IsGlobal;
5900 if (ImGui::Checkbox("G##bbglobal", &isGlobal))
5901 {
5902 entry.IsGlobal = isGlobal;
5903 m_dirty = true;
5904 }
5905 if (ImGui::IsItemHovered())
5906 ImGui::SetTooltip("Mark as global variable");
5907
5908 // ── Delete button ──
5909 ImGui::SameLine();
5910 if (ImGui::Button("X##bbdel"))
5911 {
5912 m_template.Blackboard.erase(m_template.Blackboard.begin() + idx);
5913 m_pendingBlackboardEdits.erase(idx);
5914 m_dirty = true;
5915 }
5916
5917 ImGui::PopID();
5918 }
5919}
5920
5921// ============================================================================
5922// Phase 24 Global Blackboard Integration — RenderGlobalVariablesPanel (Enhanced)
5923// ============================================================================
5924
5925void VisualScriptEditorPanel::RenderGlobalVariablesPanel()
5926{
5927 ImGui::TextDisabled("Global Variables (Editor Instance)");
5928 ImGui::Separator();
5929
5930 // Get reference to the global template registry (non-const for Add)
5931 GlobalTemplateBlackboard& gtb = GlobalTemplateBlackboard::Get();
5932 const std::vector<GlobalEntryDefinition>& globalVars = gtb.GetAllVariables();
5933
5934 // Add Global Variable button
5935 if (ImGui::Button("+##globalVarAdd", ImVec2(30, 0)))
5936 {
5937 ImGui::OpenPopup("AddGlobalVariablePopup");
5938 }
5939 ImGui::SameLine();
5940 ImGui::TextDisabled("Add global variable");
5941
5942 // Add Global Variable Modal Dialog
5943 if (ImGui::BeginPopupModal("AddGlobalVariablePopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
5944 {
5945 static char newVarName[128] = "newGlobal";
5946 static int newVarTypeIdx = 2; // Default to Int
5947 static char newVarDescription[256] = "Enter description...";
5948
5949 ImGui::InputText("Variable Name##new", newVarName, sizeof(newVarName));
5950
5951 const char* typeOptions[] = { "Bool", "Int", "Float", "String", "Vector", "EntityID" };
5952 const VariableType typeValues[] = {
5953 VariableType::Bool, VariableType::Int, VariableType::Float,
5954 VariableType::String, VariableType::Vector, VariableType::EntityID
5955 };
5956 ImGui::Combo("Type##new", &newVarTypeIdx, typeOptions, 6);
5957
5958 ImGui::InputTextMultiline("Description##new", newVarDescription, sizeof(newVarDescription), ImVec2(0, 60));
5959
5960 if (ImGui::Button("Create", ImVec2(120, 0)))
5961 {
5962 if (strlen(newVarName) > 0 && !gtb.HasVariable(newVarName))
5963 {
5964 TaskValue defaultVal = GetDefaultValueForType(typeValues[newVarTypeIdx]);
5966 {
5967 SYSTEM_LOG << "[VSEditor] Created new global variable: " << newVarName << "\n";
5968 gtb.SaveToFile(); // Use last loaded path automatically
5969
5970 // Phase 24: Hot reload to refresh registry and propagate to all panels
5971 GlobalTemplateBlackboard::Reload();
5972
5973 m_dirty = true;
5974
5975 // Reset form
5976 memset(newVarName, 0, sizeof(newVarName));
5977 strcpy_s(newVarName, sizeof(newVarName), "newGlobal");
5978 newVarTypeIdx = 2;
5980 strcpy_s(newVarDescription, sizeof(newVarDescription), "Enter description...");
5981
5982 ImGui::CloseCurrentPopup();
5983 }
5984 }
5985 }
5986 ImGui::SameLine();
5987 if (ImGui::Button("Cancel", ImVec2(120, 0)))
5988 {
5989 ImGui::CloseCurrentPopup();
5990 }
5991
5992 ImGui::EndPopup();
5993 }
5994
5995 ImGui::Separator();
5996
5997 if (globalVars.empty())
5998 {
5999 ImGui::TextDisabled("(no global variables defined)");
6000 ImGui::TextDisabled("Click [+] above to create new global variables");
6001 return;
6002 }
6003
6004 ImGui::TextDisabled("Global variables from project registry");
6005 ImGui::TextDisabled("Values shown are editor-specific (persisted with graph)");
6006 ImGui::Separator();
6007
6008 // Check if EntityBlackboard is initialized
6009 if (!m_entityBlackboard)
6010 {
6011 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "[ERROR] EntityBlackboard not initialized");
6012 return;
6013 }
6014
6015 // Display each global variable with editable entity-specific value
6016 for (size_t gi = 0; gi < globalVars.size(); ++gi)
6017 {
6019
6020 ImGui::PushID(static_cast<int>(gi));
6021
6022 // ---- Variable name (read-only) with type label + Delete button ----
6023 ImGui::TextColored(ImVec4(0.8f, 0.95f, 1.0f, 1.0f), "(%s) %s",
6024 VariableTypeToString(globalDef.Type).c_str(),
6025 globalDef.Key.c_str());
6026
6027 ImGui::SameLine();
6028 ImGui::TextDisabled("(%.1f KB)", 0.1f); // Placeholder space
6029 ImGui::SameLine();
6030
6031 // Delete button for global variable
6032 if (ImGui::SmallButton("Delete##globalvar"))
6033 {
6034 // Mark for deletion (we'll process after the loop to avoid iterator invalidation)
6035 std::string varToDelete = globalDef.Key;
6036 if (gtb.RemoveVariable(varToDelete))
6037 {
6038 SYSTEM_LOG << "[VSEditor] Deleted global variable: " << varToDelete << "\n";
6039 gtb.SaveToFile(); // Use last loaded path automatically
6040
6041 // Phase 24: Hot reload to refresh registry
6042 GlobalTemplateBlackboard::Reload();
6043
6044 m_dirty = true;
6045 }
6046 ImGui::PopID();
6047 continue; // Skip rendering the rest of this variable's UI
6048 }
6049
6050 // ---- Description (if available) ----
6051 if (!globalDef.Description.empty())
6052 {
6053 ImGui::TextDisabled(" %s", globalDef.Description.c_str());
6054 }
6055
6056 // Create unique table ID per global variable to avoid ImGui::BeginTable() failures
6057 std::string tableId = "##GlobalVarTable_" + std::to_string(gi);
6058 if (ImGui::BeginTable(tableId.c_str(), 2, ImGuiTableFlags_SizingStretchSame, ImVec2(0, 0)))
6059 {
6060 ImGui::TableSetupColumn("Label", 0);
6061 ImGui::TableSetupColumn("Value", 0);
6062
6063 // ---- Default Value (read-only) ----
6064 ImGui::TableNextRow();
6065 ImGui::TableSetColumnIndex(0);
6066 ImGui::TextDisabled("Default:");
6067 ImGui::TableSetColumnIndex(1);
6068
6069 const TaskValue& defaultValue = globalDef.DefaultValue;
6070 std::string defaultStr;
6071 switch (globalDef.Type)
6072 {
6073 case VariableType::Bool:
6074 defaultStr = defaultValue.IsNone() ? "false" : (defaultValue.AsBool() ? "true" : "false");
6075 break;
6076 case VariableType::Int:
6077 defaultStr = defaultValue.IsNone() ? "0" : std::to_string(defaultValue.AsInt());
6078 break;
6079 case VariableType::Float:
6080 {
6081 std::ostringstream oss;
6082 oss << std::fixed << std::setprecision(2);
6083 oss << (defaultValue.IsNone() ? 0.0f : defaultValue.AsFloat());
6084 defaultStr = oss.str();
6085 break;
6086 }
6087 case VariableType::String:
6088 defaultStr = defaultValue.IsNone() ? "" : defaultValue.AsString();
6089 break;
6090 case VariableType::Vector:
6091 defaultStr = "(vector)";
6092 break;
6093 case VariableType::EntityID:
6094 defaultStr = defaultValue.IsNone() ? "0" : std::to_string(static_cast<int>(defaultValue.AsEntityID()));
6095 break;
6096 default:
6097 defaultStr = "(unknown)";
6098 break;
6099 }
6100 ImGui::TextDisabled("%s", defaultStr.c_str());
6101
6102 // ---- Current Value (editable with scope resolution) ----
6103 ImGui::TableNextRow();
6104 ImGui::TableSetColumnIndex(0);
6105 ImGui::TextDisabled("Current:");
6106 ImGui::TableSetColumnIndex(1);
6107
6108 // Use scoped variable access to get/set entity-specific value
6109 std::string scopedVarName = "(G)" + globalDef.Key;
6110 TaskValue currentValue = m_entityBlackboard->GetValueScoped(scopedVarName);
6111
6112 // Create type-specific input widget
6113 bool valueChanged = false;
6114 switch (globalDef.Type)
6115 {
6116 case VariableType::Bool:
6117 {
6118 bool bVal = currentValue.IsNone() ? false : currentValue.AsBool();
6119 if (ImGui::Checkbox("##bool_val", &bVal))
6120 {
6121 m_entityBlackboard->SetValueScoped(scopedVarName, TaskValue(bVal));
6122 m_dirty = true;
6123 valueChanged = true;
6124 }
6125 break;
6126 }
6127 case VariableType::Int:
6128 {
6129 int iVal = currentValue.IsNone() ? 0 : currentValue.AsInt();
6130 if (ImGui::InputInt("##int_val", &iVal))
6131 {
6132 m_entityBlackboard->SetValueScoped(scopedVarName, TaskValue(iVal));
6133 m_dirty = true;
6134 valueChanged = true;
6135 }
6136 break;
6137 }
6138 case VariableType::Float:
6139 {
6140 float fVal = currentValue.IsNone() ? 0.0f : currentValue.AsFloat();
6141 if (ImGui::InputFloat("##float_val", &fVal))
6142 {
6143 m_entityBlackboard->SetValueScoped(scopedVarName, TaskValue(fVal));
6144 m_dirty = true;
6145 valueChanged = true;
6146 }
6147 break;
6148 }
6149 case VariableType::String:
6150 {
6151 static std::unordered_map<size_t, std::vector<char>> stringBuffers;
6152 size_t bufKey = gi; // Use index as unique key for buffer storage
6153 if (stringBuffers.find(bufKey) == stringBuffers.end())
6154 {
6155 std::string initialStr = currentValue.IsNone() ? "" : currentValue.AsString();
6156 stringBuffers[bufKey] = std::vector<char>(initialStr.begin(), initialStr.end());
6157 stringBuffers[bufKey].push_back('\0');
6158 stringBuffers[bufKey].resize(256); // Allocate buffer
6159 }
6160
6161 ImGui::SetNextItemWidth(-1.0f);
6162 if (ImGui::InputText("##string_val", stringBuffers[bufKey].data(), 256, ImGuiInputTextFlags_EnterReturnsTrue))
6163 {
6164 std::string newStr(stringBuffers[bufKey].data());
6165 m_entityBlackboard->SetValueScoped(scopedVarName, TaskValue(newStr));
6166 m_dirty = true;
6167 valueChanged = true;
6168 }
6169 break;
6170 }
6171 case VariableType::Vector:
6172 {
6173 Vector vVal = currentValue.IsNone() ? Vector{0.0f, 0.0f, 0.0f} : currentValue.AsVector();
6174 float vArray[3] = {vVal.x, vVal.y, vVal.z};
6175 if (ImGui::InputFloat3("##vector_val", vArray))
6176 {
6177 Vector newVec{vArray[0], vArray[1], vArray[2]};
6178 m_entityBlackboard->SetValueScoped(scopedVarName, TaskValue(newVec));
6179 m_dirty = true;
6180 valueChanged = true;
6181 }
6182 break;
6183 }
6184 case VariableType::EntityID:
6185 {
6186 int eID = currentValue.IsNone() ? 0 : static_cast<int>(currentValue.AsEntityID());
6187 if (ImGui::InputInt("##entityid_val", &eID))
6188 {
6189 m_entityBlackboard->SetValueScoped(scopedVarName, TaskValue(eID >= 0 ? eID : 0));
6190 m_dirty = true;
6191 valueChanged = true;
6192 }
6193 break;
6194 }
6195 default:
6196 ImGui::TextDisabled("(unsupported type)");
6197 break;
6198 }
6199
6200 // ---- Persistent flag ----
6201 if (globalDef.IsPersistent)
6202 {
6203 ImGui::TableNextRow();
6204 ImGui::TableSetColumnIndex(0);
6205 ImGui::TextDisabled("Flags:");
6206 ImGui::TableSetColumnIndex(1);
6207 ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.5f, 1.0f), "[Persistent]");
6208 }
6209
6210 ImGui::EndTable();
6211 }
6212 ImGui::Separator();
6213 ImGui::PopID();
6214 }
6215}
6216
6217} // 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.
ImNodes-based graph editor for ATS Visual Script graphs (Phase 5).
Records an "add exec connection" operation for undo/redo.
Records an "add data connection" operation for undo/redo.
Records "add dynamic exec-out pin" on a VSSequence or VSSwitch node for undo/redo.
Command to add a node to the tree.
Non-singleton registry populated from the active TaskGraphTemplate.
void LoadFromTemplate(const TaskGraphTemplate &tmpl)
Rebuilds the registry from the blackboard entries of a template.
ImGui panel for creating, editing, duplicating, and deleting global condition presets.
void LoadFromPresetList(const std::vector< ConditionPreset > &presets)
Loads presets from a vector of ConditionPreset objects (clears existing data first).
void Clear()
Clears all presets and resets error state.
std::vector< std::string > GetAllPresetIDs() const
Returns all preset UUIDs in display order.
ConditionPreset * GetPreset(const std::string &id)
Returns a mutable pointer to the preset, or nullptr if not found.
Records a "delete link" operation for undo/redo.
Command to delete a node from the tree.
Generates, tracks, and invalidates DynamicDataPin objects for a node.
Records a property edit on a single node for undo/redo.
ImGui sub-panel for editing GetBBValue node's blackboard variable selection.
static void Reload()
Force reload of the registry from file (useful for hot reload)
ImGui sub-panel for editing MathOp operands and operator.
Command to move a node.
Renders a NodeBranch using ImGui/ImNodes with 4 sections.
ImGui sub-panel for managing the condition list of a single NodeBranch.
Records "remove dynamic exec-out pin" on a VSSequence or VSSwitch node for undo/redo.
ImGui sub-panel for editing SetBBValue node's variable selection and value.
Immutable, shareable task graph asset.
std::vector< TaskNodeDefinition > Nodes
All graph nodes.
std::vector< ExecPinConnection > ExecConnections
Explicit exec connections (ATS VS only)
std::string Name
Friendly name of this template (e.g. "PatrolBehaviour")
std::vector< BlackboardEntry > Blackboard
Local blackboard declared in this graph.
int32_t RootNodeID
ID of the root node (must exist in Nodes)
void BuildLookupCache()
Rebuilds the internal ID-to-node lookup map from the Nodes vector.
const TaskNodeDefinition * GetNode(int32_t nodeId) const
Returns a pointer to the node with the given ID, or nullptr.
json GlobalVariableValues
Stores JSON representation of global variable values for this specific graph instance.
int32_t EntryPointID
ID of the EntryPoint node (for VS graphs)
std::vector< DataPinConnection > DataConnections
Explicit data connections (ATS VS only)
std::vector< ConditionPreset > Presets
Presets are now stored in the graph JSON, not in external files.
C++14-compliant type-safe value container for task parameters.
int AsInt() const
Returns the int value.
::Vector AsVector() const
Returns the Vector value.
EntityID AsEntityID() const
Returns the EntityID value.
float AsFloat() const
Returns the float value.
bool IsNone() const
Returns true if the value has not been set (type == None).
std::string AsString() const
Returns the string value.
bool AsBool() const
Returns the bool value.
void PushCommand(std::unique_ptr< ICommand > cmd, TaskGraphTemplate &graph)
Executes the command on graph, then pushes it onto the undo stack.
ImGui sub-panel for editing Variable node's blackboard variable selection.
std::unique_ptr< DynamicDataPinManager > m_pinManager
Dynamic pin manager shared across all Branch nodes in this panel.
UndoRedoStack m_undoStack
Undo/Redo command stack for reversible graph editing operations.
bool Save()
Saves the current canvas state to JSON v4 at the loaded path.
char m_saveAsFilename[256]
Buffer for the user-entered filename (without extension)
std::vector< VSEditorLink > m_editorLinks
Editor links (exec + data)
std::unique_ptr< VariablePropertyPanel > m_variablePanel
Properties-panel sub-widget for the selected Variable node (data pure).
VisualScriptEditorPanel()
VisualScriptEditorPanel Constructor.
int ExecOutAttrUID(int nodeID, int pinIndex) const
Maps node ID + pin index -> ImNodes attribute UID for exec-out pins.
TaskGraphTemplate m_template
The template currently being edited.
void Shutdown()
Shutdown the editor panel and release all resources.
void CommitPendingBlackboardEdits()
Commits any pending key-name edits stored in m_pendingBlackboardEdits.
std::unique_ptr< MathOpPropertyPanel > m_mathOpPanel
Properties-panel sub-widget for the selected MathOp node.
void Initialize()
Initialize the editor panel with ImNodes context and UI helpers.
void RemoveNode(int nodeID)
Removes a node from the canvas.
std::unique_ptr< EntityBlackboard > m_entityBlackboard
Per-entity blackboard instance (combines local + global variables) Created in Initialize() and manage...
void RemoveLink(int linkID)
Removes an ImNodes link (and its underlying template connection) by link ID.
int m_condPanelNodeID
ID of the node currently loaded into m_conditionsPanel (-1 = none).
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.
std::unique_ptr< NodeConditionsPanel > m_conditionsPanel
Properties-panel sub-widget for the selected Branch node.
std::unique_ptr< NodeBranchRenderer > m_branchRenderer
Specialized renderer for Branch nodes (4-section layout with conditions).
void ConnectExec(int srcNodeID, const std::string &srcPinName, int dstNodeID, const std::string &dstPinName)
Creates an exec connection between two nodes.
void SyncCanvasFromTemplate()
Builds the editor canvas from the in-memory TaskGraphTemplate.
void SyncTemplateFromCanvas()
Builds the in-memory TaskGraphTemplate from the editor nodes/links.
void AfterSave()
Restores the ImNodes canvas panning saved by ResetViewportBeforeSave().
static std::vector< std::string > GetDataOutputPins(TaskNodeType type)
Returns the data-out pin names for a node type.
void ValidateAndCleanBlackboardEntries()
Removes blackboard entries with empty keys or VariableType::None.
int AddNode(TaskNodeType type, float x, float y)
Creates a new node on the canvas.
std::unique_ptr< GetBBValuePropertyPanel > m_getBBPanel
Properties-panel sub-widget for the selected GetBBValue node.
void SyncPresetsFromRegistryToTemplate()
Syncs ALL presets from the registry to the template.
static std::vector< std::string > GetDataInputPins(TaskNodeType type)
Returns the data-in pin names for a node type.
void ResetViewportBeforeSave()
Resets the ImNodes canvas panning to (0,0) before saving node positions.
std::unique_ptr< SetBBValuePropertyPanel > m_setBBPanel
Properties-panel sub-widget for the selected SetBBValue node.
std::unordered_set< int > m_positionedNodes
Nodes for which ImNodes has been given a position.
bool SaveAs(const std::string &path)
Saves the current canvas state to a new JSON v4 file.
int DataInAttrUID(int nodeID, int pinIndex) const
Maps node ID + data pin index -> ImNodes attribute UID for data-in pins.
void RebuildLinks()
Rebuilds ImNodes exec/data link arrays from the template.
void SyncNodePositionsFromImNodes()
Pulls the current node positions from ImNodes into m_editorNodes.
int m_nextNodeID
Next available node ID.
bool SerializeAndWrite(const std::string &path)
Serializes the template to JSON v4 and writes to a file.
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...
int DataOutAttrUID(int nodeID, int pinIndex) const
Maps node ID + data pin index -> ImNodes attribute UID for data-out pins.
int ExecInAttrUID(int nodeID) const
Maps node ID -> ImNodes attribute UID for an exec-in pin.
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.
std::vector< VSEditorNode > m_editorNodes
Editor nodes (mirrors m_template.Nodes + position/selection state)
int m_selectedNodeID
Currently selected node (for properties panel)
void SyncEditorNodesFromTemplate()
Rebuilds m_editorNodes from m_template, preserving existing node positions.
~VisualScriptEditorPanel()
VisualScriptEditorPanel Destructor.
int m_nextLinkID
Next available ImNodes link ID.
ConditionPresetRegistry m_presetRegistry
Global registry of ConditionPreset objects.
std::unique_ptr< ConditionPresetLibraryPanel > m_libraryPanel
Global condition preset library panel (UI for creating/editing/deleting presets).
void LoadTemplate(const TaskGraphTemplate *tmpl, const std::string &path)
Loads a VS graph template into the editor canvas.
static std::vector< std::string > GetExecOutputPins(TaskNodeType type)
Returns the exec-out pin names for a node type.
static Vector FromImVec2(const ImVec2 &v)
Definition vector.h:66
float x
Definition vector.h:25
< Provides AssetID and INVALID_ASSET_ID
nlohmann::json json
const char * GetNodeTypeLabel(TaskNodeType type)
Returns a human-readable label for a TaskNodeType.
VariableType
Type tags used by TaskValue to identify stored data.
@ Int
32-bit signed integer
@ Float
Single-precision float.
@ String
std::string
@ Vector
3-component vector (Vector from vector.h)
@ None
Uninitialized / empty value.
@ EntityID
Entity identifier (uint64_t)
@ MathOperator
Math operator symbol (+, -, *, /, %) (from OperatorRegistry)
@ ConditionID
ID of a condition type (from ConditionRegistry)
@ AtomicTaskID
ID of an atomic task (from AtomicTaskUIRegistry)
@ SubGraphPath
File path to a sub-graph .ats file.
@ LocalVariable
Value is read from the local blackboard at runtime.
@ Literal
Value is embedded directly in the template.
@ ComparisonOp
Comparison operator (==, !=, <, <=, >, >=) (from OperatorRegistry)
ComparisonOp
The relational operator used in a ConditionPreset.
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)
constexpr int32_t NODE_INDEX_NONE
Sentinel value for "no node" in node index / ID fields.
@ Output
Value produced by the node.
@ Input
Value consumed by the node.
static std::string VariableTypeToString(VariableType type)
Converts a VariableType to its canonical string representation.
Single entry in the graph's declared blackboard schema (local or global).
A globally-stored, reusable condition expression.
Stores the complete reference for one condition including operand-to-DynamicDataPin mapping.
Describes a single condition expression for Branch/While nodes.
Explicit connection between an output data pin of a source node and an input data pin of a target nod...
Describes a data pin declared on a Visual Script node.
std::string PinName
Pin name ("Value", "Result", etc.)
Explicit connection between a named exec-out pin of a source node and the exec-in pin of a target nod...
@ Const
Literal constant value.
std::string variableName
Blackboard key (mode == Variable), e.g. "mMoveSpeed".
std::string constValue
Literal string (mode == Const), e.g. "5.0".
Mode mode
Active mode.
MathOpOperand leftOperand
Left-hand side operand (A)
std::string mathOperator
Arithmetic operator: "+", "-", "*", "/", "%", "^".
nlohmann::json ToJson() const
Serializes this MathOpRef to a JSON object.
MathOpOperand rightOperand
Right-hand side operand (B)
Lightweight snapshot of a NodeBranch required for rendering.
int nodeID
Numeric node identifier (for ImNodes attribute UIDs)
@ Variable
References a blackboard variable by name.
@ Const
Literal constant value.
@ Pin
External data-input pin on the owning node.
One side of a ConditionPreset comparison expression.
Definition Operand.h:45
Describes how a single parameter value is supplied to a task node.
ParameterBindingType Type
Binding mode.
Describes a single case branch on a Switch node.
Full description of a single node in the task graph.
MathOpRef mathOpRef
For MathOp: complete operand configuration (left operand, operator, right operand).
std::vector< NodeConditionRef > conditionRefs
Multi-condition refs to global presets (Phase 24)
std::string SubGraphPath
For SubGraph: path to the sub-graph JSON.
std::vector< Condition > conditions
For Branch/While: structured condition list (implicit AND)
std::string BBKey
For GetBBValue/SetBBValue: BB key (scope:key)
std::string ConditionID
For Branch/While/Switch: ATS condition ID.
std::string MathOperator
For MathOp: "+", "-", "*", "/".
float EditorPosY
Canvas Y position loaded from JSON.
std::string AtomicTaskID
Atomic task type identifier (used when Type == AtomicTask)
std::string switchVariable
For Switch: BB key of the variable to switch on.
float EditorPosX
Canvas X position loaded from JSON.
TaskNodeType Type
Node role.
std::vector< DataPinDefinition > DataPins
Data pins declared on this node.
std::vector< ConditionRef > conditionOperandRefs
Parallel to conditions[]: each entry stores the OperandRef->DynamicDataPin UUID mapping for the corre...
std::unordered_map< std::string, ParameterBinding > InputParams
Input parameter bindings.
std::unordered_map< std::string, std::string > OutputParams
Output param -> BB key mapping.
std::vector< SwitchCaseDefinition > switchCases
For Switch: structured case definitions.
std::string NodeName
Human-readable name.
std::unordered_map< std::string, ParameterBinding > Parameters
Named parameter bindings passed to the atomic task.
std::vector< DynamicDataPin > dynamicPins
Dynamic data-input pins for Pin-mode operands (Phase 24)
std::vector< std::string > DynamicExecOutputPins
For VSSequence: dynamically-added exec-out pins beyond the default "Out".
float DelaySeconds
For Delay: duration in seconds.
bool HasEditorPos
True when EditorPosX/Y were loaded from JSON.
int32_t NodeID
Unique ID within this template.
Display metadata for a single atomic task type.
Editor-side representation of a node in the VS graph canvas.
Metadata for a single blackboard variable entry.
#define SYSTEM_LOG