Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
PrefabCanvas.cpp
Go to the documentation of this file.
1#include "PrefabCanvas.h"
3#include "../../Source/third_party/imgui/imgui.h"
4#include "../../system/system_utils.h"
5#include "../Utilities/CanvasGridRenderer.h"
6#include "../Utilities/ICanvasEditor.h"
7#include <cmath>
8#include <vector>
9#include <tuple>
10
11namespace Olympe
12{
13 PrefabCanvas::PrefabCanvas() : m_canvasEditor(nullptr), m_isPanning(false), m_gridSpacing(50.0f), m_showGrid(true), m_showDebugInfo(false), m_snapToGrid(true)
14 { m_renderer = std::make_unique<ComponentNodeRenderer>(); }
15
17
19
21
23
25 {
26 if (!m_document || !m_renderer) { return; }
27 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.2f, 0.2f, 0.2f, 1.0f));
28 ImGui::BeginChild("##PrefabCanvas", ImVec2(-1.0f, -1.0f), true);
29
30 // Handle ImGui input events
31 ImVec2 canvasPos = ImGui::GetCursorScreenPos();
32 m_canvasScreenPos = canvasPos; // Store for use in drag-drop context
33 ImVec2 canvasSize = ImGui::GetContentRegionAvail();
34 ImVec2 canvasEnd(canvasPos.x + canvasSize.x, canvasPos.y + canvasSize.y);
35
36 // CRITICAL: Update canvasEditor with current frame's position and size for minimap
38 {
40 m_canvasEditor->SetCanvasSize(canvasSize);
41 }
42
43 ImVec2 mousePos = ImGui::GetMousePos();
44 bool isMouseInCanvas = (mousePos.x >= canvasPos.x && mousePos.x <= canvasEnd.x &&
45 mousePos.y >= canvasPos.y && mousePos.y <= canvasEnd.y &&
46 ImGui::IsWindowHovered());
47
48 // Poll and process ImGui input
50 {
51 // Mouse movement
53
54 // Mouse button events
55 if (ImGui::IsMouseClicked(0)) { OnMouseDown(0, mousePos.x, mousePos.y); }
56 if (ImGui::IsMouseReleased(0)) { OnMouseUp(0, mousePos.x, mousePos.y); }
57 if (ImGui::IsMouseClicked(2)) { OnMouseDown(2, mousePos.x, mousePos.y); }
58 if (ImGui::IsMouseReleased(2)) { OnMouseUp(2, mousePos.x, mousePos.y); }
59
60 // Mouse scroll
61 if (ImGui::GetIO().MouseWheel != 0.0f) { OnMouseScroll(ImGui::GetIO().MouseWheel); }
62 }
63
64 // Keyboard events
65 ImGuiIO& io = ImGui::GetIO();
66 if (ImGui::IsKeyPressed(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) { m_ctrlPressed = true; OnKeyDown(17); }
67 else { m_ctrlPressed = false; }
68
69 if (ImGui::IsKeyPressed(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_LeftShift)) { m_shiftPressed = true; OnKeyDown(16); }
70 else { m_shiftPressed = false; }
71
72 if (ImGui::IsKeyPressed(ImGuiKey_A) && m_ctrlPressed) { OnKeyDown(65); }
73 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) { OnKeyDown(46); }
74
75 // Render canvas content
76 if (m_showGrid) { RenderGrid(); }
82
83 // Context menu handling (right-click)
86
87 // Phase 37 — Update minimap data before rendering
88 // CRITICAL: CustomCanvasEditor minimap requires data updates before RenderMinimap()
90 {
91 const auto& allNodes = m_document->GetAllNodes();
92
93 // Build node data for minimap: tuple<nodeId, posX, posY, width, height>
94 std::vector<std::tuple<int, float, float, float, float>> nodeData;
95 nodeData.reserve(allNodes.size());
96
97 // Calculate graph bounds (min/max X/Y)
98 float graphMinX = 0.0f, graphMaxX = 0.0f;
99 float graphMinY = 0.0f, graphMaxY = 0.0f;
100 bool hasNodes = false;
101
102 for (const auto& node : allNodes)
103 {
104 float nodeMinX = node.position.x;
105 float nodeMaxX = node.position.x + node.size.x;
106 float nodeMinY = node.position.y;
107 float nodeMaxY = node.position.y + node.size.y;
108
109 if (!hasNodes)
110 {
115 hasNodes = true;
116 }
117 else
118 {
123 }
124
125 // Add to node data
126 nodeData.emplace_back(
127 static_cast<int>(node.nodeId),
128 node.position.x,
129 node.position.y,
130 node.size.x,
131 node.size.y
132 );
133 }
134
135 // If no nodes, set default bounds
136 if (!hasNodes)
137 {
138 graphMinX = -500.0f;
139 graphMaxX = 500.0f;
140 graphMinY = -500.0f;
141 graphMaxY = 500.0f;
142 }
143 else
144 {
145 // Add padding to bounds for better minimap appearance
146 float padX = (graphMaxX - graphMinX) * 0.1f;
147 float padY = (graphMaxY - graphMinY) * 0.1f;
148 graphMinX -= padX;
149 graphMaxX += padX;
150 graphMinY -= padY;
151 graphMaxY += padY;
152 }
153
154 // Calculate viewport bounds (current visible area with zoom/pan)
157
158 float viewMinX = canvasMinVis.x;
159 float viewMaxX = canvasMaxVis.x;
160 float viewMinY = canvasMinVis.y;
161 float viewMaxY = canvasMaxVis.y;
162
163 // Update minimap with node and viewport data
167 }/**/
168 // Phase 37 — Render minimap overlay on canvas
169 if (m_canvasEditor) // useless already checked above, but keeping for clarity
171
172
173
174 ImGui::EndChild();
175 ImGui::PopStyleColor();
176 }
177
178 void PrefabCanvas::Update(float deltaTime) { (void)deltaTime; }
179
180 void PrefabCanvas::OnMouseMove(float x, float y)
181 {
182 m_currentMousePos = Vector(x, y, 0.0f);
183
185 {
186 HandleNodeDrag(x, y);
187 }
189 {
190 HandlePan(x, y);
191 }
193 {
195 }
196
197 // Update rectangle selection during drag
198 // FIX #2: Don't update rectangle selection if we're creating a connection
200 {
202 }
203
204 // Update hovered connection for visual feedback
206 {
208
209 const std::vector<std::pair<NodeId, NodeId>>& connections = m_document->GetConnections();
210 const float hoverTolerance = 10.0f; // Tolerance for hover detection (in screen pixels)
211
212 for (size_t connIdx = 0; connIdx < connections.size(); ++connIdx)
213 {
216
217 if (sourceNode != nullptr && targetNode != nullptr)
218 {
219 // Get connection endpoints in canvas space
220 Vector from = sourceNode->position;
221 from.x += sourceNode->size.x * 0.5f;
222 Vector to = targetNode->position;
223 to.x -= targetNode->size.x * 0.5f;
224
225 // Get distance from mouse position to connection curve
226 float distance = m_renderer->GetDistanceToConnection(
227 Vector(x, y, 0.0f), // Screen space position
228 from, to
229 );
230
232 {
233 m_hoveredConnectionIndex = static_cast<int>(connIdx);
234 break; // Only highlight one connection
235 }
236 }
237 }
238 }
239
241 }
242
243 void PrefabCanvas::OnMouseDown(int button, float x, float y)
244 {
245 if (!m_document) { return; }
246
247 m_currentMousePos = Vector(x, y, 0.0f);
248 Vector canvasPos = ScreenToCanvas(x, y);
249
250 if (button == 0) // Left mouse button
251 {
252 // First check if clicking on a port
253 const std::vector<ComponentNode>& nodes = m_document->GetAllNodes();
254 for (size_t i = 0; i < nodes.size(); ++i)
255 {
256 PortId portId = InvalidPortId;
257 if (m_renderer->IsPointInPort(canvasPos, nodes[i], portId))
258 {
259 // Starting connection from this port
260 StartConnectionCreation(nodes[i].nodeId);
261 return;
262 }
263 }
264
265 // If not on a port, check if on a node
268 {
269 // FIX #1: Preserve multi-selection when dragging already-selected node
270 // Only deselect all if the clicked node is NOT already selected
271 const std::vector<NodeId>& selectedNodes = m_document->GetSelectedNodes();
272 bool isNodeAlreadySelected = false;
273 for (size_t i = 0; i < selectedNodes.size(); ++i)
274 {
275 if (selectedNodes[i] == nodeAtPos)
276 {
278 break;
279 }
280 }
281
283 {
284 // Node not in selection: deselect all and select only this one
286 }
288 {
289 // Ctrl+click on already selected: deselect it
291 return; // Don't start drag if deselecting
292 }
294 {
295 // Ctrl+click on unselected: add to selection (don't call DeselectAll)
296 }
297
299 }
300 else if (!m_ctrlPressed)
301 {
302 // Empty space clicked - start rectangle selection
305 m_selectionRectStart = canvasPos;
306 m_selectionRectEnd = canvasPos;
307 }
308 else
309 {
310 // Ctrl+click on empty space - start rectangle selection with additive mode
312 m_selectionRectStart = canvasPos;
313 m_selectionRectEnd = canvasPos;
314 }
315 }
316 else if (button == 2) // Middle mouse button
317 {
318 HandlePanStart(x, y);
319 }
320 // Right mouse button (button==1) is handled by RenderContextMenu() for priority
321
323 }
324
325 void PrefabCanvas::OnMouseUp(int button, float x, float y)
326 {
327 m_currentMousePos = Vector(x, y, 0.0f);
328
329 if (button == 0)
330 {
331 // FIX #2: Check for connection completion FIRST (highest priority)
332 // before checking rectangle selection
334 {
335 // Check if releasing on a port
336 Vector canvasPos = ScreenToCanvas(x, y);
337 const std::vector<ComponentNode>& nodes = m_document->GetAllNodes();
338 bool connectionCompleted = false;
339
340 for (size_t i = 0; i < nodes.size(); ++i)
341 {
342 PortId portId = InvalidPortId;
343 if (m_renderer->IsPointInPort(canvasPos, nodes[i], portId))
344 {
345 // Check if different node
346 if (nodes[i].nodeId != m_connectionSourceNodeId)
347 {
348 CompleteConnection(nodes[i].nodeId);
349 connectionCompleted = true;
350 }
351 break;
352 }
353 }
354
356 {
358 }
359
360 // Clear rectangle selection flag if it was set during connection creation
362 }
363 else if (m_isSelectingRectangle)
364 {
365 // Complete rectangle selection
366 Vector canvasPos = ScreenToCanvas(x, y);
367 m_selectionRectEnd = canvasPos;
370 }
372 {
374 }
375 }
377 {
379 m_isPanning = false;
380 }
381 }
382
387
389 {
390 if (keyCode == 17) { m_ctrlPressed = true; } // Ctrl key
391 if (keyCode == 16) { m_shiftPressed = true; } // Shift key
392
393 if (keyCode == 65 && m_ctrlPressed) { SelectAll(); } // Ctrl+A
394 if (keyCode == 46) { DeleteSelectedNodes(); } // Delete key
395 }
396
398 {
399 if (keyCode == 17) { m_ctrlPressed = false; } // Ctrl key
400 if (keyCode == 16) { m_shiftPressed = false; } // Shift key
401 }
402
404 {
405 if (m_canvasEditor)
406 {
408 }
409 }
410
412 {
413 if (!m_canvasEditor) return;
414
415 float oldZoom = m_canvasEditor->GetZoom();
416 float newZoom = oldZoom + zoomDelta;
417
420 }
421
423 {
424 if (m_canvasEditor)
425 {
427 }
428 }
429
431
433 {
434 if (!m_document || !m_renderer) { return InvalidNodeId; }
435 Vector pos = ScreenToCanvas(x, y);
436 const std::vector<ComponentNode>& nodes = m_document->GetAllNodes();
437 for (size_t i = 0; i < nodes.size(); ++i)
438 {
439 if (m_renderer->IsPointInNode(pos, nodes[i]))
440 {
441 return nodes[i].nodeId;
442 }
443 }
444 return InvalidNodeId;
445 }
446
447 void PrefabCanvas::SelectNodeAt(float x, float y, bool addToSelection)
448 {
449 if (!m_document) { return; }
450 NodeId nodeId = GetNodeAtPosition(x, y);
451 if (nodeId != InvalidNodeId)
452 {
453 if (!addToSelection)
454 {
456 }
457 m_document->SelectNode(nodeId);
458 }
459 }
460
462 {
463 if (!m_document) { return; }
464
465 // Normalize rectangle (handle both drag directions)
466 float minX = (rectStart.x < rectEnd.x) ? rectStart.x : rectEnd.x;
467 float maxX = (rectStart.x > rectEnd.x) ? rectStart.x : rectEnd.x;
468 float minY = (rectStart.y < rectEnd.y) ? rectStart.y : rectEnd.y;
469 float maxY = (rectStart.y > rectEnd.y) ? rectStart.y : rectEnd.y;
470
471 if (!addToSelection)
472 {
474 }
475
476 // Check all nodes for intersection with rectangle
477 const std::vector<ComponentNode>& nodes = m_document->GetAllNodes();
478 for (size_t i = 0; i < nodes.size(); ++i)
479 {
480 const ComponentNode& node = nodes[i];
481 // Check if node bounding box intersects with selection rectangle
482 float nodeMinX = node.position.x;
483 float nodeMaxX = node.position.x + node.size.x;
484 float nodeMinY = node.position.y;
485 float nodeMaxY = node.position.y + node.size.y;
486
488 {
489 // Rectangle intersects node bounds
490 m_document->SelectNode(node.nodeId);
491 }
492 }
493 }
494
496
498 {
499 if (!m_document) { return; }
501 const std::vector<ComponentNode>& nodes = m_document->GetAllNodes();
502 for (size_t i = 0; i < nodes.size(); ++i)
503 {
504 m_document->SelectNode(nodes[i].nodeId);
505 }
506 }
507
509 {
510 if (!m_document) { return; }
511 const std::vector<NodeId>& selectedNodes = m_document->GetSelectedNodes();
512 std::vector<NodeId> nodesToDelete(selectedNodes.begin(), selectedNodes.end());
513 for (size_t i = 0; i < nodesToDelete.size(); ++i)
514 {
516 }
518 }
519
520 void PrefabCanvas::AddComponentNode(const std::string& componentType, const std::string& componentName, float screenX, float screenY)
521 {
522 if (!m_document) { return; }
523
524 // Convert screen position to canvas position
525 Vector canvasPos = ScreenToCanvas(screenX, screenY);
526
527 // Create new node
528 NodeId newNodeId = m_document->CreateComponentNode(componentType, componentName);
530 if (newNode)
531 {
532 newNode->position = canvasPos;
533 newNode->size = Vector(150.0f, 80.0f, 0.0f);
534 newNode->enabled = true;
535
536 SYSTEM_LOG << "[PrefabCanvas::AddComponentNode] Created node: " << componentName
537 << " at canvas pos (" << canvasPos.x << ", " << canvasPos.y << ")\n";
538 }
539 }
540
541 void PrefabCanvas::AcceptComponentDropAtScreenPos(const std::string& componentType, const std::string& componentName, float screenX, float screenY)
542 {
543 if (!m_document) { return; }
544
545 // Transform from absolute screen position to logical canvas position
546 // Using stored canvas screen position for correct context
547 // Matches formula: screen = canvas * zoom + offset + screenBase
548 // Therefore: canvas = (screen - screenBase - offset) / zoom
550
551 float zoom = m_canvasEditor ? m_canvasEditor->GetZoom() : 1.0f;
553
555 canvas.x = (screenX - storedCanvasPos.x - pan.x) / zoom;
556 canvas.y = (screenY - storedCanvasPos.y - pan.y) / zoom;
557 canvas.z = 0.0f;
558
559 // Create new node at the computed canvas position
560 NodeId newNodeId = m_document->CreateComponentNode(componentType, componentName);
562 if (newNode)
563 {
565 newNode->size = Vector(150.0f, 80.0f, 0.0f);
566 newNode->enabled = true;
567 }
568 }
569
579
588
595
598
599 void PrefabCanvas::SetGridEnabled(bool enabled) { m_showGrid = enabled; }
601 void PrefabCanvas::SetGridSpacing(float spacing) { m_gridSpacing = (spacing > 5.0f) ? spacing : 5.0f; }
605
608
610 {
611 // DEFENSIVE: Ensure m_canvasEditor is valid before dereferencing
612 // This protects against use-after-free if adapter is recreated in EntityPrefabRenderer
613 if (m_canvasEditor)
614 {
616 return Vector(pan.x, pan.y, 0.0f);
617 }
618 return Vector(0.0f, 0.0f, 0.0f);
619 }
620
622 {
623 if (m_canvasEditor)
624 {
626 }
627 }
628
630 {
631 if (m_canvasEditor)
632 {
633 return m_canvasEditor->GetZoom();
634 }
635 return 1.0f;
636 }
637
639 {
640 if (m_canvasEditor)
641 {
643 float clamped = (zoom > limits.x && zoom < limits.y) ? zoom : m_canvasEditor->GetZoom();
645 }
646 }
647
649 {
650 // NEW: Use ICanvasEditor for coordinate transformation
651 // This now handles zoom/pan through the standardized interface
652 if (m_canvasEditor)
653 {
655 return Vector(result.x, result.y, 0.0f);
656 }
657
658 // Fallback if editor not yet initialized (should not happen in normal flow)
660 ImVec2 canvasPos = ImGui::GetCursorScreenPos();
661 screen.x = (screen.x - canvasPos.x) / 1.0f;
662 screen.y = (screen.y - canvasPos.y) / 1.0f;
663 return screen;
664 }
665
667 {
668 // NEW: Use ICanvasEditor for coordinate transformation
669 if (m_canvasEditor)
670 {
672 return Vector(result.x, result.y, 0.0f);
673 }
674
675 // Fallback if editor not yet initialized
677 ImVec2 canvasPos = ImGui::GetCursorScreenPos();
678 canvas.x = canvas.x * 1.0f + canvasPos.x;
679 canvas.y = canvas.y * 1.0f + canvasPos.y;
680 return canvas;
681 }
682
687
691
693 {
694 ImVec2 canvasPos = ImGui::GetCursorScreenPos();
695 ImVec2 canvasSize = ImGui::GetContentRegionAvail();
696
697 // Get VisualScript-style grid configuration (matching imnodes professional appearance)
700 );
701
702 // Apply canvas transformation (via adapter)
703 gridConfig.canvasPos = canvasPos;
704 gridConfig.canvasSize = canvasSize;
705
706 if (m_canvasEditor)
707 {
710 gridConfig.offsetX = pan.x;
711 gridConfig.offsetY = pan.y;
712 }
713 else
714 {
715 gridConfig.zoom = 1.0f;
716 gridConfig.offsetX = 0.0f;
717 gridConfig.offsetY = 0.0f;
718 }
719
720 // Render the grid using shared utility with VisualScript styling
722 }
723
725 {
726 if (!m_document || !m_renderer) { return; }
727
728 // Pass canvas transformation context to renderer (via adapter)
729 if (m_canvasEditor)
730 {
732 m_renderer->SetCanvasTransform(Vector(pan.x, pan.y, 0.0f), m_canvasEditor->GetZoom());
733 }
734 else
735 {
736 m_renderer->SetCanvasTransform(Vector(0.0f, 0.0f, 0.0f), 1.0f);
737 }
738 m_renderer->SetCanvasScreenPos(ImGui::GetCursorScreenPos());
739
740 ImGui::PushClipRect(
741 ImGui::GetCursorScreenPos(),
742 ImVec2(ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x,
743 ImGui::GetCursorScreenPos().y + ImGui::GetContentRegionAvail().y),
744 true
745 );
746
747 m_renderer->RenderNodes(m_document);
748
749 ImGui::PopClipRect();
750 }
751
753 {
754 if (!m_document || !m_renderer) { return; }
755
756 // Pass canvas transformation context to renderer (via adapter)
757 if (m_canvasEditor)
758 {
760 m_renderer->SetCanvasTransform(Vector(pan.x, pan.y, 0.0f), m_canvasEditor->GetZoom());
761 }
762 else
763 {
764 m_renderer->SetCanvasTransform(Vector(0.0f, 0.0f, 0.0f), 1.0f);
765 }
766 m_renderer->SetCanvasScreenPos(ImGui::GetCursorScreenPos());
767
768 ImGui::PushClipRect(
769 ImGui::GetCursorScreenPos(),
770 ImVec2(ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x,
771 ImGui::GetCursorScreenPos().y + ImGui::GetContentRegionAvail().y),
772 true
773 );
774
776
777 ImGui::PopClipRect();
778 }
779
781 {
782 ImDrawList* drawList = ImGui::GetWindowDrawList();
783 if (drawList == nullptr) { return; }
784
785 ImVec2 debugPos(ImGui::GetCursorScreenPos().x + 10.0f, ImGui::GetCursorScreenPos().y + 10.0f);
786 ImU32 debugTextColor = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
787
790
791 char debugBuffer[512];
793 "Zoom: %.2f | Offset: %.0f, %.0f | Nodes: %zu | Mode: %d | Dragging: %s",
797 IsNodeDragging() ? "YES" : "NO");
798
800 }
801
803 {
804 ImDrawList* drawList = ImGui::GetWindowDrawList();
805 if (drawList == nullptr || !m_document) { return; }
806
808 if (sourceNode == nullptr) { return; }
809
810 // Find output port of source node (use first output port)
811 const NodePort* outputPort = nullptr;
812 for (const auto& port : sourceNode->GetPorts())
813 {
814 if (port.isOutput)
815 {
816 outputPort = &port;
817 break;
818 }
819 }
820
821 if (!outputPort)
822 {
823 // Fallback to node center if no output port
824 Vector screenSourcePos = CanvasToScreen(sourceNode->position.x, sourceNode->position.y);
826
827 ImU32 previewLineColor = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 0.0f, 0.8f));
828 drawList->AddLine(
832 2.5f
833 );
834 }
835 else
836 {
837 // Calculate port position on node edge (same logic as RenderPort)
838 Vector screenCenter = CanvasToScreen(sourceNode->position.x, sourceNode->position.y);
839 float zoom = m_canvasEditor ? m_canvasEditor->GetZoom() : 1.0f;
840 float scaledWidth = sourceNode->size.x * 0.5f * m_renderer->GetNodeScale() * zoom;
841 float scaledHeight = sourceNode->size.y * 0.5f * m_renderer->GetNodeScale() * zoom;
842
843 std::vector<const NodePort*> outputPorts;
844 for (const auto& port : sourceNode->GetPorts())
845 {
846 if (port.isOutput)
847 {
848 outputPorts.push_back(&port);
849 }
850 }
851
853 for (size_t i = 0; i < outputPorts.size(); ++i)
854 {
855 if (outputPorts[i]->portId == outputPort->portId)
856 {
858 break;
859 }
860 }
861
862 Vector portPos = sourceNode->position;
863 if (outputPorts.size() > 0)
864 {
865 float spacing = (2.0f * scaledHeight) / (outputPorts.size() + 1);
866 float yOffset = -scaledHeight + spacing * (portIndexInType + 1);
867 portPos.x += scaledWidth / zoom;
868 portPos.y += yOffset / zoom;
869 }
870
873
874 ImU32 previewLineColor = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 0.0f, 0.8f));
875 ImDrawList* drawList = ImGui::GetWindowDrawList();
876 drawList->AddLine(
880 2.5f
881 );
882
883 // Draw circle at source to indicate starting port
884 drawList->AddCircle(ImVec2(screenSourcePos.x, screenSourcePos.y), 6.0f, previewLineColor, 0, 1.5f);
885 }
886 }
887
889 {
890 // TODO: Implement selection box rendering for drag-to-select
891 }
892
894 {
895 if (!m_isSelectingRectangle) { return; }
896
897 ImDrawList* drawList = ImGui::GetWindowDrawList();
898
899 // Convert canvas positions to screen positions
902
903 // Draw rectangle outline
904 ImU32 rectOutlineColor = ImGui::GetColorU32(ImVec4(0.5f, 0.7f, 1.0f, 1.0f)); // Blue
905 ImU32 rectFillColor = ImGui::GetColorU32(ImVec4(0.5f, 0.7f, 1.0f, 0.15f)); // Semi-transparent blue
906
907 drawList->AddRect(
911 0.0f,
912 0,
913 2.0f
914 );
915
916 drawList->AddRectFilled(
920 );
921 }
922
924 {
925 // Apply pending node position updates
926 }
927
929 {
930 // Render context menu for connection deletion
931 if (ImGui::BeginPopup("ConnectionContextMenu"))
932 {
933 if (ImGui::MenuItem("Delete Connection"))
934 {
936 {
937 const std::vector<std::pair<NodeId, NodeId>>& connections = m_document->GetConnections();
938 if (m_contextMenuConnectionIndex < static_cast<int>(connections.size()))
939 {
941 m_document->DisconnectNodes(conn.first, conn.second);
942 m_document->SetDirty(true);
943 }
944 }
945 ImGui::CloseCurrentPopup();
947 }
948
949 ImGui::EndPopup();
950 }
951 }
952
953 void PrefabCanvas::HandleNodeDragStart(NodeId nodeId, float x, float y)
954 {
955 if (nodeId == InvalidNodeId || !m_document) { return; }
956
958 if (node == nullptr) { return; }
959
961 m_draggedNodeId = nodeId;
963 m_nodeDragOffset = Vector(node->position.x - m_dragStartPos.x, node->position.y - m_dragStartPos.y, 0.0f);
964
965 // FIX #1: Always select the node being dragged (unless already selected via multi-selection)
966 // The selection logic is now handled in OnMouseDown()
967 // This ensures the dragged node is part of the selection to be moved
968 m_document->SelectNode(nodeId);
969 }
970
971 void PrefabCanvas::HandleNodeDrag(float x, float y)
972 {
973 if (m_draggedNodeId == InvalidNodeId || !m_document) { return; }
974
976 if (node == nullptr) { return; }
977
979
980 // Calculate delta from pure mouse movement, not from offset
981 // This ensures all selected nodes move by the same amount
983
984 // Move the primary dragged node
985 node->position = Vector(node->position.x + delta.x, node->position.y + delta.y, 0.0f);
986
987 // Move all other selected nodes by the same delta
988 const std::vector<NodeId>& selectedNodes = m_document->GetSelectedNodes();
989 for (size_t i = 0; i < selectedNodes.size(); ++i)
990 {
991 if (selectedNodes[i] != m_draggedNodeId)
992 {
993 ComponentNode* selectedNode = m_document->GetNode(selectedNodes[i]);
994 if (selectedNode != nullptr)
995 {
997 selectedNode->position.x + delta.x,
998 selectedNode->position.y + delta.y,
999 0.0f
1000 );
1001 selectedNode->position = updatedPos;
1002 }
1003 }
1004 }
1005
1006 // Update drag start position for next frame
1008 }
1009
1015
1017 {
1018 // TODO: Implement connection creation logic
1019 }
1020
1022 {
1023 // TODO: Implement connection finalization
1024 }
1025
1026 void PrefabCanvas::HandlePanStart(float x, float y)
1027 {
1029
1031 m_isPanning = true;
1032
1033 // Store current pan state via adapter
1034 if (m_canvasEditor)
1035 {
1038 }
1039 else
1040 {
1041 m_panStartOffset = Vector(0.0f, 0.0f, 0.0f);
1042 }
1043
1044 m_dragStartPos = Vector(x, y, 0.0f);
1045 }
1046
1047 void PrefabCanvas::HandlePan(float x, float y)
1048 {
1049 if (!m_isPanning || !m_canvasEditor) { return; }
1050
1051 Vector currentPos(x, y, 0.0f);
1053
1054 // Update pan via adapter
1057 }
1058
1060 {
1061 float gridSize = m_gridSpacing;
1062 position.x = (float)((int)((position.x + gridSize * 0.5f) / gridSize) * gridSize);
1063 position.y = (float)((int)((position.y + gridSize * 0.5f) / gridSize) * gridSize);
1064 }
1065
1067 {
1068 if (!m_document || !m_renderer) { return; }
1069
1070 ImVec2 mousePos = ImGui::GetMousePos();
1071
1072 // Detect right-click on canvas
1073 if (ImGui::IsMouseClicked(1) && ImGui::IsWindowHovered())
1074 {
1075 // First check if right-clicking on a connection
1076 Vector canvasPos = ScreenToCanvas(mousePos.x, mousePos.y);
1077 const std::vector<std::pair<NodeId, NodeId>>& connections = m_document->GetConnections();
1078 const std::vector<ComponentNode>& nodes = m_document->GetAllNodes();
1079
1080 const float hitTolerance = 10.0f;
1081 bool connectionFound = false;
1082
1083 for (size_t connIdx = 0; connIdx < connections.size(); ++connIdx)
1084 {
1087
1088 if (sourceNode != nullptr && targetNode != nullptr)
1089 {
1090 // Get connection endpoints in canvas space
1091 Vector from = sourceNode->position;
1092 from.x += sourceNode->size.x * 0.5f;
1093 Vector to = targetNode->position;
1094 to.x -= targetNode->size.x * 0.5f;
1095
1096 // Get distance from click point to connection curve
1097 float distance = m_renderer->GetDistanceToConnection(
1098 Vector(mousePos.x, mousePos.y, 0.0f),
1099 from, to
1100 );
1101
1102 if (distance <= hitTolerance)
1103 {
1104 m_contextMenuConnectionIndex = static_cast<int>(connIdx);
1107 ImGui::OpenPopup("ConnectionContextMenu");
1108 connectionFound = true;
1109 return; // Don't open canvas context menu
1110 }
1111 }
1112 }
1113
1114 // If no connection found, check for node or canvas context menu
1115 // Store the node at click position for context menu
1118 ImGui::OpenPopup("CanvasContextMenu");
1119 }
1120
1121 // Render context menu
1122 if (ImGui::BeginPopup("CanvasContextMenu"))
1123 {
1125 {
1126 // Context menu on a node
1127 if (ImGui::MenuItem("Delete Node"))
1128 {
1131 ImGui::CloseCurrentPopup();
1132 }
1133
1134 ImGui::Separator();
1135
1136 if (ImGui::MenuItem("Select Node"))
1137 {
1138 if (!m_ctrlPressed)
1139 {
1141 }
1143 ImGui::CloseCurrentPopup();
1144 }
1145 }
1146 else
1147 {
1148 // Context menu on empty canvas
1149 if (ImGui::MenuItem("Clear Selection"))
1150 {
1152 ImGui::CloseCurrentPopup();
1153 }
1154
1155 if (m_document->GetNodeCount() > 0)
1156 {
1157 if (ImGui::MenuItem("Select All"))
1158 {
1159 SelectAll();
1160 ImGui::CloseCurrentPopup();
1161 }
1162 }
1163
1164 ImGui::Separator();
1165
1166 if (ImGui::MenuItem("Reset View"))
1167 {
1168 ResetView();
1169 ImGui::CloseCurrentPopup();
1170 }
1171 }
1172
1173 ImGui::EndPopup();
1174 }
1175 }
1176
1177} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
static GridConfig GetStylePreset(GridStylePreset preset)
Get pre-configured grid style (VisualScript/Compact/Spacious)
static void RenderGrid(ImDrawList *drawList, const GridConfig &config)
Render a grid on the given ImDrawList.
const std::vector< NodeId > & GetSelectedNodes() const
bool ConnectNodes(NodeId sourceId, NodeId targetId)
NodeId CreateComponentNode(const std::string &componentType)
const std::vector< ComponentNode > & GetAllNodes() const
bool DisconnectNodes(NodeId sourceId, NodeId targetId)
const std::vector< std::pair< NodeId, NodeId > > & GetConnections() const
Abstract interface for canvas editors with unified pan/zoom/grid management.
virtual void ResetView()=0
Reset both pan and zoom to defaults.
virtual void SetCanvasScreenPos(const ImVec2 &screenPos)=0
Set canvas screen position (call each frame to update)
virtual void SetPan(const ImVec2 &offset)=0
Set pan offset directly.
virtual ImVec2 ScreenToCanvas(const ImVec2 &screenPos) const =0
Transform screen space coordinates to canvas space.
virtual void RenderMinimap()=0
Render minimap overlay.
virtual ImVec2 CanvasToScreen(const ImVec2 &canvasPos) const =0
Transform canvas space coordinates to screen space.
virtual ImVec2 GetPan() const =0
Get current pan offset.
virtual ImVec2 GetZoomLimits() const =0
Get zoom limits (min, max)
virtual void SetZoom(float scale, const ImVec2 *zoomCenter=nullptr)=0
Set zoom level directly.
virtual void UpdateMinimapNodes(const std::vector< std::tuple< int, float, float, float, float > > &nodes, float graphMinX, float graphMaxX, float graphMinY, float graphMaxY)=0
Update minimap with current graph node data.
virtual float GetZoom() const =0
Get current zoom level.
virtual void UpdateMinimapViewport(float viewMinX, float viewMaxX, float viewMinY, float viewMaxY, float graphMinX, float graphMaxX, float graphMinY, float graphMaxY)=0
Update minimap with current viewport bounds.
virtual void SetCanvasSize(const ImVec2 &size)=0
Set canvas size (call each frame to update)
virtual void PanBy(const ImVec2 &delta)=0
Pan by delta amount.
void CompleteConnection(NodeId targetNodeId)
void Update(float deltaTime)
Vector m_contextMenuConnectionMousePos
void OnKeyUp(int keyCode)
EntityPrefabGraphDocument * GetDocument() const
void HandleNodeDragStart(NodeId nodeId, float x, float y)
ICanvasEditor * m_canvasEditor
void SetSnapToGrid(bool snap)
void HandlePanStart(float x, float y)
void SetShowDebugInfo(bool show)
bool IsCreatingConnection() const
bool GetShowDebugInfo() const
std::unique_ptr< ComponentNodeRenderer > m_renderer
bool IsNodeDragging() const
void AcceptComponentDropAtScreenPos(const std::string &componentType, const std::string &componentName, float screenX, float screenY)
void StartConnectionCreation(NodeId sourceNodeId)
void SelectNodesInRectangle(const Vector &rectStart, const Vector &rectEnd, bool addToSelection=false)
void HandleNodeDrag(float x, float y)
ImVec2 GetCanvasScreenPos() const
void SetCanvasOffset(const Vector &offset)
void SetGridEnabled(bool enabled)
float GetCanvasZoom() const
void HandleConnectionCreation(float x, float y)
void OnMouseUp(int button, float x, float y)
void SelectNodeAt(float x, float y, bool addToSelection=false)
void OnMouseMove(float x, float y)
void SnapNodePositionToGrid(Vector &position)
bool IsGridEnabled() const
void Initialize(EntityPrefabGraphDocument *document)
void SetCanvasZoom(float zoom)
Vector GetCanvasOffset() const
Vector ScreenToCanvas(float screenX, float screenY) const
void HandlePan(float x, float y)
EntityPrefabGraphDocument * m_document
bool IsSnapToGridEnabled() const
CanvasInteractionMode m_interactionMode
void HandleConnectionEnd(float x, float y)
void SetGridSpacing(float spacing)
void OnMouseDown(int button, float x, float y)
Vector CanvasToScreen(float canvasX, float canvasY) const
void AddComponentNode(const std::string &componentType, const std::string &componentName, float x, float y)
CanvasInteractionMode GetInteractionMode() const
void SetCanvasEditor(ICanvasEditor *canvasEditor)
void OnKeyDown(int keyCode)
void ZoomCanvas(float zoomDelta, float centerX, float centerY)
void PanCanvas(float deltaX, float deltaY)
NodeId GetNodeAtPosition(float x, float y)
float GetGridSpacing() const
NodeId GetConnectionSourceNode() const
void OnMouseScroll(float delta)
float y
Definition vector.h:25
float x
Definition vector.h:25
< Provides AssetID and INVALID_ASSET_ID
@ Vector
3-component vector (Vector from vector.h)
const PortId InvalidPortId
uint32_t NodeId
uint32_t PortId
CanvasInteractionMode
const NodeId InvalidNodeId
Configuration parameters for grid rendering.
#define SYSTEM_LOG