Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
BTGraphLayoutEngine.cpp
Go to the documentation of this file.
1/**
2 * @file BTGraphLayoutEngine.cpp
3 * @brief Implementation of graph layout engine for behavior tree visualization
4 */
5
7#include <queue>
8#include <algorithm>
9#include <cmath>
10#include <iostream>
11
12namespace Olympe
13{
17
18 std::vector<BTNodeLayout> BTGraphLayoutEngine::ComputeLayout(
20 float nodeSpacingX,
21 float nodeSpacingY,
22 float zoomFactor)
23 {
24 if (!tree || tree->nodes.empty())
25 {
26 return {};
27 }
28
29 // Clear previous state
30 m_layouts.clear();
31 m_nodeIdToIndex.clear();
32 m_layers.clear();
33 m_parentMap.clear();
34
35 // Phase 1: Assign nodes to layers via BFS
37
38 // Phase 2: Initial ordering within layers
40
41 // Phase 3: Reduce crossings (10 passes)
43
44 // Phase 4: Apply Buchheim-Walker optimal layout for better parent centering
45 // This sets position.x in abstract units (0, 1, 2, etc.)
47
48 // Phase 5: Force-directed collision resolution with generous padding
49 // This works in abstract unit space
50 const float nodePadding = 2.5f; // 2.5 abstract units of padding (increased from 1.5 for better spacing)
51 const int maxIterations = 30; // 30 iterations for better convergence (doubled from 15)
53
54 // Use the spacing values passed from UI sliders
55 float baseSpacingX = nodeSpacingX; // ✅ Respects UI slider
56 float baseSpacingY = nodeSpacingY; // ✅ Respects UI slider
57
58 // Optional adaptive scaling (only if needed)
59 size_t maxNodesInLayer = 1;
60 for (const auto& layer : m_layers)
61 {
62 maxNodesInLayer = std::max(maxNodesInLayer, layer.size());
63 }
64
65 // Scale up ONLY for very wide/deep trees
66 constexpr size_t WIDE_TREE_THRESHOLD = 8; // Increased from 5
67 constexpr size_t DEEP_TREE_THRESHOLD = 8; // Increased from 5
68 constexpr float SPACING_INCREASE_FACTOR = 1.15f; // Reduced from 1.2
69
71 {
72 baseSpacingX *= SPACING_INCREASE_FACTOR; // +15% only for very wide trees
73 }
74
75 if (m_layers.size() > DEEP_TREE_THRESHOLD)
76 {
77 baseSpacingY *= SPACING_INCREASE_FACTOR; // +15% only for very deep trees
78 }
79
80 // Apply zoom factor to spacing BEFORE converting to pixels
83
84 // Debug output to verify
85 std::cout << "[BTGraphLayout] Using spacing: "
86 << finalSpacingX << "px × " << finalSpacingY << "px"
87 << " (base: " << baseSpacingX << " × " << baseSpacingY
88 << ", zoom: " << (int)(zoomFactor * 100) << "%)"
89 << std::endl;
90
91 // Convert from abstract units to world coordinates and apply layout direction
93 {
94 // Vertical layout (default): layers go top-to-bottom
95 for (auto& layout : m_layouts)
96 {
97 // Convert abstract X position to world coordinates with zoomed spacing
98 layout.position.x *= finalSpacingX;
99
100 // Convert layer index to vertical world position with zoomed spacing
101 layout.position.y = layout.layer * finalSpacingY;
102 }
103 }
104 else // LeftToRight
105 {
106 // Horizontal layout: rotate 90° clockwise
107 // Layers become left-to-right, abstract X units become vertical positions
108 for (auto& layout : m_layouts)
109 {
110 float abstractX = layout.position.x;
111 // Layers become horizontal position
112 layout.position.x = layout.layer * finalSpacingY;
113 // Abstract X units become vertical position
114 layout.position.y = abstractX * finalSpacingX;
115 }
116 }
117
118 // ok - NEW: Debug output - verify positions are in pixel range (hundreds, not 0-1)
119 if (!m_layouts.empty())
120 {
121 std::cout << "[BTGraphLayout] Sample node positions (should be 100s-1000s of pixels):" << std::endl;
122 for (size_t i = 0; i < std::min(size_t(3), m_layouts.size()); ++i)
123 {
124 std::cout << " Node " << m_layouts[i].nodeId
125 << " at (" << m_layouts[i].position.x
126 << ", " << m_layouts[i].position.y << ")" << std::endl;
127 }
128 }
129
130 std::cout << "[BTGraphLayout] Layout complete: " << m_layouts.size()
131 << " nodes positioned" << std::endl;
132
133 return m_layouts;
134 }
135
137 {
138 auto it = m_nodeIdToIndex.find(nodeId);
139 if (it != m_nodeIdToIndex.end())
140 {
141 return &m_layouts[it->second];
142 }
143 return nullptr;
144 }
145
147 {
148 // BFS from root to assign layers
149 std::queue<std::pair<uint32_t, int>> queue; // (nodeId, layer)
150 std::map<uint32_t, int> visitedLayers;
151
152 queue.push({tree->rootNodeId, 0});
153 visitedLayers[tree->rootNodeId] = 0;
154
155 int maxLayer = 0;
156
157 while (!queue.empty())
158 {
159 std::pair<uint32_t, int> front = queue.front();
160 uint32_t nodeId = front.first;
161 int layer = front.second;
162 queue.pop();
163
164 const BTNode* node = tree->GetNode(nodeId);
165 if (!node)
166 continue;
167
168 // Create layout for this node
170 layout.nodeId = nodeId;
171 layout.layer = layer;
172 layout.orderInLayer = 0; // Will be set in InitialOrdering
173
174 size_t idx = m_layouts.size();
175 m_layouts.push_back(layout);
176 m_nodeIdToIndex[nodeId] = idx;
177
178 maxLayer = std::max(maxLayer, layer);
179
180 // Get children and add to queue
181 auto children = GetChildren(node);
182 for (uint32_t childId : children)
183 {
184 // Only visit each node once (shortest path from root)
185 if (visitedLayers.find(childId) == visitedLayers.end())
186 {
187 visitedLayers[childId] = layer + 1;
188 queue.push({childId, layer + 1});
189 }
190 }
191 }
192
193 // Organize nodes into layers
194 m_layers.resize(maxLayer + 1);
195 for (const auto& layout : m_layouts)
196 {
197 m_layers[layout.layer].push_back(layout.nodeId);
198 }
199
200 // Build parent map for later phases
202 }
203
205 {
206 // Simple initial ordering: maintain order from BFS
207 for (size_t layerIdx = 0; layerIdx < m_layers.size(); ++layerIdx)
208 {
209 auto& layer = m_layers[layerIdx];
210 for (size_t i = 0; i < layer.size(); ++i)
211 {
212 uint32_t nodeId = layer[i];
213 auto it = m_nodeIdToIndex.find(nodeId);
214 if (it != m_nodeIdToIndex.end())
215 {
216 m_layouts[it->second].orderInLayer = static_cast<int>(i);
217 }
218 }
219 }
220 }
221
223 {
224 // Barycenter heuristic - 20 passes alternating between forward and backward (doubled from 10)
225 const int numPasses = 20;
226
227 for (int pass = 0; pass < numPasses; ++pass)
228 {
229 // Forward pass (top to bottom)
230 if (pass % 2 == 0)
231 {
232 for (size_t layerIdx = 1; layerIdx < m_layers.size(); ++layerIdx)
233 {
234 auto& layer = m_layers[layerIdx];
235
236 // Calculate barycenter for each node based on parents
237 std::vector<std::pair<float, uint32_t>> barycenters;
238 for (uint32_t nodeId : layer)
239 {
240 auto parentIt = m_parentMap.find(nodeId);
241 if (parentIt != m_parentMap.end() && !parentIt->second.empty())
242 {
243 // Get parent layouts
244 std::vector<BTNodeLayout*> parents;
245 for (uint32_t parentId : parentIt->second)
246 {
247 auto it = m_nodeIdToIndex.find(parentId);
248 if (it != m_nodeIdToIndex.end())
249 {
250 parents.push_back(&m_layouts[it->second]);
251 }
252 }
253
254 float barycenter = CalculateBarycenter(nodeId, parents);
255 barycenters.push_back({barycenter, nodeId});
256 }
257 else
258 {
259 // No parents, keep current order
260 auto it = m_nodeIdToIndex.find(nodeId);
261 if (it != m_nodeIdToIndex.end())
262 {
263 float currentOrder = static_cast<float>(m_layouts[it->second].orderInLayer);
264 barycenters.push_back({currentOrder, nodeId});
265 }
266 }
267 }
268
269 // Sort by barycenter
270 std::sort(barycenters.begin(), barycenters.end());
271
272 // Update order
273 layer.clear();
274 for (size_t i = 0; i < barycenters.size(); ++i)
275 {
276 uint32_t nodeId = barycenters[i].second;
277 layer.push_back(nodeId);
278 auto it = m_nodeIdToIndex.find(nodeId);
279 if (it != m_nodeIdToIndex.end())
280 {
281 m_layouts[it->second].orderInLayer = static_cast<int>(i);
282 }
283 }
284 }
285 }
286 else // Backward pass (bottom to top)
287 {
288 for (int layerIdx = static_cast<int>(m_layers.size()) - 2; layerIdx >= 0; --layerIdx)
289 {
290 auto& layer = m_layers[layerIdx];
291
292 // Calculate barycenter for each node based on children
293 std::vector<std::pair<float, uint32_t>> barycenters;
294 for (uint32_t nodeId : layer)
295 {
296 auto it = m_nodeIdToIndex.find(nodeId);
297 if (it == m_nodeIdToIndex.end())
298 continue;
299
300 const BTNode* node = tree->GetNode(nodeId);
301 if (!node)
302 continue;
303
304 auto children = GetChildren(node);
305 if (!children.empty())
306 {
307 // Get child layouts
308 std::vector<BTNodeLayout*> childLayouts;
309 for (uint32_t childId : children)
310 {
311 auto childIt = m_nodeIdToIndex.find(childId);
312 if (childIt != m_nodeIdToIndex.end())
313 {
314 childLayouts.push_back(&m_layouts[childIt->second]);
315 }
316 }
317
319 barycenters.push_back({barycenter, nodeId});
320 }
321 else
322 {
323 float currentOrder = static_cast<float>(m_layouts[it->second].orderInLayer);
324 barycenters.push_back({currentOrder, nodeId});
325 }
326 }
327
328 // Sort by barycenter
329 std::sort(barycenters.begin(), barycenters.end());
330
331 // Update order
332 layer.clear();
333 for (size_t i = 0; i < barycenters.size(); ++i)
334 {
335 uint32_t nodeId = barycenters[i].second;
336 layer.push_back(nodeId);
337 auto it = m_nodeIdToIndex.find(nodeId);
338 if (it != m_nodeIdToIndex.end())
339 {
340 m_layouts[it->second].orderInLayer = static_cast<int>(i);
341 }
342 }
343 }
344 }
345 }
346
347 // Debug output to verify crossing reduction effectiveness
348 #ifdef DEBUG_BT_LAYOUT
350 std::cout << "[BTGraphLayout] Edge crossings after reduction: "
351 << totalCrossings << std::endl;
352 #endif
353 }
354
356 {
357 // Assign X coordinates based on order in layer
358 for (const auto& layer : m_layers)
359 {
360 float totalWidth = (layer.size() - 1) * nodeSpacingX;
361 float startX = -totalWidth / 2.0f; // Center around 0
362
363 for (size_t i = 0; i < layer.size(); ++i)
364 {
365 uint32_t nodeId = layer[i];
366 auto it = m_nodeIdToIndex.find(nodeId);
367 if (it != m_nodeIdToIndex.end())
368 {
369 m_layouts[it->second].position.x = startX + i * nodeSpacingX;
370 }
371 }
372 }
373 }
374
376 {
377 // Simple collision resolution: expand spacing if nodes are too close
378 const float minSpacing = nodeSpacingX * 0.8f;
379
380 for (auto& layer : m_layers)
381 {
382 if (layer.size() < 2)
383 continue;
384
385 // Sort nodes by X coordinate
386 std::sort(layer.begin(), layer.end(), [this](uint32_t a, uint32_t b) {
387 auto itA = m_nodeIdToIndex.find(a);
388 auto itB = m_nodeIdToIndex.find(b);
389 if (itA == m_nodeIdToIndex.end() || itB == m_nodeIdToIndex.end())
390 return false;
391 return m_layouts[itA->second].position.x < m_layouts[itB->second].position.x;
392 });
393
394 // Check for collisions and adjust
395 for (size_t i = 1; i < layer.size(); ++i)
396 {
397 uint32_t prevNodeId = layer[i - 1];
398 uint32_t currNodeId = layer[i];
399
400 auto itPrev = m_nodeIdToIndex.find(prevNodeId);
401 auto itCurr = m_nodeIdToIndex.find(currNodeId);
402
403 if (itPrev == m_nodeIdToIndex.end() || itCurr == m_nodeIdToIndex.end())
404 continue;
405
408
409 float distance = currLayout.position.x - prevLayout.position.x;
410 if (distance < minSpacing)
411 {
412 // Push current node to the right
413 currLayout.position.x = prevLayout.position.x + minSpacing;
414 }
415 }
416 }
417 }
418
419 std::vector<uint32_t> BTGraphLayoutEngine::GetChildren(const BTNode* node) const
420 {
421 if (!node)
422 return {};
423
424 std::vector<uint32_t> children;
425
426 // Composite nodes (Selector, Sequence)
427 if (node->type == BTNodeType::Selector || node->type == BTNodeType::Sequence)
428 {
429 children = node->childIds;
430 }
431 // Decorator nodes (Inverter, Repeater)
432 else if (node->type == BTNodeType::Inverter || node->type == BTNodeType::Repeater)
433 {
434 if (node->decoratorChildId != 0)
435 {
436 children.push_back(node->decoratorChildId);
437 }
438 }
439
440 return children;
441 }
442
444 {
445 m_parentMap.clear();
446
447 for (const auto& node : tree->nodes)
448 {
449 auto children = GetChildren(&node);
450 for (uint32_t childId : children)
451 {
452 m_parentMap[childId].push_back(node.id);
453 }
454 }
455 }
456
457 float BTGraphLayoutEngine::CalculateBarycenter(uint32_t nodeId, const std::vector<BTNodeLayout*>& neighbors) const
458 {
459 if (neighbors.empty())
460 return 0.0f;
461
462 float sum = 0.0f;
463 for (const BTNodeLayout* neighbor : neighbors)
464 {
465 sum += neighbor->orderInLayer;
466 }
467
468 return sum / neighbors.size();
469 }
470
472 {
473 /*
474 * Buchheim-Walker Algorithm (2002)
475 * Reference: "Improving Walker's Algorithm to Run in Linear Time"
476 *
477 * Guarantees:
478 * 1. Parents centered on their children
479 * 2. No collisions between sibling subtrees
480 * 3. Optimal horizontal space usage
481 * 4. Linear time complexity O(n)
482 */
483
484 if (!tree || m_layers.empty() || m_layouts.empty())
485 return;
486
487 // Start from root and recursively place subtrees
488 if (!m_layers.empty() && !m_layers[0].empty())
489 {
490 uint32_t rootId = m_layers[0][0];
491 float startX = 0.0f;
493 }
494 }
495
497 {
498 const BTNode* node = tree->GetNode(nodeId);
499 if (!node)
500 return;
501
502 auto itLayout = m_nodeIdToIndex.find(nodeId);
503 if (itLayout == m_nodeIdToIndex.end())
504 return;
505
507 auto children = GetChildren(node);
508
509 if (children.empty())
510 {
511 // Leaf: place at next available position
512 layout.position.x = nextAvailableX;
513 nextAvailableX += 1.0f; // Reserve 1 unit
514 return;
515 }
516
517 // Recursively place all children
519 for (uint32_t childId : children)
520 {
521 PlaceSubtree(childId, tree, depth + 1, nextAvailableX);
522 }
524
525 // Center parent on children
526 // Note: Position values are in abstract units where each leaf occupies 1.0 unit.
527 // childrenStartX = position where first child starts (e.g., 0)
528 // childrenEndX = nextAvailableX after all children placed (e.g., 2 for two children)
529 // Since nextAvailableX is one past the last child's position, we subtract 1.0
530 // Example: Two children at 0 and 1 -> midpoint = (0 + 2 - 1) / 2 = 0.5 ✓
531 // Example: One child at 0 -> midpoint = (0 + 1 - 1) / 2 = 0 ✓
532 float childrenMidpoint = (childrenStartX + childrenEndX - 1.0f) / 2.0f;
533 layout.position.x = childrenMidpoint;
534
535 // If parent position collides with previous sibling's subtree, shift everything
536 if (layout.position.x < childrenStartX)
537 {
538 float shift = childrenStartX - layout.position.x;
539 layout.position.x += shift;
540
541 // Shift all children by the same amount
542 for (uint32_t childId : children)
543 {
544 ShiftSubtree(childId, tree, shift);
545 }
546 }
547 }
548
550 {
551 auto itLayout = m_nodeIdToIndex.find(nodeId);
552 if (itLayout == m_nodeIdToIndex.end())
553 return;
554
555 m_layouts[itLayout->second].position.x += offset;
556
557 const BTNode* node = tree->GetNode(nodeId);
558 if (!node)
559 return;
560
561 auto children = GetChildren(node);
562 for (uint32_t childId : children)
563 {
564 ShiftSubtree(childId, tree, offset);
565 }
566 }
567
569 {
570 for (int iter = 0; iter < maxIterations; ++iter)
571 {
572 bool hadCollision = false;
573
574 // Check all pairs within each layer
575 for (size_t layerIdx = 0; layerIdx < m_layers.size(); ++layerIdx)
576 {
577 auto& layer = m_layers[layerIdx];
578
579 for (size_t i = 0; i < layer.size(); ++i)
580 {
581 for (size_t j = i + 1; j < layer.size(); ++j)
582 {
583 uint32_t nodeA = layer[i];
584 uint32_t nodeB = layer[j];
585
586 auto itA = m_nodeIdToIndex.find(nodeA);
587 auto itB = m_nodeIdToIndex.find(nodeB);
588
589 if (itA == m_nodeIdToIndex.end() || itB == m_nodeIdToIndex.end())
590 continue;
591
592 BTNodeLayout& layoutA = m_layouts[itA->second];
593 BTNodeLayout& layoutB = m_layouts[itB->second];
594
596 {
598 hadCollision = true;
599 }
600 }
601 }
602 }
603
604 if (!hadCollision)
605 {
606 // Converged early
607 break;
608 }
609 }
610 }
611
613 {
614 // Note: During collision resolution, positions are in abstract units (0, 1, 2, etc.)
615 // We treat each node as occupying 1.0 abstract unit width
616 // Height is not relevant since we only check horizontal collisions within the same layer
617
618 const float abstractNodeWidth = 1.0f; // Each node occupies 1 abstract unit
619
620 float aLeft = a.position.x - abstractNodeWidth / 2.0f - padding;
621 float aRight = a.position.x + abstractNodeWidth / 2.0f + padding;
622
623 float bLeft = b.position.x - abstractNodeWidth / 2.0f;
624 float bRight = b.position.x + abstractNodeWidth / 2.0f;
625
626 // Check horizontal overlap (vertical not relevant since both nodes in same layer)
628 }
629
631 {
632 auto itA = m_nodeIdToIndex.find(nodeA);
633 auto itB = m_nodeIdToIndex.find(nodeB);
634
635 if (itA == m_nodeIdToIndex.end() || itB == m_nodeIdToIndex.end())
636 return;
637
638 BTNodeLayout& layoutA = m_layouts[itA->second];
639 BTNodeLayout& layoutB = m_layouts[itB->second];
640
641 // Note: Positions are in abstract units where each node occupies 1.0 unit
642 const float abstractNodeWidth = 1.0f;
643
644 // Calculate center-to-center distance
645 float dx = layoutB.position.x - layoutA.position.x;
646 float centerDistance = std::abs(dx);
647
648 // Minimum center-to-center distance needed = abstractNodeWidth + minDistance
650
652 {
653 // Calculate how much total separation is needed
655 // Each node moves half the distance
656 float pushAmount = totalPushNeeded / 2.0f;
657
658 // Push nodes apart in the direction they're already separated
659 // If B is to the right of A (dx > 0), push A left and B right
660 // If B is to the left of A (dx < 0), push A right and B left
661 if (dx > 0)
662 {
663 layoutA.position.x -= pushAmount;
664 layoutB.position.x += pushAmount;
665 }
666 else if (dx < 0)
667 {
668 layoutA.position.x += pushAmount;
669 layoutB.position.x -= pushAmount;
670 }
671 // If dx == 0 (nodes at same X), push them apart arbitrarily
672 else
673 {
674 layoutA.position.x -= pushAmount;
675 layoutB.position.x += pushAmount;
676 }
677 }
678 }
679
681 {
682 int crossingCount = 0;
683
684 // Check all pairs of edges between adjacent layers
685 for (size_t layerIdx = 0; layerIdx + 1 < m_layers.size(); ++layerIdx)
686 {
687 const auto& upperLayer = m_layers[layerIdx];
688 const auto& lowerLayer = m_layers[layerIdx + 1];
689
690 // Get all edges from upper to lower layer
691 std::vector<std::pair<int, int>> edges; // C++14: use std::pair explicitly
692
693 for (size_t i = 0; i < upperLayer.size(); ++i)
694 {
695 uint32_t parentId = upperLayer[i];
696 const BTNode* parentNode = tree->GetNode(parentId);
697 if (!parentNode)
698 continue;
699
700 auto children = GetChildren(parentNode);
701 for (uint32_t childId : children)
702 {
703 // Find child position in lower layer
704 auto childIt = std::find(lowerLayer.begin(), lowerLayer.end(), childId);
705 if (childIt != lowerLayer.end())
706 {
707 int childPos = static_cast<int>(std::distance(lowerLayer.begin(), childIt));
708 edges.push_back(std::make_pair(static_cast<int>(i), childPos));
709 }
710 }
711 }
712
713 // Count crossings between all edge pairs
714 for (size_t i = 0; i < edges.size(); ++i)
715 {
716 for (size_t j = i + 1; j < edges.size(); ++j)
717 {
718 // C++14: use explicit variable names instead of structured bindings
719 int a1 = edges[i].first;
720 int a2 = edges[i].second;
721 int b1 = edges[j].first;
722 int b2 = edges[j].second;
723
724 // Check if edges cross: (a1 < b1 && a2 > b2) || (a1 > b1 && a2 < b2)
725 if ((a1 < b1 && a2 > b2) || (a1 > b1 && a2 < b2))
726 {
728 }
729 }
730 }
731 }
732
733 return crossingCount;
734 }
735}
Graph layout engine for behavior tree visualization.
@ Selector
OR node - succeeds if any child succeeds.
@ Sequence
AND node - succeeds if all children succeed.
@ Inverter
Decorator - inverts child result.
@ Repeater
Decorator - repeats child N times.
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
bool DoNodesOverlap(const BTNodeLayout &a, const BTNodeLayout &b, float padding) const
void ShiftSubtree(uint32_t nodeId, const BehaviorTreeAsset *tree, float offset)
const BTNodeLayout * GetNodeLayout(uint32_t nodeId) const
Get computed layout for a specific node.
void PlaceSubtree(uint32_t nodeId, const BehaviorTreeAsset *tree, int depth, float &nextAvailableX)
void ResolveCollisions(float nodeSpacingX)
std::map< uint32_t, size_t > m_nodeIdToIndex
void ApplyBuchheimWalkerLayout(const BehaviorTreeAsset *tree)
std::vector< uint32_t > GetChildren(const BTNode *node) const
void BuildParentMap(const BehaviorTreeAsset *tree)
void AssignXCoordinates(float nodeSpacingX)
BTLayoutDirection m_layoutDirection
Default vertical.
std::map< uint32_t, std::vector< uint32_t > > m_parentMap
std::vector< BTNodeLayout > ComputeLayout(const BehaviorTreeAsset *tree, float nodeSpacingX=180.0f, float nodeSpacingY=120.0f, float zoomFactor=1.0f)
Compute layout for a behavior tree.
void ReduceCrossings(const BehaviorTreeAsset *tree)
void ResolveNodeCollisionsForceDirected(float nodePadding, int maxIterations)
void PushNodeApart(uint32_t nodeA, uint32_t nodeB, float minDistance)
int CountEdgeCrossings(const BehaviorTreeAsset *tree) const
float CalculateBarycenter(uint32_t nodeId, const std::vector< BTNodeLayout * > &neighbors) const
std::vector< BTNodeLayout > m_layouts
void AssignLayers(const BehaviorTreeAsset *tree)
std::vector< std::vector< uint32_t > > m_layers
float x
Definition vector.h:27
@ TopToBottom
Traditional top-down layout (vertical)
Represents a single node in a behavior tree.
Layout information for a single behavior tree node.
Vector position
Final position (x, y)
uint32_t nodeId
BT node ID.