Ember
Loading...
Searching...
No Matches
TreeCanvas.cpp
Go to the documentation of this file.
2#include <algorithm>
3#include <cmath>
4#include <wx/filename.h>
5
6namespace EmberUI {
7
11 EVT_MOUSEWHEEL(TreeCanvas::OnMouseWheel) EVT_MIDDLE_DOWN(TreeCanvas::OnMouseMiddleDown)
13
14 TreeCanvas::TreeCanvas(wxWindow *parent, wxWindowID id)
15 : wxPanel(parent, id), m_titleFont(12, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL),
16 m_typeFont(9, wxFONTFAMILY_SWISS, wxFONTSTYLE_ITALIC, wxFONTWEIGHT_NORMAL),
17 m_headerFont(8, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD) {
18 SetBackgroundStyle(wxBG_STYLE_PAINT);
19 SetBackgroundColour(m_config.background_color);
20 SetCanFocus(true);
21 UpdateScaledConfig();
22
23 Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent &e) { OnKeyDown(e); });
24 Bind(wxEVT_KEY_UP, [this](wxKeyEvent &e) { OnKeyUp(e); });
25
26 m_lastFrameTime = wxGetUTCTimeMillis();
27
28 m_refreshTimer = new wxTimer(this, wxID_ANY);
29 m_refreshTimer->Start(16);
30
31 Bind(wxEVT_TIMER, [this](wxTimerEvent &) {
32 wxLongLong currentTime = wxGetUTCTimeMillis();
33 double deltaTime = (currentTime - m_lastFrameTime).ToDouble() / 1000.0;
34 m_lastFrameTime = currentTime;
35
36 if (deltaTime > 0.1)
37 deltaTime = 0.033;
38
39 if (m_config.enable_smooth_panning) {
40 float dx = static_cast<float>(m_targetOffset.x - m_viewOffset.x);
41 float dy = static_cast<float>(m_targetOffset.y - m_viewOffset.y);
42
43 if (std::abs(dx) > 0.5f || std::abs(dy) > 0.5f) {
44 float speed = std::max(5.0f, 100.0f - m_config.pan_smoothness);
45 float alpha = 1.0f - std::exp(-speed * static_cast<float>(deltaTime));
46 int stepX = static_cast<int>(dx * alpha);
47 int stepY = static_cast<int>(dy * alpha);
48 if (stepX == 0 && dx != 0.0f)
49 stepX = dx > 0 ? 1 : -1;
50 if (stepY == 0 && dy != 0.0f)
51 stepY = dy > 0 ? 1 : -1;
52 m_viewOffset.x += stepX;
53 m_viewOffset.y += stepY;
54 m_dirty = true;
55 } else {
56 m_viewOffset = m_targetOffset;
57 }
58 } else {
59 m_viewOffset = m_targetOffset;
60 }
61
62 if (m_dirty) {
63 m_dirty = false;
64 Refresh();
65 }
66 });
67}
68
70 if (m_refreshTimer) {
71 m_refreshTimer->Stop();
72 delete m_refreshTimer;
73 m_refreshTimer = nullptr;
74 }
75}
76
78 if (m_config.icon_directory.empty())
79 return;
80
81 wxString dir = m_config.icon_directory;
82 if (!dir.EndsWith("/") && !dir.EndsWith("\\"))
83 dir += "/";
84
85 struct IconEntry {
87 wxString filename;
88 };
89
90 IconEntry entries[] = {
91 {EmberCore::ITreeNode::NodeType::Action, "action_node_20.png"},
92 {EmberCore::ITreeNode::NodeType::Control, "control_node_20.png"},
93 {EmberCore::ITreeNode::NodeType::Condition, "condition_node_20.png"},
94 {EmberCore::ITreeNode::NodeType::Decorator, "decorator_node_20.png"},
96 };
97
98 int iconSize = std::max(8, m_scaled.type_header_height - 4);
99
100 for (const auto &entry : entries) {
101 wxString path = dir + entry.filename;
102 if (wxFileName::Exists(path)) {
103 wxImage img(path, wxBITMAP_TYPE_PNG);
104 if (img.IsOk()) {
105 if (img.GetWidth() != iconSize || img.GetHeight() != iconSize) {
106 img = img.Scale(iconSize, iconSize, wxIMAGE_QUALITY_BICUBIC);
107 }
108 m_typeIcons[entry.type] = wxBitmap(img);
109 }
110 }
111 }
112}
113
115 if (!node)
116 return;
117
118 if (depth >= m_config.autoCollapseDepth && node->GetChildCount() > 0) {
119 node->SetChildrenVisible(false);
120 return;
121 }
122
123 for (size_t i = 0; i < node->GetChildCount(); ++i) {
124 EmberCore::ITreeNode *child = node->GetChild(i);
125 if (child) {
126 AutoCollapseTree(child, depth + 1);
127 }
128 }
129}
130
132 if (!node)
133 return;
134 node->SetChildrenVisible(true);
135 for (size_t i = 0; i < node->GetChildCount(); ++i) {
136 if (node->GetChild(i))
137 ExpandAllChildren(node->GetChild(i));
138 }
139}
140
142 if (!node)
143 return;
144 node->SetChildrenVisible(false);
145 for (size_t i = 0; i < node->GetChildCount(); ++i) {
146 if (node->GetChild(i))
148 }
149}
150
151void TreeCanvas::ExpandToDepth(EmberCore::ITreeNode *node, int relativeDepth) {
152 if (!node)
153 return;
154 if (relativeDepth <= 0 || node->GetChildCount() == 0) {
155 node->SetChildrenVisible(false);
156 return;
157 }
158 node->SetChildrenVisible(true);
159 for (size_t i = 0; i < node->GetChildCount(); ++i) {
160 if (node->GetChild(i))
161 ExpandToDepth(node->GetChild(i), relativeDepth - 1);
162 }
163}
164
165void TreeCanvas::SetTree(std::shared_ptr<EmberCore::ITreeStructure> tree) {
166 m_tree = tree;
167 m_selectedNode = nullptr;
168 m_hoveredNode = nullptr;
169 m_hoveredArrowNode = nullptr;
170 m_pressedArrowNode = nullptr;
171 m_focusRoot = nullptr;
172 m_widthCache.clear();
173 m_minimapDirty = true;
174
175 if (m_tree && m_tree->HasRootNode() &&
176 m_tree->GetNodeCount() > static_cast<size_t>(m_config.autoCollapseThreshold)) {
177 AutoCollapseTree(m_tree->GetRootNode(), 0);
178 }
179
180 if (m_tree && m_tree->HasRootNode()) {
181 m_zoomFactor = 0.75f;
182 CenterOnNode(m_tree->GetRootNode());
183 } else {
184 ResetView();
185 }
186
187 MarkDirty();
188}
189
195
201
203
205 wxColour fill = m_config.node_color;
206 if (m_statusProvider) {
207 int status = m_statusProvider->GetNodeStatus(static_cast<int64_t>(node->GetId()));
208 if (status != 0)
209 fill = StatusColors::GetFillColor(status);
210 }
211 return fill;
212}
213
214wxColour TreeCanvas::GetNodeBorderColor(EmberCore::ITreeNode *node, bool selected, bool hovered) {
215 wxColour border = m_config.border_color;
216 int64_t nodeId = static_cast<int64_t>(node->GetId());
217
218 if (m_statusProvider) {
219 int status = m_statusProvider->GetNodeStatus(nodeId);
220 if (status != 0)
221 border = StatusColors::GetBorderColor(status);
222 }
223 if (m_executionPathIds.count(nodeId) > 0)
225
226 if (selected)
227 border = m_config.selected_border_color;
228 else if (hovered)
229 border = m_config.hovered_border_color;
230
231 return border;
232}
233
235 wxColour text = m_config.text_color;
236 if (m_statusProvider) {
237 int status = m_statusProvider->GetNodeStatus(static_cast<int64_t>(node->GetId()));
238 if (status != 0)
239 text = StatusColors::GetTextColor(status);
240 }
241 return text;
242}
243
245 m_zoomFactor = 1.0f;
246 wxSize panel_size = GetSize();
247 m_viewOffset = {panel_size.GetWidth() / 2, 100};
249 MarkDirty();
250}
251
253 m_widthCache.clear();
254 m_minimapDirty = true;
255 wxSize panelSize = GetSize();
256
257 if (!m_tree || !m_tree->HasRootNode() || panelSize.GetWidth() <= 0) {
258 ResetView();
259 return;
260 }
261
262 int treeWidth = CalculateSubtreeWidth(m_tree->GetRootNode());
263
264 if (treeWidth <= panelSize.GetWidth()) {
265 ResetView();
266 return;
267 }
268
269 m_zoomFactor = std::max(m_minZoom, static_cast<float>(panelSize.GetWidth()) / static_cast<float>(treeWidth) * 0.9f);
270 m_viewOffset = {panelSize.GetWidth() / 2, 30};
272 MarkDirty();
273}
274
276 if (!node || !m_tree)
277 return;
278
279 wxSize panel_size = GetSize();
280 wxPoint node_world_pos = FindNodeWorldPosition(node);
281
282 if (node_world_pos.x != -1 && node_world_pos.y != -1) {
283 wxPoint dest = {panel_size.x / 2 - static_cast<int>(node_world_pos.x * m_zoomFactor),
284 panel_size.y / 2 - static_cast<int>(node_world_pos.y * m_zoomFactor)};
285 if (m_config.enable_smooth_panning) {
286 m_targetOffset = dest;
287 } else {
288 m_viewOffset = dest;
289 m_targetOffset = dest;
290 }
291 }
292 MarkDirty();
293}
294
295void TreeCanvas::SetZoom(float zoom) {
296 m_zoomFactor = std::max(m_minZoom, std::min(m_maxZoom, zoom));
297 MarkDirty();
298}
299
300// --- Event Handlers ---
301
304 m_scaled.node_width = DPI::Scale(this, m_scaled.node_width);
305 m_scaled.node_height = DPI::Scale(this, m_scaled.node_height);
306 m_scaled.vertical_spacing = DPI::Scale(this, m_scaled.vertical_spacing);
307 m_scaled.horizontal_spacing = DPI::Scale(this, m_scaled.horizontal_spacing);
308 m_scaled.grid_size = DPI::Scale(this, m_scaled.grid_size);
309 m_scaled.type_header_height = DPI::Scale(this, m_scaled.type_header_height);
310 m_scaled.title_padding = DPI::Scale(this, m_config.title_padding);
311 m_scaled.viewport_culling_margin = DPI::Scale(this, m_config.viewport_culling_margin);
312}
313
315 m_pathToSelectedIds.clear();
317 if (!m_config.highlight_path_to_selected || !m_selectedNode)
318 return;
319
321 while (curr) {
322 m_pathToSelectedIds.insert(curr->GetId());
323 curr = curr->GetParent();
324 }
325}
326
327void TreeCanvas::OnPaint(wxPaintEvent &) {
329 wxBufferedPaintDC dc(this);
330
331 dc.SetBackground(wxBrush(m_config.background_color));
332 dc.Clear();
333
334 if (!m_tree || !m_tree->HasRootNode()) {
335 dc.SetUserScale(1.0, 1.0);
336 dc.SetDeviceOrigin(0, 0);
337 wxSize sz = GetSize();
338 dc.SetTextForeground(wxColour(128, 128, 128));
339 dc.SetFont(wxFont(14, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
340 wxString msg = "Waiting for tree data...";
341 wxCoord tw, th;
342 dc.GetTextExtent(msg, &tw, &th);
343 dc.DrawText(msg, (sz.x - tw) / 2, sz.y / 2 - th);
344 return;
345 }
346
347 dc.SetUserScale(m_zoomFactor, m_zoomFactor);
348 dc.SetDeviceOrigin(m_viewOffset.x, m_viewOffset.y);
349
350 wxRect viewport = GetViewportBounds();
351
352 if (m_showGrid && m_zoomFactor >= 0.3f) {
353 DrawGrid(dc, viewport);
354 }
355
356 m_executionPathIds.clear();
357 m_highlightedSegments.clear();
359 m_collapseArrows.clear();
360
361 if (m_statusProvider) {
362 m_statusProvider->GetExecutionPathIds(m_executionPathIds);
363 }
364
366
367 EmberCore::ITreeNode *effectiveRoot = GetEffectiveRoot();
368 if (effectiveRoot) {
369 wxPoint root_pos = CalculateRootPosition();
370 DrawNode(dc, effectiveRoot, root_pos.x, root_pos.y, 0, viewport);
371 }
372
373 if (!m_highlightedSegments.empty()) {
374 wxColour pathColor = StatusColors::GetExecutionPathColor();
375 dc.SetPen(wxPen(pathColor, 4));
376 for (const auto &seg : m_highlightedSegments) {
377 dc.DrawLine(seg.x1, seg.y1, seg.x2, seg.y2);
378 }
379 }
380
381 if (!m_selectedPathSegments.empty()) {
382 dc.SetPen(wxPen(m_config.path_highlight_color, 4));
383 for (const auto &seg : m_selectedPathSegments) {
384 dc.DrawLine(seg.x1, seg.y1, seg.x2, seg.y2);
385 }
386 }
387
388 for (const auto &arrow : m_collapseArrows) {
389 DrawCollapseArrow(dc, arrow.node, arrow.x, arrow.y);
390 }
391
393 DrawOverlayInfo(dc);
394 DrawMinimap(dc);
396 DrawBreadcrumb(dc);
397}
398
399void TreeCanvas::OnSize(wxSizeEvent &event) {
400 MarkDirty();
401 event.Skip();
402}
403
404void TreeCanvas::OnMouseLeftDown(wxMouseEvent &event) {
405 SetFocus();
406
407 if (m_config.pan_key_code == -1 && m_config.pan_mouse_button == 1) {
408 event.Skip();
409 return;
410 }
411
412 if (!m_tree || !m_tree->HasRootNode()) {
413 event.Skip();
414 return;
415 }
416
417 wxPoint mouse_pos = event.GetPosition();
418
419 if (m_showMinimap && m_minimapRect.Contains(mouse_pos)) {
421 if (root) {
422 wxPoint rootPos = CalculateRootPosition();
423 // cppcheck-suppress duplicateAssignExpression
424 int treeMinX = rootPos.x;
425 int treeMaxX = rootPos.x;
426 int treeMaxY = rootPos.y + m_scaled.node_height;
427 ComputeTreeExtent(root, rootPos.x, rootPos.y, treeMinX, treeMaxX, treeMaxY);
428
429 int treeW = std::max(1, treeMaxX - treeMinX);
430 int treeH = std::max(1, treeMaxY - rootPos.y);
431
432 int pad = 10;
433 float scX = static_cast<float>(m_minimapRect.GetWidth() - 2 * pad) / static_cast<float>(treeW);
434 float scY = static_cast<float>(m_minimapRect.GetHeight() - 2 * pad) / static_cast<float>(treeH);
435 float sc = std::min(scX, scY);
436
437 int ofsX = m_minimapRect.GetLeft() + pad +
438 static_cast<int>((m_minimapRect.GetWidth() - 2 * pad - treeW * sc) / 2.0f);
439 int ofsY = m_minimapRect.GetTop() + pad;
440
441 float worldX = (mouse_pos.x - ofsX) / sc + treeMinX;
442 float worldY = (mouse_pos.y - ofsY) / sc + rootPos.y;
443
444 wxSize panelSize = GetSize();
445 m_viewOffset.x = panelSize.x / 2 - static_cast<int>(worldX * m_zoomFactor);
446 m_viewOffset.y = panelSize.y / 2 - static_cast<int>(worldY * m_zoomFactor);
448 MarkDirty();
449 }
450 return;
451 }
452
453 for (const auto &bc : m_breadcrumbHitTargets) {
454 if (bc.first.Contains(mouse_pos)) {
455 if (bc.second == nullptr) {
457 } else {
458 SetSelectedNode(bc.second);
459 CenterOnNode(bc.second);
460 }
461 return;
462 }
463 }
464
465 wxPoint world_pos = ScreenToWorld(mouse_pos);
466
467 EmberCore::ITreeNode *arrow_node = FindArrowAtPosition(world_pos);
468 if (arrow_node) {
469 m_pressedArrowNode = arrow_node;
470 bool visible = arrow_node->AreChildrenVisible();
471 arrow_node->SetChildrenVisible(!visible);
472 m_widthCache.clear();
473 m_minimapDirty = true;
476 MarkDirty();
477 return;
478 }
479
480 wxPoint root_pos = CalculateRootPosition();
481
482 EmberCore::ITreeNode *effectiveRoot = GetEffectiveRoot();
483 EmberCore::ITreeNode *clicked_node =
484 effectiveRoot ? FindNodeAtPosition(effectiveRoot, root_pos, world_pos) : nullptr;
485 SetSelectedNode(clicked_node);
486}
487
488void TreeCanvas::OnMouseLeftUp(wxMouseEvent &event) {
489 (void)event;
490 if (m_pressedArrowNode) {
491 m_pressedArrowNode = nullptr;
492 MarkDirty();
493 }
494}
495
496void TreeCanvas::OnMouseLeftDClick(wxMouseEvent &event) {
497 if (!m_tree || !m_tree->HasRootNode())
498 return;
499
500 wxPoint mouse_pos = event.GetPosition();
501
502 if (m_showMinimap && m_minimapRect.Contains(mouse_pos))
503 return;
504
505 wxPoint world_pos = ScreenToWorld(mouse_pos);
506 wxPoint root_pos = CalculateRootPosition();
507 EmberCore::ITreeNode *effectiveRoot = GetEffectiveRoot();
508 EmberCore::ITreeNode *clicked_node =
509 effectiveRoot ? FindNodeAtPosition(effectiveRoot, root_pos, world_pos) : nullptr;
510
511 if (clicked_node) {
512 SetSelectedNode(clicked_node);
513 CenterOnNode(clicked_node);
514 }
515}
516
517void TreeCanvas::OnMouseMotion(wxMouseEvent &event) {
518 wxPoint mouse_pos = event.GetPosition();
519
520 bool shouldPan = false;
521 if (m_config.pan_key_code == -1) {
522 if (m_config.pan_mouse_button == 1)
523 shouldPan = event.LeftIsDown();
524 else if (m_config.pan_mouse_button == 2)
525 shouldPan = event.MiddleIsDown();
526 else if (m_config.pan_mouse_button == 3)
527 shouldPan = event.RightIsDown();
528 } else {
529 shouldPan = m_panKeyHeld;
530 }
531
532 if (shouldPan) {
533 if (!m_isPanning) {
534 m_isPanning = true;
535 m_lastMousePos = mouse_pos;
536 } else {
537 wxPoint delta = mouse_pos - m_lastMousePos;
538 delta.x = static_cast<int>(delta.x * m_config.pan_sensitivity);
539 delta.y = static_cast<int>(delta.y * m_config.pan_sensitivity);
540 m_viewOffset += delta;
542 m_lastMousePos = mouse_pos;
543 MarkDirty();
544 }
545 return;
546 }
547
548 if (m_isPanning) {
549 m_isPanning = false;
550 }
551
552 EmberCore::ITreeNode *effectiveRoot = GetEffectiveRoot();
553 if (effectiveRoot) {
554 wxPoint world_pos = ScreenToWorld(mouse_pos);
555 EmberCore::ITreeNode *arrow_hovered = FindArrowAtPosition(world_pos);
556
557 if (arrow_hovered) {
558 if (arrow_hovered != m_hoveredArrowNode) {
559 m_hoveredArrowNode = arrow_hovered;
560 m_hoveredNode = nullptr;
561 MarkDirty();
562 }
563 } else {
564 if (m_hoveredArrowNode) {
565 m_hoveredArrowNode = nullptr;
566 MarkDirty();
567 }
568 wxPoint root_pos = CalculateRootPosition();
569 EmberCore::ITreeNode *new_hovered = FindNodeAtPosition(effectiveRoot, root_pos, world_pos);
570 if (new_hovered != m_hoveredNode) {
571 m_hoveredNode = new_hovered;
572 MarkDirty();
573 }
574 }
575 }
576}
577
578void TreeCanvas::OnMouseWheel(wxMouseEvent &event) {
579 float old_zoom = m_zoomFactor;
580
581 float zoom_delta =
582 (event.GetWheelRotation() > 0 ? m_config.zoom_step : -m_config.zoom_step) * m_config.mouse_wheel_sensitivity;
583 m_zoomFactor += zoom_delta;
584 m_zoomFactor = std::max(m_minZoom, std::min(m_maxZoom, m_zoomFactor));
585
586 if (old_zoom != m_zoomFactor) {
587 if (m_config.zoom_follows_cursor) {
588 wxPoint mouse_screen = event.GetPosition();
589 wxPoint mouse_world = {static_cast<int>((mouse_screen.x - m_viewOffset.x) / old_zoom),
590 static_cast<int>((mouse_screen.y - m_viewOffset.y) / old_zoom)};
591
592 m_viewOffset.x = mouse_screen.x - static_cast<int>(mouse_world.x * m_zoomFactor);
593 m_viewOffset.y = mouse_screen.y - static_cast<int>(mouse_world.y * m_zoomFactor);
595 }
596 MarkDirty();
597 }
598}
599
600void TreeCanvas::OnMouseMiddleDown(wxMouseEvent &event) {
601 m_isPanning = true;
602 m_lastMousePos = event.GetPosition();
603}
604
605void TreeCanvas::OnMouseMiddleUp(wxMouseEvent &) { m_isPanning = false; }
606
607void TreeCanvas::OnKeyDown(wxKeyEvent &event) {
608 int keyCode = event.GetKeyCode();
609
610 if (keyCode == m_config.pan_key_code && m_config.pan_key_code != -1) {
611 m_panKeyHeld = true;
612 return;
613 }
614
615 float pan_step = m_config.pan_step_size * m_config.pan_sensitivity;
616
617 switch (keyCode) {
618 case WXK_UP:
619 m_targetOffset.y += static_cast<int>(pan_step);
620 break;
621 case WXK_DOWN:
622 m_targetOffset.y -= static_cast<int>(pan_step);
623 break;
624 case WXK_LEFT:
625 m_targetOffset.x += static_cast<int>(pan_step);
626 break;
627 case WXK_RIGHT:
628 m_targetOffset.x -= static_cast<int>(pan_step);
629 break;
630 case 'R':
631 ResetView();
632 break;
633 case 'E':
634 if (m_selectedNode && m_selectedNode->GetChildCount() > 0) {
635 bool visible = m_selectedNode->AreChildrenVisible();
636 m_selectedNode->SetChildrenVisible(!visible);
637 m_widthCache.clear();
638 m_minimapDirty = true;
641 }
642 break;
643 case 'F':
644 if (m_selectedNode && m_selectedNode->GetChildCount() > 0) {
646 } else {
648 }
649 break;
650 case WXK_ESCAPE:
651 if (m_focusRoot) {
653 }
654 break;
655 default:
656 event.Skip();
657 return;
658 }
659
660 MarkDirty();
661}
662
663void TreeCanvas::OnKeyUp(wxKeyEvent &event) {
664 int keyCode = event.GetKeyCode();
665 if (keyCode == m_config.pan_key_code && m_config.pan_key_code != -1) {
666 m_panKeyHeld = false;
667 m_isPanning = false;
668 return;
669 }
670 event.Skip();
671}
672
673void TreeCanvas::OnMouseRightUp(wxMouseEvent &event) {
674 if (!m_tree || !m_tree->HasRootNode()) {
675 event.Skip();
676 return;
677 }
678
679 wxPoint mouse_pos = event.GetPosition();
680 wxPoint world_pos = ScreenToWorld(mouse_pos);
681 wxPoint root_pos = CalculateRootPosition();
682
683 EmberCore::ITreeNode *effectiveRoot = GetEffectiveRoot();
684 EmberCore::ITreeNode *clicked_node =
685 effectiveRoot ? FindNodeAtPosition(effectiveRoot, root_pos, world_pos) : nullptr;
686
687 if (clicked_node) {
688 SetSelectedNode(clicked_node);
689 }
690
691 enum {
692 ID_EXPAND_ALL = wxID_HIGHEST + 100,
693 ID_COLLAPSE_ALL,
694 ID_EXPAND_DEPTH_1,
695 ID_EXPAND_DEPTH_2,
696 ID_EXPAND_DEPTH_3,
697 ID_FOCUS_HERE,
698 ID_CENTER_NODE,
699 ID_FIT_VIEW,
700 ID_RESET_VIEW
701 };
702
703 wxMenu menu;
704
705 if (clicked_node && clicked_node->GetChildCount() > 0) {
706 menu.Append(ID_EXPAND_ALL, "Expand All");
707 menu.Append(ID_COLLAPSE_ALL, "Collapse All");
708 menu.AppendSeparator();
709 menu.Append(ID_EXPAND_DEPTH_1, "Expand to Depth 1");
710 menu.Append(ID_EXPAND_DEPTH_2, "Expand to Depth 2");
711 menu.Append(ID_EXPAND_DEPTH_3, "Expand to Depth 3");
712 menu.AppendSeparator();
713 menu.Append(ID_FOCUS_HERE, "Focus Here");
714 menu.Append(ID_CENTER_NODE, "Center on Node");
715
716 menu.Bind(
717 wxEVT_MENU,
718 [this, clicked_node](wxCommandEvent &) {
719 ExpandAllChildren(clicked_node);
720 m_widthCache.clear();
721 m_minimapDirty = true;
722 MarkDirty();
725 },
726 ID_EXPAND_ALL);
727
728 menu.Bind(
729 wxEVT_MENU,
730 [this, clicked_node](wxCommandEvent &) {
731 CollapseAllChildren(clicked_node);
732 m_widthCache.clear();
733 m_minimapDirty = true;
734 MarkDirty();
737 },
738 ID_COLLAPSE_ALL);
739
740 menu.Bind(
741 wxEVT_MENU,
742 [this, clicked_node](wxCommandEvent &) {
743 ExpandToDepth(clicked_node, 1);
744 m_widthCache.clear();
745 m_minimapDirty = true;
746 MarkDirty();
749 },
750 ID_EXPAND_DEPTH_1);
751
752 menu.Bind(
753 wxEVT_MENU,
754 [this, clicked_node](wxCommandEvent &) {
755 ExpandToDepth(clicked_node, 2);
756 m_widthCache.clear();
757 m_minimapDirty = true;
758 MarkDirty();
761 },
762 ID_EXPAND_DEPTH_2);
763
764 menu.Bind(
765 wxEVT_MENU,
766 [this, clicked_node](wxCommandEvent &) {
767 ExpandToDepth(clicked_node, 3);
768 m_widthCache.clear();
769 m_minimapDirty = true;
770 MarkDirty();
773 },
774 ID_EXPAND_DEPTH_3);
775
776 menu.Bind(
777 wxEVT_MENU, [this, clicked_node](wxCommandEvent &) { EnterFocusMode(clicked_node); }, ID_FOCUS_HERE);
778
779 menu.Bind(
780 wxEVT_MENU, [this, clicked_node](wxCommandEvent &) { CenterOnNode(clicked_node); }, ID_CENTER_NODE);
781 } else {
783
784 menu.Append(ID_EXPAND_ALL, "Expand All");
785 menu.Append(ID_COLLAPSE_ALL, "Collapse All");
786 menu.AppendSeparator();
787 menu.Append(ID_FIT_VIEW, "Fit Tree in View");
788 menu.Append(ID_RESET_VIEW, "Reset View");
789
790 menu.Bind(
791 wxEVT_MENU,
792 [this, root](wxCommandEvent &) {
793 if (root)
794 ExpandAllChildren(root);
795 m_widthCache.clear();
796 m_minimapDirty = true;
797 MarkDirty();
800 },
801 ID_EXPAND_ALL);
802
803 menu.Bind(
804 wxEVT_MENU,
805 [this, root](wxCommandEvent &) {
806 if (root)
808 m_widthCache.clear();
809 m_minimapDirty = true;
810 MarkDirty();
813 },
814 ID_COLLAPSE_ALL);
815
816 menu.Bind(
817 wxEVT_MENU, [this](wxCommandEvent &) { FitTreeInView(); }, ID_FIT_VIEW);
818
819 menu.Bind(
820 wxEVT_MENU, [this](wxCommandEvent &) { ResetView(); }, ID_RESET_VIEW);
821 }
822
823 PopupMenu(&menu, mouse_pos);
824}
825
826// --- Drawing Methods ---
827
828void TreeCanvas::DrawGrid(wxDC &dc, const wxRect &viewport) {
829 dc.SetBrush(wxBrush(m_config.background_color));
830 dc.SetPen(*wxTRANSPARENT_PEN);
831 dc.DrawRectangle(viewport);
832
833 dc.SetPen(wxPen(m_config.grid_color, 1));
834 dc.SetBrush(*wxTRANSPARENT_BRUSH);
835
836 int gridSpacing = m_scaled.grid_size;
837 if (m_zoomFactor < 0.5f)
838 gridSpacing = m_scaled.grid_size * 4;
839 else if (m_zoomFactor < 0.8f)
840 gridSpacing = m_scaled.grid_size * 2;
841 else if (m_zoomFactor > 2.0f)
842 gridSpacing = m_scaled.grid_size / 2;
843
844 int startX = (viewport.GetLeft() / gridSpacing) * gridSpacing;
845 for (int x = startX; x <= viewport.GetRight(); x += gridSpacing) {
846 dc.DrawLine(x, viewport.GetTop(), x, viewport.GetBottom());
847 }
848
849 int startY = (viewport.GetTop() / gridSpacing) * gridSpacing;
850 for (int y = startY; y <= viewport.GetBottom(); y += gridSpacing) {
851 dc.DrawLine(viewport.GetLeft(), y, viewport.GetRight(), y);
852 }
853}
854
855void TreeCanvas::DrawNode(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y, int level,
856 const wxRect &viewport) {
857 if (!node)
858 return;
859
860 int margin =
861 m_config.enable_viewport_culling ? static_cast<int>(m_config.viewport_culling_margin / m_zoomFactor) : 100000;
862
863 if (y > viewport.GetBottom() + margin) {
864 return;
865 }
866
867 int subtreeWidth = CalculateSubtreeWidth(node);
868
869 if (m_config.enable_viewport_culling) {
870 if (x + subtreeWidth / 2 < viewport.GetLeft() - margin || x - subtreeWidth / 2 > viewport.GetRight() + margin) {
871 return;
872 }
873 }
874
875 bool nodeVisible = (y + m_scaled.node_height >= viewport.GetTop() - margin);
876
877 if (nodeVisible) {
878 DrawNodeBox(dc, node, x, y);
879 DrawNodeText(dc, node, x, y);
880
881 if (node->GetChildCount() > 0) {
882 m_collapseArrows.push_back({node, x, y});
883 }
884 }
885
886 if (node->AreChildrenVisible() && node->GetChildCount() > 0) {
887 if (nodeVisible) {
888 DrawNodeConnections(dc, node, x, y);
889 }
890
891 int total_width = subtreeWidth;
892 wxCoord child_x = x - total_width / 2;
893 wxCoord child_y = y + m_scaled.node_height + m_scaled.vertical_spacing;
894
895 for (size_t i = 0; i < node->GetChildCount(); ++i) {
896 EmberCore::ITreeNode *child = node->GetChild(i);
897 if (child) {
898 int child_subtree_width = CalculateSubtreeWidth(child);
899 wxCoord center_x = child_x + child_subtree_width / 2;
900 DrawNode(dc, child, center_x, child_y, level + 1, viewport);
901 child_x += child_subtree_width;
902 }
903 }
904 }
905}
906
907void TreeCanvas::DrawNodeBox(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y) {
908 bool isSelected = (m_selectedNode && node->GetId() == m_selectedNode->GetId());
909 bool isHovered = (node == m_hoveredNode);
910
911 wxColour fill_color = GetNodeFillColor(node, isSelected, isHovered);
912 wxColour border_color = GetNodeBorderColor(node, isSelected, isHovered);
913
914 int border_width = 1;
915 int64_t nodeId = static_cast<int64_t>(node->GetId());
916 if (m_statusProvider && m_statusProvider->GetNodeStatus(nodeId) != 0)
917 border_width = 2;
918 if (m_executionPathIds.count(nodeId) > 0)
919 border_width = 3;
920 if (isSelected)
921 border_width = 3;
922 else if (isHovered)
923 border_width = 2;
924
925 dc.SetBrush(wxBrush(fill_color));
926 dc.SetPen(wxPen(border_color, border_width));
927
928 wxRect node_rect(x - m_scaled.node_width / 2, y, m_scaled.node_width, m_scaled.node_height);
929
930 if (m_zoomFactor < 0.25f) {
931 dc.DrawRectangle(node_rect);
932 } else {
933 dc.DrawRoundedRectangle(node_rect, 5);
934 }
935
936 DrawTypeHeader(dc, node, node_rect);
937}
938
939void TreeCanvas::DrawTypeHeader(wxDC &dc, EmberCore::ITreeNode *node, const wxRect &nodeRect) {
940 wxColour typeColor = GetNodeTypeColor(node->GetType());
941 int headerH = m_scaled.type_header_height;
942 wxRect headerRect(nodeRect.GetLeft() + 1, nodeRect.GetTop() + 1, nodeRect.GetWidth() - 2, headerH);
943
944 dc.SetBrush(wxBrush(typeColor));
945 dc.SetPen(*wxTRANSPARENT_PEN);
946 dc.DrawRectangle(headerRect);
947
948 if (m_zoomFactor < 0.5f)
949 return;
950
951 int contentX = headerRect.GetLeft() + 4;
952 int contentCenterY = headerRect.GetTop() + headerH / 2;
953
954 auto iconIt = m_typeIcons.find(node->GetType());
955 if (m_config.show_type_icons && iconIt != m_typeIcons.end() && iconIt->second.IsOk()) {
956 const wxBitmap &icon = iconIt->second;
957 int iconH = icon.GetHeight();
958
959 double savedScaleX, savedScaleY;
960 dc.GetUserScale(&savedScaleX, &savedScaleY);
961 wxPoint savedOrigin = dc.GetDeviceOrigin();
962
963 dc.SetUserScale(1.0, 1.0);
964 dc.SetDeviceOrigin(0, 0);
965
966 int screenX = static_cast<int>(contentX * savedScaleX) + savedOrigin.x;
967 int screenY = static_cast<int>((contentCenterY - iconH / 2) * savedScaleY) + savedOrigin.y;
968
969 dc.DrawBitmap(icon, screenX, screenY, true);
970
971 dc.SetUserScale(savedScaleX, savedScaleY);
972 dc.SetDeviceOrigin(savedOrigin.x, savedOrigin.y);
973
974 contentX += static_cast<int>(icon.GetWidth() / savedScaleX) + 3;
975 }
976
977 if (m_config.show_type_text) {
978 dc.SetFont(m_headerFont);
979 dc.SetTextForeground(m_config.type_header_text_color);
980
981 wxString typeStr = node->GetTypeString();
982 wxCoord tw, th;
983 dc.GetTextExtent(typeStr, &tw, &th);
984
985 int maxTextW = headerRect.GetRight() - contentX - 4;
986 if (tw > maxTextW && maxTextW > 0) {
987 size_t lo = 0, hi = typeStr.Length();
988 while (lo < hi) {
989 size_t mid = (lo + hi + 1) / 2;
990 dc.GetTextExtent(typeStr.Left(mid) + "..", &tw, &th);
991 if (tw <= maxTextW)
992 lo = mid;
993 else
994 hi = mid - 1;
995 }
996 typeStr = (lo > 0 ? typeStr.Left(lo) : wxString()) + "..";
997 dc.GetTextExtent(typeStr, &tw, &th);
998 }
999
1000 dc.DrawText(typeStr, contentX, contentCenterY - th / 2);
1001 }
1002}
1003
1004void TreeCanvas::DrawNodeText(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y) {
1005 if (m_zoomFactor < 0.25f)
1006 return;
1007
1008 bool isSelected = (m_selectedNode && node->GetId() == m_selectedNode->GetId());
1009 bool isHovered = (node == m_hoveredNode);
1010 wxColour text_color = GetNodeTextColor(node, isSelected, isHovered);
1011 dc.SetTextForeground(text_color);
1012
1013 int textStartY = y + m_scaled.type_header_height + 2;
1014
1015 dc.SetFont(m_titleFont);
1016 EmberCore::String text = node->GetName();
1017 wxCoord tw, th;
1018 dc.GetTextExtent(text, &tw, &th);
1019
1020 int maxW = m_scaled.node_width - 20;
1021 if (tw > m_scaled.node_width - 10 && maxW > 0) {
1022 size_t lo = 0, hi = text.length();
1023 while (lo < hi) {
1024 size_t mid = (lo + hi + 1) / 2;
1025 wxCoord mw, mh;
1026 dc.GetTextExtent(wxString(text.substr(0, mid) + "..."), &mw, &mh);
1027 if (mw <= maxW)
1028 lo = mid;
1029 else
1030 hi = mid - 1;
1031 }
1032 text = (lo > 0 ? text.substr(0, lo) : EmberCore::String()) + "...";
1033 dc.GetTextExtent(text, &tw, &th);
1034 }
1035
1036 dc.DrawText(text, x - tw / 2, textStartY + 2);
1037}
1038
1039void TreeCanvas::DrawNodeConnections(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y) {
1040 if (!node->AreChildrenVisible() || node->GetChildCount() == 0)
1041 return;
1042
1043 int total_width = CalculateSubtreeWidth(node);
1044 wxCoord child_y = y + m_scaled.node_height + m_scaled.vertical_spacing;
1045
1046 int64_t parentId = static_cast<int64_t>(node->GetId());
1047 bool parentInExecPath = m_executionPathIds.count(parentId) > 0;
1048 bool parentInSelectedPath = m_pathToSelectedIds.count(node->GetId()) > 0;
1049
1050 dc.SetPen(wxPen(m_config.connection_color, 2));
1051 dc.DrawLine(x, y + m_scaled.node_height, x, child_y - 10);
1052
1053 if (parentInExecPath)
1054 m_highlightedSegments.push_back({x, y + m_scaled.node_height, x, child_y - 10});
1055 if (parentInSelectedPath)
1056 m_selectedPathSegments.push_back({x, y + m_scaled.node_height, x, child_y - 10});
1057
1058 wxCoord child_x = x - total_width / 2;
1059
1060 for (size_t i = 0; i < node->GetChildCount(); ++i) {
1061 EmberCore::ITreeNode *child = node->GetChild(i);
1062 if (child) {
1063 int subtree_width = CalculateSubtreeWidth(child);
1064 wxCoord center_x = child_x + subtree_width / 2;
1065
1066 int64_t childId = static_cast<int64_t>(child->GetId());
1067 bool childInExecPath = m_executionPathIds.count(childId) > 0;
1068 bool childInSelectedPath = m_pathToSelectedIds.count(child->GetId()) > 0;
1069
1070 dc.SetPen(wxPen(m_config.connection_color, 2));
1071 dc.DrawLine(x, child_y - 10, center_x, child_y - 10);
1072 dc.DrawLine(center_x, child_y - 10, center_x, child_y);
1073
1074 if (parentInExecPath && childInExecPath) {
1075 m_highlightedSegments.push_back({x, child_y - 10, center_x, child_y - 10});
1076 m_highlightedSegments.push_back({center_x, child_y - 10, center_x, child_y});
1077 }
1078 if (parentInSelectedPath && childInSelectedPath) {
1079 m_selectedPathSegments.push_back({x, child_y - 10, center_x, child_y - 10});
1080 m_selectedPathSegments.push_back({center_x, child_y - 10, center_x, child_y});
1081 }
1082
1083 child_x += subtree_width;
1084 }
1085 }
1086}
1087
1088void TreeCanvas::DrawCollapseArrow(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y) {
1089 if (node->GetChildCount() == 0)
1090 return;
1091
1092 bool isHovered = (node == m_hoveredArrowNode);
1093 bool isPressed = (node == m_pressedArrowNode);
1094
1095 int circle_radius = 8;
1096 wxCoord arrow_x = x;
1097 wxCoord arrow_y = y + m_scaled.node_height;
1098
1099 wxColour circleFill(70, 70, 70), circlePen(100, 100, 100);
1100 if (isPressed) {
1101 circleFill = wxColour(50, 50, 50);
1102 circlePen = wxColour(80, 80, 80);
1103 } else if (isHovered) {
1104 circleFill = wxColour(90, 90, 90);
1105 circlePen = wxColour(130, 130, 130);
1106 }
1107 dc.SetBrush(wxBrush(circleFill));
1108 dc.SetPen(wxPen(circlePen, 1));
1109 dc.DrawCircle(arrow_x, arrow_y, circle_radius);
1110
1111 wxPoint triangle[3];
1112 wxColour triFill, triPen;
1113
1114 if (node->AreChildrenVisible()) {
1115 int sz = 5;
1116 triangle[0] = {arrow_x - sz, arrow_y - 2};
1117 triangle[1] = {arrow_x + sz, arrow_y - 2};
1118 triangle[2] = {arrow_x, arrow_y + 3};
1119 triFill = isPressed ? wxColour(100, 180, 235) : (isHovered ? wxColour(150, 220, 255) : wxColour(120, 200, 255));
1120 triPen = triFill;
1121 } else {
1122 int sz = 5;
1123 triangle[0] = {arrow_x - 2, arrow_y - sz};
1124 triangle[1] = {arrow_x - 2, arrow_y + sz};
1125 triangle[2] = {arrow_x + 3, arrow_y};
1126 triFill = isPressed ? wxColour(140, 140, 140) : (isHovered ? wxColour(220, 220, 220) : wxColour(180, 180, 180));
1127 triPen = triFill;
1128 }
1129 dc.SetPen(wxPen(triPen, 1));
1130 dc.SetBrush(wxBrush(triFill));
1131 dc.DrawPolygon(3, triangle);
1132}
1133
1135 if (!m_showOverlayInfo)
1136 return;
1137
1138 dc.SetUserScale(1.0, 1.0);
1139 dc.SetDeviceOrigin(0, 0);
1140
1141 wxSize panel_size = GetSize();
1142
1143 int topOffset = m_selectedNode ? 28 : 4;
1144
1145 dc.SetTextForeground(wxColour(150, 150, 150));
1146 dc.SetFont(wxFont(9, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
1147
1148 wxString zoomText =
1149 wxString::Format("Zoom: %.0f%% | Pan: (%d, %d)", m_zoomFactor * 100, m_viewOffset.x, m_viewOffset.y);
1150 if (m_focusRoot) {
1151 zoomText += " | FOCUS MODE";
1152 }
1153 dc.DrawText(zoomText, 10, topOffset);
1154
1155 if (m_selectedNode) {
1156 EmberCore::String selectedText =
1157 "Selected: " + m_selectedNode->GetName() + " [" + m_selectedNode->GetTypeString() + "]";
1158 dc.DrawText(selectedText, 10, topOffset + 18);
1159
1160 if (m_statusProvider) {
1161 int status = m_statusProvider->GetNodeStatus(static_cast<int64_t>(m_selectedNode->GetId()));
1162 wxString statusNames[] = {"Idle", "Running", "Success", "Failure", "Halted"};
1163 wxString statusText = "Status: " + (status >= 0 && status <= 4 ? statusNames[status] : "Unknown");
1164 dc.DrawText(statusText, 10, topOffset + 36);
1165 }
1166 }
1167
1168 if (m_tree && m_tree->HasRootNode()) {
1169 wxString statsText = wxString::Format("Nodes: %lu", static_cast<unsigned long>(m_tree->GetNodeCount()));
1170 wxCoord tw, th;
1171 dc.GetTextExtent(statsText, &tw, &th);
1172 dc.DrawText(statsText, panel_size.x - tw - 10, topOffset);
1173 }
1174
1175 dc.SetFont(wxFont(8, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
1176 dc.SetTextForeground(wxColour(100, 100, 100));
1177 wxString hints = "Click=Select | DblClick=Center | Wheel=Zoom | "
1178 "R=Reset | E=Collapse | F=Focus";
1179 if (m_focusRoot)
1180 hints += " | Esc=Exit Focus";
1181 dc.DrawText(hints, 10, panel_size.y - 20);
1182}
1183
1185 if (!node)
1186 return;
1187 m_focusRoot = node;
1188 m_widthCache.clear();
1189 m_minimapDirty = true;
1190 m_zoomFactor = 0.75f;
1191 CenterOnNode(node);
1192 MarkDirty();
1193}
1194
1196 m_focusRoot = nullptr;
1197 m_widthCache.clear();
1198 m_minimapDirty = true;
1199 if (m_tree && m_tree->HasRootNode()) {
1200 m_zoomFactor = 0.75f;
1201 CenterOnNode(m_tree->GetRootNode());
1202 }
1203 MarkDirty();
1204}
1205
1207 if (m_focusRoot)
1208 return m_focusRoot;
1209 if (m_tree && m_tree->HasRootNode())
1210 return m_tree->GetRootNode();
1211 return nullptr;
1212}
1213
1215 switch (type) {
1217 return m_config.action_type_color;
1219 return m_config.control_type_color;
1221 return m_config.condition_type_color;
1223 return m_config.decorator_type_color;
1225 return m_config.behavior_tree_type_color;
1226 default:
1227 return m_config.border_color;
1228 }
1229}
1230
1231void TreeCanvas::ComputeTreeExtent(EmberCore::ITreeNode *node, wxCoord x, wxCoord y, int &minX, int &maxX, int &maxY) {
1232 if (!node)
1233 return;
1234
1235 int halfW = m_scaled.node_width / 2;
1236 if (x - halfW < minX)
1237 minX = x - halfW;
1238 if (x + halfW > maxX)
1239 maxX = x + halfW;
1240 if (y + m_scaled.node_height > maxY)
1241 maxY = y + m_scaled.node_height;
1242
1243 if (node->AreChildrenVisible() && node->GetChildCount() > 0) {
1244 int totalW = CalculateSubtreeWidth(node);
1245 wxCoord cx = x - totalW / 2;
1246 wxCoord cy = y + m_scaled.node_height + m_scaled.vertical_spacing;
1247 for (size_t i = 0; i < node->GetChildCount(); ++i) {
1248 EmberCore::ITreeNode *child = node->GetChild(i);
1249 if (child) {
1250 int csw = CalculateSubtreeWidth(child);
1251 ComputeTreeExtent(child, cx + csw / 2, cy, minX, maxX, maxY);
1252 cx += csw;
1253 }
1254 }
1255 }
1256}
1257
1259 if (!m_showMinimap || !m_tree || !m_tree->HasRootNode())
1260 return;
1261
1262 dc.SetUserScale(1.0, 1.0);
1263 dc.SetDeviceOrigin(0, 0);
1264
1265 wxSize panelSize = GetSize();
1266 int mmW = 200, mmH = 150;
1267 int mmX = 10;
1268 int mmY = panelSize.y - mmH - 30;
1269
1270 m_minimapRect = wxRect(mmX, mmY, mmW, mmH);
1271
1272 if (m_minimapDirty || !m_minimapCache.IsOk() || m_minimapCache.GetWidth() != mmW ||
1273 m_minimapCache.GetHeight() != mmH) {
1274
1275 m_minimapCache = wxBitmap(mmW, mmH, 32);
1276 wxMemoryDC memDC(m_minimapCache);
1277 memDC.SetBackground(wxBrush(wxColour(30, 30, 30)));
1278 memDC.Clear();
1279
1281 if (root) {
1282 wxPoint rootPos = CalculateRootPosition();
1283 int treeMinX = rootPos.x;
1284 int treeMaxX = rootPos.x;
1285 int treeMaxY = rootPos.y + m_scaled.node_height;
1286 ComputeTreeExtent(root, rootPos.x, rootPos.y, treeMinX, treeMaxX, treeMaxY);
1287
1288 int treeW = std::max(1, treeMaxX - treeMinX);
1289 int treeH = std::max(1, treeMaxY - rootPos.y);
1290
1291 int pad = 10;
1292 float scaleX = static_cast<float>(mmW - 2 * pad) / static_cast<float>(treeW);
1293 float scaleY = static_cast<float>(mmH - 2 * pad) / static_cast<float>(treeH);
1294 float scale = std::min(scaleX, scaleY);
1295
1296 int ofsX = pad + static_cast<int>((mmW - 2 * pad - treeW * scale) / 2.0f);
1297 int ofsY = pad;
1298
1299 DrawMinimapNode(memDC, root, rootPos.x, rootPos.y, scale, scale, ofsX - static_cast<int>(treeMinX * scale),
1300 ofsY - static_cast<int>(rootPos.y * scale), wxRect(0, 0, mmW, mmH));
1301
1302 m_minimapCacheTreeMinX = treeMinX;
1303 m_minimapCacheRootY = rootPos.y;
1304 m_minimapCacheScale = scale;
1305 m_minimapCacheOfsX = ofsX;
1306 m_minimapCacheOfsY = ofsY;
1307 }
1308
1309 memDC.SelectObject(wxNullBitmap);
1310 m_minimapDirty = false;
1311 }
1312
1313 dc.SetPen(wxPen(wxColour(80, 80, 80), 1));
1314 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1315 dc.DrawBitmap(m_minimapCache, mmX, mmY, false);
1316 dc.DrawRectangle(m_minimapRect);
1317
1318 wxRect viewport = GetViewportBounds();
1319 int vx = mmX + m_minimapCacheOfsX +
1320 static_cast<int>((viewport.GetLeft() - m_minimapCacheTreeMinX) * m_minimapCacheScale);
1321 int vy =
1322 mmY + m_minimapCacheOfsY + static_cast<int>((viewport.GetTop() - m_minimapCacheRootY) * m_minimapCacheScale);
1323 int vw = static_cast<int>(viewport.GetWidth() * m_minimapCacheScale);
1324 int vh = static_cast<int>(viewport.GetHeight() * m_minimapCacheScale);
1325
1326 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1327 dc.SetPen(wxPen(wxColour(255, 255, 255, 180), 1));
1328 dc.DrawRectangle(std::max(vx, mmX), std::max(vy, mmY), std::min(vw, mmW), std::min(vh, mmH));
1329}
1330
1331void TreeCanvas::DrawMinimapNode(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y, float scaleX, float scaleY,
1332 int offsetX, int offsetY, const wxRect &minimapArea) {
1333 if (!node)
1334 return;
1335
1336 int sx = offsetX + static_cast<int>(x * scaleX);
1337 int sy = offsetY + static_cast<int>(y * scaleY);
1338 int sw = std::max(2, static_cast<int>(m_scaled.node_width * scaleX));
1339 int sh = std::max(2, static_cast<int>(m_scaled.node_height * scaleY));
1340
1341 wxColour fill = GetNodeTypeColor(node->GetType());
1342 if (m_statusProvider) {
1343 int status = m_statusProvider->GetNodeStatus(static_cast<int64_t>(node->GetId()));
1344 if (status != 0) {
1345 fill = StatusColors::GetFillColor(status);
1346 }
1347 }
1348
1349 dc.SetBrush(wxBrush(fill));
1350 dc.SetPen(*wxTRANSPARENT_PEN);
1351 dc.DrawRectangle(sx - sw / 2, sy, sw, sh);
1352
1353 if (node->AreChildrenVisible() && node->GetChildCount() > 0) {
1354 int totalW = CalculateSubtreeWidth(node);
1355 wxCoord cx = x - totalW / 2;
1356 wxCoord cy = y + m_scaled.node_height + m_scaled.vertical_spacing;
1357 for (size_t i = 0; i < node->GetChildCount(); ++i) {
1358 EmberCore::ITreeNode *child = node->GetChild(i);
1359 if (child) {
1360 int csw = CalculateSubtreeWidth(child);
1361 DrawMinimapNode(dc, child, cx + csw / 2, cy, scaleX, scaleY, offsetX, offsetY, minimapArea);
1362 cx += csw;
1363 }
1364 }
1365 }
1366}
1367
1369 if (!m_selectedNode)
1370 return;
1371
1372 dc.SetUserScale(1.0, 1.0);
1373 dc.SetDeviceOrigin(0, 0);
1374
1375 wxSize panelSize = GetSize();
1376 int barH = 24;
1377
1378 dc.SetBrush(wxBrush(wxColour(35, 35, 35, 220)));
1379 dc.SetPen(*wxTRANSPARENT_PEN);
1380 dc.DrawRectangle(0, 0, panelSize.x, barH);
1381
1382 std::vector<EmberCore::ITreeNode *> ancestors;
1384 while (curr) {
1385 ancestors.push_back(curr);
1386 curr = curr->GetParent();
1387 }
1388 std::reverse(ancestors.begin(), ancestors.end());
1389
1390 if (m_focusRoot) {
1391 bool foundFocus = false;
1392 std::vector<EmberCore::ITreeNode *> trimmed;
1393 for (auto *a : ancestors) {
1394 if (a == m_focusRoot)
1395 foundFocus = true;
1396 if (foundFocus)
1397 trimmed.push_back(a);
1398 }
1399 if (!trimmed.empty())
1400 ancestors = trimmed;
1401 }
1402
1403 m_breadcrumbHitTargets.clear();
1404
1405 wxFont bcFont(9, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
1406 dc.SetFont(bcFont);
1407
1408 int xPos = 10;
1409 int yCenter = barH / 2;
1410
1411 if (m_focusRoot) {
1412 wxString label = "Full Tree";
1413 wxCoord tw, th;
1414 dc.GetTextExtent(label, &tw, &th);
1415
1416 dc.SetTextForeground(wxColour(100, 180, 255));
1417 dc.DrawText(label, xPos, yCenter - th / 2);
1418 m_breadcrumbHitTargets.push_back({wxRect(xPos, 0, tw, barH), nullptr});
1419 xPos += tw;
1420
1421 dc.SetTextForeground(wxColour(100, 100, 100));
1422 dc.DrawText(" > ", xPos, yCenter - th / 2);
1423 xPos += 20;
1424 }
1425
1426 for (size_t i = 0; i < ancestors.size(); ++i) {
1427 EmberCore::ITreeNode *node = ancestors[i];
1428 bool isLast = (i == ancestors.size() - 1);
1429
1430 wxString name = node->GetName();
1431 if (name.length() > 20)
1432 name = name.Left(17) + "...";
1433
1434 wxCoord tw, th;
1435 dc.GetTextExtent(name, &tw, &th);
1436
1437 if (xPos + tw > panelSize.x - 20) {
1438 dc.SetTextForeground(wxColour(100, 100, 100));
1439 dc.DrawText("...", xPos, yCenter - th / 2);
1440 break;
1441 }
1442
1443 dc.SetTextForeground(isLast ? wxColour(255, 255, 255) : wxColour(150, 200, 255));
1444 dc.DrawText(name, xPos, yCenter - th / 2);
1445 m_breadcrumbHitTargets.push_back({wxRect(xPos, 0, tw, barH), node});
1446 xPos += tw;
1447
1448 if (!isLast) {
1449 dc.SetTextForeground(wxColour(100, 100, 100));
1450 dc.DrawText(" > ", xPos, yCenter - th / 2);
1451 xPos += 20;
1452 }
1453 }
1454}
1455
1456// --- Utility Methods ---
1457
1459 if (!node)
1460 return 0;
1461
1462 auto it = m_widthCache.find(node);
1463 if (it != m_widthCache.end()) {
1464 return it->second;
1465 }
1466
1467 int width;
1468 if (!node->AreChildrenVisible() || node->GetChildCount() == 0) {
1469 width = m_scaled.node_width + m_scaled.horizontal_spacing;
1470 } else {
1471 int total_width = 0;
1472 for (size_t i = 0; i < node->GetChildCount(); ++i) {
1473 EmberCore::ITreeNode *child = node->GetChild(i);
1474 if (child) {
1475 total_width += CalculateSubtreeWidth(child);
1476 }
1477 }
1478 width = std::max(total_width, m_scaled.node_width + m_scaled.horizontal_spacing);
1479 }
1480
1481 m_widthCache[node] = width;
1482 return width;
1483}
1484
1485wxPoint TreeCanvas::CalculateRootPosition() { return {0, 50}; }
1486
1487EmberCore::ITreeNode *TreeCanvas::FindNodeAtPosition(EmberCore::ITreeNode *node, wxPoint node_pos, wxPoint target_pos) {
1488 if (!node)
1489 return nullptr;
1490
1491 wxRect node_rect(node_pos.x - m_scaled.node_width / 2, node_pos.y, m_scaled.node_width, m_scaled.node_height);
1492
1493 if (node_rect.Contains(target_pos)) {
1494 return node;
1495 }
1496
1497 if (node->AreChildrenVisible() && node->GetChildCount() > 0) {
1498 int total_width = CalculateSubtreeWidth(node);
1499 wxCoord child_x = node_pos.x - total_width / 2;
1500 wxCoord child_y = node_pos.y + m_scaled.node_height + m_scaled.vertical_spacing;
1501
1502 for (size_t i = 0; i < node->GetChildCount(); ++i) {
1503 EmberCore::ITreeNode *child = node->GetChild(i);
1504 if (child) {
1505 int subtree_width = CalculateSubtreeWidth(child);
1506 wxCoord center_x = child_x + subtree_width / 2;
1507
1508 EmberCore::ITreeNode *found = FindNodeAtPosition(child, {center_x, child_y}, target_pos);
1509 if (found) {
1510 return found;
1511 }
1512
1513 child_x += subtree_width;
1514 }
1515 }
1516 }
1517
1518 return nullptr;
1519}
1520
1522 const int hit_radius = 10;
1523 for (auto it = m_collapseArrows.rbegin(); it != m_collapseArrows.rend(); ++it) {
1524 wxCoord arrow_x = it->x;
1525 wxCoord arrow_y = it->y + m_scaled.node_height;
1526 int dx = world_pos.x - arrow_x;
1527 int dy = world_pos.y - arrow_y;
1528 if (dx * dx + dy * dy <= hit_radius * hit_radius)
1529 return it->node;
1530 }
1531 return nullptr;
1532}
1533
1536 if (!target_node || !root)
1537 return {-1, -1};
1538
1539 std::function<wxPoint(EmberCore::ITreeNode *, wxPoint, int)> findPosition =
1540 [&](EmberCore::ITreeNode *node, wxPoint node_pos, int level) -> wxPoint {
1541 if (!node)
1542 return {-1, -1};
1543
1544 if (node == target_node || (node->GetId() == target_node->GetId() && node->GetName() == target_node->GetName()))
1545 return node_pos;
1546
1547 if (node->AreChildrenVisible() && node->GetChildCount() > 0) {
1548 int total_width = CalculateSubtreeWidth(node);
1549 wxCoord child_x = node_pos.x - total_width / 2;
1550 wxCoord child_y = node_pos.y + m_scaled.node_height + m_scaled.vertical_spacing;
1551
1552 for (size_t i = 0; i < node->GetChildCount(); ++i) {
1553 EmberCore::ITreeNode *child = node->GetChild(i);
1554 if (child) {
1555 int subtree_width = CalculateSubtreeWidth(child);
1556 wxCoord center_x = child_x + subtree_width / 2;
1557
1558 wxPoint found = findPosition(child, {center_x, child_y}, level + 1);
1559 if (found.x != -1 && found.y != -1) {
1560 return found;
1561 }
1562
1563 child_x += subtree_width;
1564 }
1565 }
1566 }
1567
1568 return {-1, -1};
1569 };
1570
1571 wxPoint root_pos = CalculateRootPosition();
1572 return findPosition(root, root_pos, 0);
1573}
1574
1575wxPoint TreeCanvas::ScreenToWorld(const wxPoint &screen_pos) const {
1576 return {static_cast<int>((screen_pos.x - m_viewOffset.x) / m_zoomFactor),
1577 static_cast<int>((screen_pos.y - m_viewOffset.y) / m_zoomFactor)};
1578}
1579
1580wxPoint TreeCanvas::WorldToScreen(const wxPoint &world_pos) const {
1581 return {static_cast<int>(world_pos.x * m_zoomFactor + m_viewOffset.x),
1582 static_cast<int>(world_pos.y * m_zoomFactor + m_viewOffset.y)};
1583}
1584
1586 wxSize panel_size = GetSize();
1587 wxPoint topLeft = ScreenToWorld({0, 0});
1588 wxPoint bottomRight = ScreenToWorld({panel_size.x, panel_size.y});
1589 return wxRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
1590}
1591
1592} // namespace EmberUI
BehaviorTreeProjectDialog::OnProjectNameChanged BehaviorTreeProjectDialog::OnRemoveFiles wxEND_EVENT_TABLE() BehaviorTreeProjectDialog
Abstract interface for tree nodes that can be visualized.
Definition ITreeNode.h:31
virtual String GetTypeString() const
Definition ITreeNode.h:115
virtual size_t GetId() const =0
virtual size_t GetChildCount() const =0
virtual NodeType GetType() const =0
virtual ITreeNode * GetParent() const =0
virtual bool AreChildrenVisible() const =0
virtual void SetChildrenVisible(bool visible)=0
virtual const String & GetName() const =0
virtual ITreeNode * GetChild(size_t index) const =0
NodeType
Node types for visualization categorization.
Definition ITreeNode.h:36
Shared tree rendering canvas usable by both EmberForge and EmberMonitor.
Definition TreeCanvas.h:76
void CenterOnNode(EmberCore::ITreeNode *node)
Centers the view on the given node.
void DrawOverlayInfo(wxDC &dc)
void OnMouseMotion(wxMouseEvent &event)
void DrawMinimapNode(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y, float scaleX, float scaleY, int offsetX, int offsetY, const wxRect &minimapArea)
std::set< int64_t > m_executionPathIds
Definition TreeCanvas.h:260
virtual ~TreeCanvas()
Destructor.
void OnMouseWheel(wxMouseEvent &event)
wxPoint FindNodeWorldPosition(EmberCore::ITreeNode *target_node)
void EnterFocusMode(EmberCore::ITreeNode *node)
Enters focus mode, showing only the subtree rooted at the given node.
std::unordered_map< EmberCore::ITreeNode *, int > m_widthCache
Definition TreeCanvas.h:289
std::vector< CollapseArrowInfo > m_collapseArrows
Definition TreeCanvas.h:273
EmberCore::ITreeNode * FindNodeAtPosition(EmberCore::ITreeNode *node, wxPoint node_pos, wxPoint target_pos)
std::shared_ptr< EmberCore::ITreeStructure > m_tree
Definition TreeCanvas.h:230
void OnSize(wxSizeEvent &event)
void DrawTypeHeader(wxDC &dc, EmberCore::ITreeNode *node, const wxRect &nodeRect)
EmberCore::ITreeNode * m_hoveredArrowNode
Definition TreeCanvas.h:235
virtual void OnNodeSelected(EmberCore::ITreeNode *node)
Called when a node is selected; override for app-specific behavior.
wxColour GetNodeTypeColor(EmberCore::ITreeNode::NodeType type) const
void OnMouseLeftDClick(wxMouseEvent &event)
void OnMouseRightUp(wxMouseEvent &event)
TreeCanvasConfig m_scaled
Definition TreeCanvas.h:229
std::map< EmberCore::ITreeNode::NodeType, wxBitmap > m_typeIcons
Definition TreeCanvas.h:291
void DrawNodeText(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y)
wxBitmap m_minimapCache
Definition TreeCanvas.h:279
void DrawGrid(wxDC &dc, const wxRect &viewport)
void DrawNodeBox(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y)
virtual void OnKeyUp(wxKeyEvent &event)
Key up handler; override for custom key handling.
void DrawNodeConnections(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y)
std::vector< std::pair< wxRect, EmberCore::ITreeNode * > > m_breadcrumbHitTargets
Definition TreeCanvas.h:287
wxPoint WorldToScreen(const wxPoint &world_pos) const
void ExpandAllChildren(EmberCore::ITreeNode *node)
void SetTree(std::shared_ptr< EmberCore::ITreeStructure > tree)
Sets the tree structure to display.
virtual wxColour GetNodeFillColor(EmberCore::ITreeNode *node, bool selected, bool hovered)
Returns the fill color for a node; override for custom coloring.
void OnMouseMiddleUp(wxMouseEvent &event)
void DrawBreadcrumb(wxDC &dc)
void AutoCollapseTree(EmberCore::ITreeNode *node, int depth)
void ResetView()
Resets zoom and pan to default values.
virtual void OnKeyDown(wxKeyEvent &event)
Key down handler; override for custom key handling.
EmberCore::ITreeNode * m_hoveredNode
Definition TreeCanvas.h:234
NodeSelectionCallback m_selectionCallback
Definition TreeCanvas.h:275
void MarkDirty()
Marks the canvas for repaint.
Definition TreeCanvas.h:123
std::vector< LineSegment > m_selectedPathSegments
Definition TreeCanvas.h:267
void DrawNode(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y, int level, const wxRect &viewport)
EmberCore::ITreeNode * m_selectedNode
Definition TreeCanvas.h:233
void LoadTypeIcons()
Loads type icons from the configured icon directory.
VisibilityChangeCallback m_visibilityChangeCallback
Definition TreeCanvas.h:276
void ExpandToDepth(EmberCore::ITreeNode *node, int relativeDepth)
void SetZoom(float zoom)
Sets the zoom factor.
TreeCanvasConfig m_config
Definition TreeCanvas.h:228
void OnMouseLeftUp(wxMouseEvent &event)
virtual wxColour GetNodeBorderColor(EmberCore::ITreeNode *node, bool selected, bool hovered)
Returns the border color for a node; override for custom coloring.
void CollapseAllChildren(EmberCore::ITreeNode *node)
void ComputeTreeExtent(EmberCore::ITreeNode *node, wxCoord x, wxCoord y, int &minX, int &maxX, int &maxY)
IStatusProvider * m_statusProvider
Definition TreeCanvas.h:231
EmberCore::ITreeNode * GetEffectiveRoot() const
std::set< int > m_pathToSelectedIds
Definition TreeCanvas.h:261
void FitTreeInView()
Adjusts view to fit the entire tree.
void DrawMinimap(wxDC &dc)
wxPoint CalculateRootPosition()
EmberCore::ITreeNode * m_focusRoot
Definition TreeCanvas.h:237
wxPoint ScreenToWorld(const wxPoint &screen_pos) const
void OnPaint(wxPaintEvent &event)
void ExitFocusMode()
Exits focus mode.
int CalculateSubtreeWidth(EmberCore::ITreeNode *node)
void OnMouseMiddleDown(wxMouseEvent &event)
void OnMouseLeftDown(wxMouseEvent &event)
virtual void OnBeforePaintOverlays(wxDC &dc)
Called before painting overlays; override to draw custom overlays.
wxTimer * m_refreshTimer
Definition TreeCanvas.h:293
EmberCore::ITreeNode * m_pressedArrowNode
Definition TreeCanvas.h:236
wxRect GetViewportBounds() const
void SetSelectedNode(EmberCore::ITreeNode *node)
Sets the selected node.
virtual wxColour GetNodeTextColor(EmberCore::ITreeNode *node, bool selected, bool hovered)
Returns the text color for a node; override for custom coloring.
std::vector< LineSegment > m_highlightedSegments
Definition TreeCanvas.h:266
void DrawCollapseArrow(wxDC &dc, EmberCore::ITreeNode *node, wxCoord x, wxCoord y)
EmberCore::ITreeNode * FindArrowAtPosition(const wxPoint &world_pos) const
std::string String
Framework-agnostic string type.
Definition String.h:14
int Scale(wxWindow *win, int px)
Scales a pixel value from logical to physical units for the given window.
Definition DPI.h:11
Definition Panel.h:8
wxBEGIN_EVENT_TABLE(Panel, wxPanel) EVT_SIZE(Panel
Definition Panel.cpp:8
static wxColour GetExecutionPathColor()
Color for nodes in the execution path.
static wxColour GetBorderColor(int status)
Returns border color for the given status code.
static wxColour GetTextColor(int status)
Returns text color for the given status code.
static wxColour GetFillColor(int status)
Returns fill color for the given status code.