Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
UndoRedoStack.cpp
Go to the documentation of this file.
1/**
2 * @file UndoRedoStack.cpp
3 * @brief Implementation of the undo/redo command stack (Phase 6).
4 * @author Olympe Engine
5 * @date 2026-03-09
6 *
7 * C++14 compliant — no std::optional, structured bindings, std::filesystem.
8 */
9
10#include "UndoRedoStack.h"
11
12#include <algorithm>
13#include <sstream>
14
15namespace Olympe {
16
17// ============================================================================
18// AddNodeCommand
19// ============================================================================
20
25
27{
28 graph.Nodes.push_back(m_node);
29 graph.BuildLookupCache();
30}
31
33{
34 auto it = std::remove_if(graph.Nodes.begin(), graph.Nodes.end(),
35 [this](const TaskNodeDefinition& n) { return n.NodeID == m_node.NodeID; });
36 graph.Nodes.erase(it, graph.Nodes.end());
37 graph.BuildLookupCache();
38}
39
40std::string AddNodeCommand::GetDescription() const
41{
42 std::ostringstream ss;
43 ss << "Add Node #" << m_node.NodeID << " (" << m_node.NodeName << ")";
44 return ss.str();
45}
46
47// ============================================================================
48// DeleteNodeCommand
49// ============================================================================
50
52 : m_nodeID(nodeID)
53{
54}
55
57{
58 // Save the node before deletion
59 for (const auto& n : graph.Nodes)
60 {
61 if (n.NodeID == m_nodeID)
62 {
63 m_savedNode = n;
64 break;
65 }
66 }
67
68 // Save exec connections referencing this node
69 m_savedExecConns.clear();
70 for (const auto& ec : graph.ExecConnections)
71 {
72 if (ec.SourceNodeID == m_nodeID || ec.TargetNodeID == m_nodeID)
73 m_savedExecConns.push_back(ec);
74 }
75
76 // Save data connections referencing this node
77 m_savedDataConns.clear();
78 for (const auto& dc : graph.DataConnections)
79 {
80 if (dc.SourceNodeID == m_nodeID || dc.TargetNodeID == m_nodeID)
81 m_savedDataConns.push_back(dc);
82 }
83
84 // Remove the node
85 auto it = std::remove_if(graph.Nodes.begin(), graph.Nodes.end(),
86 [this](const TaskNodeDefinition& n) { return n.NodeID == m_nodeID; });
87 graph.Nodes.erase(it, graph.Nodes.end());
88
89 // Remove exec connections referencing this node
90 auto eit = std::remove_if(graph.ExecConnections.begin(), graph.ExecConnections.end(),
91 [this](const ExecPinConnection& ec)
92 { return ec.SourceNodeID == m_nodeID || ec.TargetNodeID == m_nodeID; });
93 graph.ExecConnections.erase(eit, graph.ExecConnections.end());
94
95 // Remove data connections referencing this node
96 auto dit = std::remove_if(graph.DataConnections.begin(), graph.DataConnections.end(),
97 [this](const DataPinConnection& dc)
98 { return dc.SourceNodeID == m_nodeID || dc.TargetNodeID == m_nodeID; });
99 graph.DataConnections.erase(dit, graph.DataConnections.end());
100
101 graph.BuildLookupCache();
102}
103
105{
106 // Restore node
107 graph.Nodes.push_back(m_savedNode);
108
109 // Restore connections
110 for (const auto& ec : m_savedExecConns)
111 graph.ExecConnections.push_back(ec);
112 for (const auto& dc : m_savedDataConns)
113 graph.DataConnections.push_back(dc);
114
115 graph.BuildLookupCache();
116}
117
118std::string DeleteNodeCommand::GetDescription() const
119{
120 std::ostringstream ss;
121 ss << "Delete Node #" << m_nodeID;
122 return ss.str();
123}
124
125// ============================================================================
126// MoveNodeCommand
127// ============================================================================
128
130 float oldX, float oldY,
131 float newX, float newY)
132 : m_nodeID(nodeID)
133 , m_oldX(oldX), m_oldY(oldY)
134 , m_newX(newX), m_newY(newY)
135{
136}
137
139 int32_t nodeID, float x, float y)
140{
141 for (auto& n : graph.Nodes)
142 {
143 if (n.NodeID == nodeID)
144 {
145 // Encode position in Parameters map under "__posX" / "__posY"
148 bx.LiteralValue = TaskValue(x);
150 by.LiteralValue = TaskValue(y);
151 n.Parameters["__posX"] = bx;
152 n.Parameters["__posY"] = by;
153 break;
154 }
155 }
156}
157
162
167
168std::string MoveNodeCommand::GetDescription() const
169{
170 std::ostringstream ss;
171 ss << "Move Node #" << m_nodeID
172 << " (" << m_oldX << "," << m_oldY
173 << ") -> (" << m_newX << "," << m_newY << ")";
174 return ss.str();
175}
176
177// ============================================================================
178// AddConnectionCommand
179// ============================================================================
180
185
187{
188 graph.ExecConnections.push_back(m_conn);
189}
190
192{
193 auto it = std::remove_if(graph.ExecConnections.begin(), graph.ExecConnections.end(),
194 [this](const ExecPinConnection& ec)
195 {
196 return ec.SourceNodeID == m_conn.SourceNodeID &&
197 ec.TargetNodeID == m_conn.TargetNodeID &&
198 ec.SourcePinName == m_conn.SourcePinName &&
199 ec.TargetPinName == m_conn.TargetPinName;
200 });
201 graph.ExecConnections.erase(it, graph.ExecConnections.end());
202}
203
205{
206 std::ostringstream ss;
207 ss << "Add Connection #" << m_conn.SourceNodeID << "." << m_conn.SourcePinName
208 << " -> #" << m_conn.TargetNodeID << "." << m_conn.TargetPinName;
209 return ss.str();
210}
211
212// ============================================================================
213// AddDataConnectionCommand
214// ============================================================================
215
220
222{
223 graph.DataConnections.push_back(m_conn);
224}
225
227{
228 auto it = std::remove_if(graph.DataConnections.begin(), graph.DataConnections.end(),
229 [this](const DataPinConnection& dc)
230 {
231 return dc.SourceNodeID == m_conn.SourceNodeID &&
232 dc.TargetNodeID == m_conn.TargetNodeID &&
233 dc.SourcePinName == m_conn.SourcePinName &&
234 dc.TargetPinName == m_conn.TargetPinName;
235 });
236 graph.DataConnections.erase(it, graph.DataConnections.end());
237}
238
240{
241 std::ostringstream ss;
242 ss << "Add Data Connection #" << m_conn.SourceNodeID << "." << m_conn.SourcePinName
243 << " -> #" << m_conn.TargetNodeID << "." << m_conn.TargetPinName;
244 return ss.str();
245}
246
247// ============================================================================
248// DeleteLinkCommand
249// ============================================================================
250
252 : m_isExecConn(true), m_savedExecConn(conn), m_savedDataConn()
253{
254}
255
257 : m_isExecConn(false), m_savedExecConn(), m_savedDataConn(conn)
258{
259}
260
262{
263 if (m_isExecConn)
264 {
265 auto it = std::remove_if(graph.ExecConnections.begin(), graph.ExecConnections.end(),
266 [this](const ExecPinConnection& ec)
267 {
268 return ec.SourceNodeID == m_savedExecConn.SourceNodeID &&
269 ec.TargetNodeID == m_savedExecConn.TargetNodeID &&
270 ec.SourcePinName == m_savedExecConn.SourcePinName &&
271 ec.TargetPinName == m_savedExecConn.TargetPinName;
272 });
273 graph.ExecConnections.erase(it, graph.ExecConnections.end());
274 }
275 else
276 {
277 auto it = std::remove_if(graph.DataConnections.begin(), graph.DataConnections.end(),
278 [this](const DataPinConnection& dc)
279 {
280 return dc.SourceNodeID == m_savedDataConn.SourceNodeID &&
281 dc.TargetNodeID == m_savedDataConn.TargetNodeID &&
282 dc.SourcePinName == m_savedDataConn.SourcePinName &&
283 dc.TargetPinName == m_savedDataConn.TargetPinName;
284 });
285 graph.DataConnections.erase(it, graph.DataConnections.end());
286 }
287 graph.BuildLookupCache();
288}
289
291{
292 if (m_isExecConn)
293 graph.ExecConnections.push_back(m_savedExecConn);
294 else
295 graph.DataConnections.push_back(m_savedDataConn);
296 graph.BuildLookupCache();
297}
298
300{
301 std::ostringstream ss;
302 if (m_isExecConn)
303 {
304 ss << "Delete Exec Link " << m_savedExecConn.SourceNodeID
306 << " -> " << m_savedExecConn.TargetNodeID
308 }
309 else
310 {
311 ss << "Delete Data Link " << m_savedDataConn.SourceNodeID
313 << " -> " << m_savedDataConn.TargetNodeID
315 }
316 return ss.str();
317}
318
319// ============================================================================
320// EditNodePropertyCommand
321// ============================================================================
322
324 const std::string& propertyKey,
325 const PropertyValue& oldValue,
326 const PropertyValue& newValue)
327 : m_nodeID(nodeID)
328 , m_propertyKey(propertyKey)
329 , m_oldValue(oldValue)
330 , m_newValue(newValue)
331{
332}
333
335 const std::string& key,
336 const PropertyValue& value)
337{
338 if (key == "NodeName")
339 node.NodeName = value.strVal;
340 else if (key == "AtomicTaskID")
341 node.AtomicTaskID = value.strVal;
342 else if (key == "ConditionID")
343 node.ConditionID = value.strVal;
344 else if (key == "BBKey")
345 node.BBKey = value.strVal;
346 else if (key == "MathOperator")
347 node.MathOperator = value.strVal;
348 else if (key == "SubGraphPath")
349 node.SubGraphPath = value.strVal;
350 else if (key == "DelaySeconds")
351 node.DelaySeconds = value.floatVal;
352}
353
355{
356 for (size_t i = 0; i < graph.Nodes.size(); ++i)
357 {
358 if (graph.Nodes[i].NodeID == m_nodeID)
359 {
361 break;
362 }
363 }
364 graph.BuildLookupCache();
365}
366
368{
369 for (size_t i = 0; i < graph.Nodes.size(); ++i)
370 {
371 if (graph.Nodes[i].NodeID == m_nodeID)
372 {
374 break;
375 }
376 }
377 graph.BuildLookupCache();
378}
379
381{
382 std::ostringstream ss;
383 ss << "Edit Node #" << m_nodeID << " " << m_propertyKey;
384 return ss.str();
385}
386
387// ============================================================================
388// AddDynamicPinCommand
389// ============================================================================
390
391AddDynamicPinCommand::AddDynamicPinCommand(int32_t nodeID, const std::string& pinName)
392 : m_nodeID(nodeID)
393 , m_pinName(pinName)
394{
395}
396
398{
399 for (size_t i = 0; i < graph.Nodes.size(); ++i)
400 {
401 if (graph.Nodes[i].NodeID == m_nodeID)
402 {
403 graph.Nodes[i].DynamicExecOutputPins.push_back(m_pinName);
404 break;
405 }
406 }
407 graph.BuildLookupCache();
408}
409
411{
412 for (size_t i = 0; i < graph.Nodes.size(); ++i)
413 {
414 if (graph.Nodes[i].NodeID == m_nodeID)
415 {
416 std::vector<std::string>& pins = graph.Nodes[i].DynamicExecOutputPins;
417 if (!pins.empty() && pins.back() == m_pinName)
418 pins.pop_back();
419 break;
420 }
421 }
422 graph.BuildLookupCache();
423}
424
426{
427 return "Add Pin " + m_pinName + " to node #" + std::to_string(m_nodeID);
428}
429
430// ============================================================================
431// RemoveExecPinCommand
432// ============================================================================
433
435 const std::string& pinName,
436 int pinIndex,
438 const std::string& linkedTargetPinName)
439 : m_nodeID(nodeID)
440 , m_pinName(pinName)
441 , m_pinIndex(pinIndex)
442 , m_linkedTargetNodeID(linkedTargetNodeID)
443 , m_linkedTargetPinName(linkedTargetPinName)
444{
445}
446
448{
449 // Remove pin from DynamicExecOutputPins
450 for (size_t i = 0; i < graph.Nodes.size(); ++i)
451 {
452 if (graph.Nodes[i].NodeID == m_nodeID)
453 {
454 std::vector<std::string>& pins = graph.Nodes[i].DynamicExecOutputPins;
455 if (m_pinIndex >= 0 && m_pinIndex < static_cast<int>(pins.size()))
456 pins.erase(pins.begin() + m_pinIndex);
457 break;
458 }
459 }
460
461 // Remove any ExecConnection originating from this pin
462 auto it = std::remove_if(graph.ExecConnections.begin(), graph.ExecConnections.end(),
463 [this](const ExecPinConnection& ec)
464 {
465 return ec.SourceNodeID == m_nodeID && ec.SourcePinName == m_pinName;
466 });
467 graph.ExecConnections.erase(it, graph.ExecConnections.end());
468
469 graph.BuildLookupCache();
470}
471
473{
474 // Re-insert pin at its original index
475 for (size_t i = 0; i < graph.Nodes.size(); ++i)
476 {
477 if (graph.Nodes[i].NodeID == m_nodeID)
478 {
479 std::vector<std::string>& pins = graph.Nodes[i].DynamicExecOutputPins;
480 int insertAt = m_pinIndex;
481 if (insertAt < 0)
482 insertAt = 0;
483 if (insertAt > static_cast<int>(pins.size()))
484 insertAt = static_cast<int>(pins.size());
485 pins.insert(pins.begin() + insertAt, m_pinName);
486 break;
487 }
488 }
489
490 // Restore the outgoing link if one existed
491 if (m_linkedTargetNodeID != -1)
492 {
495 conn.SourcePinName = m_pinName;
496 conn.TargetNodeID = m_linkedTargetNodeID;
497 conn.TargetPinName = m_linkedTargetPinName;
498 graph.ExecConnections.push_back(conn);
499 }
500
501 graph.BuildLookupCache();
502}
503
505{
506 return "Remove Pin " + m_pinName + " from node #" + std::to_string(m_nodeID);
507}
508
509// ============================================================================
510// UndoRedoStack
511// ============================================================================
512
516
517void UndoRedoStack::PushCommand(std::unique_ptr<ICommand> cmd,
519{
520 if (!cmd) return;
521
522 // Execute the command on the graph
523 cmd->Execute(graph);
524
525 // Evict oldest entry if the stack is full
526 if (m_undoStack.size() >= MAX_STACK_SIZE)
527 {
528 m_undoStack.erase(m_undoStack.begin());
529 }
530
531 m_undoStack.push_back(std::move(cmd));
532
533 // Any new command clears the redo stack
534 m_redoStack.clear();
535}
536
538{
539 if (m_undoStack.empty()) return;
540
541 auto cmd = std::move(m_undoStack.back());
542 m_undoStack.pop_back();
543
544 cmd->Undo(graph);
545
546 m_redoStack.push_back(std::move(cmd));
547}
548
550{
551 if (m_redoStack.empty()) return;
552
553 auto cmd = std::move(m_redoStack.back());
554 m_redoStack.pop_back();
555
556 cmd->Execute(graph);
557
558 m_undoStack.push_back(std::move(cmd));
559}
560
562{
563 m_undoStack.clear();
564 m_redoStack.clear();
565}
566
568{
569 return !m_undoStack.empty();
570}
571
573{
574 return !m_redoStack.empty();
575}
576
577std::size_t UndoRedoStack::UndoSize() const
578{
579 return m_undoStack.size();
580}
581
582std::size_t UndoRedoStack::RedoSize() const
583{
584 return m_redoStack.size();
585}
586
588{
589 if (m_undoStack.empty()) return "";
590 return m_undoStack.back()->GetDescription();
591}
592
594{
595 if (m_redoStack.empty()) return "";
596 return m_redoStack.back()->GetDescription();
597}
598
599// ============================================================================
600// EditParameterCommand (Phase 22-C)
601// ============================================================================
602
604 const std::string& paramName,
607 : m_nodeID(nodeID)
608 , m_paramName(paramName)
609 , m_oldBinding(oldBinding)
610 , m_newBinding(newBinding)
611{}
612
614{
615 for (size_t i = 0; i < graph.Nodes.size(); ++i)
616 {
617 if (graph.Nodes[i].NodeID == m_nodeID)
618 {
619 graph.Nodes[i].Parameters[m_paramName] = m_newBinding;
620 break;
621 }
622 }
623}
624
626{
627 for (size_t i = 0; i < graph.Nodes.size(); ++i)
628 {
629 if (graph.Nodes[i].NodeID == m_nodeID)
630 {
631 graph.Nodes[i].Parameters[m_paramName] = m_oldBinding;
632 break;
633 }
634 }
635}
636
637std::string EditParameterCommand::GetDescription() const
638{
639 return "Edit parameter '" + m_paramName + "' on node #" + std::to_string(m_nodeID);
640}
641
642// ============================================================================
643// EditNodePropertiesCommand (Phase 22-C)
644// ============================================================================
645
647 const ParameterMap& oldParams,
648 const ParameterMap& newParams)
649 : m_nodeID(nodeID)
650 , m_oldParams(oldParams)
651 , m_newParams(newParams)
652{}
653
655{
656 for (size_t i = 0; i < graph.Nodes.size(); ++i)
657 {
658 if (graph.Nodes[i].NodeID == m_nodeID)
659 {
660 graph.Nodes[i].Parameters = m_newParams;
661 break;
662 }
663 }
664}
665
667{
668 for (size_t i = 0; i < graph.Nodes.size(); ++i)
669 {
670 if (graph.Nodes[i].NodeID == m_nodeID)
671 {
672 graph.Nodes[i].Parameters = m_oldParams;
673 break;
674 }
675 }
676}
677
679{
680 return "Edit node properties on node #" + std::to_string(m_nodeID);
681}
682
683} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Undo/Redo command stack for ATS Visual Scripting editor (Phase 6).
std::string GetDescription() const override
Returns a short human-readable description (e.g.
void Execute(TaskGraphTemplate &graph) override
Applies the command to the graph.
void Undo(TaskGraphTemplate &graph) override
Reverses the command on the graph.
AddConnectionCommand(const ExecPinConnection &conn)
void Undo(TaskGraphTemplate &graph) override
Reverses the command on the graph.
std::string GetDescription() const override
Returns a short human-readable description (e.g.
AddDataConnectionCommand(const DataPinConnection &conn)
void Execute(TaskGraphTemplate &graph) override
Applies the command to the graph.
void Execute(TaskGraphTemplate &graph) override
Applies the command to the graph.
std::string GetDescription() const override
Returns a short human-readable description (e.g.
AddDynamicPinCommand(int32_t nodeID, const std::string &pinName)
void Undo(TaskGraphTemplate &graph) override
Reverses the command on the graph.
TaskNodeDefinition m_node
std::string GetDescription() const override
Get a human-readable description of the command.
void Undo() override
Undo the command.
AddNodeCommand(BehaviorTreeAsset *tree, BTNodeType type, const std::string &name, const Vector &position)
void Execute() override
Execute the command.
void Execute(TaskGraphTemplate &graph) override
Applies the command to the graph.
DataPinConnection m_savedDataConn
ExecPinConnection m_savedExecConn
void Undo(TaskGraphTemplate &graph) override
Reverses the command on the graph.
std::string GetDescription() const override
Returns a short human-readable description (e.g.
DeleteLinkCommand(const ExecPinConnection &conn)
void Undo() override
Undo the command.
std::vector< DataPinConnection > m_savedDataConns
void Execute() override
Execute the command.
std::string GetDescription() const override
Get a human-readable description of the command.
std::vector< ExecPinConnection > m_savedExecConns
DeleteNodeCommand(BehaviorTreeAsset *tree, uint32_t nodeId)
std::string GetDescription() const override
Returns a short human-readable description (e.g.
void Execute(TaskGraphTemplate &graph) override
Applies the command to the graph.
EditNodePropertiesCommand(int32_t nodeID, const ParameterMap &oldParams, const ParameterMap &newParams)
void Undo(TaskGraphTemplate &graph) override
Reverses the command on the graph.
std::unordered_map< std::string, ParameterBinding > ParameterMap
EditNodePropertyCommand(int32_t nodeID, const std::string &propertyKey, const PropertyValue &oldValue, const PropertyValue &newValue)
void Undo(TaskGraphTemplate &graph) override
Reverses the command on the graph.
void Execute(TaskGraphTemplate &graph) override
Applies the command to the graph.
static void ApplyValue(TaskNodeDefinition &node, const std::string &key, const PropertyValue &value)
Applies value to the named field of node.
std::string GetDescription() const override
Returns a short human-readable description (e.g.
std::string GetDescription() const override
Get a human-readable description of the command.
EditParameterCommand(BehaviorTreeAsset *tree, uint32_t nodeId, const std::string &paramName, const std::string &oldValue, const std::string &newValue, ParamType type)
void Undo() override
Undo the command.
void Execute() override
Execute the command.
std::string GetDescription() const override
Get a human-readable description of the command.
void Undo() override
Undo the command.
static void SetNodePos(TaskGraphTemplate &graph, int32_t nodeID, float x, float y)
MoveNodeCommand(BehaviorTreeAsset *tree, uint32_t nodeId, const Vector &oldPos, const Vector &newPos)
void Execute() override
Execute the command.
RemoveExecPinCommand(int32_t nodeID, const std::string &pinName, int pinIndex, int32_t linkedTargetNodeID, const std::string &linkedTargetPinName)
std::string GetDescription() const override
Returns a short human-readable description (e.g.
int32_t m_linkedTargetNodeID
-1 if no outgoing link was present
void Execute(TaskGraphTemplate &graph) override
Applies the command to the graph.
void Undo(TaskGraphTemplate &graph) override
Reverses the command on the graph.
Immutable, shareable task graph asset.
C++14-compliant type-safe value container for task parameters.
bool CanUndo() const
Returns true if there is at least one command to undo.
std::size_t RedoSize() const
Returns the number of commands currently on the redo stack.
void Clear()
Clears both undo and redo stacks (e.g.
std::size_t UndoSize() const
Returns the number of commands currently on the undo stack.
bool CanRedo() const
Returns true if there is at least one command to redo.
std::string PeekUndoDescription() const
Returns the description of the top undo command, or "" if empty.
void Redo(TaskGraphTemplate &graph)
Re-applies the last undone command, moving it back to the undo stack.
std::string PeekRedoDescription() const
Returns the description of the top redo command, or "" if empty.
void Undo(TaskGraphTemplate &graph)
Undoes the last command, moving it to the redo stack.
std::vector< std::unique_ptr< ICommand > > m_undoStack
std::vector< std::unique_ptr< ICommand > > m_redoStack
void PushCommand(std::unique_ptr< ICommand > cmd, TaskGraphTemplate &graph)
Executes the command on graph, then pushes it onto the undo stack.
static const std::size_t MAX_STACK_SIZE
Maximum number of undo entries kept in memory.
< Provides AssetID and INVALID_ASSET_ID
@ Literal
Value is embedded directly in the template.
Explicit connection between an output data pin of a source node and an input data pin of a target nod...
Explicit connection between a named exec-out pin of a source node and the exec-in pin of a target nod...
std::string SourcePinName
e.g. "Then", "Else", "Loop", "Completed"
std::string TargetPinName
e.g. "In"
Describes how a single parameter value is supplied to a task node.
ParameterBindingType Type
Binding mode.
Discriminated union of property value types (String / Float).
Full description of a single node in the task graph.
std::string NodeName
Human-readable name.
int32_t NodeID
Unique ID within this template.