Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
VisualScriptEditorPanel_TemplateSync.cpp
Go to the documentation of this file.
1/**
2 * @file VisualScriptEditorPanel_TemplateSync.cpp
3 * @brief Template-canvas synchronization methods for VisualScriptEditorPanel (Phase 6 extraction).
4 * @author Olympe Engine
5 * @date 2026-03-09
6 *
7 * @details
8 * This file contains 5 template synchronization methods extracted from VisualScriptEditorPanel.cpp
9 * for improved code organization and maintainability:
10 *
11 * 1. SyncCanvasFromTemplate() — Load nodes from template into editor canvas (~70 LOC)
12 * 2. SyncTemplateFromCanvas() — Update template with current editor node state (~10 LOC)
13 * 3. RebuildLinks() — Rebuild all visual links from template connections (~140 LOC)
14 * 4. SyncEditorNodesFromTemplate() — Restore editor nodes during undo/redo (~135 LOC)
15 * 5. RemoveLink(int linkID) — Delete a link and push undo command (~95 LOC)
16 *
17 * Phase 6 Refactoring: Extract template synchronization logic to separate compilation unit
18 * for cleaner separation of concerns and faster iteration during graph manipulation development.
19 *
20 * Key responsibilities:
21 * - Template ↔ Canvas state synchronization
22 * - Position validation and fallback positioning
23 * - Link resolution and reconstruction
24 * - Undo/Redo state management integration
25 */
26
28#include "../system/system_utils.h"
29#include "../system/system_consts.h"
30#include "../third_party/imnodes/imnodes.h"
31
32#include <algorithm>
33#include <iostream>
34#include <unordered_map>
35
36namespace Olympe {
37
38// ============================================================================
39// Template / Canvas Sync
40// ============================================================================
41
42/**
43 * @brief SyncCanvasFromTemplate
44 *
45 * Load all nodes from m_template into the editor canvas representation (m_editorNodes).
46 * This is the primary method for initializing the canvas from a loaded/pasted blueprint.
47 *
48 * Process:
49 * 1. Clear existing canvas state (m_editorNodes, m_positionedNodes, m_nextNodeID)
50 * 2. For each node in m_template.Nodes:
51 * a. Create a VSEditorNode and copy TaskNodeDefinition
52 * b. Load position from JSON if available; validate against corruption
53 * c. Fall back to auto-layout if position is missing or invalid
54 * d. For Branch nodes: regenerate dynamic pins from conditions
55 * 3. Rebuild links from template connections (RebuildLinks)
56 * 4. Set m_needsPositionSync flag so RenderCanvas() will push positions to ImNodes
57 *
58 * Position validation: Checks for finite values and ±100,000 bounds. This prevents
59 * crashes from corrupted JSON (e.g., NaN, ±Infinity). Garbage values are logged and
60 * replaced with auto-layout positions.
61 *
62 * Phase 24 integration: Branch nodes' dynamic pins are regenerated from conditionRefs
63 * to ensure data-in connectors are available even if not explicitly saved.
64 *
65 * @note Called by LoadTemplate() and after blueprint paste operations.
66 */
68{
69 m_editorNodes.clear();
70 m_positionedNodes.clear();
71 m_nextNodeID = 1;
72
73 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
74 {
75 const TaskNodeDefinition& def = m_template.Nodes[i];
76
77 VSEditorNode eNode;
78 eNode.nodeID = def.NodeID;
79 eNode.def = def;
80
81 // Use position loaded from JSON if available; otherwise fall back to auto-layout.
82 if (def.HasEditorPos)
83 {
84 // Validate loaded position to prevent garbage values from corrupted JSON
85 if (std::isfinite(def.EditorPosX) && std::isfinite(def.EditorPosY) &&
86 def.EditorPosX >= -100000.0f && def.EditorPosX <= 100000.0f &&
87 def.EditorPosY >= -100000.0f && def.EditorPosY <= 100000.0f)
88 {
89 eNode.posX = def.EditorPosX;
90 eNode.posY = def.EditorPosY;
91 }
92 else
93 {
94 SYSTEM_LOG << "[VSEditor] SyncCanvasFromTemplate: node #" << def.NodeID
95 << " had garbage position (" << def.EditorPosX << ", " << def.EditorPosY
96 << "), using auto-layout\n";
97 eNode.posX = 200.0f * static_cast<float>(i);
98 eNode.posY = 100.0f;
99 }
100 }
101 else
102 {
103 eNode.posX = 200.0f * static_cast<float>(i); // Default auto-layout
104 eNode.posY = 100.0f;
105 }
106
107 if (def.NodeID >= m_nextNodeID)
108 m_nextNodeID = def.NodeID + 1;
109
110 // Phase 24: Regenerate dynamic pins for Branch nodes after load
111 // This ensures Pin-in connectors are available even if they weren't saved
112 // (they are derived from conditionRefs/conditionOperandRefs)
113 if (def.Type == TaskNodeType::Branch && (!def.conditionRefs.empty() || !def.conditionOperandRefs.empty()))
114 {
115 m_pinManager->RegeneratePinsFromConditions(eNode.def.conditionRefs,
116 eNode.def.conditionOperandRefs);
117 eNode.def.dynamicPins = m_pinManager->GetAllPins();
118
119 // Also update template for consistency
120 for (size_t ti = 0; ti < m_template.Nodes.size(); ++ti)
121 {
122 if (m_template.Nodes[ti].NodeID == eNode.nodeID)
123 {
124 m_template.Nodes[ti].dynamicPins = eNode.def.dynamicPins;
125 break;
126 }
127 }
128 }
129
130 m_editorNodes.push_back(eNode);
131 }
132
133 RebuildLinks();
134 // Request position restore on the next RenderCanvas() call so that
135 // ImNodes places each node at its stored (posX, posY) coordinates.
136 m_needsPositionSync = true;
137}
138
139/**
140 * @brief SyncTemplateFromCanvas
141 *
142 * Update m_template.Nodes with the current state of m_editorNodes.
143 * This is called before serialization (Save/SaveAs) to reflect user edits on the canvas.
144 *
145 * Process:
146 * 1. Clear m_template.Nodes
147 * 2. Copy all TaskNodeDefinitions from m_editorNodes into m_template
148 * 3. Rebuild template's lookup cache for fast node access by ID
149 *
150 * @note This method does NOT sync positions (that's done separately by
151 * SyncNodePositionsFromImNodes). It only syncs node definitions.
152 */
154{
155 // Update template nodes from editor nodes
156 m_template.Nodes.clear();
157 for (size_t i = 0; i < m_editorNodes.size(); ++i)
158 {
159 m_template.Nodes.push_back(m_editorNodes[i].def);
160 }
162}
163
164/**
165 * @brief RebuildLinks
166 *
167 * Reconstruct all visual links (m_editorLinks) from template connections
168 * (m_template.ExecConnections and m_template.DataConnections).
169 *
170 * Process:
171 * 1. Clear m_editorLinks
172 * 2. For each ExecPinConnection in m_template:
173 * a. Find source node and resolve pin index from stored name
174 * b. Include dynamic exec-out pins for VSSequence/Switch nodes
175 * c. Create VSEditorLink with encoded attribute IDs
176 * 3. For each DataPinConnection in m_template:
177 * a. Find source and destination nodes
178 * b. Resolve pin indices from stored names (try static, then DataPins vector)
179 * c. Handle Phase 24 dynamic pins for Branch nodes
180 * d. Create VSEditorLink with encoded attribute IDs
181 *
182 * Attribute ID encoding scheme (nodeID * 10000 + offset):
183 * 0–99: exec-in pins (exec input for node)
184 * 100–199: exec-out pins (exec outputs indexed by position)
185 * 200–299: data-in pins (data inputs indexed by position)
186 * 300–399: data-out pins (data outputs indexed by position)
187 *
188 * This method is resilient to missing nodes and out-of-range pin indices,
189 * defaulting to "Value" or "In"/"Out" pin names as fallback.
190 *
191 * @note Called by SyncCanvasFromTemplate(), SyncEditorNodesFromTemplate(), and
192 * after link creation/deletion operations.
193 */
195{
196 m_editorLinks.clear();
197
198 // Exec links
199 for (size_t i = 0; i < m_template.ExecConnections.size(); ++i)
200 {
201 const ExecPinConnection& conn = m_template.ExecConnections[i];
202
203 // Determine pin index for source
204 std::vector<std::string> outPins;
205 const TaskNodeDefinition* srcNode = m_template.GetNode(conn.SourceNodeID);
206 if (srcNode != nullptr)
207 {
209 if (srcNode->Type == TaskNodeType::VSSequence ||
211 {
212 for (size_t d = 0; d < srcNode->DynamicExecOutputPins.size(); ++d)
213 outPins.push_back(srcNode->DynamicExecOutputPins[d]);
214 }
215 }
216
217 int pinIdx = 0;
218 for (size_t p = 0; p < outPins.size(); ++p)
219 {
220 if (outPins[p] == conn.SourcePinName)
221 {
222 pinIdx = static_cast<int>(p);
223 break;
224 }
225 }
226
227 VSEditorLink link;
228 link.linkID = AllocLinkID();
229 link.srcAttrID = ExecOutAttrUID(conn.SourceNodeID, pinIdx);
230 link.dstAttrID = ExecInAttrUID(conn.TargetNodeID);
231 link.isData = false;
232 m_editorLinks.push_back(link);
233 }
234
235 // Data links — resolve pin indices from stored pin names
236 for (size_t i = 0; i < m_template.DataConnections.size(); ++i)
237 {
238 const DataPinConnection& conn = m_template.DataConnections[i];
239
240 // Determine data-out pin index for source
241 int srcPinIdx = 0;
242 const TaskNodeDefinition* srcNode = m_template.GetNode(conn.SourceNodeID);
243 if (srcNode != nullptr)
244 {
245 // Try static list first
246 auto outPins = GetDataOutputPins(srcNode->Type);
247 bool found = false;
248 for (size_t p = 0; p < outPins.size(); ++p)
249 {
250 if (outPins[p] == conn.SourcePinName)
251 {
252 srcPinIdx = static_cast<int>(p);
253 found = true;
254 break;
255 }
256 }
257 if (!found)
258 {
259 // Fall back to DataPins vector
260 int outIdx = 0;
261 for (size_t p = 0; p < srcNode->DataPins.size(); ++p)
262 {
263 if (srcNode->DataPins[p].Dir == DataPinDir::Output)
264 {
265 if (srcNode->DataPins[p].PinName == conn.SourcePinName)
266 {
268 break;
269 }
270 ++outIdx;
271 }
272 }
273 }
274 }
275
276 // Determine data-in pin index for destination
277 int dstPinIdx = 0;
278 const TaskNodeDefinition* dstNode = m_template.GetNode(conn.TargetNodeID);
279 if (dstNode != nullptr)
280 {
281 // Phase 24: Check if destination is a Branch node with dynamic pins
282 bool foundDynamicPin = false;
283 if (dstNode->Type == TaskNodeType::Branch && !dstNode->dynamicPins.empty())
284 {
285 // Try to find a matching dynamic pin ID
286 for (size_t p = 0; p < dstNode->dynamicPins.size(); ++p)
287 {
288 if (dstNode->dynamicPins[p].id == conn.TargetPinName)
289 {
290 dstPinIdx = static_cast<int>(p);
291 foundDynamicPin = true;
292 break;
293 }
294 }
295 }
296
297 if (!foundDynamicPin)
298 {
299 // Fall back to static data pins
300 auto inPins = GetDataInputPins(dstNode->Type);
301 bool found = false;
302 for (size_t p = 0; p < inPins.size(); ++p)
303 {
304 if (inPins[p] == conn.TargetPinName)
305 {
306 dstPinIdx = static_cast<int>(p);
307 found = true;
308 break;
309 }
310 }
311 if (!found)
312 {
313 int inIdx = 0;
314 for (size_t p = 0; p < dstNode->DataPins.size(); ++p)
315 {
316 if (dstNode->DataPins[p].Dir == DataPinDir::Input)
317 {
318 if (dstNode->DataPins[p].PinName == conn.TargetPinName)
319 {
321 break;
322 }
323 ++inIdx;
324 }
325 }
326 }
327 }
328 }
329
330 VSEditorLink link;
331 link.linkID = AllocLinkID();
332 link.srcAttrID = DataOutAttrUID(conn.SourceNodeID, srcPinIdx);
333 link.dstAttrID = DataInAttrUID(conn.TargetNodeID, dstPinIdx);
334 link.isData = true;
335 m_editorLinks.push_back(link);
336 }
337}
338
339/**
340 * @brief SyncEditorNodesFromTemplate
341 *
342 * Restore editor nodes (m_editorNodes) from m_template during undo/redo operations.
343 * This is more complex than SyncCanvasFromTemplate because it must:
344 * - Preserve canvas positions for nodes that still exist
345 * - Support MoveNodeCommand parameters (__posX, __posY) for undo/redo states
346 * - Fall back through multiple position sources (parameter -> saved -> loaded -> default)
347 *
348 * Position fallback order:
349 * 1. Parameters["__posX/__posY"] stored by MoveNodeCommand (undo/redo target)
350 * 2. Previously known position (from m_editorNodes before clear)
351 * 3. File-loaded position (m_template.EditorPosX/Y)
352 * 4. Auto-layout default (grid spacing)
353 *
354 * Process:
355 * 1. Save current positions from m_editorNodes (for fallback)
356 * 2. Clear m_editorNodes, m_positionedNodes, m_nodeDragStartPositions
357 * 3. For each node in m_template, determine position using fallback order
358 * 4. Validate all positions against corruption (NaN, ±100,000)
359 * 5. Rebuild links to remove "ghost" links from deleted nodes
360 * 6. Set m_needsPositionSync for the next render
361 *
362 * @note Called by PerformUndo() and PerformRedo() to restore graph state.
363 * Also called internally by LoadTemplate() on initial load.
364 */
366{
367 // Default grid spacing used when a node has no recorded canvas position
368 // (e.g. after a Redo restores a previously-deleted node).
369 static const float DEFAULT_NODE_X_OFFSET = 50.0f;
370 static const float DEFAULT_NODE_X_SPACING = 200.0f;
371 static const float DEFAULT_NODE_Y = 100.0f;
372
373 // Preserve canvas positions for nodes that still exist
374 std::unordered_map<int, std::pair<float, float> > savedPos;
375 for (size_t i = 0; i < m_editorNodes.size(); ++i)
376 {
377 float posX = m_editorNodes[i].posX;
378 float posY = m_editorNodes[i].posY;
379
380 // Validate saved positions before preserving them
381 if (!std::isfinite(posX) || !std::isfinite(posY) ||
382 posX < -100000.0f || posX > 100000.0f ||
384 {
386 posY = DEFAULT_NODE_Y;
387 SYSTEM_LOG << "[VSEditor] SyncEditorNodesFromTemplate: node #" << m_editorNodes[i].nodeID
388 << " had garbage position, reset to defaults\n";
389 }
390
391 savedPos[m_editorNodes[i].nodeID] = std::make_pair(posX, posY);
392 }
393
394 m_editorNodes.clear();
395 m_positionedNodes.clear();
396 // Clear drag-start positions on undo/redo: any in-progress drag is
397 // invalidated when the graph state changes underneath it. The user must
398 // re-drag after undo/redo; the old drag-start is no longer meaningful.
400
401 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
402 {
403 const TaskNodeDefinition& def = m_template.Nodes[i];
404
405 VSEditorNode eNode;
406 eNode.nodeID = def.NodeID;
407 eNode.def = def;
408
409 // Prefer position stored in template Parameters by MoveNodeCommand
410 // (reflects the undo/redo target state for this node).
411 auto posXIt = def.Parameters.find("__posX");
412 auto posYIt = def.Parameters.find("__posY");
413 if (posXIt != def.Parameters.end() &&
414 posYIt != def.Parameters.end() &&
415 posXIt->second.Type == ParameterBindingType::Literal &&
417 {
418 float paramX = posXIt->second.LiteralValue.AsFloat();
419 float paramY = posYIt->second.LiteralValue.AsFloat();
420
421 // Validate parameter positions
422 if (std::isfinite(paramX) && std::isfinite(paramY) &&
423 paramX >= -100000.0f && paramX <= 100000.0f &&
424 paramY >= -100000.0f && paramY <= 100000.0f)
425 {
426 eNode.posX = paramX;
427 eNode.posY = paramY;
428 }
429 else
430 {
431 SYSTEM_LOG << "[VSEditor] SyncEditorNodesFromTemplate: node #" << def.NodeID
432 << " had garbage params (" << paramX << ", " << paramY
433 << "), falling back\n";
434 auto it = savedPos.find(def.NodeID);
435 if (it != savedPos.end())
436 {
437 eNode.posX = it->second.first;
438 eNode.posY = it->second.second;
439 }
440 else if (def.HasEditorPos)
441 {
442 eNode.posX = def.EditorPosX;
443 eNode.posY = def.EditorPosY;
444 }
445 else
446 {
447 eNode.posX = DEFAULT_NODE_X_OFFSET + DEFAULT_NODE_X_SPACING * static_cast<float>(i);
448 eNode.posY = DEFAULT_NODE_Y;
449 }
450 }
451 }
452 else
453 {
454 auto it = savedPos.find(def.NodeID);
455 if (it != savedPos.end())
456 {
457 // Restore previously known position
458 eNode.posX = it->second.first;
459 eNode.posY = it->second.second;
460 }
461 else if (def.HasEditorPos)
462 {
463 // Validate file-loaded position
464 if (std::isfinite(def.EditorPosX) && std::isfinite(def.EditorPosY) &&
465 def.EditorPosX >= -100000.0f && def.EditorPosX <= 100000.0f &&
466 def.EditorPosY >= -100000.0f && def.EditorPosY <= 100000.0f)
467 {
468 eNode.posX = def.EditorPosX;
469 eNode.posY = def.EditorPosY;
470 }
471 else
472 {
473 eNode.posX = DEFAULT_NODE_X_OFFSET + DEFAULT_NODE_X_SPACING * static_cast<float>(i);
474 eNode.posY = DEFAULT_NODE_Y;
475 }
476 }
477 else
478 {
479 // New node (e.g. restored by Redo) – use a default spread position
480 eNode.posX = DEFAULT_NODE_X_OFFSET + DEFAULT_NODE_X_SPACING * static_cast<float>(i);
481 eNode.posY = DEFAULT_NODE_Y;
482 }
483 }
484
485 if (def.NodeID >= m_nextNodeID)
486 m_nextNodeID = def.NodeID + 1;
487
488 SYSTEM_LOG << "[VSEditor] SyncEditorNodesFromTemplate: node #" << eNode.nodeID
489 << " restored to (" << eNode.posX << "," << eNode.posY << ")\n";
490
491 m_editorNodes.push_back(eNode);
492 }
493
494 // FIX 1: Rebuild links from template so that ghost links (links that
495 // belong to a deleted node) are removed from m_editorLinks after undo/redo.
496 RebuildLinks();
497
498 // Request a position-restore pass on the next RenderCanvas() call
499 m_needsPositionSync = true;
500}
501
502/**
503 * @brief RemoveLink
504 *
505 * Delete a link and push a DeleteLinkCommand onto the undo stack.
506 * This integrates link deletion with the undo/redo system for reversible operations.
507 *
508 * Process:
509 * 1. Find the VSEditorLink by ID in m_editorLinks
510 * 2. Decode attribute IDs to extract node IDs and pin indices
511 * 3. Resolve pin names from indices using GetExecOutputPins/GetDataOutputPins
512 * 4. Create ExecPinConnection or DataPinConnection based on link type
513 * 5. Push DeleteLinkCommand to undo stack
514 * 6. Rebuild links to update visual state
515 * 7. Mark graph as dirty and clear verification cache
516 *
517 * Attribute ID decoding:
518 * - Node ID = attrID / 10000
519 * - Pin offset = attrID % 10000
520 * - Pin index = offset - base (0 for exec-in, 100 for exec-out, etc.)
521 *
522 * @param linkID ImNodes link ID to delete
523 *
524 * @note This is the primary method for user-initiated link deletion via context menu
525 * or Ctrl+Click. Automatic link removal during node deletion is handled separately
526 * by the command system.
527 */
529{
530 // Find the link descriptor
531 VSEditorLink* link = nullptr;
532 for (size_t i = 0; i < m_editorLinks.size(); ++i)
533 {
534 if (m_editorLinks[i].linkID == linkID)
535 {
536 link = &m_editorLinks[i];
537 break;
538 }
539 }
540 if (!link)
541 return;
542
543 if (link->isData)
544 {
545 // Decode data-out -> data-in
546 int srcNodeID = link->srcAttrID / 10000;
547 int srcPinIdx = link->srcAttrID % 10000 - 300; // data-out range 300-399
548 int dstNodeID = link->dstAttrID / 10000;
549 int dstPinIdx = link->dstAttrID % 10000 - 200; // data-in range 200-299
550
551 std::string srcPinName = "Value";
552 std::string dstPinName = "Value";
553
554 const TaskNodeDefinition* srcNode = m_template.GetNode(srcNodeID);
555 const TaskNodeDefinition* dstNode = m_template.GetNode(dstNodeID);
556
557 if (srcNode)
558 {
559 auto pins = GetDataOutputPins(srcNode->Type);
560 if (srcPinIdx >= 0 && srcPinIdx < static_cast<int>(pins.size()))
561 srcPinName = pins[static_cast<size_t>(srcPinIdx)];
562 }
563 if (dstNode)
564 {
565 auto pins = GetDataInputPins(dstNode->Type);
566 if (dstPinIdx >= 0 && dstPinIdx < static_cast<int>(pins.size()))
567 dstPinName = pins[static_cast<size_t>(dstPinIdx)];
568 }
569
570 DataPinConnection conn;
571 conn.SourceNodeID = srcNodeID;
572 conn.SourcePinName = srcPinName;
573 conn.TargetNodeID = dstNodeID;
574 conn.TargetPinName = dstPinName;
576 std::unique_ptr<ICommand>(new DeleteLinkCommand(conn)),
577 m_template);
578 }
579 else
580 {
581 // Decode exec-out -> exec-in
582 int srcNodeID = link->srcAttrID / 10000;
583 int srcPinIdx = link->srcAttrID % 10000 - 100; // exec-out range 100-199
584 int dstNodeID = link->dstAttrID / 10000;
585 int dstPinIdx = link->dstAttrID % 10000; // exec-in range 0-99
586
587 std::string srcPinName = "Out";
588 std::string dstPinName = "In";
589
590 const TaskNodeDefinition* srcNode = m_template.GetNode(srcNodeID);
591 if (srcNode)
592 {
593 auto pins = GetExecOutputPins(srcNode->Type);
594 if (srcNode->Type == TaskNodeType::VSSequence ||
596 {
597 for (size_t d = 0; d < srcNode->DynamicExecOutputPins.size(); ++d)
598 pins.push_back(srcNode->DynamicExecOutputPins[d]);
599 }
600 if (srcPinIdx >= 0 && srcPinIdx < static_cast<int>(pins.size()))
601 srcPinName = pins[static_cast<size_t>(srcPinIdx)];
602 }
603
604 const TaskNodeDefinition* dstNode = m_template.GetNode(dstNodeID);
605 if (dstNode)
606 {
607 auto pins = GetExecInputPins(dstNode->Type);
608 if (dstPinIdx >= 0 && dstPinIdx < static_cast<int>(pins.size()))
609 dstPinName = pins[static_cast<size_t>(dstPinIdx)];
610 }
611
612 ExecPinConnection conn;
613 conn.SourceNodeID = srcNodeID;
614 conn.SourcePinName = srcPinName;
615 conn.TargetNodeID = dstNodeID;
616 conn.TargetPinName = dstPinName;
618 std::unique_ptr<ICommand>(new DeleteLinkCommand(conn)),
619 m_template);
620 }
621
622 RebuildLinks();
623 m_dirty = true;
624 m_verificationDone = false;
625}
626
627} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
ImNodes-based graph editor for ATS Visual Script graphs (Phase 5).
std::vector< TaskNodeDefinition > Nodes
All graph nodes.
std::vector< ExecPinConnection > ExecConnections
Explicit exec connections (ATS VS only)
void BuildLookupCache()
Rebuilds the internal ID-to-node lookup map from the Nodes vector.
const TaskNodeDefinition * GetNode(int32_t nodeId) const
Returns a pointer to the node with the given ID, or nullptr.
std::vector< DataPinConnection > DataConnections
Explicit data connections (ATS VS only)
void PushCommand(std::unique_ptr< ICommand > cmd, TaskGraphTemplate &graph)
Executes the command on graph, then pushes it onto the undo stack.
std::unique_ptr< DynamicDataPinManager > m_pinManager
Dynamic pin manager shared across all Branch nodes in this panel.
UndoRedoStack m_undoStack
Undo/Redo command stack for reversible graph editing operations.
std::vector< VSEditorLink > m_editorLinks
Editor links (exec + data)
int ExecOutAttrUID(int nodeID, int pinIndex) const
Maps node ID + pin index -> ImNodes attribute UID for exec-out pins.
TaskGraphTemplate m_template
The template currently being edited.
void RemoveLink(int linkID)
Removes an ImNodes link (and its underlying template connection) by link ID.
static std::vector< std::string > GetExecInputPins(TaskNodeType type)
Returns the exec-in pin names for a node type.
void SyncCanvasFromTemplate()
Builds the editor canvas from the in-memory TaskGraphTemplate.
void SyncTemplateFromCanvas()
Builds the in-memory TaskGraphTemplate from the editor nodes/links.
static std::vector< std::string > GetDataOutputPins(TaskNodeType type)
Returns the data-out pin names for a node type.
static std::vector< std::string > GetDataInputPins(TaskNodeType type)
Returns the data-in pin names for a node type.
std::unordered_set< int > m_positionedNodes
Nodes for which ImNodes has been given a position.
int DataInAttrUID(int nodeID, int pinIndex) const
Maps node ID + data pin index -> ImNodes attribute UID for data-in pins.
void RebuildLinks()
Rebuilds ImNodes exec/data link arrays from the template.
int m_nextNodeID
Next available node ID.
std::unordered_map< int, std::pair< float, float > > m_nodeDragStartPositions
Per-node drag-start positions used to record a single MoveNodeCommand per drag gesture instead of one...
int DataOutAttrUID(int nodeID, int pinIndex) const
Maps node ID + data pin index -> ImNodes attribute UID for data-out pins.
int ExecInAttrUID(int nodeID) const
Maps node ID -> ImNodes attribute UID for an exec-in pin.
bool m_verificationDone
True once RunVerification() has been called at least once for the current graph.
std::vector< VSEditorNode > m_editorNodes
Editor nodes (mirrors m_template.Nodes + position/selection state)
void SyncEditorNodesFromTemplate()
Rebuilds m_editorNodes from m_template, preserving existing node positions.
static std::vector< std::string > GetExecOutputPins(TaskNodeType type)
Returns the exec-out pin names for a node type.
< Provides AssetID and INVALID_ASSET_ID
@ Literal
Value is embedded directly in the template.
@ Switch
Multi-branch on value (N exec outputs)
@ Branch
If/Else conditional (Then / Else exec outputs)
@ VSSequence
Execute N outputs in order ("VS" prefix avoids collision with BT Sequence=1)
@ Output
Value produced by the node.
@ Input
Value consumed by the node.
#define SYSTEM_LOG