Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
NodeConditionsPanel.cpp
Go to the documentation of this file.
1/**
2 * @file NodeConditionsPanel.cpp
3 * @brief Implementation of NodeConditionsPanel (Phase 24.2).
4 * @author Olympe Engine
5 * @date 2026-03-17
6 *
7 * C++14 compliant — no std::optional, structured bindings, std::filesystem.
8 */
9
10#include "NodeConditionsPanel.h"
11
12#include <algorithm>
13#include <iomanip>
14#include <sstream>
15#include "../../NodeGraphCore/GlobalTemplateBlackboard.h"
16
17// ImGui is only compiled in the full editor build.
18#ifndef OLYMPE_HEADLESS
19# include "../../third_party/imgui/imgui.h"
20#endif
21
22namespace Olympe {
23
24// ============================================================================
25// Constructor
26// ============================================================================
27
33
34// ============================================================================
35// State accessors
36// ============================================================================
37
38void NodeConditionsPanel::SetConditionRefs(const std::vector<NodeConditionRef>& refs)
39{
42
43 // Resize the operand-ref list to stay in sync with conditionRefs.
44 // Preserve existing entries where possible; add defaults for new entries.
45 if (m_conditionOperandRefs.size() != m_conditionRefs.size())
46 {
48 for (size_t i = 0; i < m_conditionOperandRefs.size(); ++i)
49 m_conditionOperandRefs[i].conditionIndex = static_cast<int>(i);
50 }
51
52 m_dirty = false;
53}
54
55const std::vector<NodeConditionRef>& NodeConditionsPanel::GetConditionRefs() const
56{
57 return m_conditionRefs;
58}
59
60void NodeConditionsPanel::SetConditionOperandRefs(const std::vector<ConditionRef>& refs)
61{
63}
64
65const std::vector<ConditionRef>& NodeConditionsPanel::GetConditionOperandRefs() const
66{
68}
69
70void NodeConditionsPanel::SetDynamicPins(const std::vector<DynamicDataPin>& pins)
71{
73}
74
75// ============================================================================
76// Condition management
77// ============================================================================
78
79void NodeConditionsPanel::AddCondition(const std::string& presetID)
80{
81 if (presetID.empty())
82 return;
83
85 m_conditionRefs.emplace_back(presetID, op);
87
88 // Add a matching ConditionRef initialized from the preset's default operands.
90 cref.conditionIndex = static_cast<int>(m_conditionRefs.size()) - 1;
91 const ConditionPreset* preset = m_registry.GetPreset(presetID);
92 if (preset)
93 {
94 // Initialize from the preset's left operand
95 switch (preset->left.mode)
96 {
98 cref.leftOperand.mode = OperandRef::Mode::Variable;
99 cref.leftOperand.variableName = preset->left.stringValue;
100 break;
101 case OperandMode::Pin:
102 cref.leftOperand.mode = OperandRef::Mode::Pin;
103 break;
105 default:
106 cref.leftOperand.mode = OperandRef::Mode::Const;
107 {
108 std::ostringstream oss;
109 oss << preset->left.constValue;
110 cref.leftOperand.constValue = oss.str();
111 }
112 break;
113 }
114 // Initialize operator string
115 switch (preset->op)
116 {
117 case ComparisonOp::Equal: cref.operatorStr = "=="; break;
118 case ComparisonOp::NotEqual: cref.operatorStr = "!="; break;
119 case ComparisonOp::Less: cref.operatorStr = "<"; break;
120 case ComparisonOp::LessEqual: cref.operatorStr = "<="; break;
121 case ComparisonOp::Greater: cref.operatorStr = ">"; break;
122 case ComparisonOp::GreaterEqual: cref.operatorStr = ">="; break;
123 default: cref.operatorStr = "=="; break;
124 }
125 // Initialize from the preset's right operand
126 switch (preset->right.mode)
127 {
129 cref.rightOperand.mode = OperandRef::Mode::Variable;
130 cref.rightOperand.variableName = preset->right.stringValue;
131 break;
132 case OperandMode::Pin:
133 cref.rightOperand.mode = OperandRef::Mode::Pin;
134 break;
136 default:
137 cref.rightOperand.mode = OperandRef::Mode::Const;
138 {
139 std::ostringstream oss;
140 oss << preset->right.constValue;
141 cref.rightOperand.constValue = oss.str();
142 }
143 break;
144 }
145 }
146 m_conditionOperandRefs.push_back(cref);
147
148 m_dirty = true;
149
150 if (OnPresetChanged)
151 OnPresetChanged(presetID);
152}
153
155{
156 if (index >= m_conditionRefs.size())
157 return;
158
159 m_conditionRefs.erase(m_conditionRefs.begin() + static_cast<int>(index));
160
161 // Keep operand refs in sync
162 if (index < m_conditionOperandRefs.size())
163 {
165 m_conditionOperandRefs.begin() + static_cast<int>(index));
166 }
167 // Re-index remaining operand refs
168 for (size_t i = index; i < m_conditionOperandRefs.size(); ++i)
169 m_conditionOperandRefs[i].conditionIndex = static_cast<int>(i);
170
172 m_dirty = true;
173}
174
176{
177 if (index >= m_conditionRefs.size())
178 return;
179
180 // Index 0 is always Start
181 m_conditionRefs[index].logicalOp = (index == 0) ? LogicalOp::Start : op;
182 m_dirty = true;
183}
184
186{
187 return m_conditionRefs.size();
188}
189
190// ============================================================================
191// Preset deletion handler
192// ============================================================================
193
195{
196 bool changed = false;
197 auto it = m_conditionRefs.begin();
198 while (it != m_conditionRefs.end())
199 {
200 if (it->presetID == deletedPresetID)
201 {
202 it = m_conditionRefs.erase(it);
203 changed = true;
204 }
205 else
206 {
207 ++it;
208 }
209 }
210
211 if (changed)
212 {
214 m_dirty = true;
215 }
216}
217
218// ============================================================================
219// Validation
220// ============================================================================
221
223{
224 return Validate().empty();
225}
226
227std::vector<std::string> NodeConditionsPanel::Validate() const
228{
229 std::vector<std::string> errors;
230
231 for (size_t i = 0; i < m_conditionRefs.size(); ++i)
232 {
234
235 if (ref.presetID.empty())
236 {
237 std::ostringstream oss;
238 oss << "Condition " << (i + 1) << ": preset ID is empty.";
239 errors.push_back(oss.str());
240 continue;
241 }
242
243 if (!m_registry.ValidatePresetID(ref.presetID))
244 {
245 std::ostringstream oss;
246 oss << "Condition " << (i + 1) << ": preset \"" << ref.presetID
247 << "\" not found in registry.";
248 errors.push_back(oss.str());
249 }
250 }
251
252 return errors;
253}
254
255// ============================================================================
256// Dropdown filter
257// ============================================================================
258
263
264std::vector<ConditionPreset>
266{
267 if (m_dropdownFilter.empty())
268 {
269 // Return all presets in registry order
270 std::vector<ConditionPreset> result;
271 for (const auto& id : m_registry.GetAllPresetIDs())
272 {
274 if (p)
275 result.push_back(*p);
276 }
277 return result;
278 }
279
280 // Filtered by name substring
281 std::vector<ConditionPreset> result;
282 for (const auto& id : m_registry.FindPresetsByName(m_dropdownFilter))
283 {
285 if (p)
286 result.push_back(*p);
287 }
288 return result;
289}
290
291// ============================================================================
292// Rendering
293// ============================================================================
294
296{
297#ifndef OLYMPE_HEADLESS
298 ImGui::PushID("NodeConditionsPanel");
299
300 // Phase 24 UX: Integrate condition editing INLINE (no modal popup)
301 // Render collapsible editor section directly in properties panel
303
304 ImGui::PopID();
305#endif
306}
307
309{
310#ifndef OLYMPE_HEADLESS
311 // Phase 24 UX Refactor: Replace modal with inline collapsible editor
312 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.0f, 0.7f, 0.4f, 1.0f));
313 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.0f, 0.8f, 0.5f, 1.0f));
314 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.0f, 0.6f, 0.3f, 1.0f));
315 const bool editorOpen = ImGui::CollapsingHeader(
316 "Conditions Editor (Inline)",
318 ImGui::PopStyleColor(3);
319
320 if (!editorOpen)
321 return;
322
323 ImGui::Indent(8.f);
324
325 // Conditions list with inline editing
326 ImGui::Text("Active Conditions:");
327 ImGui::BeginChild("ConditionsList##Inline", ImVec2(0.f, 120.f), true);
328
329 // Render each condition with edit controls
330 for (size_t i = 0; i < m_conditionRefs.size(); ++i)
331 {
333 }
334
335 ImGui::EndChild();
336 ImGui::Spacing();
337
338 // Add condition button + preset picker inline
340
341 ImGui::Unindent(8.f);
342#endif
343}
344
346 const NodeConditionRef& ref)
347{
348#ifndef OLYMPE_HEADLESS
349 ImGui::PushID(static_cast<int>(index));
350
351 // Logical operator dropdown (disabled for first entry)
352 if (index == 0)
353 {
354 ImGui::TextDisabled(" ");
355 }
356 else
357 {
358 const char* ops[] = { "And", "Or" };
359 int current = (ref.logicalOp == LogicalOp::And) ? 0 : 1;
360 ImGui::SetNextItemWidth(60.f);
361 if (ImGui::Combo("##op", &current, ops, 2))
362 {
364 m_dirty = true;
367 }
368 }
369
370 ImGui::SameLine();
371
372 // Condition preset preview — green text
373 const ConditionPreset* preset = m_registry.GetPreset(ref.presetID);
374 if (preset)
375 {
376 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
377 "[%s] %s", preset->name.c_str(), preset->GetPreview().c_str());
378 }
379 else
380 {
381 ImGui::TextColored(ImVec4(1.f, 0.3f, 0.3f, 1.f),
382 "(missing: %s)", ref.presetID.c_str());
383 }
384
385 ImGui::SameLine();
386
387 // Move up button
388 if (ImGui::SmallButton("^##up"))
389 {
390 if (index > 0)
391 {
392 std::swap(m_conditionRefs[index - 1], m_conditionRefs[index]);
393 m_dirty = true;
394 }
395 }
396
397 ImGui::SameLine();
398
399 // Move down button
400 if (ImGui::SmallButton("v##dn"))
401 {
402 if (index < m_conditionRefs.size() - 1)
403 {
404 std::swap(m_conditionRefs[index], m_conditionRefs[index + 1]);
405 m_dirty = true;
406 }
407 }
408
409 ImGui::SameLine();
410
411 // Delete button
412 if (ImGui::SmallButton("X##del"))
413 {
414 m_conditionRefs.erase(m_conditionRefs.begin() + static_cast<int>(index));
415 // Adjust operandRefs if they exist
416 if (index < m_conditionOperandRefs.size())
417 m_conditionOperandRefs.erase(m_conditionOperandRefs.begin() + static_cast<int>(index));
418 m_dirty = true;
421 }
422
423 ImGui::PopID();
424#endif
425}
426
428{
429#ifndef OLYMPE_HEADLESS
430 // Toggle button for preset picker
431 if (ImGui::Button("+ Add Condition", ImVec2(-1.f, 24.f)))
433
434 if (m_pickerOpen)
435 {
436 // Search filter
437 ImGui::Spacing();
438 ImGui::TextDisabled("Search presets:");
439 static char filterBuf[256] = {};
440 ImGui::InputText("##PresetFilter", filterBuf, sizeof(filterBuf));
441
442 // Get filtered preset list from registry
443 std::string filter = std::string(filterBuf);
444 std::vector<ConditionPreset> filteredPresets = m_registry.GetFilteredPresets(filter);
445
446 // Filtered preset list
447 ImGui::BeginChild("PresetPickerList##Inline", ImVec2(0.f, 150.f), true);
448
449 for (const auto& preset : filteredPresets)
450 {
451 const std::string label = preset.name + " " + preset.GetPreview();
452 if (ImGui::Selectable(label.c_str()))
453 {
454 // Add new condition
456 m_conditionRefs.emplace_back(preset.id, op);
457 m_dirty = true;
458
459 // Clear filter and close picker
460 filterBuf[0] = '\0';
461 m_pickerOpen = false;
462
465 }
466 }
467
468 if (filteredPresets.empty())
469 ImGui::TextDisabled("No presets found.");
470
471 ImGui::EndChild();
472 }
473#endif
474}
475
477{
478#ifndef OLYMPE_HEADLESS
479 // Blue background (#0066CC equivalent) with white text — matches canvas node
480 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.0f, 0.4f, 0.8f, 1.0f));
481 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.0f, 0.5f, 0.9f, 1.0f));
482 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.0f, 0.3f, 0.7f, 1.0f));
483 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
484 const std::string& title = m_nodeName.empty() ? "Node" : m_nodeName;
485 ImGui::Selectable(title.c_str(), true, ImGuiSelectableFlags_None, ImVec2(0.f, 28.f));
486 ImGui::PopStyleColor(4);
487#endif
488}
489
491{
492#ifndef OLYMPE_HEADLESS
493 // Static exec pins — never editable from this panel
494 const float columnWidth = 120.f;
495
496 ImGui::TextDisabled("In");
497 ImGui::SameLine(columnWidth);
498 ImGui::TextDisabled("Then");
499
500 ImGui::TextDisabled(" ");
501 ImGui::SameLine(columnWidth);
502 ImGui::TextDisabled("Else");
503#endif
504}
505
507{
508#ifndef OLYMPE_HEADLESS
509 // Green text for condition previews (READ-ONLY)
510 const ImVec4 condColor(0.f, 1.f, 0.f, 1.f);
511
512 ImGui::TextDisabled("Conditions Preview:");
513
514 if (m_conditionRefs.empty())
515 {
516 ImGui::TextDisabled(" (no conditions)");
517 }
518 else
519 {
520 for (size_t i = 0; i < m_conditionRefs.size(); ++i)
521 {
523
524 ImGui::PushID(static_cast<int>(i));
525
526 // Format: "[LogicalOp] Condition #N: [preview]"
527 std::string linePrefix = " "; // Indentation
528 if (i > 0)
529 {
530 if (ref.logicalOp == LogicalOp::And)
531 linePrefix += "And ";
532 else if (ref.logicalOp == LogicalOp::Or)
533 linePrefix += "Or ";
534 }
535 else
536 {
537 linePrefix += "If ";
538 }
539
540 const ConditionPreset* preset = m_registry.GetPreset(ref.presetID);
541 ImGui::PushStyleColor(ImGuiCol_Text, condColor);
542
543 // Format: " If Condition #1: [mHealth] <= [100.00]"
544 // or: " And Condition #2: [mSpeed] == [Pin-in #1]"
545 // Phase 24: Build preview from ACTUAL operand data (ConditionRef), not static preset
546 // This ensures Pin-mode operands display correctly (e.g., "Pin-in #1" instead of old const value)
547 std::string conditionPreview;
548 if (preset && i < m_conditionOperandRefs.size())
549 {
551 std::ostringstream oss;
552
553 // Left operand
554 oss << "[";
555 if (cref.leftOperand.mode == OperandRef::Mode::Variable)
557 else if (cref.leftOperand.mode == OperandRef::Mode::Pin)
558 oss << "Pin-in #" << (i + 1) << "L";
559 else
560 oss << cref.leftOperand.constValue;
561 oss << "]";
562
563 // Operator
564 oss << " " << cref.operatorStr << " ";
565
566 // Right operand
567 oss << "[";
568 if (cref.rightOperand.mode == OperandRef::Mode::Variable)
569 oss << cref.rightOperand.variableName;
570 else if (cref.rightOperand.mode == OperandRef::Mode::Pin)
571 oss << "Pin-in #" << (i + 1) << "R";
572 else
573 oss << cref.rightOperand.constValue;
574 oss << "]";
575
576 conditionPreview = oss.str();
577 }
578 else if (preset)
579 {
580 // Fallback to preset preview if operand data not available
581 conditionPreview = preset->GetPreview();
582 }
583 else
584 {
585 conditionPreview = "(missing: " + ref.presetID + ")";
586 }
587
588 ImGui::Text("%sCondition #%zu: %s", linePrefix.c_str(), i + 1, conditionPreview.c_str());
589 ImGui::PopStyleColor();
590
591 ImGui::PopID();
592 }
593 }
594
595 ImGui::Spacing();
596
597 // "Edit Conditions" button — full width — opens the owned modal
598 if (ImGui::Button("Edit Conditions", ImVec2(-1.f, 24.f)))
599 {
601 // Phase 24: Pass both conditionRefs AND operandRefs to the modal
603 }
604#endif
605}
606
608{
609#ifndef OLYMPE_HEADLESS
610 // UI layout constants
611 static const float kOpComboWidth = 60.f; ///< Width of the And/Or combo
612 static const float kDeleteBtnWidth = 22.f; ///< Width of the [X] delete button
613 static const size_t kFilterBufSize = 256u; ///< Max filter string length incl. NUL
614
615 // Collapsible section header — green tint to match condition colour convention
616 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.f, 0.9f, 0.f, 1.f));
617 const bool open = ImGui::CollapsingHeader(
618 "Structured Conditions (evaluated with implicit AND)",
620 ImGui::PopStyleColor();
621
622 if (!open)
623 return;
624
625 const ImVec4 condColor(0.f, 1.f, 0.f, 1.f); // green for preview text (unused in list, kept for IDE compat)
627
628 // Deferred-deletion index (avoids invalidating the iterator inside the loop)
629 size_t deleteIdx = static_cast<size_t>(-1);
630
631 for (size_t i = 0; i < m_conditionRefs.size(); ++i)
632 {
634 ImGui::PushID(static_cast<int>(i));
635
636 // ── Operand editors (left operand | operator | right operand) ─────────
637 // Ensure parallel operand-ref list is always in sync
638 if (i >= m_conditionOperandRefs.size())
639 {
641 newCref.conditionIndex = static_cast<int>(i);
643 }
645
646 // Resolve short pin labels for Pin-mode operands
647 std::string leftPinLabel, rightPinLabel;
648 for (const auto& pin : m_dynamicPins)
649 {
650 if (pin.conditionIndex == static_cast<int>(i))
651 {
652 if (pin.position == OperandPosition::Left)
653 leftPinLabel = pin.GetShortLabel();
654 else
655 rightPinLabel = pin.GetShortLabel();
656 }
657 }
658
659 ImGui::PushID("left");
660 RenderOperandDropdown(cref, /*isLeft=*/true, leftPinLabel);
661 ImGui::PopID();
662
663 ImGui::SameLine();
664 ImGui::PushID("op");
666 ImGui::PopID();
667
668 ImGui::SameLine();
669 ImGui::PushID("right");
670 RenderOperandDropdown(cref, /*isLeft=*/false, rightPinLabel);
671 ImGui::PopID();
672
673 ImGui::SameLine();
674 if (i > 0)
675 {
676 ImGui::SetNextItemWidth(kOpComboWidth);
677 const char* opLabel = (ref.logicalOp == LogicalOp::Or) ? "Or" : "And";
678 if (ImGui::BeginCombo("##op", opLabel, ImGuiComboFlags_NoArrowButton))
679 {
680 if (ImGui::Selectable("And", ref.logicalOp == LogicalOp::And))
681 {
682 ref.logicalOp = LogicalOp::And;
683 m_dirty = true;
684 }
685 if (ImGui::Selectable("Or", ref.logicalOp == LogicalOp::Or))
686 {
687 ref.logicalOp = LogicalOp::Or;
688 m_dirty = true;
689 }
690 ImGui::EndCombo();
691 }
692 ImGui::SameLine();
693 }
694 else
695 {
696 // Reserve the same horizontal space as the combo so [X] buttons align
697 ImGui::Dummy(ImVec2(kOpComboWidth, 0.f));
698 ImGui::SameLine();
699 }
700
701 // ── Delete button (X) ─────────────────────────────────────────────────
702 ImGui::SetNextItemWidth(kDeleteBtnWidth);
703 if (ImGui::SmallButton("X"))
704 deleteIdx = i;
705
706 ImGui::PopID();
707 }
708
709 // Apply deferred deletion outside the loop to avoid iterator invalidation
710 if (deleteIdx != static_cast<size_t>(-1))
712
713 // ── Empty-list placeholder ────────────────────────────────────────────────
714 if (m_conditionRefs.empty())
715 ImGui::TextDisabled("(no conditions — click [+ Add Condition] below)");
716
717 ImGui::Spacing();
718
719 // ── [+ Add Condition] button -> opens preset-selector popup ───────────────
720 if (ImGui::Button("[+ Add Condition]", ImVec2(-1.f, 0.f)))
721 ImGui::OpenPopup("##AddCondPopup");
722
723 if (ImGui::BeginPopup("##AddCondPopup"))
724 {
725 // Filter input
726 char filterBuf[kFilterBufSize] = {};
727 {
728 const std::string& cur = m_dropdownFilter;
729 const size_t copyLen =
730 cur.size() < (kFilterBufSize - 1u) ? cur.size() : kFilterBufSize - 1u;
731 cur.copy(filterBuf, copyLen);
732 }
733 if (ImGui::InputText("Filter", filterBuf, kFilterBufSize))
735
736 ImGui::Separator();
737
739 for (const auto& p : presets)
740 {
741 const std::string rowLabel = p.name + " — " + p.GetPreview();
742 if (ImGui::Selectable(rowLabel.c_str()))
743 {
744 AddCondition(p.id);
746 ImGui::CloseCurrentPopup();
747
750 }
751 }
752
753 if (presets.empty())
754 ImGui::TextDisabled("(no presets in registry)");
755
756 ImGui::EndPopup();
757 }
758#endif
759}
760
761
763{
764#ifndef OLYMPE_HEADLESS
765 // Yellow color (#FFD700) for dynamic pins
766 const ImVec4 pinColor(1.0f, 0.843f, 0.0f, 1.0f);
767 for (const auto& pin : m_dynamicPins)
768 {
769 const std::string displayLabel = pin.GetDisplayLabel();
770 ImGui::TextColored(pinColor, "%s", displayLabel.c_str());
771
772 // Hover tooltip shows full label
773 if (ImGui::IsItemHovered())
774 {
775 ImGui::SetTooltip("%s", displayLabel.c_str());
776 }
777 }
778#endif
779}
780
782 const std::string& pinLabel)
783{
784#ifndef OLYMPE_HEADLESS
785 OperandRef& op = isLeft ? cref.leftOperand : cref.rightOperand;
786
787 // Build unified dropdown: [Pin-in #1] [Pin-in #2] ... [Const] <value> ... Variables (sorted)
788 std::vector<std::string> allOptions;
789 std::vector<int> optionTypes; // 0=Variable, 1=Const, 2=Pin
790 std::vector<std::string> optionValues;
791
792 int currentSelectionIdx = -1;
793
794 // ── Add all available pins FIRST ─────────────────────────────────────
795 for (const auto& pin : m_dynamicPins)
796 {
797 if (pin.conditionIndex == cref.conditionIndex)
798 {
799 if ((isLeft && pin.position == OperandPosition::Left) ||
800 (!isLeft && pin.position == OperandPosition::Right))
801 {
802 // Phase 24: Show just the pin's short label, not duplicated "[Pin-in] Pin-in #N"
803 allOptions.push_back(pin.GetShortLabel());
804 optionTypes.push_back(2); // Pin
805 optionValues.push_back(pin.id);
806
807 if (op.mode == OperandRef::Mode::Pin && op.dynamicPinID == pin.id)
808 {
809 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
810 }
811 }
812 }
813 }
814
815 // ── Add [Const] option SECOND ────────────────────────────────────────
816 {
817 std::string constLabel = "[Const] ";
818 std::ostringstream oss;
819 oss << std::fixed << std::setprecision(3) << std::stod(op.constValue);
820 std::string constVal = oss.str();
821 // Trim trailing zeros
822 size_t dot = constVal.find('.');
823 if (dot != std::string::npos)
824 {
825 size_t last = constVal.find_last_not_of('0');
826 if (last != std::string::npos && last > dot)
827 constVal = constVal.substr(0, last + 1);
828 else if (last == dot)
829 constVal = constVal.substr(0, dot);
830 }
832
833 allOptions.push_back(constLabel);
834 optionTypes.push_back(1); // Const
835 optionValues.push_back(op.constValue);
836
838 {
839 currentSelectionIdx = static_cast<int>(allOptions.size() - 1);
840 }
841 }
842
843 // ── Add all variables (local + global) LAST (sorted alphabetically) ────
844 // Separate local and global variables with a visual separator
845 int variableStartIdx = static_cast<int>(allOptions.size());
846
847 // Add local variables first
848 for (const auto& localVar : m_localVariables)
849 {
850 allOptions.push_back(localVar.Key);
851 optionTypes.push_back(0); // Variable
852 optionValues.push_back(localVar.Key);
853
855 {
856 currentSelectionIdx = static_cast<int>(allOptions.size()) - 1;
857 }
858 }
859
860 // Add separator if we have both local and global variables
861 if (!m_localVariables.empty())
862 {
863 allOptions.push_back("--- Global Variables ---");
864 optionTypes.push_back(-1); // Separator (non-selectable)
865 optionValues.push_back("");
866 }
867
868 // Add global variables
870 const std::vector<GlobalEntryDefinition>& globalVars = gtb.GetAllVariables();
871 for (const auto& globalVar : globalVars)
872 {
873 allOptions.push_back(globalVar.Key);
874 optionTypes.push_back(0); // Variable
875 optionValues.push_back(globalVar.Key);
876
878 {
879 currentSelectionIdx = static_cast<int>(allOptions.size()) - 1;
880 }
881 }
882
883 ImGui::SetNextItemWidth(120.0f);
886 : "Variable";
887
888 if (ImGui::BeginCombo("##operand_dropdown", displayText))
889 {
890 // Const options
891 for (int i = 0; i < static_cast<int>(allOptions.size()); ++i)
892 {
893 if (optionTypes[i] == -1)
894 {
895 // Separator (non-selectable)
896 ImGui::TextDisabled("%s", allOptions[i].c_str());
897 ImGui::Separator();
898 }
899 else if (optionTypes[i] == 0)
900 {
901 // Variable option
902 bool selected = (i == currentSelectionIdx);
903 if (ImGui::Selectable(allOptions[i].c_str(), selected))
904 {
908 m_dirty = true;
911 }
912 }
913 else if (optionTypes[i] == 1 || optionTypes[i] == 2)
914 {
915 // Const or Pin option
916 bool selected = (i == currentSelectionIdx);
917 if (ImGui::Selectable(allOptions[i].c_str(), selected))
918 {
919 switch (optionTypes[i])
920 {
921 case 1: // Const
924 break;
925 case 2: // Pin
928 break;
929 }
931 m_dirty = true;
934 }
935 }
936 }
937
938 ImGui::EndCombo();
939 }
940
941 ImGui::SameLine();
942
943 // ── Input field based on current mode ────────────────────────────────
944 static const size_t kBufSize = 128u;
945 switch (op.mode)
946 {
948 {
949 char buf[kBufSize] = {};
950 const std::string& cur = op.variableName;
951 const size_t copyLen = cur.size() < (kBufSize - 1u) ? cur.size() : kBufSize - 1u;
952 cur.copy(buf, copyLen);
953 ImGui::SetNextItemWidth(80.f);
954 if (ImGui::InputText("##varname", buf, kBufSize))
955 {
956 op.variableName = buf;
957 m_dirty = true;
958 }
959 break;
960 }
962 {
963 // Try to parse as double, but show as text in dropdown
964 double doubleVal = 0.0;
965 try {
966 doubleVal = std::stod(op.constValue);
967 } catch (...) {
968 // Parse failed, keep 0.0
969 }
970
971 ImGui::SetNextItemWidth(80.f);
972 if (ImGui::InputDouble("##constval", &doubleVal, 0.0, 0.0, "%.3f"))
973 {
974 std::ostringstream oss;
975 oss << doubleVal;
976 op.constValue = oss.str();
977 m_dirty = true;
978 }
979 break;
980 }
982 {
983 // Read-only: show the "Pin-in #N" label
984 const std::string display = pinLabel.empty() ? "[Pin-in]" : pinLabel;
985 ImGui::SetNextItemWidth(80.f);
986 ImGui::TextDisabled("%s", display.c_str());
987 break;
988 }
989 default:
990 break;
991 }
992#endif
993}
994
996{
997#ifndef OLYMPE_HEADLESS
998 static const char* kOperators[] = { "==", "!=", "<", "<=", ">", ">=" };
999 static const int kNumOps = 6;
1000
1001 int currentOp = 0;
1002 for (int i = 0; i < kNumOps; ++i)
1003 {
1004 if (cref.operatorStr == kOperators[i])
1005 {
1006 currentOp = i;
1007 break;
1008 }
1009 }
1010
1011 ImGui::SetNextItemWidth(44.f);
1012 if (ImGui::BeginCombo("##op2", kOperators[currentOp], ImGuiComboFlags_NoArrowButton))
1013 {
1014 for (int i = 0; i < kNumOps; ++i)
1015 {
1016 bool selected = (i == currentOp);
1017 if (ImGui::Selectable(kOperators[i], selected))
1018 {
1019 cref.operatorStr = kOperators[i];
1020 m_dirty = true;
1021 }
1022 }
1023 ImGui::EndCombo();
1024 }
1025#endif
1026}
1027
1028// ============================================================================
1029// Internal helpers
1030// ============================================================================
1031
1033{
1034 if (!m_conditionRefs.empty())
1035 m_conditionRefs[0].logicalOp = LogicalOp::Start;
1036}
1037
1038} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
UI Properties panel for a NodeBranch – 4-section layout (Phase 24-REFONTE).
Manages the global pool of ConditionPreset objects.
std::vector< std::string > GetAllPresetIDs() const
Returns all preset UUIDs in display order.
bool ValidatePresetID(const std::string &id) const
Returns true when a preset with the given UUID exists.
std::vector< ConditionPreset > GetFilteredPresets(const std::string &filter) const
Returns all presets whose name contains the filter substring.
std::vector< std::string > FindPresetsByName(const std::string &substring) const
Returns UUIDs of all presets whose name contains a substring.
ConditionPreset * GetPreset(const std::string &id)
Returns a mutable pointer to the preset, or nullptr if not found.
static GlobalTemplateBlackboard & Get()
void Open(const std::vector< NodeConditionRef > &currentRefs, const std::vector< ConditionRef > &operandRefs=std::vector< ConditionRef >())
Opens the modal, loading the given condition refs for editing.
void RenderOperatorDropdown(ConditionRef &cref)
Renders the operator combo box (==, !=, <, <=, >, >=).
std::string m_nodeName
Node display name for title section.
std::vector< NodeConditionRef > m_conditionRefs
Current node's conditions.
void RenderTitleSection()
Section 1: title bar (blue background, node name).
const std::vector< ConditionRef > & GetConditionOperandRefs() const
Returns the current inline operand list.
void SetConditionRefs(const std::vector< NodeConditionRef > &refs)
Replaces the panel's internal condition list.
void RemoveCondition(size_t index)
Removes the condition at the given index.
void RenderInlineConditionRow(size_t index, const NodeConditionRef &ref)
Renders a single condition row with edit/move/delete controls.
void RenderConditionsPreview()
Section 3: read-only conditions preview (green text) + "Edit Conditions" button.
NodeConditionsEditModal m_editModal
Owned modal for condition editing.
std::vector< DynamicDataPin > m_dynamicPins
Read-only dynamic pins for display.
void NormalizeLogicalOps()
Fixes up the first condition's logicalOp to always be Start.
void RenderConditionList()
Renders all conditions inline as an editable list.
const std::vector< NodeConditionRef > & GetConditionRefs() const
Returns the current (possibly modified) condition list.
ConditionPresetRegistry & m_registry
Shared global registry.
NodeConditionsPanel(ConditionPresetRegistry &registry)
Constructs the panel bound to a global preset registry.
void RenderInlineConditionEditor()
Phase 24 UX Refactor: Renders condition editor inline (no modal popup).
void AddCondition(const std::string &presetID)
Appends a new condition reference with the given preset.
void RenderExecPinsSection()
Section 2: static exec pins (In / Then / Else, never editable).
std::vector< ConditionRef > m_conditionOperandRefs
Inline operand data (parallel to m_conditionRefs)
std::vector< std::string > Validate() const
Returns a list of error strings describing validation failures.
void RenderDynamicPinsSection()
Section 4: dynamic data pins (yellow, only when non-empty).
void SetConditionOperandRefs(const std::vector< ConditionRef > &refs)
Replaces the panel's inline operand list (parallel to conditionRefs).
bool IsValid() const
Returns true when every condition ref points to an existing preset.
void OnPresetDeleted(const std::string &deletedPresetID)
Should be called by the host when a preset is deleted from the registry.
bool m_dirty
Modification flag.
std::string m_dropdownFilter
Filter text for "Add Condition" dropdown.
bool m_editModalRequested
Set when Edit button is clicked.
std::function< void()> OnDynamicPinsNeedRegeneration
Fired after the edit modal is confirmed and condition refs are updated, signalling that dynamic pins ...
void SetDynamicPins(const std::vector< DynamicDataPin > &pins)
Provides the read-only list of dynamic pins for display.
void Render()
Renders the condition list and dynamic-pin display using ImGui.
std::vector< BlackboardEntry > m_localVariables
Local variables from entity (Phase 24)
std::vector< ConditionPreset > GetFilteredPresetsForDropdown() const
Returns presets matching the current dropdown filter.
void SetLogicalOp(size_t index, LogicalOp op)
Changes the logical operator for the condition at the given index.
std::function< void(const std::string &)> OnPresetChanged
Fired when a preset is selected or changed.
void RenderInlineAddCondition()
Renders the "Add Condition" button and preset picker inline.
size_t GetConditionCount() const
Returns the number of conditions currently in the list.
void SetDropdownFilter(const std::string &filter)
Sets the search/filter string used by the "Add Condition" dropdown.
bool m_pickerOpen
Phase 24: Inline preset picker visibility.
void RenderOperandDropdown(ConditionRef &cref, bool isLeft, const std::string &pinLabel="")
Renders the operand mode selector + value field for one operand.
< Provides AssetID and INVALID_ASSET_ID
LogicalOp
How this condition is combined with the one preceding it.
@ Or
Combined with OR.
@ Start
First condition in the list (no logical combinator)
@ And
Combined with AND.
@ Right
Pin provides data for the right operand of the condition.
@ Left
Pin provides data for the left operand of the condition.
@ 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)
A globally-stored, reusable condition expression.
Stores the complete reference for one condition including operand-to-DynamicDataPin mapping.
OperandRef leftOperand
Left-hand side of the comparison.
int conditionIndex
Index in the Branch/While node's conditions[] array.
One entry in a NodeBranch's conditions list.
std::string presetID
UUID of the referenced ConditionPreset.
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.
std::string constValue
Literal string (mode == Const), e.g. "100.0".
std::string variableName
Blackboard key (mode == Variable), e.g. "mHealth".
Mode mode
Active mode.
std::string dynamicPinID
UUID of the DynamicDataPin that supplies this operand's value.
std::string stringValue
Variable ID or Pin label (mode == Variable|Pin)
Definition Operand.h:47