Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
VisualScriptNodeRenderer.cpp
Go to the documentation of this file.
1/**
2 * @file VisualScriptNodeRenderer.cpp
3 * @brief Implementation of VS node rendering helpers (Phase 5).
4 * @author Olympe Engine
5 * @date 2026-03-09
6 *
7 * @details C++14 compliant.
8 */
9
11#include "../system/system_consts.h"
13#include "ConditionRef.h"
14
15#include "../third_party/imgui/imgui.h"
16#include "../third_party/imnodes/imnodes.h"
17
18#include <string>
19#include <vector>
20#include <unordered_set>
21
22namespace Olympe {
23
24
45
46unsigned int GetNodeTitleColor(VSNodeStyle style)
47{
48 switch (style)
49 {
50 case VSNodeStyle::EntryPoint: return IM_COL32(30, 140, 60, 255); // Green
51 case VSNodeStyle::FlowControl: return IM_COL32(40, 80, 160, 255); // Blue
52 case VSNodeStyle::Action: return IM_COL32(160, 90, 30, 255); // Orange
53 case VSNodeStyle::Data: return IM_COL32(100, 40, 140, 255); // Purple
54 case VSNodeStyle::SubGraph: return IM_COL32(30, 120, 120, 255); // Teal
55 case VSNodeStyle::Delay: return IM_COL32(160, 140, 20, 255); // Yellow
56 default: return IM_COL32(80, 80, 80, 255);
57 }
58}
59
61{
62 switch (style)
63 {
64 case VSNodeStyle::EntryPoint: return IM_COL32(50, 180, 80, 255);
65 case VSNodeStyle::FlowControl: return IM_COL32(60, 110, 200, 255);
66 case VSNodeStyle::Action: return IM_COL32(200, 120, 50, 255);
67 case VSNodeStyle::Data: return IM_COL32(130, 60, 180, 255);
68 case VSNodeStyle::SubGraph: return IM_COL32(50, 160, 160, 255);
69 case VSNodeStyle::Delay: return IM_COL32(200, 180, 40, 255);
70 default: return IM_COL32(110, 110, 110, 255);
71 }
72}
73
74unsigned int GetExecPinColor()
75{
77}
78
79unsigned int GetDataPinColor(VariableType type)
80{
81 // All data pins use the same violet color regardless of type
83}
84
85
86
88{
89 switch (type)
90 {
91 case TaskNodeType::EntryPoint: return "EntryPoint";
92 case TaskNodeType::Branch: return "Branch";
93 case TaskNodeType::VSSequence: return "Sequence";
94 case TaskNodeType::While: return "While";
95 case TaskNodeType::ForEach: return "ForEach";
96 case TaskNodeType::DoOnce: return "DoOnce";
97 case TaskNodeType::Switch: return "Switch";
98 case TaskNodeType::AtomicTask: return "AtomicTask";
99 case TaskNodeType::GetBBValue: return "GetBBValue";
100 case TaskNodeType::SetBBValue: return "SetBBValue";
101 case TaskNodeType::MathOp: return "MathOp";
102 case TaskNodeType::SubGraph: return "SubGraph";
103 case TaskNodeType::Delay: return "Delay";
104 case TaskNodeType::Sequence: return "Sequence(BT)";
105 case TaskNodeType::Root: return "Root";
106 default: return "Unknown";
107 }
108}
109
111{
112 switch (type)
113 {
114 case VariableType::Bool: return "Bool";
115 case VariableType::Int: return "Int";
116 case VariableType::Float: return "Float";
117 case VariableType::Vector: return "Vector";
118 case VariableType::EntityID: return "EntityID";
119 case VariableType::String: return "String";
120 default: return "None";
121 }
122}
123
124// ============================================================================
125// Helper: Build condition expression display string
126// ============================================================================
127
128/**
129 * @brief Builds a display string for a ConditionRef operand.
130 *
131 * Format examples:
132 * - Variable mode: "[health]"
133 * - Const mode: "[50]"
134 * - Pin mode: "[pin]"
135 */
137{
138 std::string result = "[";
139
141 {
142 result += operand.variableName;
143 }
144 else if (operand.mode == OperandRef::Mode::Const)
145 {
146 result += operand.constValue;
147 }
148 else if (operand.mode == OperandRef::Mode::Pin)
149 {
150 result += "pin";
151 }
152
153 result += "]";
154 return result;
155}
156
157/**
158 * @brief Builds the complete condition expression string.
159 *
160 * Example: "[health] > [50]"
161 */
163{
164 std::string left = GetOperandDisplayString(condition.leftOperand);
165 std::string right = GetOperandDisplayString(condition.rightOperand);
166
167 return left + " " + condition.operatorStr + " " + right;
168}
169
170// ============================================================================
171// Phase 26-B: Node Sizing Helpers (with invisible spacer approach)
172// ============================================================================
173
174/**
175 * @brief Calculates the width of a Switch case output pin label.
176 *
177 * Format: "Case_N [customLabel(value)]" or "Case_N (value)" or "Case_N"
178 * This includes the full decorated label that might be wider than just the pin name.
179 */
181 const std::string& baseName,
183{
184 std::string displayLabel = baseName;
185 if (!caseData.customLabel.empty() && !caseData.value.empty())
186 {
187 displayLabel = baseName + " [" + caseData.customLabel + "(" + caseData.value + ")]";
188 }
189 else if (!caseData.value.empty())
190 {
191 displayLabel = baseName + " (" + caseData.value + ")";
192 }
193
194 ImVec2 textSize = ImGui::CalcTextSize(displayLabel.c_str());
195 return textSize.x;
196}
197
198/**
199 * @brief Calculates the minimum width required for a node to display all content.
200 *
201 * Phase 26-B: Calculate node frame width needed to accommodate all pin labels
202 * without truncation. Uses an invisible spacer button to force ImNodes to size
203 * the node frame appropriately.
204 *
205 * @return Width in pixels needed for the node (minimum guaranteed width)
206 */
208 const TaskNodeDefinition& def,
209 const std::vector<std::string>& execInputPins,
210 const std::vector<std::string>& execOutputPins,
211 const std::vector<std::pair<std::string, VariableType>>& dataInputPins,
212 const std::vector<std::pair<std::string, VariableType>>& dataOutputPins)
213{
214 const float PIN_ICON_WIDTH = 10.0f;
215 const float TEXT_SPACING = 4.0f;
216 const float COLUMN_SEPARATOR = 20.0f;
217 const float MIN_COLUMN_WIDTH = 60.0f;
218 const float MIN_NODE_WIDTH = 150.0f;
219
220 // Measure left column (exec input + data input + dynamic pins)
221 float leftMaxWidth = 0.0f;
222
223 for (size_t i = 0; i < execInputPins.size(); ++i)
224 {
225 ImVec2 textSize = ImGui::CalcTextSize(execInputPins[i].c_str());
228 }
229
230 for (size_t i = 0; i < dataInputPins.size(); ++i)
231 {
232 ImVec2 textSize = ImGui::CalcTextSize(dataInputPins[i].first.c_str());
235 }
236
237 // Dynamic pins (Branch/While conditions)
239 {
240 for (size_t i = 0; i < def.dynamicPins.size(); ++i)
241 {
242 std::string lbl = def.dynamicPins[i].GetDisplayLabel();
243 ImVec2 textSize = ImGui::CalcTextSize(lbl.c_str());
246 }
247 }
248
250
251 // Measure right column (exec output + data output)
252 float rightMaxWidth = 0.0f;
253
254 for (size_t i = 0; i < execOutputPins.size(); ++i)
255 {
256 float labelWidth = 0.0f;
257
258 // For Switch nodes, measure the full decorated label
259 if (def.Type == TaskNodeType::Switch && i < def.switchCases.size())
260 {
262 }
263 else
264 {
265 ImVec2 textSize = ImGui::CalcTextSize(execOutputPins[i].c_str());
267 }
268
269 // Add pin icon and spacing
271
272 // Add space for remove button if this is a dynamic pin
273 const bool hasDynamicPins = (def.Type == TaskNodeType::VSSequence ||
275 const int numStaticPins = hasDynamicPins
276 ? static_cast<int>(execOutputPins.size()) -
277 static_cast<int>(def.DynamicExecOutputPins.size())
278 : static_cast<int>(execOutputPins.size());
279
280 if (hasDynamicPins && static_cast<int>(i) >= numStaticPins)
281 {
282 totalWidth += 35.0f; // Space for [-] button
283 }
284
286 }
287
288 for (size_t i = 0; i < dataOutputPins.size(); ++i)
289 {
290 ImVec2 textSize = ImGui::CalcTextSize(dataOutputPins[i].first.c_str());
293 }
294
296
297 // Total node width: left column + separator + right column
299
300 // Also consider title width
301 ImVec2 titleSize = ImGui::CalcTextSize(def.NodeName.c_str());
302 float titleWidth = titleSize.x + 20.0f; // +20 for padding/icons in title bar
303
304 // Node width = max(title, content)
306
307 // Enforce minimum
310
311 return minimumNodeWidth;
312}
313
314// ============================================================================
315// VisualScriptNodeRenderer
316// ============================================================================
317
319 int nodeUID,
320 int nodeID,
321 int graphID,
322 const std::string& nodeName,
323 TaskNodeType type,
324 bool hasBreakpoint,
325 bool isActive,
326 const std::vector<std::string>& execInputPins,
327 const std::vector<std::string>& execOutputPins,
328 const std::vector<std::pair<std::string, VariableType>>& dataInputPins,
329 const std::vector<std::pair<std::string, VariableType>>& dataOutputPins,
330 const std::unordered_set<int>& connectedAttrIDs)
331{
332 (void)nodeID; (void)graphID; // reserved for future per-node UID derivation
333
334 VSNodeStyle style = GetNodeStyle(type);
335
336 // Apply colour styles
337 unsigned int titleCol = hasBreakpoint
338 ? IM_COL32(200, 30, 30, 255)
339 : GetNodeTitleColor(style);
340 unsigned int titleHoveredCol = hasBreakpoint
341 ? IM_COL32(240, 50, 50, 255)
343 // Keep title color consistent when selected (don't change to yellow)
344 unsigned int titleSelectedCol = titleCol;
345
346 ImNodes::PushColorStyle(ImNodesCol_TitleBar, titleCol);
347 ImNodes::PushColorStyle(ImNodesCol_TitleBarHovered, titleHoveredCol);
348 ImNodes::PushColorStyle(ImNodesCol_TitleBarSelected, titleSelectedCol);
349
350 if (isActive)
351 {
352 ImNodes::PushColorStyle(ImNodesCol_NodeOutline, IM_COL32(80, 255, 80, 255));
353 }
354
355 ImNodes::BeginNode(nodeUID);
356
357 // Title bar
358 ImNodes::BeginNodeTitleBar();
359 ImGui::TextUnformatted(nodeName.c_str());
360 ImNodes::EndNodeTitleBar();
361
362 // Phase 26-B: Add invisible spacer to force node width adaptation
363 // This makes ImNodes size the node frame to accommodate all pin labels
364 float minNodeWidth = 150.0f; // Minimum baseline
365
366 // Calculate width from title
367 ImVec2 titleSize = ImGui::CalcTextSize(nodeName.c_str());
368 float titleWidth = titleSize.x + 20.0f;
370
371 // Calculate width from pins (simplified for basic overload)
372 const float PIN_ICON_WIDTH = 10.0f;
373 const float TEXT_SPACING = 4.0f;
374 float maxPinLabelWidth = 0.0f;
375
376 for (size_t i = 0; i < execInputPins.size(); ++i)
377 {
378 ImVec2 textSize = ImGui::CalcTextSize(execInputPins[i].c_str());
380 }
381 for (size_t i = 0; i < dataInputPins.size(); ++i)
382 {
383 ImVec2 textSize = ImGui::CalcTextSize(dataInputPins[i].first.c_str());
385 }
386 for (size_t i = 0; i < execOutputPins.size(); ++i)
387 {
388 ImVec2 textSize = ImGui::CalcTextSize(execOutputPins[i].c_str());
390 }
391 for (size_t i = 0; i < dataOutputPins.size(); ++i)
392 {
393 ImVec2 textSize = ImGui::CalcTextSize(dataOutputPins[i].first.c_str());
395 }
396
397 // Add column spacing and icon width
398 float contentWidth = (PIN_ICON_WIDTH + TEXT_SPACING) * 2 + maxPinLabelWidth * 2 + 20.0f;
400
401 // Render invisible spacer to force node width
402 ImGui::InvisibleButton("##node_width_spacer", ImVec2(minNodeWidth - 12.0f, 1.0f));
403
404 // Attribute UIDs use the same scheme as VisualScriptEditorPanel helpers:
405 // nodeUID * 10000 + offset
406 // offset 0–99 -> exec-in (Input)
407 // offset 100–199 -> exec-out (Output)
408 // offset 200–299 -> data-in (Input)
409 // offset 300–399 -> data-out (Output)
410
411 // Use 2-column layout to align input pins (left) with output pins (right) on the same Y
412 ImGui::Columns(2, "node_pins", false);
413 ImGui::SetColumnWidth(0, 80.0f);
414
415 // ---- LEFT COLUMN: Input Pins (Exec + Data) ----
416
417 // Exec input pins (left side triangles)
418 for (size_t i = 0; i < execInputPins.size(); ++i)
419 {
420 int attrID = nodeUID * 10000 + static_cast<int>(i);
421 bool connected = connectedAttrIDs.find(attrID) != connectedAttrIDs.end();
422 ImNodes::PushColorStyle(ImNodesCol_Pin, GetExecPinColor());
423 ImNodes::BeginInputAttribute(attrID, connected ? ImNodesPinShape_TriangleFilled : ImNodesPinShape_Triangle);
424 ImGui::Text("%s", execInputPins[i].c_str());
425 ImNodes::EndInputAttribute();
426 ImNodes::PopColorStyle();
427 }
428
429 // Data input pins (left side circles) — offset 200–299
430 for (size_t i = 0; i < dataInputPins.size(); ++i)
431 {
432 int attrID = nodeUID * 10000 + 200 + static_cast<int>(i);
433 bool connected = connectedAttrIDs.find(attrID) != connectedAttrIDs.end();
434 ImNodes::PushColorStyle(ImNodesCol_Pin,
436 ImNodes::BeginInputAttribute(attrID, connected ? ImNodesPinShape_CircleFilled : ImNodesPinShape_Circle);
437 ImGui::Text("%s", dataInputPins[i].first.c_str());
438 ImNodes::EndInputAttribute();
439 ImNodes::PopColorStyle();
440 }
441
442 // ---- RIGHT COLUMN: Output Pins (Exec + Data) ----
443 ImGui::NextColumn();
444
445 // Exec output pins (right side triangles) — offset 100–199
446 for (size_t i = 0; i < execOutputPins.size(); ++i)
447 {
448 int attrID = nodeUID * 10000 + 100 + static_cast<int>(i);
449 bool connected = connectedAttrIDs.find(attrID) != connectedAttrIDs.end();
450 ImNodes::PushColorStyle(ImNodesCol_Pin, GetExecPinColor());
451 ImNodes::BeginOutputAttribute(attrID, connected ? ImNodesPinShape_TriangleFilled : ImNodesPinShape_Triangle);
452 ImGui::Text("%s", execOutputPins[i].c_str());
453 ImNodes::EndOutputAttribute();
454 ImNodes::PopColorStyle();
455 }
456
457 // Data output pins (right side circles) — offset 300–399
458 for (size_t i = 0; i < dataOutputPins.size(); ++i)
459 {
460 int attrID = nodeUID * 10000 + 300 + static_cast<int>(i);
461 bool connected = connectedAttrIDs.find(attrID) != connectedAttrIDs.end();
462 ImNodes::PushColorStyle(ImNodesCol_Pin,
464 ImNodes::BeginOutputAttribute(attrID, connected ? ImNodesPinShape_CircleFilled : ImNodesPinShape_Circle);
465 ImGui::Text("%s", dataOutputPins[i].first.c_str());
466 ImNodes::EndOutputAttribute();
467 ImNodes::PopColorStyle();
468 }
469
470 ImGui::Columns(1); // End columns
471
472 ImNodes::EndNode();
473
474 if (isActive)
475 ImNodes::PopColorStyle();
476
477 ImNodes::PopColorStyle(); // TitleBarSelected
478 ImNodes::PopColorStyle(); // TitleBarHovered
479 ImNodes::PopColorStyle(); // TitleBar
480}
481
482// ============================================================================
483// VisualScriptNodeRenderer::RenderNode — extended overload with inline params
484// ============================================================================
485
487 int nodeUID,
488 int nodeID,
489 int graphID,
490 const TaskNodeDefinition& def,
491 bool hasBreakpoint,
492 bool isActive,
493 const std::vector<std::string>& execInputPins,
494 const std::vector<std::string>& execOutputPins,
495 const std::vector<std::pair<std::string, VariableType>>& dataInputPins,
496 const std::vector<std::pair<std::string, VariableType>>& dataOutputPins,
497 void (*onAddPin)(int nodeID, void* userData),
498 void* onAddPinUserData,
499 void (*onRemovePin)(int nodeID, int dynamicPinIndex, void* userData),
501 const std::unordered_set<int>& connectedAttrIDs)
502{
503 (void)graphID;
504
505 VSNodeStyle style = GetNodeStyle(def.Type);
506
507 unsigned int titleCol = hasBreakpoint
508 ? IM_COL32(200, 30, 30, 255)
509 : GetNodeTitleColor(style);
510 unsigned int titleHoveredCol = hasBreakpoint
511 ? IM_COL32(240, 50, 50, 255)
513 // Keep title color consistent when selected (don't change to yellow)
514 unsigned int titleSelectedCol = titleCol;
515
516 ImNodes::PushColorStyle(ImNodesCol_TitleBar, titleCol);
517 ImNodes::PushColorStyle(ImNodesCol_TitleBarHovered, titleHoveredCol);
518 ImNodes::PushColorStyle(ImNodesCol_TitleBarSelected, titleSelectedCol);
519
520 if (isActive)
521 ImNodes::PushColorStyle(ImNodesCol_NodeOutline, IM_COL32(80, 255, 80, 255));
522
523 ImNodes::BeginNode(nodeUID);
524
525 // Title bar
526 ImNodes::BeginNodeTitleBar();
527 ImGui::TextUnformatted(def.NodeName.c_str());
528 ImNodes::EndNodeTitleBar();
529
530 // Phase 26-B: Add invisible spacer to force node width adaptation
531 // This makes ImNodes size the node frame to accommodate all pin labels,
532 // including Switch case decorations like "[Idle(100)]"
534 def,
539
540 // Render invisible spacer to force node width
541 ImGui::InvisibleButton("##node_width_spacer", ImVec2(minNodeWidth - 12.0f, 1.0f));
542
543 // Use 2-column layout to align input pins (left) with output pins (right) on the same Y
544 // PINS FIRST for better UX consistency
545 ImGui::Columns(2, "node_pins_extended", false);
546 ImGui::SetColumnWidth(0, 80.0f);
547
548 // ---- LEFT COLUMN: Input Pins (Exec + Data) ----
549
550 // Exec input pins (left side triangles) — offset 0–99
551 for (size_t i = 0; i < execInputPins.size(); ++i)
552 {
553 int attrID = nodeUID * 10000 + static_cast<int>(i);
554 bool connected = connectedAttrIDs.find(attrID) != connectedAttrIDs.end();
555 ImNodes::PushColorStyle(ImNodesCol_Pin, GetExecPinColor());
556 ImNodes::BeginInputAttribute(attrID, connected ? ImNodesPinShape_TriangleFilled : ImNodesPinShape_Triangle);
557 ImGui::Text("%s", execInputPins[i].c_str());
558 ImNodes::EndInputAttribute();
559 ImNodes::PopColorStyle();
560 }
561
562 // Data input pins (left side circles) — offset 200–299
563 for (size_t i = 0; i < dataInputPins.size(); ++i)
564 {
565 int attrID = nodeUID * 10000 + 200 + static_cast<int>(i);
566 bool connected = connectedAttrIDs.find(attrID) != connectedAttrIDs.end();
567 ImNodes::PushColorStyle(ImNodesCol_Pin,
569 ImNodes::BeginInputAttribute(attrID, connected ? ImNodesPinShape_CircleFilled : ImNodesPinShape_Circle);
570 ImGui::Text("%s", dataInputPins[i].first.c_str());
571 ImNodes::EndInputAttribute();
572 ImNodes::PopColorStyle();
573 }
574
575 // Phase 24 — Dynamic data pins from conditionRefs (violet, offset 400–499)
577 {
578 for (size_t i = 0; i < def.dynamicPins.size(); ++i)
579 {
580 int attrID = nodeUID * 10000 + 400 + static_cast<int>(i);
581 bool connected = connectedAttrIDs.find(attrID) != connectedAttrIDs.end();
582 ImNodes::PushColorStyle(ImNodesCol_Pin, SystemColors::DATA_PIN_COLOR);
583 ImNodes::BeginInputAttribute(attrID, connected ? ImNodesPinShape_CircleFilled : ImNodesPinShape_Circle);
584 const std::string lbl = def.dynamicPins[i].GetDisplayLabel();
585 ImGui::Text("%s", lbl.c_str());
586 ImNodes::EndInputAttribute();
587 ImNodes::PopColorStyle();
588 }
589 }
590
591 // ---- RIGHT COLUMN: Output Pins (Exec + Data + Dynamic removal button) ----
592 ImGui::NextColumn();
593
594 // Determine how many static (non-removable) exec-out pins this node has.
595 // Dynamic pins start at index numStaticPins in execOutputPins.
596 const bool hasDynamicPins = (def.Type == TaskNodeType::VSSequence ||
598 const int numStaticPins = hasDynamicPins
599 ? static_cast<int>(execOutputPins.size()) -
600 static_cast<int>(def.DynamicExecOutputPins.size())
601 : static_cast<int>(execOutputPins.size());
602
603 // Exec output pins (right side triangles) — offset 100–199
604 // Dynamic pins render with an inline [-] remove button.
605 for (size_t i = 0; i < execOutputPins.size(); ++i)
606 {
607 int attrID = nodeUID * 10000 + 100 + static_cast<int>(i);
608 bool connected = connectedAttrIDs.find(attrID) != connectedAttrIDs.end();
609 ImNodes::PushColorStyle(ImNodesCol_Pin, GetExecPinColor());
610
611 // Handle dynamic pin removal button placement
612 if (hasDynamicPins && static_cast<int>(i) >= numStaticPins && onRemovePin)
613 {
614 int dynIdx = static_cast<int>(i) - numStaticPins;
615
616 ImNodes::BeginOutputAttribute(attrID, connected ? ImNodesPinShape_TriangleFilled : ImNodesPinShape_Triangle);
617
618 // Phase 3 FIX: For Switch nodes, display the label with match value
619 // even for dynamic pins with remove button
620 std::string displayLabel = execOutputPins[i];
621 if (def.Type == TaskNodeType::Switch && i < def.switchCases.size())
622 {
624 if (!caseData.customLabel.empty() && !caseData.value.empty())
625 {
626 displayLabel = execOutputPins[i] + " [" + caseData.customLabel + "(" + caseData.value + ")]";
627 }
628 else if (!caseData.value.empty())
629 {
630 displayLabel = execOutputPins[i] + " (" + caseData.value + ")";
631 }
632 }
633
634 ImGui::PushID(nodeUID * 10000 + 5000 + static_cast<int>(i));
635 ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(140, 30, 30, 200));
636 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(200, 50, 50, 220));
637 ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(230, 80, 80, 240));
638
639 // Show label before button
640 ImGui::Text("%s", displayLabel.c_str());
641 ImGui::SameLine();
642
643 if (ImGui::SmallButton("[-]"))
645 if (ImGui::IsItemHovered())
646 ImGui::SetTooltip("Remove Execution Output");
647 ImGui::PopStyleColor(3);
648 ImGui::PopID();
649
650 ImNodes::EndOutputAttribute();
651 }
652 else
653 {
654 // Regular exec output pin
655 ImNodes::BeginOutputAttribute(attrID, connected ? ImNodesPinShape_TriangleFilled : ImNodesPinShape_Triangle);
656
657 // Phase 3 FIX: For Switch nodes, display the label with match value
658 // Format: "Case_1 [Probe(1)]" instead of just "Case_1"
659 std::string displayLabel = execOutputPins[i];
660 if (def.Type == TaskNodeType::Switch && i < def.switchCases.size())
661 {
663 if (!caseData.customLabel.empty() && !caseData.value.empty())
664 {
665 displayLabel = execOutputPins[i] + " [" + caseData.customLabel + "(" + caseData.value + ")]";
666 }
667 else if (!caseData.value.empty())
668 {
669 displayLabel = execOutputPins[i] + " (" + caseData.value + ")";
670 }
671 }
672
673 ImGui::Text("%s", displayLabel.c_str());
674 ImNodes::EndOutputAttribute();
675 }
676
677 ImNodes::PopColorStyle();
678 }
679
680 // [+] button — below the last exec-out pin, for VSSequence and Switch
682 {
683 ImGui::PushID(nodeUID);
684 ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(30, 100, 30, 200));
685 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(50, 160, 50, 220));
686 ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(80, 200, 80, 240));
687 if (ImGui::SmallButton("[+]"))
688 onAddPin(nodeID, onAddPinUserData);
689 if (ImGui::IsItemHovered())
690 ImGui::SetTooltip("Add Execution Output");
691 ImGui::PopStyleColor(3);
692 ImGui::PopID();
693 }
694
695 // Data output pins (right side circles) — offset 300–399
696 for (size_t i = 0; i < dataOutputPins.size(); ++i)
697 {
698 int attrID = nodeUID * 10000 + 300 + static_cast<int>(i);
699 bool connected = connectedAttrIDs.find(attrID) != connectedAttrIDs.end();
700 ImNodes::PushColorStyle(ImNodesCol_Pin,
702 ImNodes::BeginOutputAttribute(attrID, connected ? ImNodesPinShape_CircleFilled : ImNodesPinShape_Circle);
703 ImGui::Text("%s", dataOutputPins[i].first.c_str());
704 ImNodes::EndOutputAttribute();
705 ImNodes::PopColorStyle();
706 }
707
708 ImGui::Columns(1); // End columns
709
710 // ---- Inline parameter display (AFTER pins for better UX consistency) ----
711 switch (def.Type)
712 {
714 {
715 if (!def.AtomicTaskID.empty())
716 {
717 // Get task spec to retrieve parameters
719 if (taskSpec && !taskSpec->parameters.empty())
720 {
721 // Display parameters in format: "paramName: value"
722 for (const auto& param : taskSpec->parameters)
723 {
724 // Get parameter value from def.Parameters
725 std::string paramValue = param.defaultValue;
726 auto paramIt = def.Parameters.find(param.name);
727 if (paramIt != def.Parameters.end() &&
729 {
730 paramValue = paramIt->second.LiteralValue.to_string();
731 }
732
733 // Display as "paramName: value" in light color
734 ImGui::TextColored(ImVec4(0.7f, 0.85f, 1.0f, 1.0f),
735 " %s: %s", param.name.c_str(), paramValue.c_str());
736 }
737 }
738 else
739 {
740 // Fallback if no parameters defined for this task
741 ImGui::TextDisabled(" %s", def.AtomicTaskID.c_str());
742 }
743 }
744 break;
745 }
746
748 ImGui::TextDisabled(" %.2f s", def.DelaySeconds);
749 break;
750
753 if (!def.BBKey.empty())
754 ImGui::TextDisabled(" %s", def.BBKey.c_str());
755 break;
756
759 {
760 // Phase 24 — render conditionRefs in green if available
761 if (!def.conditionRefs.empty())
762 {
763 const ImVec4 condColor(0.0f, 1.0f, 0.0f, 1.0f);
764 for (size_t ci = 0; ci < def.conditionRefs.size(); ++ci)
765 {
766 const NodeConditionRef& ref = def.conditionRefs[ci];
767 const char* opLabel;
768 if (ci == 0)
769 opLabel = " ";
770 else if (ref.logicalOp == LogicalOp::And)
771 opLabel = "And";
772 else
773 opLabel = "Or ";
774
775 ImGui::PushStyleColor(ImGuiCol_Text, condColor);
776
777 // Display condition expression if available in conditionOperandRefs
778 if (ci < def.conditionOperandRefs.size())
779 {
781 ImGui::Text(" %s %s", opLabel, exprStr.c_str());
782 }
783 else
784 {
785 // Fallback: display preset ID if no operand refs available
786 ImGui::Text(" %s %s", opLabel, ref.presetID.c_str());
787 }
788
789 ImGui::PopStyleColor();
790 }
791 }
792 else if (!def.ConditionID.empty())
793 {
794 // Fallback: legacy ConditionID (Phase 23 / pre-Phase 24)
795 ImGui::TextDisabled(" %s", def.ConditionID.c_str());
796 }
797 // Phase 24 — render dynamic pins in yellow (Section 4 preview)
798 if (!def.dynamicPins.empty())
799 {
800 ImGui::Separator();
801 const ImVec4 pinColor(1.0f, 0.843f, 0.0f, 1.0f);
802 for (const auto& pin : def.dynamicPins)
803 {
804 const std::string lbl = pin.GetDisplayLabel();
805 ImGui::TextColored(pinColor, " %s", lbl.c_str());
806 }
807 }
808 break;
809 }
810
812 {
813 if (!def.SubGraphPath.empty())
814 {
815 // Extract basename without path or extension
816 const std::string& p = def.SubGraphPath;
817 size_t slashPos = p.find_last_of("/\\");
818 std::string base = (slashPos != std::string::npos)
819 ? p.substr(slashPos + 1)
820 : p;
821 size_t dotPos = base.rfind('.');
822 if (dotPos != std::string::npos)
823 base = base.substr(0, dotPos);
824 ImGui::TextDisabled(" %s", base.c_str());
825 }
826 break;
827 }
828
830 {
831 // Phase 24 — Display algebraic expression: "A + B" in gray text
832 // Build left operand display
833 std::string leftStr = "A";
834 switch (def.mathOpRef.leftOperand.mode)
835 {
838 break;
840 leftStr = "[" + def.mathOpRef.leftOperand.variableName + "]";
841 break;
843 leftStr = "[Pin]";
844 break;
845 default:
846 leftStr = "A";
847 break;
848 }
849
850 // Build right operand display
851 std::string rightStr = "B";
852 switch (def.mathOpRef.rightOperand.mode)
853 {
856 break;
859 break;
861 rightStr = "[Pin]";
862 break;
863 default:
864 rightStr = "B";
865 break;
866 }
867
868 // Get operator (default to + if empty)
869 const std::string& op = def.mathOpRef.mathOperator.empty()
870 ? std::string("+")
872
873 // Display expression in gray (same style as GetBBValue/SetBBValue)
874 ImGui::TextDisabled(" %s %s %s", leftStr.c_str(), op.c_str(), rightStr.c_str());
875 break;
876 }
877
878 default:
879 break;
880 }
881
882 ImNodes::EndNode();
883
884 if (isActive)
885 ImNodes::PopColorStyle();
886
887 ImNodes::PopColorStyle(); // TitleBarSelected
888 ImNodes::PopColorStyle(); // TitleBarHovered
889 ImNodes::PopColorStyle(); // TitleBar
890}
891
893{
894 ImVec2 nodePos = ImNodes::GetNodeEditorSpacePos(nodeUID);
895 ImVec2 screenPos = ImGui::GetWindowPos();
897 circlePos.x = screenPos.x + nodePos.x - 12.0f;
898 circlePos.y = screenPos.y + nodePos.y + 10.0f;
899
900 ImGui::GetWindowDrawList()->AddCircleFilled(
901 circlePos, 7.0f, IM_COL32(255, 0, 0, 255));
902 ImGui::GetWindowDrawList()->AddCircle(
903 circlePos, 7.0f, IM_COL32(255, 120, 120, 255), 12, 1.5f);
904}
905
907{
908 ImVec2 nodePos = ImNodes::GetNodeEditorSpacePos(nodeUID);
909 ImVec2 nodeDims = ImNodes::GetNodeDimensions(nodeUID);
910 ImVec2 screenPos = ImGui::GetWindowPos();
911
912 ImVec2 min, max;
913 min.x = screenPos.x + nodePos.x - 4.0f;
914 min.y = screenPos.y + nodePos.y - 4.0f;
915 max.x = min.x + nodeDims.x + 8.0f;
916 max.y = min.y + nodeDims.y + 8.0f;
917
918 ImGui::GetWindowDrawList()->AddRect(
919 min, max, IM_COL32(80, 255, 80, 200), 6.0f, 0, 3.0f);
920}
921
922} // namespace Olympe
UI-side registry of available atomic tasks with display metadata.
Defines OperandRef and ConditionRef — standalone operand-to-pin mapping structures.
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Node style rendering helpers for VisualScriptEditorPanel (Phase 5).
static AtomicTaskUIRegistry & Get()
Returns the singleton instance.
const TaskSpec * GetTaskSpec(const std::string &id) const
Returns the TaskSpec for the given id, or nullptr if not found.
static void RenderNode(int nodeUID, int nodeID, int graphID, const std::string &nodeName, TaskNodeType type, bool hasBreakpoint, bool isActive, const std::vector< std::string > &execInputPins, const std::vector< std::string > &execOutputPins, const std::vector< std::pair< std::string, VariableType > > &dataInputPins, const std::vector< std::pair< std::string, VariableType > > &dataOutputPins, const std::unordered_set< int > &connectedAttrIDs={})
Renders a complete VS node (title + exec pins + data pins).
static void RenderBreakpointIndicator(int nodeUID)
Renders a breakpoint indicator (red circle) next to a node.
static void RenderActiveNodeGlow(int nodeUID)
Renders a "currently executing" glow overlay around a node.
constexpr uint32_t EXEC_PIN_COLOR
White color for execution flow (exec) pins and connections.
constexpr uint32_t DATA_PIN_COLOR
Violet color for data pins and connections.
< Provides AssetID and INVALID_ASSET_ID
@ And
Combined with AND.
static std::string BuildConditionExpressionString(const ConditionRef &condition)
Builds the complete condition expression string.
static float CalculateNodeMinimumWidth(const TaskNodeDefinition &def, const std::vector< std::string > &execInputPins, const std::vector< std::string > &execOutputPins, const std::vector< std::pair< std::string, VariableType > > &dataInputPins, const std::vector< std::pair< std::string, VariableType > > &dataOutputPins)
Calculates the minimum width required for a node to display all content.
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)
@ EntityID
Entity identifier (uint64_t)
VSNodeStyle
Visual style category for a VS node.
@ Action
Orange — AtomicTask.
@ FlowControl
Blue — Branch, Sequence, While, ForEach, DoOnce.
@ SubGraph
Teal — SubGraph call.
@ Delay
Yellow — Delay timer.
@ EntryPoint
Green — single "Out" exec pin.
@ Data
Purple — GetBBValue, SetBBValue, MathOp.
static float CalculateSwitchCaseLabelWidth(const std::string &baseName, const SwitchCaseDefinition &caseData)
Calculates the width of a Switch case output pin label.
unsigned int GetNodeTitleHoveredColor(VSNodeStyle style)
Returns the title-bar hovered RGBA colour.
@ Literal
Value is embedded directly in the template.
unsigned int GetDataPinColor(VariableType type)
Returns the RGBA colour for a data pin by variable type.
unsigned int GetExecPinColor()
VSNodeStyle GetNodeStyle(TaskNodeType type)
Returns the VSNodeStyle appropriate for a given node type.
static std::string GetOperandDisplayString(const OperandRef &operand)
Builds a display string for a ConditionRef operand.
const char * GetVariableTypeLabel(VariableType type)
Returns a human-readable label for a VariableType.
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)
@ 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)
@ 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)
@ Root
Entry point of the graph (exactly one per template)
@ VSSequence
Execute N outputs in order ("VS" prefix avoids collision with BT Sequence=1)
unsigned int GetNodeTitleColor(VSNodeStyle style)
Returns the title-bar RGBA colour for a given style.
Stores the complete reference for one condition including operand-to-DynamicDataPin mapping.
@ 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)
One entry in a NodeBranch's conditions list.
References one operand of a condition (left or right).
@ Variable
References a blackboard variable by name.
@ Const
Literal constant value.
@ Pin
External data-input pin on the owning node.
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::string BBKey
For GetBBValue/SetBBValue: BB key (scope:key)
std::string ConditionID
For Branch/While/Switch: ATS condition ID.
std::string AtomicTaskID
Atomic task type identifier (used when Type == AtomicTask)
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.