Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
VisualScriptEditorPanel_Properties.cpp
Go to the documentation of this file.
1/**
2 * @file VisualScriptEditorPanel_Properties.cpp
3 * @brief Properties panel rendering methods for VS graph node editors (Phase 10).
4 * @author Olympe Engine
5 * @date 2026-03-15
6 *
7 * @details
8 * This file contains 5 methods extracted from VisualScriptEditorPanel.cpp
9 * for better code organization and maintainability:
10 * - RenderProperties() — Main properties panel dispatcher
11 * - RenderNodePropertiesPanel() — Node properties panel (alternative renderer)
12 * - RenderBranchNodeProperties() — Branch/While node properties
13 * - RenderMathOpNodeProperties() — MathOp node properties
14 * - RenderNodeDataParameters() — Generic parameter editor for data nodes
15 *
16 * These methods handle all UI rendering for the Properties panel on the right side
17 * of the editor, supporting type-specific field editing with undo/redo integration.
18 *
19 * C++14 compliant — no std::optional, structured bindings, std::filesystem.
20 */
21
23#include "DebugController.h"
25#include "ConditionRegistry.h"
26#include "OperatorRegistry.h"
27#include "BBVariableRegistry.h"
28#include "MathOpOperand.h"
29#include "../system/system_utils.h"
30#include "../system/system_consts.h"
31
32#include <unordered_map>
33#include "../NodeGraphCore/GlobalTemplateBlackboard.h"
34
35#include "../third_party/imgui/imgui.h"
36#include "../third_party/imnodes/imnodes.h"
37#include "../json_helper.h"
38#include "../TaskSystem/TaskGraphLoader.h"
39
40#include <fstream>
41#include <iostream>
42#include <algorithm>
43#include <cmath>
44#include <cstring>
45#include <sstream>
46#include <iomanip>
47#include <cstdlib>
48#include <unordered_set>
49
50namespace Olympe {
51
52// ============================================================================
53// Branch node — dedicated Properties panel renderer
54// ============================================================================
55
57 TaskNodeDefinition& def)
58{
59 // ── Blue header: node name (matches canvas Section 1 title bar) ──────────
60 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.0f, 0.4f, 0.8f, 1.0f));
61 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.0f, 0.5f, 0.9f, 1.0f));
62 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.0f, 0.3f, 0.7f, 1.0f));
63 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
64 ImGui::Selectable(def.NodeName.c_str(), true,
66 ImGui::PopStyleColor(4);
67
68 ImGui::Separator();
69 ImGui::Spacing();
70
71 // ── Structured Conditions (Phase 24 — NodeConditionsPanel) ───────────────
73 {
74 // Reload the panel when the selected node changes.
75 if (m_condPanelNodeID != eNode.nodeID)
76 {
77 m_condPanelNodeID = eNode.nodeID;
78 m_conditionsPanel->SetNodeName(def.NodeName);
79 m_conditionsPanel->SetConditionRefs(def.conditionRefs);
80 m_conditionsPanel->SetDynamicPins(def.dynamicPins);
81 m_conditionsPanel->ClearDirty();
82 }
83 else
84 {
85 // Keep node name in sync with any in-frame name edits.
86 m_conditionsPanel->SetNodeName(def.NodeName);
87 }
88
89 m_conditionsPanel->Render();
90
91 if (m_conditionsPanel->IsDirty())
92 {
93 def.conditionRefs = m_conditionsPanel->GetConditionRefs();
94
95 // Keep m_template in sync for serialization.
96 for (size_t ti = 0; ti < m_template.Nodes.size(); ++ti)
97 {
98 if (m_template.Nodes[ti].NodeID == m_selectedNodeID)
99 {
100 m_template.Nodes[ti].conditionRefs = def.conditionRefs;
101 break;
102 }
103 }
104 m_conditionsPanel->ClearDirty();
105 m_dirty = true;
106 }
107 }
108
109 ImGui::Separator();
110 ImGui::Spacing();
111
112 // ── Breakpoint checkbox (F9) ─────────────────────────────────────────────
114 if (ImGui::Checkbox("Breakpoint (F9)##vsbp_branch", &hasBP))
115 {
118 def.NodeName);
119 }
120
122
123 (void)eNode; // suppress unused-warning when branches have no eNode-specific fields
124}
125
126// ============================================================================
127// MathOp node — dedicated Properties panel renderer
128// ============================================================================
129
131 TaskNodeDefinition& def)
132{
133 // ── Blue header: node name (matches canvas Section 1 title bar) ──────────
134 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.0f, 0.4f, 0.8f, 1.0f));
135 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.0f, 0.5f, 0.9f, 1.0f));
136 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.0f, 0.3f, 0.7f, 1.0f));
137 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
138 ImGui::Selectable(def.NodeName.c_str(), true,
140 ImGui::PopStyleColor(4);
141
142 ImGui::Separator();
143 ImGui::Spacing();
144
145 // ── Operand Editor (Phase 24 Milestone 2 — MathOpPropertyPanel) ───────────
146 if (m_mathOpPanel)
147 {
148 // Lazy-initialize the panel when node changes
149 if (!m_mathOpPanel)
150 {
151 m_mathOpPanel = std::unique_ptr<MathOpPropertyPanel>(
152 new MathOpPropertyPanel(m_presetRegistry, *m_pinManager));
153 }
154
155 m_mathOpPanel->SetNodeName(def.NodeName);
156 m_mathOpPanel->SetMathOpRef(def.mathOpRef);
157 m_mathOpPanel->SetDynamicPins(def.dynamicPins);
158
159 m_mathOpPanel->SetOnOperandChange([this]() {
160 // Callback when operands change: regenerate dynamic pins
161 if (m_pinManager && m_selectedNodeID >= 0)
162 {
163 for (size_t i = 0; i < m_editorNodes.size(); ++i)
164 {
165 if (m_editorNodes[i].nodeID == m_selectedNodeID)
166 {
167 m_editorNodes[i].def.mathOpRef = m_mathOpPanel->GetMathOpRef();
168 break;
169 }
170 }
171 m_dirty = true;
172 }
173 });
174
175 m_mathOpPanel->Render();
176
177 if (m_mathOpPanel->IsDirty())
178 {
179 def.mathOpRef = m_mathOpPanel->GetMathOpRef();
180
181 // Keep m_template in sync for serialization
182 for (size_t ti = 0; ti < m_template.Nodes.size(); ++ti)
183 {
184 if (m_template.Nodes[ti].NodeID == m_selectedNodeID)
185 {
186 m_template.Nodes[ti].mathOpRef = def.mathOpRef;
187 break;
188 }
189 }
190 m_mathOpPanel->ClearDirty();
191 m_dirty = true;
192 }
193 }
194
195 ImGui::Separator();
196 ImGui::Spacing();
197
199
200 (void)eNode; // suppress unused-warning
201}
202
203// ============================================================================
204// Switch node — dedicated Properties panel renderer
205// ============================================================================
206
209{
210 // ── Blue header: node name (matches canvas Section 1 title bar) ──────────
211 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.0f, 0.4f, 0.8f, 1.0f));
212 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.0f, 0.5f, 0.9f, 1.0f));
213 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.0f, 0.3f, 0.7f, 1.0f));
214 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
215 ImGui::Selectable(def.NodeName.c_str(), true,
217 ImGui::PopStyleColor(4);
218
219 ImGui::Separator();
220 ImGui::Spacing();
221
222 // ── Switch Variable Selector (Dropdown) ──────────────────────────────────
223 // Only Int/String-typed blackboard variables can be used for switch control
226 const std::vector<VarSpec> switchVars = bbReg.GetVariablesByType(VariableType::Int);
227 // Note: Could also support String in future phases
228
229 const std::string& curVar = def.switchVariable;
230 const char* previewVar = curVar.empty() ? "(select variable...)" : curVar.c_str();
231
232 ImGui::SetNextItemWidth(-1.0f);
233 if (ImGui::BeginCombo("Switch On##switch_var_combo", previewVar))
234 {
235 for (const auto& var : switchVars)
236 {
237 bool selected = (var.name == curVar);
238 if (ImGui::Selectable(var.displayLabel.c_str(), selected))
239 {
240 const std::string oldVar = def.switchVariable;
241 def.switchVariable = var.name;
242
243 // Sync to template
244 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
245 {
246 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
247 {
248 m_template.Nodes[i].switchVariable = def.switchVariable;
249 break;
250 }
251 }
252
253 // Push undo command if changed
254 if (def.switchVariable != oldVar)
255 {
257 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
258 m_selectedNodeID, "switchVariable",
261 m_template);
262 }
263 m_dirty = true;
264 }
265 if (selected)
266 ImGui::SetItemDefaultFocus();
267 }
268 ImGui::EndCombo();
269 }
270
271 // ── Display case count ────────────────────────────────────────────────────
272 ImGui::Text("Cases: %zu", def.switchCases.size());
273
274 ImGui::Separator();
275
276 // ── Modal Button to edit switch cases ──────────────────────────────────────
277 if (ImGui::Button("Edit Switch Cases", ImVec2(150, 0)))
278 {
280 m_switchCaseModal = std::make_unique<SwitchCaseEditorModal>();
281
282 // Phase 26-A: Pass context information for better UX
283 std::string switchVarName = def.switchVariable;
284 std::string switchVarType = "Int"; // Default, could detect from registry
285 std::string currentVarValue = ""; // Could query at runtime
286
288 }
289
290 // ── Modal Rendering + Apply Integration (PHASE 2 FIX) ──────────────────────
292 {
293 m_switchCaseModal->Render();
294
295 // If the user confirmed changes (clicked Apply), sync them back
296 if (m_switchCaseModal->IsConfirmed())
297 {
298 def.switchCases = m_switchCaseModal->GetSwitchCases();
299
300 // ── PHASE 1 FIX: Regenerate DynamicExecOutputPins from switchCases ──
301 // This ensures canvas pins are synchronized with semantic data
302 def.DynamicExecOutputPins.clear();
303 for (size_t caseIdx = 1; caseIdx < def.switchCases.size(); ++caseIdx)
304 {
306 def.DynamicExecOutputPins.push_back(caseData.pinName);
307 }
308
309 // Sync to template
310 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
311 {
312 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
313 {
314 m_template.Nodes[i].switchCases = def.switchCases;
315 m_template.Nodes[i].DynamicExecOutputPins = def.DynamicExecOutputPins;
316 break;
317 }
318 }
319
320 m_dirty = true;
321 m_switchCaseModal->Close();
322 }
323 }
324
325 ImGui::Separator();
326 ImGui::Spacing();
327
328 // ── Breakpoint checkbox (F9) ─────────────────────────────────────────────
330 if (ImGui::Checkbox("Breakpoint (F9)##vsbp_switch", &hasBP))
331 {
334 def.NodeName);
335 }
336
338
339 (void)eNode; // suppress unused-warning when switch nodes have no eNode-specific fields
340}
341
342// ============================================================================
343// Generic parameter editor for data nodes
344// ============================================================================
345
347{
348 // Phase 24 — Generic parameter editor for data nodes (GetBBValue, SetBBValue, MathOp)
349 // Allows storing and serializing additional parameters on data nodes
350
351 // Filter out system parameters (those starting with __)
352 std::vector<std::string> userParams;
353 for (const auto& paramPair : def.Parameters)
354 {
355 const std::string& paramName = paramPair.first;
356 // Skip system parameters
357 if (paramName.length() >= 2 && paramName[0] == '_' && paramName[1] == '_')
358 continue;
359 userParams.push_back(paramName);
360 }
361
362 ImGui::Separator();
363 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Node Parameters:");
364
365 if (userParams.empty())
366 {
367 ImGui::TextDisabled("(no user parameters - add one below)");
368 }
369
370 // Display user parameters
371 for (const auto& paramName : userParams)
372 {
373 auto paramIt = def.Parameters.find(paramName);
374 if (paramIt == def.Parameters.end())
375 continue;
376
377 ParameterBinding& binding = paramIt->second;
378
379 ImGui::PushID(paramName.c_str());
380
381 // Display parameter name
382 ImGui::TextColored(ImVec4(0.8f, 0.95f, 1.0f, 1.0f), "%s", paramName.c_str());
383
384 // Build a label showing the binding type
385 const char* typeLabel = "?";
386 switch (binding.Type)
387 {
388 case ParameterBindingType::Literal: typeLabel = "Literal"; break;
389 case ParameterBindingType::LocalVariable: typeLabel = "Variable"; break;
390 case ParameterBindingType::AtomicTaskID: typeLabel = "AtomicTaskID"; break;
391 case ParameterBindingType::ConditionID: typeLabel = "ConditionID"; break;
392 case ParameterBindingType::MathOperator: typeLabel = "MathOp"; break;
393 case ParameterBindingType::ComparisonOp: typeLabel = "CompOp"; break;
394 case ParameterBindingType::SubGraphPath: typeLabel = "SubGraph"; break;
395 }
396 ImGui::SameLine();
397 ImGui::TextDisabled("(%s)", typeLabel);
398
399 // Input field for editing parameter value
401 {
402 // For literal values, show an input field
403 std::string currentValue;
404 if (!binding.LiteralValue.IsNone())
405 {
406 currentValue = binding.LiteralValue.AsString();
407 }
408
409 char buf[256];
410 strncpy_s(buf, sizeof(buf), currentValue.c_str(), _TRUNCATE);
411 ImGui::SetNextItemWidth(-1.0f);
412 if (ImGui::InputText(("##" + paramName + "_val").c_str(), buf, sizeof(buf)))
413 {
414 // Parse and store the value
415 std::string strVal(buf);
416 binding.LiteralValue = TaskValue(strVal);
417
418 // Keep template in sync
419 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
420 {
421 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
422 {
423 m_template.Nodes[i].Parameters[paramName] = binding;
424 break;
425 }
426 }
427 m_dirty = true;
428 }
429 }
431 {
432 // For local variables, show a dropdown of available variables
433 BBVariableRegistry bbReg;
434 bbReg.LoadFromTemplate(m_template);
435 const std::vector<VarSpec> vars = bbReg.GetAllVariables();
436
437 const char* preview = binding.VariableName.empty() ? "(select...)" : binding.VariableName.c_str();
438 ImGui::SetNextItemWidth(-1.0f);
439 if (ImGui::BeginCombo(("##" + paramName + "_var").c_str(), preview))
440 {
441 for (const auto& var : vars)
442 {
443 bool selected = (var.name == binding.VariableName);
444 if (ImGui::Selectable(var.displayLabel.c_str(), selected))
445 {
446 binding.VariableName = var.name;
447
448 // Keep template in sync
449 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
450 {
451 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
452 {
453 m_template.Nodes[i].Parameters[paramName] = binding;
454 break;
455 }
456 }
457 m_dirty = true;
458 }
459 if (selected)
460 ImGui::SetItemDefaultFocus();
461 }
462 ImGui::EndCombo();
463 }
464 }
465 else
466 {
467 // For other types, show a text field for the identifier
468 char buf[256];
469 strncpy_s(buf, sizeof(buf), binding.VariableName.c_str(), _TRUNCATE);
470 ImGui::SetNextItemWidth(-1.0f);
471 if (ImGui::InputText(("##" + paramName + "_id").c_str(), buf, sizeof(buf)))
472 {
473 binding.VariableName = buf;
474
475 // Keep template in sync
476 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
477 {
478 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
479 {
480 m_template.Nodes[i].Parameters[paramName] = binding;
481 break;
482 }
483 }
484 m_dirty = true;
485 }
486 }
487
488 ImGui::PopID();
489 }
490
491 // Add parameter section
492 ImGui::Separator();
493 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Add Parameter:");
494
495 static char paramNameBuf[256] = "";
496 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 80.0f);
497 ImGui::InputText("##new_param_name", paramNameBuf, sizeof(paramNameBuf), ImGuiInputTextFlags_CharsNoBlank);
498 ImGui::SameLine();
499 if (ImGui::Button("Add", ImVec2(70.0f, 0.0f)))
500 {
501 std::string newParamName(paramNameBuf);
502 if (!newParamName.empty() && def.Parameters.find(newParamName) == def.Parameters.end())
503 {
504 // Create new parameter with default Literal binding
505 ParameterBinding newBinding;
507 newBinding.LiteralValue = TaskValue("");
508
510
511 // Keep template in sync
512 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
513 {
514 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
515 {
517 break;
518 }
519 }
520
521 m_dirty = true;
522 paramNameBuf[0] = '\0'; // Clear the input field
523 }
524 }
525}
526
527// ============================================================================
528// While Node Properties (LEGACY - NO LONGER USED)
529// ============================================================================
530// This function is DEPRECATED. While nodes now use RenderBranchNodeProperties()
531// with the Phase 24 NodeConditionsPanel system for consistency with Branch nodes.
532// Kept for reference only.
533//
534// void VisualScriptEditorPanel::RenderWhileNodeProperties()
535// {
536// ... (removed legacy code - see git history for old implementation)
537// }
538
539// ============================================================================
540// ForEach Node Properties
541// ============================================================================
542
544{
545 if (m_selectedNodeID < 0)
546 return;
547
548 // Find the selected node in the template
549 TaskNodeDefinition* nodePtr = nullptr;
550 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
551 {
552 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
553 {
555 break;
556 }
557 }
558
559 if (!nodePtr || nodePtr->Type != TaskNodeType::ForEach)
560 return;
561
562 ImGui::TextDisabled("ForEach Loop Configuration");
563 ImGui::Separator();
564
565 // Display node name
566 static char nodeName[256] = "";
567 strncpy_s(nodeName, sizeof(nodeName), nodePtr->NodeName.c_str(),
568 sizeof(nodeName) - 1);
569
570 if (ImGui::InputText("##foreach_name", nodeName, sizeof(nodeName)))
571 {
572 nodePtr->NodeName = nodeName;
573 m_dirty = true;
574 }
575
576 ImGui::Separator();
577 ImGui::TextDisabled("Loop Configuration");
578 ImGui::Separator();
579
580 // Display available blackboard variables as suggestions
581 ImGui::TextDisabled("Suggested List Variables:");
582 ImGui::BeginChild("##foreach_bb_list", ImVec2(0, 100), true);
583 for (size_t i = 0; i < m_template.Blackboard.size(); ++i)
584 {
586
587 // Only show List-type variables
588 if (entry.Type == VariableType::List)
589 {
590 ImGui::TextDisabled("%s (List)", entry.Key.c_str());
591 }
592 }
593 ImGui::EndChild();
594
595 ImGui::TextDisabled("(ForEach node specific parameters pending implementation)");
596}
597
598// ============================================================================
599// SubGraph Node Properties
600// ============================================================================
601
603{
604 if (m_selectedNodeID < 0)
605 return;
606
607 // Find the selected node in the template
608 TaskNodeDefinition* nodePtr = nullptr;
609 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
610 {
611 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
612 {
614 break;
615 }
616 }
617
618 if (!nodePtr || nodePtr->Type != TaskNodeType::SubGraph)
619 return;
620
621 // ========================================================================
622 // SubGraph File Path - stored as editable parameter (Phase 24)
623 // ========================================================================
624 ImGui::TextDisabled("SubGraph File");
625 ImGui::Separator();
626
627 // Store path as a parameter binding for true serialization
628 // This ensures the path is saved with the graph in JSON format
629 ParameterBinding* pathBinding = nullptr;
630 auto pathIt = nodePtr->Parameters.find("subgraph_path");
631 if (pathIt == nodePtr->Parameters.end())
632 {
633 // Create the binding ONLY on first edit - initialize with current SubGraphPath value
634 // Don't create an empty binding upfront; let the input field manage the value
637
638 // Get the current value from SubGraphPath (may be empty)
639 std::string initialPath = nodePtr->SubGraphPath;
640 newBinding.LiteralValue = TaskValue(initialPath);
641
642 nodePtr->Parameters["subgraph_path"] = newBinding;
643 pathIt = nodePtr->Parameters.find("subgraph_path");
644 }
645 pathBinding = &pathIt->second;
646
647 // Display path as editable text field
648 // Use non-static buffer to ensure each node has independent state
649 std::string currentPath = pathBinding->LiteralValue.to_string();
650
651 // Use ImGui InputText with manual buffer management
652 static std::unordered_map<int, std::string> pathBufferCache; // Cache per nodeID
654 {
655 pathBufferCache[m_selectedNodeID] = currentPath;
656 }
658 char pathBufferArray[512] = "";
660 sizeof(pathBufferArray) - 1);
661
662 ImGui::SetNextItemWidth(-80.0f);
663 bool pathChanged = ImGui::InputText("##subgraph_path_input", pathBufferArray, sizeof(pathBufferArray));
664
665 if (pathChanged || ImGui::IsItemDeactivatedAfterEdit())
666 {
667 std::string newPath(pathBufferArray);
668
669 // PHASE 26 FIX: Extract relative path if user enters full path
670 // If path starts with "Blueprints/", extract relative portion
671 const std::string blueprintsPrefix = "Blueprints/";
672 const std::string blueprintsPrefixWin = "Blueprints\\";
673
674 if (newPath.find(blueprintsPrefix) == 0)
675 {
676 newPath = newPath.substr(blueprintsPrefix.length());
677 }
678 else if (newPath.find(blueprintsPrefixWin) == 0)
679 {
680 newPath = newPath.substr(blueprintsPrefixWin.length());
681 }
682
683 // Normalize path: forward slashes to backslashes for Windows
684 for (char& c : newPath)
685 {
686 if (c == '/')
687 c = '\\';
688 }
689
690 // Update BOTH storage locations immediately when user types
691 pathBinding->LiteralValue = TaskValue(newPath);
692 nodePtr->SubGraphPath = newPath;
694
695 // CRITICAL FIX: Also update the editor node to keep canvas in sync
696 for (auto& eNode : m_editorNodes)
697 {
698 if (eNode.nodeID == m_selectedNodeID)
699 {
700 eNode.def.SubGraphPath = newPath;
701 // Also update Parameters in the editor node
702 auto editorPathIt = eNode.def.Parameters.find("subgraph_path");
703 if (editorPathIt != eNode.def.Parameters.end())
704 {
705 editorPathIt->second.LiteralValue = TaskValue(newPath);
706 }
707 else
708 {
711 editorBinding.LiteralValue = TaskValue(newPath);
712 eNode.def.Parameters["subgraph_path"] = editorBinding;
713 }
714 break;
715 }
716 }
717
718 // Log the change for debugging
719 SYSTEM_LOG << "[RenderSubGraphNodeProperties] Updated SubGraphPath for node "
720 << nodePtr->NodeID << " = '" << newPath << "'\n";
721
722 m_dirty = true;
723 }
724 else
725 {
726 // Keep cache in sync with buffer content
727 cachedPath = std::string(pathBufferArray);
728 }
729
730 // Browse button for file picker
731 ImGui::SameLine();
732 if (ImGui::Button("Browse##subgraph_browse", ImVec2(75, 0)))
733 {
734 m_subGraphModal->Open("Blueprints");
735 }
736
737 // Handle modal result
738 m_subGraphModal->Render();
739 if (m_subGraphModal->IsConfirmed())
740 {
741 std::string selectedFile = m_subGraphModal->GetSelectedFile();
742
743 // PHASE 26 FIX: Extract relative path by removing "Blueprints/" prefix
744 // Modal returns full path like "Blueprints/AI/Boss2.ats"
745 // We need to store just "AI/Boss2.ats" for search directory concatenation to work
746 std::string relativePath = selectedFile;
747
748 // Remove "Blueprints/" or "Blueprints\" prefix
749 const std::string blueprintsPrefix = "Blueprints/";
750 const std::string blueprintsPrefixWin = "Blueprints\\";
751
752 if (relativePath.find(blueprintsPrefix) == 0)
753 {
754 relativePath = relativePath.substr(blueprintsPrefix.length());
755 }
756 else if (relativePath.find(blueprintsPrefixWin) == 0)
757 {
759 }
760
761 // Normalize path: forward slashes to backslashes for Windows
762 for (char& c : relativePath)
763 {
764 if (c == '/')
765 c = '\\';
766 }
767
768 // Update path binding and definition in BOTH storage locations
769 pathBinding->LiteralValue = TaskValue(relativePath);
770 nodePtr->SubGraphPath = relativePath;
772
773 // CRITICAL FIX: Also update the editor node to keep canvas in sync
774 // This ensures SyncTemplateFromCanvas() doesn't overwrite the path we just set
775 for (auto& eNode : m_editorNodes)
776 {
777 if (eNode.nodeID == m_selectedNodeID)
778 {
779 eNode.def.SubGraphPath = relativePath;
780 // Also update Parameters in the editor node
781 auto editorPathIt = eNode.def.Parameters.find("subgraph_path");
782 if (editorPathIt != eNode.def.Parameters.end())
783 {
784 editorPathIt->second.LiteralValue = TaskValue(relativePath);
785 }
786 else
787 {
790 editorBinding.LiteralValue = TaskValue(relativePath);
791 eNode.def.Parameters["subgraph_path"] = editorBinding;
792 }
793 SYSTEM_LOG << "[RenderSubGraphNodeProperties] Synchronized SubGraphPath to editor node: "
794 << relativePath << "\n";
795 break;
796 }
797 }
798
799 SYSTEM_LOG << "[RenderSubGraphNodeProperties] Selected SubGraph file: " << relativePath << "\n";
800
801 m_dirty = true;
802 m_subGraphModal->Close();
803 }
804
805 ImGui::Spacing();
806 if (nodePtr->SubGraphPath.empty())
807 {
808 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f),
809 "⚠ SubGraph path is empty - set a valid .ats file");
810 }
811 else
812 {
813 ImGui::TextDisabled("Path: %s", nodePtr->SubGraphPath.c_str());
814 }
815
816 ImGui::Separator();
817 ImGui::Spacing();
818
819 // ========================================================================
820 // Input Parameters Section
821 // Parameters passed from parent graph into this SubGraph.
822 // Select "LocalVariable" to map to a variable in this graph's blackboard,
823 // or "Literal" to pass a constant value.
824 // ========================================================================
825 ImGui::TextDisabled("Input Parameters");
826 ImGui::Separator();
827
828 // Display input parameters with full editor
829 if (!nodePtr->InputParams.empty())
830 {
831 ImGui::BeginChild("##subgraph_params", ImVec2(0, 200), true);
832
833 size_t paramIdx = 0;
834 std::vector<std::string> paramsToDelete;
835
836 for (auto it = nodePtr->InputParams.begin();
837 it != nodePtr->InputParams.end(); ++it, ++paramIdx)
838 {
839 ImGui::PushID(static_cast<int>(paramIdx));
840
841 const std::string& paramName = it->first;
842 ParameterBinding& binding = it->second;
843
844 // Parameter name (yellow, read-only)
845 ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), "%s", paramName.c_str());
846 ImGui::SameLine(150.0f);
847
848 // Binding type selection (Literal vs LocalVariable)
849 const char* bindingTypes[] = { "Literal", "LocalVariable" };
850 int typeIdx = static_cast<int>(binding.Type);
851
852 ImGui::SetNextItemWidth(100.0f);
853 if (ImGui::Combo("##binding_type", &typeIdx, bindingTypes, 2))
854 {
855 binding.Type = static_cast<ParameterBindingType>(typeIdx);
856 m_dirty = true;
857 }
858 ImGui::SameLine();
859
860 // Value editor based on binding type
862 {
863 // Literal value editor - display as text
864 std::string literalValue = binding.LiteralValue.to_string();
865 static char valueBuffer[256] = "";
867 sizeof(valueBuffer) - 1);
868
869 ImGui::SetNextItemWidth(120.0f);
870 if (ImGui::InputText("##param_value", valueBuffer, sizeof(valueBuffer)))
871 {
872 // Update the literal value from text
873 binding.LiteralValue = TaskValue(std::string(valueBuffer));
874 m_dirty = true;
875 }
876 }
878 {
879 // LocalVariable selector - show available BB keys
880 static char varBuffer[256] = "";
881 strncpy_s(varBuffer, sizeof(varBuffer),
882 binding.VariableName.c_str(),
883 sizeof(varBuffer) - 1);
884
885 ImGui::SetNextItemWidth(120.0f);
886 if (ImGui::InputText("##param_var", varBuffer, sizeof(varBuffer)))
887 {
888 binding.VariableName = varBuffer;
889 m_dirty = true;
890 }
891
892 // Show hint with available blackboard keys
893 ImGui::SetNextItemWidth(120.0f);
894 if (ImGui::BeginCombo("##var_hint", "(suggestions)"))
895 {
896 for (size_t bbIdx = 0; bbIdx < m_template.Blackboard.size(); ++bbIdx)
897 {
899 if (ImGui::Selectable(bbEntry.Key.c_str()))
900 {
901 binding.VariableName = bbEntry.Key;
902 m_dirty = true;
903 }
904 }
905 ImGui::EndCombo();
906 }
907 }
908 ImGui::SameLine();
909
910 // Delete button for this parameter
911 if (ImGui::Button("X##del_param", ImVec2(25, 0)))
912 {
913 paramsToDelete.push_back(paramName);
914 }
915
916 ImGui::Separator();
917 ImGui::PopID();
918 }
919
920 ImGui::EndChild();
921
922 // Process deletions (can't delete while iterating)
923 for (const std::string& paramName : paramsToDelete)
924 {
925 nodePtr->InputParams.erase(paramName);
926 m_dirty = true;
927 }
928 }
929 else
930 {
931 ImGui::TextDisabled("(no input parameters - check SubGraph file)");
932 }
933
934 ImGui::Separator();
935
936 // Add new parameter button
937 if (ImGui::Button("+##add_param", ImVec2(25, 0)))
938 {
939 // Generate unique parameter name
940 int idx = 1;
941 std::string newParamName = "param_1";
942 while (nodePtr->InputParams.find(newParamName) != nodePtr->InputParams.end())
943 {
944 ++idx;
945 newParamName = "param_" + std::to_string(idx);
946 }
947
948 // Create new parameter with default Literal binding
951 newBinding.LiteralValue = TaskValue(std::string(""));
952 nodePtr->InputParams[newParamName] = newBinding;
953 m_dirty = true;
954 }
955 ImGui::SameLine();
956 ImGui::TextDisabled("Add Parameter");
957
958 // ========================================================================
959 // Output Parameters Section
960 // Values returned from this SubGraph to the parent graph.
961 // These match the "Return Values" defined in the SubGraph file.
962 // ========================================================================
963
964 ImGui::Spacing();
965 ImGui::Separator();
966 ImGui::TextDisabled("Output Parameters (Return Values)");
967 ImGui::Separator();
968
969 // Display output parameter mappings (SubGraphOutput -> Blackboard Key)
970 if (!nodePtr->OutputParams.empty())
971 {
972 ImGui::BeginChild("##subgraph_outputs", ImVec2(0, 150), true);
973
974 size_t outIdx = 0;
975 std::vector<std::string> outputsToDelete;
976
977 for (auto it = nodePtr->OutputParams.begin();
978 it != nodePtr->OutputParams.end(); ++it, ++outIdx)
979 {
980 ImGui::PushID(static_cast<int>(1000 + outIdx)); // Offset to avoid ID collisions
981
982 const std::string& outputName = it->first;
983 std::string& bbKey = it->second;
984
985 // Output name (cyan, read-only)
986 ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s", outputName.c_str());
987 ImGui::SameLine(150.0f);
988
989 // Blackboard key assignment
990 std::string bbKeyValue = bbKey;
991 static char bbKeyBuffer[256] = "";
992 strncpy_s(bbKeyBuffer, sizeof(bbKeyBuffer), bbKeyValue.c_str(),
993 sizeof(bbKeyBuffer) - 1);
994
995 ImGui::SetNextItemWidth(120.0f);
996 if (ImGui::InputText("##output_bb", bbKeyBuffer, sizeof(bbKeyBuffer)))
997 {
999 m_dirty = true;
1000 }
1001 ImGui::SameLine();
1002
1003 // Suggestions dropdown
1004 ImGui::SetNextItemWidth(120.0f);
1005 if (ImGui::BeginCombo("##out_hint", "(suggestions)"))
1006 {
1007 for (size_t bbIdx = 0; bbIdx < m_template.Blackboard.size(); ++bbIdx)
1008 {
1010 if (ImGui::Selectable(bbEntry.Key.c_str()))
1011 {
1012 bbKey = bbEntry.Key;
1013 m_dirty = true;
1014 }
1015 }
1016 ImGui::EndCombo();
1017 }
1018 ImGui::SameLine();
1019
1020 // Delete button for this output mapping
1021 if (ImGui::Button("X##del_output", ImVec2(25, 0)))
1022 {
1023 outputsToDelete.push_back(outputName);
1024 }
1025
1026 ImGui::Separator();
1027 ImGui::PopID();
1028 }
1029
1030 ImGui::EndChild();
1031
1032 // Process deletions
1033 for (const std::string& outputName : outputsToDelete)
1034 {
1035 nodePtr->OutputParams.erase(outputName);
1036 m_dirty = true;
1037 }
1038 }
1039 else
1040 {
1041 ImGui::TextDisabled("(no output parameters - check SubGraph file)");
1042 }
1043
1044 ImGui::Separator();
1045 ImGui::Spacing();
1046
1047}
1048
1049// ============================================================================
1050// Main Properties panel dispatcher
1051// ============================================================================
1052
1054{
1055 ImGui::TextDisabled("Properties");
1056
1057 if (m_selectedNodeID < 0)
1058 {
1059 ImGui::TextDisabled("(select a node)");
1060 return;
1061 }
1062
1063 // Find the editor node
1064 VSEditorNode* eNode = nullptr;
1065 for (size_t i = 0; i < m_editorNodes.size(); ++i)
1066 {
1067 if (m_editorNodes[i].nodeID == m_selectedNodeID)
1068 {
1069 eNode = &m_editorNodes[i];
1070 break;
1071 }
1072 }
1073 if (eNode == nullptr)
1074 return;
1075
1076 TaskNodeDefinition& def = eNode->def;
1077
1078 // Reset focus-node tracking when the selected node changes.
1079 // Old-value snapshots do NOT need explicit resetting here — they are
1080 // naturally overwritten by the next IsItemActivated() event.
1082
1083 // ---- NodeName (present for all node types) ----
1084 {
1085 char nameBuf[128];
1086 strncpy_s(nameBuf, sizeof(nameBuf), def.NodeName.c_str(), _TRUNCATE);
1087 if (ImGui::InputText("Name##vsname", nameBuf, sizeof(nameBuf)))
1088 {
1089 def.NodeName = nameBuf;
1090 // Sync live to template for immediate canvas display and serialization
1091 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1092 {
1093 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1094 {
1095 m_template.Nodes[i].NodeName = def.NodeName;
1096 break;
1097 }
1098 }
1099 m_dirty = true;
1100 }
1101 if (ImGui::IsItemActivated())
1102 {
1103 m_propEditOldName = def.NodeName;
1105 }
1106 if (ImGui::IsItemDeactivatedAfterEdit() &&
1108 def.NodeName != m_propEditOldName)
1109 {
1111 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
1112 m_selectedNodeID, "NodeName",
1114 PropertyValue::FromString(def.NodeName))),
1115 m_template);
1116 }
1117 }
1118
1119 // ---- Type-specific fields — all buffers are local (non-static) to avoid
1120 // stale data when switching between selected nodes. ----
1121 switch (def.Type)
1122 {
1124 {
1125 // --- AtomicTaskID dropdown ---
1126 const std::vector<TaskSpec> tasks = AtomicTaskUIRegistry::Get().GetSortedForUI();
1127 const std::string& currentTask = def.AtomicTaskID;
1128 const char* previewLabel = currentTask.empty() ? "(select task...)" : currentTask.c_str();
1129
1130 if (ImGui::IsItemActivated())
1131 {
1132 m_propEditOldTaskID = def.AtomicTaskID;
1134 }
1135
1136 if (ImGui::BeginCombo("TaskType##vstask", previewLabel))
1137 {
1138 if (m_propEditOldTaskID != def.AtomicTaskID)
1139 {
1140 m_propEditOldTaskID = def.AtomicTaskID;
1142 }
1143 std::string lastCat;
1144 for (size_t ti = 0; ti < tasks.size(); ++ti)
1145 {
1146 const TaskSpec& spec = tasks[ti];
1147 // Show category header separator when category changes
1148 if (spec.category != lastCat)
1149 {
1150 if (!lastCat.empty())
1151 ImGui::Separator();
1152 ImGui::TextDisabled("%s", spec.category.c_str());
1153 lastCat = spec.category;
1154 }
1155 bool selected = (spec.id == currentTask);
1156 std::string label = " " + spec.displayName + "##" + spec.id;
1157 if (ImGui::Selectable(label.c_str(), selected))
1158 {
1159 const std::string oldTaskID = def.AtomicTaskID;
1160 def.AtomicTaskID = spec.id;
1161 // Auto-fill node name with the action's display name
1162 def.NodeName = spec.displayName;
1163 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1164 {
1165 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1166 {
1167 m_template.Nodes[i].AtomicTaskID = def.AtomicTaskID;
1168 m_template.Nodes[i].NodeName = def.NodeName;
1169 break;
1170 }
1171 }
1172 if (def.AtomicTaskID != oldTaskID)
1173 {
1175 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
1176 m_selectedNodeID, "AtomicTaskID",
1178 PropertyValue::FromString(def.AtomicTaskID))),
1179 m_template);
1180 }
1181 m_dirty = true;
1182 }
1183 if (selected)
1184 ImGui::SetItemDefaultFocus();
1185 // Tooltip with description
1186 if (ImGui::IsItemHovered() && !spec.description.empty())
1187 {
1188 ImGui::BeginTooltip();
1189 ImGui::TextUnformatted(spec.description.c_str());
1190 ImGui::EndTooltip();
1191 }
1192 }
1193 ImGui::EndCombo();
1194 }
1195 break;
1196 }
1198 {
1199 float delay = def.DelaySeconds;
1200 if (ImGui::InputFloat("Delay (s)##vsdelay", &delay, 0.1f, 1.0f))
1201 {
1202 def.DelaySeconds = delay;
1203 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1204 {
1205 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1206 {
1207 m_template.Nodes[i].DelaySeconds = def.DelaySeconds;
1208 break;
1209 }
1210 }
1211 m_dirty = true;
1212 }
1213 if (ImGui::IsItemActivated())
1214 {
1215 m_propEditOldDelay = def.DelaySeconds;
1217 }
1218 if (ImGui::IsItemDeactivatedAfterEdit() &&
1220 def.DelaySeconds != m_propEditOldDelay)
1221 {
1223 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
1224 m_selectedNodeID, "DelaySeconds",
1226 PropertyValue::FromFloat(def.DelaySeconds))),
1227 m_template);
1228 }
1229 break;
1230 }
1232 {
1233 // Phase 24.2: Variable node (data pure read node) - use dedicated renderer
1234 // Variables are rendered with m_variablePanel instead of m_getBBPanel
1235 if (m_variablePanel)
1236 {
1237 m_variablePanel->SetNodeName(def.NodeName);
1238 m_variablePanel->SetTemplate(&m_template);
1239 m_variablePanel->SetBBKey(def.BBKey);
1240
1241 m_variablePanel->Render();
1242
1243 if (m_variablePanel->IsDirty())
1244 {
1245 const std::string oldKey = def.BBKey;
1246 def.BBKey = m_variablePanel->GetBBKey();
1247
1248 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1249 {
1250 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1251 {
1252 m_template.Nodes[i].BBKey = def.BBKey;
1253 break;
1254 }
1255 }
1256
1257 if (def.BBKey != oldKey)
1258 {
1260 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
1261 m_selectedNodeID, "BBKey",
1263 PropertyValue::FromString(def.BBKey))),
1264 m_template);
1265 }
1266 m_variablePanel->ClearDirty();
1267 m_dirty = true;
1268 }
1269 }
1270
1271 // Render node parameters (Phase 24 — node data serialization)
1273 break;
1274 }
1276 {
1277 // Phase 24 Milestone 3: Delegate to dedicated SetBBValue properties renderer
1278 if (m_setBBPanel)
1279 {
1280 m_setBBPanel->SetNodeName(def.NodeName);
1281 m_setBBPanel->SetTemplate(&m_template);
1282 m_setBBPanel->SetBBKey(def.BBKey);
1283
1284 m_setBBPanel->Render();
1285
1286 if (m_setBBPanel->IsDirty())
1287 {
1288 const std::string oldKey = def.BBKey;
1289 def.BBKey = m_setBBPanel->GetBBKey();
1290
1291 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1292 {
1293 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1294 {
1295 m_template.Nodes[i].BBKey = def.BBKey;
1296 break;
1297 }
1298 }
1299
1300 if (def.BBKey != oldKey)
1301 {
1303 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
1304 m_selectedNodeID, "BBKey",
1306 PropertyValue::FromString(def.BBKey))),
1307 m_template);
1308 }
1309 m_setBBPanel->ClearDirty();
1310 m_dirty = true;
1311 }
1312 }
1313
1314 // Render node parameters (Phase 24 — node data serialization)
1316 break;
1317 }
1320 {
1321 // Delegate to the dedicated Phase 24-Rendering branch properties renderer.
1322 // This shows: blue header -> NodeConditionsPanel -> Breakpoint checkbox.
1323 // The return prevents any legacy condition UI from also rendering.
1325 return;
1326 }
1328 {
1329 // Phase 24: Delegate to the dedicated SubGraph properties renderer.
1330 // This shows: blue header -> editable path -> input/output parameter editors.
1331 // The return prevents any legacy UI from also rendering.
1333 return;
1334 }
1336 {
1337 // Phase 24 Milestone 2: Delegate to the dedicated MathOp properties renderer.
1338 // This shows: blue header -> MathOpPropertyPanel -> operand editors.
1340
1341 // Render node parameters (Phase 24 — node data serialization)
1343 return;
1344 }
1346 {
1347 // Sync m_propEditSwitchCases with the node's switchCases when node changes
1349 m_propEditSwitchCases = def.switchCases;
1350
1351 // Find the corresponding template node once for all edits below
1352 TaskNodeDefinition* tmplNode = nullptr;
1353 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1354 {
1355 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1356 {
1358 break;
1359 }
1360 }
1361
1362 // ---- Switch Variable ----
1363 {
1364 // Dropdown populated from Int-typed blackboard variables only
1365 // (Switch node evaluates an Int variable against integer case values)
1366 BBVariableRegistry bbReg;
1367 bbReg.LoadFromTemplate(m_template);
1368 const std::vector<VarSpec> vars = bbReg.GetVariablesByType(VariableType::Int);
1369 const std::string& curVar = def.switchVariable;
1370 const char* previewVar = curVar.empty() ? "(select variable...)" : curVar.c_str();
1371
1372 if (ImGui::BeginCombo("Switch Var##vsswitchvar", previewVar))
1373 {
1374 for (size_t vi = 0; vi < vars.size(); ++vi)
1375 {
1376 const VarSpec& v = vars[vi];
1377 bool selected = (v.name == curVar);
1378 if (ImGui::Selectable(v.displayLabel.c_str(), selected))
1379 {
1380 def.switchVariable = v.name;
1381 if (tmplNode)
1382 tmplNode->switchVariable = def.switchVariable;
1383 m_dirty = true;
1384 }
1385 if (selected)
1386 ImGui::SetItemDefaultFocus();
1387 }
1388 ImGui::EndCombo();
1389 }
1390 }
1391
1392 // ---- Case Editor Button ----
1393 ImGui::Separator();
1394 ImGui::Text("Cases: %zu", def.switchCases.size());
1395 if (ImGui::Button("Edit Cases##vseditswitch", ImVec2(100, 0)))
1396 {
1397 if (!m_switchCaseModal)
1398 m_switchCaseModal = std::make_unique<SwitchCaseEditorModal>();
1399 m_switchCaseModal->Open(def.switchCases);
1400 }
1401
1402 // Render the modal
1404 {
1405 m_switchCaseModal->Render();
1406
1407 // If the user confirmed changes, sync them back
1408 if (m_switchCaseModal->IsConfirmed())
1409 {
1410 def.switchCases = m_switchCaseModal->GetSwitchCases();
1411
1412 // Sync to template
1413 if (tmplNode)
1414 tmplNode->switchCases = def.switchCases;
1415
1416 m_dirty = true;
1417 m_switchCaseModal->Close();
1418 }
1419 }
1420
1421 // ---- Display Current Cases (read-only) ----
1422 if (!def.switchCases.empty())
1423 {
1424 ImGui::Separator();
1425 ImGui::TextDisabled("Current Cases (read-only)");
1426 for (size_t ci = 0; ci < def.switchCases.size(); ++ci)
1427 {
1428 const std::string pinLabel = def.switchCases[ci].pinName
1429 + " (val=" + def.switchCases[ci].value + ")"
1430 + (def.switchCases[ci].customLabel.empty() ? "" : " [" + def.switchCases[ci].customLabel + "]");
1431 ImGui::BulletText("%s", pinLabel.c_str());
1432 }
1433 }
1434 break;
1435 }
1437 {
1438 // Phase 24: Delegate to the dedicated ForEach properties renderer.
1439 // This shows: blue header -> list variable selector -> loop control options.
1441 return;
1442 }
1443 default:
1444 break;
1445 }
1446
1447 // Breakpoint toggle button
1449 if (ImGui::Checkbox("Breakpoint (F9)##vsbp", &hasBP))
1450 {
1453 def.NodeName);
1454 }
1455
1457}
1458
1459// ============================================================================
1460// Alternative Node Properties Panel renderer
1461// ============================================================================
1462
1464{
1465 // Phase 31 — Top panel with tabs: Properties | Nodes
1466 if (ImGui::BeginTabBar("TopPanelTabs", ImGuiTabBarFlags_None))
1467 {
1468 // Tab 0: Properties (node metadata and type-specific properties)
1469 if (ImGui::BeginTabItem("Properties"))
1470 {
1472 ImGui::EndTabItem();
1473 }
1474
1475 // Tab 1: Nodes (available node types for dragging onto canvas)
1476 if (ImGui::BeginTabItem("Nodes"))
1477 {
1479 ImGui::EndTabItem();
1480 }
1481
1482 ImGui::EndTabBar();
1483 }
1484}
1485
1487{
1488 ImGui::TextDisabled("Node Properties");
1489
1490 if (m_selectedNodeID < 0)
1491 {
1492 ImGui::TextDisabled("(select a node)");
1493 return;
1494 }
1495
1496 // Find the editor node
1497 VSEditorNode* eNode = nullptr;
1498 for (size_t i = 0; i < m_editorNodes.size(); ++i)
1499 {
1500 if (m_editorNodes[i].nodeID == m_selectedNodeID)
1501 {
1502 eNode = &m_editorNodes[i];
1503 break;
1504 }
1505 }
1506 if (eNode == nullptr)
1507 return;
1508
1509 TaskNodeDefinition& def = eNode->def;
1510
1511 // ---- ALL node types: standard fields ----
1512 {
1513 // Node Name
1514 char nameBuf[128];
1515 strncpy_s(nameBuf, sizeof(nameBuf), def.NodeName.c_str(), _TRUNCATE);
1516 if (ImGui::InputText("Name##nodeprops_name", nameBuf, sizeof(nameBuf)))
1517 {
1518 def.NodeName = nameBuf;
1519 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1520 {
1521 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1522 {
1523 m_template.Nodes[i].NodeName = def.NodeName;
1524 break;
1525 }
1526 }
1527 m_dirty = true;
1528 }
1529
1530 ImGui::Separator();
1531 }
1532
1533 // ---- Type-specific fields (for non-Branch nodes) ----
1534 // For Branch nodes, the specialized renderer is handled separately
1535 if (def.Type != TaskNodeType::Branch)
1536 {
1537 // Call RenderProperties() which already handles all type-specific fields
1538 // BUT we need to inline it here to avoid infinite recursion / double-rendering
1539 // So instead, render just the critical type-specific parts:
1540
1541 switch (def.Type)
1542 {
1544 {
1545 const std::vector<TaskSpec> tasks = AtomicTaskUIRegistry::Get().GetSortedForUI();
1546 const std::string& currentTask = def.AtomicTaskID;
1547 const char* previewLabel = currentTask.empty() ? "(select task...)" : currentTask.c_str();
1548
1549 ImGui::SetNextItemWidth(-1.0f);
1550 if (ImGui::BeginCombo("Task##nodeprops_task", previewLabel))
1551 {
1552 for (const auto& spec : tasks)
1553 {
1554 bool selected = (spec.id == currentTask);
1555 if (ImGui::Selectable(spec.displayName.c_str(), selected))
1556 {
1557 def.AtomicTaskID = spec.id;
1558 // Auto-fill node name with the action's display name
1559 def.NodeName = spec.displayName;
1560 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1561 {
1562 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1563 {
1564 m_template.Nodes[i].AtomicTaskID = def.AtomicTaskID;
1565 m_template.Nodes[i].NodeName = def.NodeName;
1566 break;
1567 }
1568 }
1569 m_dirty = true;
1570 }
1571 if (selected)
1572 ImGui::SetItemDefaultFocus();
1573 }
1574 ImGui::EndCombo();
1575 }
1576
1577 // Display task parameters
1578 if (!currentTask.empty())
1579 {
1581 if (taskSpec && !taskSpec->parameters.empty())
1582 {
1583 ImGui::Separator();
1584 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Parameters:");
1585
1586 for (const auto& param : taskSpec->parameters)
1587 {
1588 ImGui::PushID(param.name.c_str());
1589
1590 // Build label: parameter name + type hint
1591 std::string label = param.name + " (" + param.type + ")";
1592
1593 // Get current value from def.Parameters if it exists
1594 std::string currentValue = param.defaultValue;
1595 auto paramIt = def.Parameters.find(param.name);
1596 if (paramIt != def.Parameters.end() && paramIt->second.Type == ParameterBindingType::Literal)
1597 {
1598 currentValue = paramIt->second.LiteralValue.AsString();
1599 }
1600
1601 // Display parameter name with description as label
1602 ImGui::TextColored(ImVec4(0.8f, 0.95f, 1.0f, 1.0f), "%s", param.name.c_str());
1603 ImGui::SameLine();
1604
1605 // Add help icon (?) next to parameter name for discoverability
1606 ImGui::TextDisabled("(?)");
1607
1608 // Add tooltip with description if available (on parameter name or help icon)
1609 if (ImGui::IsItemHovered() && !param.description.empty())
1610 {
1611 ImGui::BeginTooltip();
1612 ImGui::TextWrapped("%s", param.description.c_str());
1613 ImGui::Separator();
1614 ImGui::TextDisabled("Type: %s", param.type.c_str());
1615 ImGui::TextDisabled("Default: %s", param.defaultValue.c_str());
1616 ImGui::EndTooltip();
1617 }
1618
1619 // Add description text below the parameter name (smaller, grayed out) for immediate clarity
1620 if (!param.description.empty())
1621 {
1622 ImGui::TextDisabled("%s", param.description.c_str());
1623 }
1624
1625 if (param.type == "Bool")
1626 {
1627 bool value = (currentValue == "true" || currentValue == "1");
1628 ImGui::SetNextItemWidth(-1.0f);
1629 if (ImGui::Checkbox(("##" + param.name + "_input").c_str(), &value))
1630 {
1633 binding.LiteralValue = TaskValue(value);
1634 def.Parameters[param.name] = binding;
1635
1636 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1637 {
1638 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1639 {
1640 m_template.Nodes[i].Parameters[param.name] = binding;
1641 break;
1642 }
1643 }
1644 m_dirty = true;
1645 }
1646 }
1647 else if (param.type == "Int")
1648 {
1649 int value = 0;
1650 try { value = std::stoi(currentValue); } catch (...) {}
1651 ImGui::SetNextItemWidth(-1.0f);
1652 if (ImGui::InputInt(("##" + param.name + "_input").c_str(), &value))
1653 {
1656 binding.LiteralValue = TaskValue(value);
1657 def.Parameters[param.name] = binding;
1658
1659 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1660 {
1661 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1662 {
1663 m_template.Nodes[i].Parameters[param.name] = binding;
1664 break;
1665 }
1666 }
1667 m_dirty = true;
1668 }
1669 }
1670 else if (param.type == "Float")
1671 {
1672 float value = 0.0f;
1673 try { value = std::stof(currentValue); } catch (...) {}
1674 ImGui::SetNextItemWidth(-1.0f);
1675 if (ImGui::InputFloat(("##" + param.name + "_input").c_str(), &value, 0.1f))
1676 {
1679 binding.LiteralValue = TaskValue(value);
1680 def.Parameters[param.name] = binding;
1681
1682 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1683 {
1684 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1685 {
1686 m_template.Nodes[i].Parameters[param.name] = binding;
1687 break;
1688 }
1689 }
1690 m_dirty = true;
1691 }
1692 }
1693 else if (param.type == "String")
1694 {
1695 static char buffer[512] = {0};
1696 strncpy_s(buffer, currentValue.c_str(), sizeof(buffer) - 1);
1697 buffer[sizeof(buffer) - 1] = '\0';
1698
1699 ImGui::SetNextItemWidth(-1.0f);
1700 if (ImGui::InputText(("##" + param.name + "_input").c_str(), buffer, sizeof(buffer)))
1701 {
1704 binding.LiteralValue = TaskValue(std::string(buffer));
1705 def.Parameters[param.name] = binding;
1706
1707 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1708 {
1709 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1710 {
1711 m_template.Nodes[i].Parameters[param.name] = binding;
1712 break;
1713 }
1714 }
1715 m_dirty = true;
1716 }
1717 }
1718
1719 ImGui::Spacing();
1720 ImGui::PopID();
1721 }
1722 }
1723 }
1724 break;
1725 }
1726
1728 {
1729 float delay = def.DelaySeconds;
1730 if (ImGui::InputFloat("Delay (s)##nodeprops_delay", &delay, 0.1f, 1.0f))
1731 {
1732 def.DelaySeconds = delay;
1733 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1734 {
1735 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1736 {
1737 m_template.Nodes[i].DelaySeconds = def.DelaySeconds;
1738 break;
1739 }
1740 }
1741 m_dirty = true;
1742 }
1743 break;
1744 }
1745
1747 {
1748 // Phase 1: Delegate to dedicated Switch properties renderer
1750 return;
1751 }
1752
1755 {
1756 const char* nodeType = (def.Type == TaskNodeType::GetBBValue) ? "Get" : "Set";
1757 ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "%s Blackboard Value", nodeType);
1758 ImGui::Separator();
1759
1760 // BBKey dropdown selector from local blackboard variables
1763 const std::vector<VarSpec> allVars = bbReg.GetAllVariables();
1764
1765 const char* previewLabel = def.BBKey.empty() ? "(select variable...)" : def.BBKey.c_str();
1766
1767 ImGui::SetNextItemWidth(-1.0f);
1768 if (ImGui::BeginCombo("Blackboard Variable##bbkey_combo", previewLabel))
1769 {
1770 for (const auto& var : allVars)
1771 {
1772 bool selected = (var.name == def.BBKey);
1773 if (ImGui::Selectable(var.displayLabel.c_str(), selected))
1774 {
1775 const std::string oldBBKey = def.BBKey;
1776 def.BBKey = var.name;
1777
1778 // Sync to template
1779 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1780 {
1781 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1782 {
1783 m_template.Nodes[i].BBKey = def.BBKey;
1784 break;
1785 }
1786 }
1787
1788 // Push undo command if changed
1789 if (def.BBKey != oldBBKey)
1790 {
1792 std::unique_ptr<ICommand>(new EditNodePropertyCommand(
1793 m_selectedNodeID, "BBKey",
1796 m_template);
1797 }
1798 m_dirty = true;
1799 }
1800 if (selected)
1801 ImGui::SetItemDefaultFocus();
1802 }
1803 ImGui::EndCombo();
1804 }
1805
1806 // Display node parameters (common for data nodes)
1807 if (!def.Parameters.empty())
1808 {
1809 ImGui::Separator();
1810 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Parameters:");
1812 }
1813
1814 break;
1815 }
1816
1818 {
1819 ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Math Operation");
1820 ImGui::Separator();
1821
1822 // MathOpRef operator editor
1823 static const char* operators[] = { "+", "-", "*", "/", "%", "^" };
1824 static int operatorIdx = 0;
1825
1826 if (!def.mathOpRef.mathOperator.empty())
1827 {
1828 for (int i = 0; i < 6; ++i)
1829 {
1830 if (def.mathOpRef.mathOperator == operators[i])
1831 {
1832 operatorIdx = i;
1833 break;
1834 }
1835 }
1836 }
1837
1838 ImGui::SetNextItemWidth(-1.0f);
1839 if (ImGui::Combo("Operator##mathop", &operatorIdx, operators, 6))
1840 {
1842 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1843 {
1844 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1845 {
1846 m_template.Nodes[i].mathOpRef.mathOperator = operators[operatorIdx];
1847 break;
1848 }
1849 }
1850 m_dirty = true;
1851 }
1852
1853 // Display operation preview with actual operand values
1854 ImGui::Separator();
1855 ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), "Operation:");
1856 ImGui::SameLine();
1857
1858 // Build left operand display string
1859 std::string leftStr = "A";
1863 leftStr = "[" + def.mathOpRef.leftOperand.variableName + "]";
1865 leftStr = "[Pin]";
1866
1867 // Build right operand display string
1868 std::string rightStr = "B";
1872 rightStr = "[" + def.mathOpRef.rightOperand.variableName + "]";
1874 rightStr = "[Pin]";
1875
1876 // Display final operation string in bright green
1877 ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.5f, 1.0f),
1878 "%s %s %s",
1879 leftStr.c_str(),
1880 def.mathOpRef.mathOperator.empty() ? "?" : def.mathOpRef.mathOperator.c_str(),
1881 rightStr.c_str());
1882
1883 // Display node parameters
1884 if (!def.Parameters.empty())
1885 {
1886 ImGui::Separator();
1887 ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Custom Parameters:");
1889 }
1890
1891 break;
1892 }
1893
1895 {
1896 // Phase 24: Delegate to the dedicated SubGraph properties renderer
1898 break;
1899 }
1900
1902 {
1903 // Phase 24: Use NodeConditionsPanel like Branch (not legacy RenderWhileNodeProperties)
1905 return;
1906 }
1907
1909 {
1910 // Phase 24: Delegate to the dedicated ForEach properties renderer
1912 break;
1913 }
1914
1918 {
1919 ImGui::TextDisabled("Control flow node");
1920 break;
1921 }
1922
1923 default:
1924 ImGui::TextDisabled("(type-specific properties)");
1925 break;
1926 }
1927
1928 ImGui::Separator();
1929 }
1930
1931 // ---- Branch-specific: Conditions panel ----
1932 if (def.Type == TaskNodeType::Branch)
1933 {
1934 // Update condition panel with current node's data
1936 {
1937 m_conditionsPanel->SetConditionRefs(def.conditionRefs);
1938 m_conditionsPanel->SetConditionOperandRefs(def.conditionOperandRefs);
1939 m_conditionsPanel->SetDynamicPins(def.dynamicPins);
1940 m_conditionsPanel->SetNodeName(def.NodeName);
1942 }
1943
1944 // Render the conditions panel
1945 m_conditionsPanel->Render();
1946
1947 // Check if dirty and sync back to node
1948 if (m_conditionsPanel->IsDirty())
1949 {
1950 def.conditionRefs = m_conditionsPanel->GetConditionRefs();
1951 def.conditionOperandRefs = m_conditionsPanel->GetConditionOperandRefs();
1952 m_conditionsPanel->ClearDirty();
1953 m_dirty = true;
1954
1955 // Sync to template
1956 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
1957 {
1958 if (m_template.Nodes[i].NodeID == m_selectedNodeID)
1959 {
1960 m_template.Nodes[i].conditionRefs = def.conditionRefs;
1961 m_template.Nodes[i].conditionOperandRefs = def.conditionOperandRefs;
1962 break;
1963 }
1964 }
1965 }
1966
1967 ImGui::Separator();
1968 }
1969
1970 // ---- ALL nodes: Breakpoint ----
1972 if (ImGui::Checkbox("Breakpoint (F9)##nodeprops_bp", &hasBP))
1973 {
1976 def.NodeName);
1977 }
1978}
1979
1981{
1982 ImGui::TextColored(ImVec4(0.4f, 0.8f, 0.4f, 1.0f), "Drag nodes to the graph to add them");
1983 ImGui::Separator();
1984
1985 // Search filter
1986 static char searchFilter[256] = "";
1987 ImGui::SetNextItemWidth(-1.0f);
1988 ImGui::InputTextWithHint("##search_nodes", "Search nodes...", searchFilter, sizeof(searchFilter));
1989 ImGui::Separator();
1990
1991 // Define node types by category
1992 struct NodeTypeInfo {
1993 TaskNodeType type;
1994 const char* name;
1995 const char* description;
1996 int category; // category index
1997 };
1998
1999 // Helper array for node type info
2000 const NodeTypeInfo nodeTypesArray[] = {
2001 // Flow Control (category 1)
2002 { TaskNodeType::EntryPoint, "EntryPoint", "Start of the graph", 1 },
2003 { TaskNodeType::VSSequence, "Sequence", "Execute nodes in order", 1 },
2004 { TaskNodeType::Branch, "Branch", "Conditional branching (if-else)", 1 },
2005 { TaskNodeType::Switch, "Switch", "Multi-branch selection", 1 },
2006 { TaskNodeType::While, "While Loop", "Repeat until condition false", 1 },
2007 { TaskNodeType::ForEach, "ForEach", "Iterate over collection", 1 },
2008 { TaskNodeType::DoOnce, "DoOnce", "Execute once (with reset)", 1 },
2009 { TaskNodeType::SubGraph, "SubGraph", "Reference another graph", 1 },
2010
2011 // Tasks (category 2)
2012 { TaskNodeType::AtomicTask, "AtomicTask", "Execute a game task", 2 },
2013
2014 // Blackboard (category 3)
2015 { TaskNodeType::GetBBValue, "GetBBValue", "Read blackboard variable", 3 },
2016 { TaskNodeType::SetBBValue, "SetBBValue", "Write blackboard variable", 3 },
2017
2018 // Math & Logic (category 4)
2019 { TaskNodeType::MathOp, "MathOp", "Perform arithmetic", 4 },
2020
2021 // Advanced (category 5)
2022 { TaskNodeType::Delay, "Delay", "Wait for specified time", 5 },
2023 };
2024
2025 const int numNodeTypes = sizeof(nodeTypesArray) / sizeof(nodeTypesArray[0]);
2026
2027 // Category info: name and index
2028 struct CategoryInfo {
2029 const char* name;
2030 int categoryIndex;
2031 };
2032
2033 const CategoryInfo categories[] = {
2034 { "Flow Control", 1 },
2035 { "Tasks", 2 },
2036 { "Blackboard", 3 },
2037 { "Math & Logic", 4 },
2038 { "Advanced", 5 }
2039 };
2040 const int numCategories = sizeof(categories) / sizeof(categories[0]);
2041
2042 // Helper function for case-insensitive search
2043 auto caseInsensitiveContains = [](const char* text, const char* searchStr) -> bool {
2044 if (searchStr[0] == '\0')
2045 return true;
2046
2047 // Simple case-insensitive substring search
2048 std::string textLower(text);
2049 std::string searchLower(searchStr);
2050 for (size_t i = 0; i < textLower.length(); ++i) {
2051 if (textLower[i] >= 'A' && textLower[i] <= 'Z') {
2052 textLower[i] = textLower[i] + ('a' - 'A');
2053 }
2054 }
2055 for (size_t i = 0; i < searchLower.length(); ++i) {
2056 if (searchLower[i] >= 'A' && searchLower[i] <= 'Z') {
2057 searchLower[i] = searchLower[i] + ('a' - 'A');
2058 }
2059 }
2060 return textLower.find(searchLower) != std::string::npos;
2061 };
2062
2063 // Render node list with hierarchical categories
2064 ImGui::BeginChild("NodeList", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false, ImGuiWindowFlags_AlwaysVerticalScrollbar);
2065
2066 // Push smaller font size for compact display
2067 ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]);
2068
2069 // Iterate through categories
2070 for (int catIdx = 0; catIdx < numCategories; ++catIdx)
2071 {
2072 const CategoryInfo& category = categories[catIdx];
2073
2074 // Count nodes in this category that match search
2075 int matchingCount = 0;
2076 for (int i = 0; i < numNodeTypes; ++i)
2077 {
2078 if (nodeTypesArray[i].category == category.categoryIndex &&
2080 {
2081 matchingCount++;
2082 }
2083 }
2084
2085 // Skip category if no matching nodes
2086 if (matchingCount == 0)
2087 continue;
2088
2089 // Render collapsible category header (compact style)
2090 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 2.0f));
2091 bool categoryOpen = ImGui::CollapsingHeader(category.name, ImGuiTreeNodeFlags_DefaultOpen);
2092 ImGui::PopStyleVar();
2093
2094 if (categoryOpen)
2095 {
2096 // Render nodes in this category
2097 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 0.0f));
2098 for (int i = 0; i < numNodeTypes; ++i)
2099 {
2101
2102 // Check category match
2103 if (nodeInfo.category != category.categoryIndex)
2104 continue;
2105
2106 // Check search filter
2108 continue;
2109
2110 // Render node as selectable item with drag-drop support
2111 ImGui::Indent(10.0f);
2112 bool selected = false;
2113 ImGui::Selectable(nodeInfo.name, &selected, 0, ImVec2(0, 16));
2114
2115 // Drag-drop source for node type
2116 if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None))
2117 {
2118 // Store the node type as payload (ENUM format)
2119 uint8_t nodeTypeInt = static_cast<uint8_t>(nodeInfo.type);
2120 ImGui::SetDragDropPayload("VS_NODE_TYPE_ENUM", &nodeTypeInt, sizeof(uint8_t));
2121
2122 // Preview
2123 ImGui::Text("Node: %s", nodeInfo.name);
2124 ImGui::EndDragDropSource();
2125 }
2126
2127 // Tooltip on hover
2128 if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort))
2129 {
2130 ImGui::SetTooltip("%s", nodeInfo.description);
2131 }
2132
2133 ImGui::Unindent(10.0f);
2134 }
2135 ImGui::PopStyleVar();
2136 }
2137 }
2138
2139 ImGui::PopFont();
2140 ImGui::EndChild();
2141
2142 // Tip text at bottom
2143 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 0.7f));
2144 ImGui::TextWrapped("Tip: Drag & drop nodes onto the graph canvas");
2145 ImGui::PopStyleColor();
2146}
2147
2148} // 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).
static AtomicTaskUIRegistry & Get()
Returns the singleton instance.
std::vector< TaskSpec > GetSortedForUI() const
Returns all tasks sorted by category then displayName, suitable for building a dropdown or combo box.
const TaskSpec * GetTaskSpec(const std::string &id) const
Returns the TaskSpec for the given id, or nullptr if not found.
Non-singleton registry populated from the active TaskGraphTemplate.
void LoadFromTemplate(const TaskGraphTemplate &tmpl)
Rebuilds the registry from the blackboard entries of a template.
void ToggleBreakpoint(int graphID, int nodeID, const std::string &graphName="", const std::string &nodeName="")
Toggles the breakpoint at (graphID, nodeID).
static DebugController & Get()
Returns the singleton instance (Meyers pattern).
bool HasBreakpoint(int graphID, int nodeID) const
Returns true if an enabled breakpoint exists at (graphID, nodeID).
Records a property edit on a single node for undo/redo.
std::vector< TaskNodeDefinition > Nodes
All graph nodes.
std::string Name
Friendly name of this template (e.g. "PatrolBehaviour")
std::vector< BlackboardEntry > Blackboard
Local blackboard declared in this graph.
C++14-compliant type-safe value container for task parameters.
void PushCommand(std::unique_ptr< ICommand > cmd, TaskGraphTemplate &graph)
Executes the command on graph, then pushes it onto the undo stack.
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.
void RenderSwitchNodeProperties(VSEditorNode &eNode, TaskNodeDefinition &def)
Renders the Properties panel content for a selected Switch node (Phase 1).
std::unique_ptr< VariablePropertyPanel > m_variablePanel
Properties-panel sub-widget for the selected Variable node (data pure).
TaskGraphTemplate m_template
The template currently being edited.
void RenderAvailableNodesList()
Phase 31 — Nodes list tab in Part A (available nodes for dragging to canvas)
void RenderNodePropertiesPanelContent()
Phase 31 — Content of Properties tab in Part A.
std::unique_ptr< MathOpPropertyPanel > m_mathOpPanel
Properties-panel sub-widget for the selected MathOp node.
std::unique_ptr< SwitchCaseEditorModal > m_switchCaseModal
Phase 26 — Switch Case Editor Modal.
int m_condPanelNodeID
ID of the node currently loaded into m_conditionsPanel (-1 = none).
std::unique_ptr< NodeConditionsPanel > m_conditionsPanel
Properties-panel sub-widget for the selected Branch node.
std::unique_ptr< SubGraphFilePickerModal > m_subGraphModal
Phase 26 — SubGraph File Picker Modal.
std::vector< SwitchCaseDefinition > m_propEditSwitchCases
Per-case label edit buffers.
void RenderNodePropertiesPanel()
Part A: Node Properties panel (top-left of right panel)
void RenderMathOpNodeProperties(VSEditorNode &eNode, TaskNodeDefinition &def)
Renders the Properties panel content for a selected MathOp node.
void RenderBranchNodeProperties(VSEditorNode &eNode, TaskNodeDefinition &def)
Renders the Properties panel content for a selected Branch (or While) node.
std::unique_ptr< SetBBValuePropertyPanel > m_setBBPanel
Properties-panel sub-widget for the selected SetBBValue node.
void RenderVerificationPanel()
Renders the verification results panel (Phase 21-B).
int m_propEditNodeIDOnFocus
Node ID that was selected when RenderProperties() last entered focus.
void RenderNodeDataParameters(TaskNodeDefinition &def)
Renders node parameters for data nodes (GetBBValue, SetBBValue, MathOp).
std::vector< VSEditorNode > m_editorNodes
Editor nodes (mirrors m_template.Nodes + position/selection state)
int m_selectedNodeID
Currently selected node (for properties panel)
ConditionPresetRegistry m_presetRegistry
Global registry of ConditionPreset objects.
std::string m_propEditOldName
Snapshot values captured at focus time for each editable field.
< Provides AssetID and INVALID_ASSET_ID
@ Int
32-bit signed integer
@ List
std::vector<TaskValue> (used by ForEach node)
ParameterBindingType
Describes how a parameter value is provided to a task node.
@ 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)
TaskNodeType
Identifies the role of a node in the task graph.
@ Selector
Executes children in order; stops on first success.
@ AtomicTask
Leaf node that executes a single atomic task.
@ While
Conditional loop (Loop / Completed exec outputs)
@ Sequence
Executes children in order; stops on first failure.
@ SubGraph
Sub-graph call (SubTask)
@ DoOnce
Single-fire execution (reset via Reset pin)
@ Delay
Timer (Completed exec output after N seconds)
@ Parallel
Executes all children simultaneously.
@ 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)
Single entry in the graph's declared blackboard schema (local or global).
@ Variable
References a blackboard variable by name.
@ Const
Literal constant value.
@ Pin
External data-input pin on the owning node.
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: "+", "-", "*", "/", "%", "^".
MathOpOperand rightOperand
Right-hand side operand (B)
Describes how a single parameter value is supplied to a task node.
ParameterBindingType Type
Binding mode.
static PropertyValue FromFloat(float f)
static PropertyValue FromString(const std::string &s)
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 BBKey
For GetBBValue/SetBBValue: BB key (scope:key)
std::string AtomicTaskID
Atomic task type identifier (used when Type == AtomicTask)
std::string switchVariable
For Switch: BB key of the variable to switch on.
TaskNodeType Type
Node role.
std::vector< ConditionRef > conditionOperandRefs
Parallel to conditions[]: each entry stores the OperandRef->DynamicDataPin UUID mapping for the corre...
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.
Display metadata for a single atomic task type.
Editor-side representation of a node in the VS graph canvas.
#define SYSTEM_LOG