Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
VisualScriptEditorPanel_ConditionUI.cpp
Go to the documentation of this file.
1/**
2 * @file VisualScriptEditorPanel_ConditionUI.cpp
3 * @brief Condition editor UI helper methods (Phase 11).
4 * @author Olympe Engine
5 * @date 2026-03-15
6 *
7 * @details
8 * This file contains 6 methods extracted from VisualScriptEditorPanel.cpp
9 * for better code organization and maintainability:
10 * - RenderConditionEditor() — Main condition editor with left/op/right configuration
11 * - RenderVariableSelector() — Dropdown for selecting blackboard variables
12 * - RenderConstValueInput() — Type-specific constant value input UI
13 * - RenderPinSelector() — Dropdown for selecting data output pins
14 * - BuildConditionPreview() — Build human-readable preview string for conditions
15 * - RenderOperandEditor() — Unified operand selector (pin/const/variable)
16 *
17 * These helpers support the condition editing UI for Branch nodes,
18 * providing type-safe input fields and visual feedback.
19 *
20 * C++14 compliant — no std::optional, structured bindings, std::filesystem.
21 */
22
24#include "../system/system_utils.h"
25#include "../system/system_consts.h"
26#include "../NodeGraphCore/GlobalTemplateBlackboard.h"
27#include "../third_party/imgui/imgui.h"
28#include "../json_helper.h"
29
30#include <sstream>
31#include <iomanip>
32#include <algorithm>
33#include <cstring>
34
35namespace Olympe {
36
37// ============================================================================
38// Condition Editor — Main UI for editing left/op/right conditions
39// ============================================================================
40
42 Condition& condition,
43 int conditionIndex,
44 const std::vector<BlackboardEntry>& allVars,
45 const std::vector<std::string>& availablePins)
46{
47 ImGui::PushID(conditionIndex);
48 ImGui::Separator();
49 ImGui::Text("Condition #%d", conditionIndex + 1);
50
51 // -- LEFT SIDE --
52 ImGui::Text("Left:");
53 ImGui::SameLine();
54
55 const bool isLeftPin = (condition.leftMode == "Pin");
56 const bool isLeftVar = (condition.leftMode == "Variable");
57 const bool isLeftConst = (condition.leftMode == "Const");
58
59 if (ImGui::Button(isLeftPin ? "[PIN]" : "Pin", ImVec2(55, 0)))
60 {
61 condition.leftMode = "Pin";
62 condition.leftPin = "";
63 m_dirty = true;
64 }
65 ImGui::SameLine();
66 if (ImGui::Button(isLeftVar ? "[VAR]" : "Var", ImVec2(55, 0)))
67 {
68 condition.leftMode = "Variable";
69 condition.leftVariable = "";
70 m_dirty = true;
71 }
72 ImGui::SameLine();
73 if (ImGui::Button(isLeftConst ? "[CST]" : "Const", ImVec2(55, 0)))
74 {
75 condition.leftMode = "Const";
76 m_dirty = true;
77 }
78
79 ImGui::Indent();
80 if (condition.leftMode == "Pin")
81 RenderPinSelector(condition.leftPin, availablePins, "##leftpin");
82 else if (condition.leftMode == "Variable")
84 condition.compareType, "##leftvar");
85 else
86 RenderConstValueInput(condition.leftConstValue,
87 condition.compareType, "##leftconst");
88 ImGui::Unindent();
89
90 // -- OPERATOR --
91 ImGui::Text("Op:");
92 ImGui::SameLine();
93 const char* operators[] = { "==", "!=", "<", ">", "<=", ">=" };
94 int opIdx = 0;
95 for (int i = 0; i < 6; ++i)
96 {
97 if (condition.operatorStr == operators[i])
98 {
99 opIdx = i;
100 break;
101 }
102 }
103 ImGui::SetNextItemWidth(70.0f);
104 if (ImGui::Combo("##op", &opIdx, operators, 6))
105 {
106 condition.operatorStr = operators[opIdx];
107 m_dirty = true;
108 }
109
110 // -- RIGHT SIDE --
111 ImGui::Text("Right:");
112 ImGui::SameLine();
113
114 const bool isRightPin = (condition.rightMode == "Pin");
115 const bool isRightVar = (condition.rightMode == "Variable");
116 const bool isRightConst = (condition.rightMode == "Const");
117
118 if (ImGui::Button(isRightPin ? "[PIN]##r" : "Pin##r", ImVec2(55, 0)))
119 {
120 condition.rightMode = "Pin";
121 condition.rightPin = "";
122 m_dirty = true;
123 }
124 ImGui::SameLine();
125 if (ImGui::Button(isRightVar ? "[VAR]##r" : "Var##r", ImVec2(55, 0)))
126 {
127 condition.rightMode = "Variable";
128 condition.rightVariable = "";
129 m_dirty = true;
130 }
131 ImGui::SameLine();
132 if (ImGui::Button(isRightConst ? "[CST]##r" : "Const##r", ImVec2(55, 0)))
133 {
134 condition.rightMode = "Const";
135 m_dirty = true;
136 }
137
138 ImGui::Indent();
139 if (condition.rightMode == "Pin")
140 RenderPinSelector(condition.rightPin, availablePins, "##rightpin");
141 else if (condition.rightMode == "Variable")
143 condition.compareType, "##rightvar");
144 else
145 RenderConstValueInput(condition.rightConstValue,
146 condition.compareType, "##rightconst");
147 ImGui::Unindent();
148
149 // -- TYPE HINT --
150 ImGui::Text("Type:");
151 ImGui::SameLine();
152 const char* types[] = { "None", "Bool", "Int", "Float", "String", "Vector" };
153 const VariableType typeValues[] = {
156 };
157 int typeIdx = 0;
158 for (int i = 0; i < 6; ++i)
159 {
160 if (condition.compareType == typeValues[i])
161 {
162 typeIdx = i;
163 break;
164 }
165 }
166 ImGui::SetNextItemWidth(80.0f);
167 if (ImGui::Combo("##cmptype", &typeIdx, types, 6))
168 {
169 condition.compareType = typeValues[typeIdx];
170 m_dirty = true;
171 }
172
173 // -- PREVIEW --
174 const std::string preview = BuildConditionPreview(condition);
175 ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f),
176 "Preview: %s", preview.c_str());
177
178 ImGui::PopID();
179}
180
181// ============================================================================
182// Variable Selector — Dropdown UI for selecting blackboard variables
183// ============================================================================
184
186 std::string& selectedVar,
187 const std::vector<BlackboardEntry>& allVars,
188 VariableType expectedType,
189 const char* label)
190{
191 // Filter by type (if a type is specified)
192 std::vector<std::string> names;
193 for (size_t i = 0; i < allVars.size(); ++i)
194 {
195 if (expectedType == VariableType::None || allVars[i].Type == expectedType)
196 {
197 if (!allVars[i].Key.empty())
198 names.push_back(allVars[i].Key);
199 }
200 }
201
202 if (names.empty())
203 {
204 ImGui::TextDisabled("(no variables)");
205 return;
206 }
207
208 // BUG-029 Fix: auto-initialise to the first available variable when the
209 // selection is empty (e.g. right after switching to Variable mode).
210 // Without this the combo visually shows the first item but selectedVar
211 // remains "" so BuildConditionPreview displays "[Var: ?]".
212 if (selectedVar.empty())
213 {
214 selectedVar = names[0];
215 m_dirty = true;
216 }
217
218 int selected = 0;
219 for (int i = 0; i < static_cast<int>(names.size()); ++i)
220 {
221 if (names[static_cast<size_t>(i)] == selectedVar)
222 {
223 selected = i;
224 break;
225 }
226 }
227
228 std::vector<const char*> cstrs;
229 cstrs.reserve(names.size());
230 for (size_t i = 0; i < names.size(); ++i)
231 cstrs.push_back(names[i].c_str());
232
233 ImGui::SetNextItemWidth(120.0f);
234 if (ImGui::Combo(label, &selected, cstrs.data(), static_cast<int>(cstrs.size())))
235 {
236 selectedVar = names[static_cast<size_t>(selected)];
237 m_dirty = true;
238 }
239}
240
241// ============================================================================
242// Const Value Input — Type-specific constant value editor
243// ============================================================================
244
246 TaskValue& value,
248 const char* label)
249{
250 // BUG-029 Fix: auto-initialise to a typed default when value is None and
251 // a type is known. Without this the preview always shows "[Const: ?]"
252 // until the user explicitly edits the field, because BuildConditionPreview
253 // only formats the value when !IsNone().
254 if (value.IsNone() && varType != VariableType::None)
255 {
256 switch (varType)
257 {
258 case VariableType::Bool: value = TaskValue(false); break;
259 case VariableType::Int: value = TaskValue(0); break;
260 case VariableType::Float: value = TaskValue(0.0f); break;
261 case VariableType::String: value = TaskValue(std::string("")); break;
262 case VariableType::Vector: value = TaskValue(::Vector{0.f, 0.f, 0.f}); break;
263 default: break;
264 }
265 if (!value.IsNone())
266 m_dirty = true;
267 }
268
269 switch (varType)
270 {
272 {
273 bool bVal = value.IsNone() ? false : value.AsBool();
274 if (ImGui::Checkbox(label, &bVal))
275 {
276 value = TaskValue(bVal);
277 m_dirty = true;
278 }
279 break;
280 }
282 {
283 int iVal = value.IsNone() ? 0 : value.AsInt();
284 ImGui::SetNextItemWidth(80.0f);
285 if (ImGui::InputInt(label, &iVal))
286 {
287 value = TaskValue(iVal);
288 m_dirty = true;
289 }
290 break;
291 }
293 {
294 float fVal = value.IsNone() ? 0.0f : value.AsFloat();
295 ImGui::SetNextItemWidth(80.0f);
296 if (ImGui::InputFloat(label, &fVal, 0.0f, 0.0f, "%.3f"))
297 {
298 value = TaskValue(fVal);
299 m_dirty = true;
300 }
301 break;
302 }
304 {
305 const std::string sVal = value.IsNone() ? "" : value.AsString();
306 char sBuf[256];
307 strncpy_s(sBuf, sizeof(sBuf), sVal.c_str(), _TRUNCATE);
308 ImGui::SetNextItemWidth(120.0f);
309 if (ImGui::InputText(label, sBuf, sizeof(sBuf)))
310 {
311 value = TaskValue(std::string(sBuf));
312 m_dirty = true;
313 }
314 break;
315 }
317 {
318 ::Vector vVal = value.IsNone() ? ::Vector{0.f, 0.f, 0.f} : value.AsVector();
319 float v[3] = { vVal.x, vVal.y, vVal.z };
320 ImGui::SetNextItemWidth(160.0f);
321 if (ImGui::InputFloat3(label, v))
322 {
323 value = TaskValue(::Vector{ v[0], v[1], v[2] });
324 m_dirty = true;
325 }
326 break;
327 }
328 default:
329 {
330 // No type set yet — show a hint
331 ImGui::TextDisabled("(set Type first)");
332 break;
333 }
334 }
335}
336
337// ============================================================================
338// Pin Selector — Dropdown UI for selecting data output pins
339// ============================================================================
340
342 std::string& selectedPin,
343 const std::vector<std::string>& availablePins,
344 const char* label)
345{
346 if (availablePins.empty())
347 {
348 ImGui::TextDisabled("(no data-output pins in graph)");
349 return;
350 }
351
352 ImGui::SetNextItemWidth(160.0f);
353 if (ImGui::BeginCombo(label, selectedPin.empty() ? "(select pin)" : selectedPin.c_str()))
354 {
355 for (size_t i = 0; i < availablePins.size(); ++i)
356 {
357 const bool isSelected = (selectedPin == availablePins[i]);
358 if (ImGui::Selectable(availablePins[i].c_str(), isSelected))
360 if (isSelected)
361 ImGui::SetItemDefaultFocus();
362 }
363 ImGui::EndCombo();
364 }
365}
366
367// ============================================================================
368// Condition Preview Builder — Generates human-readable condition string
369// ============================================================================
370
371std::string VisualScriptEditorPanel::BuildConditionPreview(const Condition& cond)
372{
373 auto descSide = [](const std::string& mode,
374 const std::string& pin,
375 const std::string& var,
376 const TaskValue& constValue) -> std::string
377 {
378 if (mode == "Pin")
379 return "[Pin: " + (pin.empty() ? "?" : pin) + "]";
380 if (mode == "Variable")
381 return "[Var: " + (var.empty() ? "?" : var) + "]";
382
383 // Const — try to format value
384 if (!constValue.IsNone())
385 {
386 std::ostringstream oss;
387 switch (constValue.GetType())
388 {
389 case VariableType::Bool: oss << (constValue.AsBool() ? "true" : "false"); break;
390 case VariableType::Int: oss << constValue.AsInt(); break;
391 case VariableType::Float: oss << constValue.AsFloat(); break;
392 case VariableType::String: oss << '"' << constValue.AsString() << '"'; break;
394 {
395 const ::Vector v = constValue.AsVector();
396 oss << "(" << v.x << "," << v.y << "," << v.z << ")";
397 break;
398 }
399 default: oss << "?"; break;
400 }
401 return "[Const: " + oss.str() + "]";
402 }
403 return "[Const: ?]";
404 };
405
406 const std::string left = descSide(cond.leftMode, cond.leftPin, cond.leftVariable, cond.leftConstValue);
407 const std::string right = descSide(cond.rightMode, cond.rightPin, cond.rightVariable, cond.rightConstValue);
408 const std::string op = cond.operatorStr.empty() ? "?" : cond.operatorStr;
409
410 return left + " " + op + " " + right;
411}
412
413// ============================================================================
414// Operand Editor — Unified selector for operands (pin/const/variable)
415// ============================================================================
416
418{
419#ifndef OLYMPE_HEADLESS
420 bool modified = false;
421
422 // Build a unified dropdown list with this ORDER:
423 // 1. [Pin-in #1], [Pin-in #2], ...
424 // 2. [Const] <value>
425 // 3. Variables (sorted alphabetically)
426
427 std::vector<std::string> allOptions;
428 std::vector<int> optionTypes; // 0=Variable, 1=Const, 2=Pin
429 std::vector<std::string> optionValues; // Store the actual value for each option
430
431 int currentSelectionIdx = -1;
432
433 // ── Add all available pins FIRST ─────────────────────────────────────
434 {
435 std::vector<DynamicDataPin> allPins = m_pinManager->GetAllPins();
436 for (const auto& pin : allPins)
437 {
438 allOptions.push_back("[Pin-in] " + pin.label);
439 optionTypes.push_back(2); // Pin
440 optionValues.push_back(pin.label);
441
442 if (operand.mode == OperandMode::Pin &&
443 operand.stringValue == pin.label)
444 {
445 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
446 }
447 }
448
449 // If no pins are available, still show the [Pin-in] option as a category
450 if (allPins.empty())
451 {
452 allOptions.push_back("[Pin-in] (none available)");
453 optionTypes.push_back(2); // Pin
454 optionValues.push_back(""); // Empty value for unavailable pin
455 }
456 }
457
458 // ── Add [Const] option SECOND ────────────────────────────────────────
459 {
460 std::string constLabel = "[Const] ";
461 std::ostringstream oss;
462 oss << std::fixed << std::setprecision(3) << operand.constValue;
463 std::string constVal = oss.str();
464 // Trim trailing zeros
465 size_t dot = constVal.find('.');
466 if (dot != std::string::npos)
467 {
468 size_t last = constVal.find_last_not_of('0');
469 if (last != std::string::npos && last > dot)
470 constVal = constVal.substr(0, last + 1);
471 else if (last == dot)
472 constVal = constVal.substr(0, dot);
473 }
475
476 allOptions.push_back(constLabel);
477 optionTypes.push_back(1); // Const
478 optionValues.push_back(constVal);
479
480 if (operand.mode == OperandMode::Const)
481 {
482 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
483 }
484 }
485
486 // ── Add all local variables LAST (sorted alphabetically) ──────────────
487 {
488 std::vector<std::string> sortedVarNames;
489 for (const auto& entry : m_template.Blackboard)
490 {
491 if (entry.Type != VariableType::None && !entry.Key.empty())
492 {
493 sortedVarNames.push_back(entry.Key);
494 }
495 }
496 // Sort alphabetically
497 std::sort(sortedVarNames.begin(), sortedVarNames.end());
498
499 for (const auto& varName : sortedVarNames)
500 {
501 allOptions.push_back(varName);
502 optionTypes.push_back(0); // Variable
503 optionValues.push_back(varName);
504
505 // Check if this is the currently selected variable
506 if (operand.mode == OperandMode::Variable &&
507 operand.stringValue == varName)
508 {
509 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
510 }
511 }
512 }
513
514 // ── Add all global variables (Phase 24) ───────────────────────────────
515 {
516 GlobalTemplateBlackboard& gtb = GlobalTemplateBlackboard::Get();
517 const std::vector<GlobalEntryDefinition>& globalVars = gtb.GetAllVariables();
518
519 // Add separator if we have both local and global
520 if (!m_template.Blackboard.empty() && !globalVars.empty())
521 {
522 allOptions.push_back("--- Global Variables ---");
523 optionTypes.push_back(-1); // Separator (no type)
524 optionValues.push_back("");
525 }
526
527 // Add global variables
528 for (const auto& globalVar : globalVars)
529 {
530 allOptions.push_back(globalVar.Key);
531 optionTypes.push_back(0); // Variable
532 optionValues.push_back(globalVar.Key);
533
534 // Check if this is the currently selected global variable
535 if (operand.mode == OperandMode::Variable &&
536 operand.stringValue == globalVar.Key)
537 {
538 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
539 }
540 }
541 }
542
543 // ── Render unified dropdown ──────────────────────────────────────────
544 ImGui::SetNextItemWidth(120.0f);
545
546 const char* displayText = (currentSelectionIdx >= 0) ? allOptions[currentSelectionIdx].c_str() : "(none)";
547
548 if (ImGui::BeginCombo(labelSuffix, displayText))
549 {
550 // Create mutable array of C strings for ImGui
551 std::vector<const char*> optionsCStr;
552 for (const auto& opt : allOptions)
554
555 for (int i = 0; i < static_cast<int>(allOptions.size()); ++i)
556 {
557 bool selected = (i == currentSelectionIdx);
558
559 // Skip rendering separator as selectable
560 if (optionTypes[i] == -1)
561 {
562 ImGui::Separator();
563 continue;
564 }
565
566 if (ImGui::Selectable(optionsCStr[i], selected))
567 {
568 // Update operand based on selected type
569 switch (optionTypes[i])
570 {
571 case 0: // Variable
573 operand.stringValue = optionValues[i];
574 break;
575 case 1: // Const
577 try {
578 operand.constValue = std::stod(optionValues[i]);
579 } catch (...) {
580 operand.constValue = 0.0;
581 }
582 break;
583 case 2: // Pin
585 // For pins, store the pin label. If no specific pin selected (empty),
586 // use a placeholder value that indicates "any available pin"
587 operand.stringValue = optionValues[i].empty() ? "[Pin-in]" : optionValues[i];
588 break;
589 }
590 modified = true;
591 }
592 }
593 ImGui::EndCombo();
594 }
595
596 // ── Add numeric input field for Const mode ──────────────────────────────
597 if (operand.mode == OperandMode::Const)
598 {
599 ImGui::SameLine(0.0f, 4.0f);
600 ImGui::SetNextItemWidth(60.0f);
601 if (ImGui::InputDouble("##const_value", &operand.constValue, 0.0, 0.0, "%.3f"))
602 {
603 modified = true;
604 }
605 }
606
607 return modified;
608#else
609 return false;
610#endif
611}
612
613} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
ImNodes-based graph editor for ATS Visual Script graphs (Phase 5).
static GlobalTemplateBlackboard & Get()
std::vector< BlackboardEntry > Blackboard
Local blackboard declared in this graph.
std::unique_ptr< DynamicDataPinManager > m_pinManager
Dynamic pin manager shared across all Branch nodes in this panel.
TaskGraphTemplate m_template
The template currently being edited.
void RenderVariableSelector(std::string &selectedVar, const std::vector< BlackboardEntry > &allVars, VariableType expectedType, const char *label)
Renders a type-filtered variable selector combo box.
void RenderPinSelector(std::string &selectedPin, const std::vector< std::string > &availablePins, const char *label)
Renders a pin selector combo box.
void RenderConstValueInput(TaskValue &value, VariableType varType, const char *label)
Renders a type-aware const value input widget.
static std::string BuildConditionPreview(const Condition &cond)
Builds a human-readable preview string for a condition.
bool RenderOperandEditor(Operand &operand, const char *labelSuffix)
Render a single operand with dropdown for mode and value editor Returns true if the operand was modif...
void RenderConditionEditor(Condition &condition, int conditionIndex, const std::vector< BlackboardEntry > &allVars, const std::vector< std::string > &availablePins)
Renders the full editor UI for one Condition entry on a Branch/While node.
float x
Definition vector.h:25
< Provides AssetID and INVALID_ASSET_ID
VariableType
Type tags used by TaskValue to identify stored data.
@ Int
32-bit signed integer
@ Float
Single-precision float.
@ String
std::string
@ Vector
3-component vector (Vector from vector.h)
@ None
Uninitialized / empty value.
@ Variable
References a blackboard variable by ID (string key)
@ Const
Literal numeric constant (double)
@ Pin
External data input pin on the node (identified by label)