//------------------------------------------------------------------------------ // EditorContext.cpp // Implementation file for EditorContext class // Consolidated from imgui_node_editor.cpp, imgui_node_editor_render.cpp, // imgui_node_editor_tools.cpp, imgui_node_editor_selection.cpp, // imgui_node_editor_links.cpp, imgui_node_editor_animation.cpp, // imgui_node_editor_runtime.cpp, and imgui_node_editor_store.cpp //------------------------------------------------------------------------------ # include "imgui_node_editor_internal.h" # include # include # include # include # include # include # include # include # include # include # include namespace ed = ax::NodeEditor::Detail; ed::EditorContext::EditorContext(const ax::NodeEditor::Config* config) : m_Config(config) , m_EditorActiveId(0) , m_IsFirstFrame(true) , m_IsFocused(false) , m_IsHovered(false) , m_IsHoveredWithoutOverlapp(false) , m_ShortcutsEnabled(true) , m_Style() , m_Nodes() , m_Pins() , m_Links() , m_SelectionId(1) , m_LastActiveLink(nullptr) , m_Canvas() , m_IsCanvasVisible(false) , m_NodeBuilder(this) , m_HintBuilder(this) , m_CurrentAction(nullptr) , m_NavigateAction(this, m_Canvas) , m_SizeAction(this) , m_DragAction(this) , m_SelectAction(this) , m_ContextMenuAction(this) , m_ShortcutAction(this) , m_CreateItemAction(this) , m_DeleteItemsAction(this) , m_AnimationControllers{ &m_FlowAnimationController } , m_FlowAnimationController(this) , m_HoveredNode(0) , m_HoveredPin(0) , m_HoveredLink(0) , m_DoubleClickedNode(0) , m_DoubleClickedPin(0) , m_DoubleClickedLink(0) , m_BackgroundClickButtonIndex(-1) , m_BackgroundDoubleClickButtonIndex(-1) , m_EditingLink(nullptr) , m_DraggingControlPointIndex(-1) , m_ControlPointDragStart(0, 0) , m_GuidedLinks() , m_IsInitialized(false) , m_Settings() , m_DrawList(nullptr) , m_ExternalChannel(0) { } ed::EditorContext::~EditorContext() { Log("=== ImGui Node Editor Context Destroyed ==="); if (m_IsInitialized) SaveSettings(); // Clean up guided link data Log("Cleaning up %zu guided links", m_GuidedLinks.size()); for (auto& pair : m_GuidedLinks) delete pair.second; m_GuidedLinks.clear(); for (auto link : m_Links) delete link.m_Object; for (auto pin : m_Pins) delete pin.m_Object; for (auto node : m_Nodes) delete node.m_Object; m_Splitter.ClearFreeMemory(); } void ed::EditorContext::Begin(const char* id, const ImVec2& size) { m_EditorActiveId = ImGui::GetID(id); ImGui::PushID(id); auto availableContentSize = ImGui::GetContentRegionAvail(); ImVec2 canvasSize = ImFloor(size); if (canvasSize.x <= 0.0f) canvasSize.x = ImMax(4.0f, availableContentSize.x); if (canvasSize.y <= 0.0f) canvasSize.y = ImMax(4.0f, availableContentSize.y); if (!m_IsInitialized) { // Cycle canvas, so it has a chance to initialize its size before settings are loaded if (m_Canvas.Begin(id, canvasSize)) m_Canvas.End(); LoadSettings(); m_IsInitialized = true; } static auto resetAndCollect = [](auto& objects) { objects.erase(std::remove_if(objects.begin(), objects.end(), [](auto objectWrapper) { if (objectWrapper->m_DeleteOnNewFrame) { delete objectWrapper.m_Object; return true; } else { objectWrapper->Reset(); return false; } }), objects.end()); }; resetAndCollect(m_Nodes); resetAndCollect(m_Pins); resetAndCollect(m_Links); m_DrawList = ImGui::GetWindowDrawList(); ImDrawList_SwapSplitter(m_DrawList, m_Splitter); m_ExternalChannel = m_DrawList->_Splitter._Current; if (m_CurrentAction && m_CurrentAction->IsDragging() && m_NavigateAction.MoveOverEdge(canvasSize)) { auto& io = ImGui::GetIO(); auto offset = m_NavigateAction.GetMoveScreenOffset(); for (int i = 0; i < 5; ++i) io.MouseClickedPos[i] = io.MouseClickedPos[i] - offset; } else m_NavigateAction.StopMoveOverEdge(); auto previousSize = m_Canvas.Rect().GetSize(); auto previousVisibleRect = m_Canvas.ViewRect(); m_IsCanvasVisible = m_Canvas.Begin(id, canvasSize); m_IsFocused = ImGui::IsWindowFocused(); m_NavigateAction.SetWindow(m_Canvas.ViewRect().Min, m_Canvas.ViewRect().GetSize()); // Handle canvas size change. Scale to Y axis, center on X. if (!ImRect_IsEmpty(previousVisibleRect) && previousSize != canvasSize) { m_NavigateAction.FinishNavigation(); auto centerX = (previousVisibleRect.Max.x + previousVisibleRect.Min.x) * 0.5f; auto centerY = (previousVisibleRect.Max.y + previousVisibleRect.Min.y) * 0.5f; auto currentVisibleRect = m_Canvas.ViewRect(); auto currentAspectRatio = currentVisibleRect.GetHeight() ? (currentVisibleRect.GetWidth() / currentVisibleRect.GetHeight()) : 0.0f; auto width = previousVisibleRect.GetWidth(); auto height = previousVisibleRect.GetHeight(); if (m_Config.CanvasSizeMode == ax::NodeEditor::CanvasSizeMode::FitVerticalView) { height = previousVisibleRect.GetHeight(); width = height * currentAspectRatio; } else if (m_Config.CanvasSizeMode == ax::NodeEditor::CanvasSizeMode::FitHorizontalView) { width = previousVisibleRect.GetWidth(); height = width / currentAspectRatio; } else if (m_Config.CanvasSizeMode == ax::NodeEditor::CanvasSizeMode::CenterOnly) { width = currentVisibleRect.GetWidth(); height = currentVisibleRect.GetHeight(); } previousVisibleRect.Min.x = centerX - 0.5f * width; previousVisibleRect.Max.x = centerX + 0.5f * width; previousVisibleRect.Min.y = centerY - 0.5f * height; previousVisibleRect.Max.y = centerY + 0.5f * height; m_NavigateAction.NavigateTo(previousVisibleRect, Detail::NavigateAction::ZoomMode::Exact, 0.0f); } m_Canvas.SetView(m_NavigateAction.GetView()); // Reserve channels for background and links ImDrawList_ChannelsGrow(m_DrawList, c_NodeStartChannel); if (HasSelectionChanged()) ++m_SelectionId; m_LastSelectedObjects = m_SelectedObjects; } bool ed::EditorContext::DoLink(LinkId id, PinId startPinId, PinId endPinId, ImU32 color, float thickness) { auto startPin = FindPin(startPinId); auto endPin = FindPin(endPinId); if (!startPin || !startPin->m_IsLive || !endPin || !endPin->m_IsLive) { return false; } startPin->m_HasConnection = true; endPin->m_HasConnection = true; auto link = GetLink(id); link->m_StartPin = startPin; link->m_EndPin = endPin; link->m_Color = color; link->m_HighlightColor= GetColor(StyleColor_HighlightLinkBorder); link->m_Thickness = thickness; link->m_IsLive = true; link->UpdateEndpoints(); // Apply guided link settings if they exist if (!link->m_GuidedData) { auto linkSettings = m_Settings.FindLink(id); if (linkSettings && linkSettings->m_Mode != LinkMode::Auto) { auto guidedData = CreateGuidedLinkData(id); if (guidedData) { guidedData->Mode = linkSettings->m_Mode; guidedData->EnableSnapping = linkSettings->m_EnableSnapping; guidedData->SnapGridSize = linkSettings->m_SnapGridSize; guidedData->ControlPoints.clear(); if (linkSettings->m_Mode == LinkMode::Guided) { for (const auto& pt : linkSettings->m_ControlPoints) { const float MAX_VALID_COORD = 1000000.0f; if (std::isfinite(pt.x) && std::isfinite(pt.y) && std::abs(pt.x) < MAX_VALID_COORD && std::abs(pt.y) < MAX_VALID_COORD) { guidedData->AddControlPoint(pt); } else { Log(" Skipping invalid control point at (%.1f, %.1f)", pt.x, pt.y); } } } link->m_GuidedData = guidedData; const float MAX_VALID_COORD = 1000000.0f; auto& points = link->m_GuidedData->ControlPoints; points.erase( std::remove_if(points.begin(), points.end(), [MAX_VALID_COORD](const auto& cp) { return !std::isfinite(cp.Position.x) || !std::isfinite(cp.Position.y) || std::abs(cp.Position.x) >= MAX_VALID_COORD || std::abs(cp.Position.y) >= MAX_VALID_COORD; }), points.end()); if (points.empty() && link->m_GuidedData->Mode == LinkMode::Guided) { Log(" All control points invalid for link %p, reverting to Auto mode", id.AsPointer()); link->m_GuidedData->Mode = LinkMode::Auto; } } } } return true; } void ed::EditorContext::MarkNodeToRestoreState(Node* node) { node->m_RestoreState = true; } void ed::EditorContext::UpdateNodeState(Node* node) { bool tryLoadState = node->m_RestoreState; node->m_RestoreState = false; auto settings = m_Settings.FindNode(node->m_ID); if (!settings) return; if (!tryLoadState && settings->m_WasUsed) return; if (!settings->m_WasUsed) { MakeDirty(SaveReasonFlags::AddNode, node); settings->m_WasUsed = true; } if (tryLoadState) { NodeSettings newSettings = *settings; if (NodeSettings::Parse(m_Config.LoadNode(node->m_ID), newSettings)) *settings = newSettings; } node->m_Bounds.Min = settings->m_Location; node->m_Bounds.Max = node->m_Bounds.Min + settings->m_Size; node->m_Bounds.Floor(); node->m_GroupBounds.Min = settings->m_Location; node->m_GroupBounds.Max = node->m_GroupBounds.Min + settings->m_GroupSize; node->m_GroupBounds.Floor(); } void ed::EditorContext::RemoveSettings(Object* object) { if (auto node = object->AsNode()) { m_Settings.RemoveNode(node->m_ID); MakeDirty(SaveReasonFlags::RemoveNode, node); } } void ed::EditorContext::Suspend(SuspendFlags flags) { IM_ASSERT(m_DrawList != nullptr && "Suspend was called outiside of Begin/End."); auto lastChannel = m_DrawList->_Splitter._Current; m_DrawList->ChannelsSetCurrent(m_ExternalChannel); if (m_IsCanvasVisible) m_Canvas.Suspend(); m_DrawList->ChannelsSetCurrent(lastChannel); if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter) ImDrawList_SwapSplitter(m_DrawList, m_Splitter); } void ed::EditorContext::Resume(SuspendFlags flags) { IM_ASSERT(m_DrawList != nullptr && "Reasume was called outiside of Begin/End."); if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter) ImDrawList_SwapSplitter(m_DrawList, m_Splitter); auto lastChannel = m_DrawList->_Splitter._Current; m_DrawList->ChannelsSetCurrent(m_ExternalChannel); if (m_IsCanvasVisible) m_Canvas.Resume(); m_DrawList->ChannelsSetCurrent(lastChannel); } bool ed::EditorContext::IsSuspended() { return m_Canvas.IsSuspended(); } bool ed::EditorContext::IsFocused() { return m_IsFocused; } bool ed::EditorContext::IsHovered() const { return m_IsHovered; } bool ed::EditorContext::IsHoveredWithoutOverlapp() const { return m_IsHoveredWithoutOverlapp; } bool ed::EditorContext::CanAcceptUserInput() const { return m_IsFocused && m_IsHovered; } int ed::EditorContext::CountLiveNodes() const { return (int)std::count_if(m_Nodes.begin(), m_Nodes.end(), [](const Node* node) { return node->m_IsLive; }); } int ed::EditorContext::CountLivePins() const { return (int)std::count_if(m_Pins.begin(), m_Pins.end(), [](const Pin* pin) { return pin->m_IsLive; }); } int ed::EditorContext::CountLiveLinks() const { return (int)std::count_if(m_Links.begin(), m_Links.end(), [](const Link* link) { return link->m_IsLive; }); } ed::Pin* ed::EditorContext::CreatePin(PinId id, PinKind kind) { IM_ASSERT(nullptr == FindObject(id)); auto pin = new Pin(this, id, kind); m_Pins.push_back({id, pin}); std::sort(m_Pins.begin(), m_Pins.end()); return pin; } ed::Node* ed::EditorContext::CreateNode(NodeId id) { IM_ASSERT(nullptr == FindObject(id)); auto node = new Node(this, id); m_Nodes.push_back({id, node}); auto settings = m_Settings.FindNode(id); if (!settings) { settings = m_Settings.AddNode(id); } UpdateNodeState(node); if (settings->m_GroupSize.x > 0 || settings->m_GroupSize.y > 0) node->m_Type = NodeType::Group; node->m_IsLive = false; return node; } ImU32 ed::EditorContext::GetColor(StyleColor colorIndex) const { return ImColor(m_Style.Colors[colorIndex]); } ImU32 ed::EditorContext::GetColor(StyleColor colorIndex, float alpha) const { auto color = m_Style.Colors[colorIndex]; return ImColor(color.x, color.y, color.z, color.w * alpha); } void ed::EditorContext::SetUserContext(bool globalSpace) { const auto mousePos = ImGui::GetMousePos(); if (globalSpace) ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos)); else ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos)); if (!IsSuspended()) { m_DrawList->ChannelsSetCurrent(c_UserChannel_Content); } } void ed::EditorContext::EnableShortcuts(bool enable) { m_ShortcutsEnabled = enable; } bool ed::EditorContext::AreShortcutsEnabled() { return m_ShortcutsEnabled; } ed::Control ed::EditorContext::BuildControl(bool allowOffscreen) { m_IsHovered = false; m_IsHoveredWithoutOverlapp = false; const auto windowHovered = ImGui::IsWindowHovered(); const auto widgetHovered = ImGui::IsMouseHoveringRect(m_Canvas.ViewRect().Min, m_Canvas.ViewRect().Max, true); if (!allowOffscreen && !windowHovered && !widgetHovered) return Control(); const auto mousePos = ImGui::GetMousePos(); auto editorRect = m_Canvas.ViewRect(); auto isMouseOffscreen = allowOffscreen && !editorRect.Contains(mousePos); if (isMouseOffscreen) { editorRect.Add(ImFloor(mousePos)); editorRect.Add(ImVec2(ImCeil(mousePos.x), ImCeil(mousePos.y))); ImGui::PushClipRect(editorRect.Min, editorRect.Max, false); } ImGuiID activeId = 0; Object* hotObject = nullptr; Object* activeObject = nullptr; Object* clickedObject = nullptr; Object* doubleClickedObject = nullptr; ImGuiButtonFlags extraFlags = ImGuiButtonFlags_None; extraFlags |= ImGuiButtonFlags_MouseButtonLeft; extraFlags |= ImGuiButtonFlags_MouseButtonRight; extraFlags |= ImGuiButtonFlags_MouseButtonMiddle; static auto invisibleButtonEx = [](const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags extraFlags) -> int { using namespace ImGui; ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return -1; if (size_arg.x == 0.0f || size_arg.y == 0.0f) return false; const ImGuiID id = window->GetID(str_id); ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); ItemSize(size); if (!ItemAdd(bb, id)) return -1; auto buttonIndex = ImGui::GetCurrentContext()->ActiveIdMouseButton; bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, extraFlags); if (pressed && buttonIndex >= 0 && buttonIndex < ImGuiMouseButton_COUNT) return buttonIndex; else if (pressed) return 0; else return -1; }; auto emitInteractiveAreaEx = [&activeId](ObjectId id, const ImRect& rect, ImGuiButtonFlags extraFlags) -> int { char idString[33] = { 0 }; snprintf(idString, 32, "%p", id.AsPointer()); ImGui::SetCursorScreenPos(rect.Min); auto buttonIndex = invisibleButtonEx(idString, rect.GetSize(), extraFlags); if (ImGui::IsItemActive()) activeId = ImGui::GetActiveID(); return buttonIndex; }; auto emitInteractiveArea = [&emitInteractiveAreaEx, extraFlags](ObjectId id, const ImRect& rect) { return emitInteractiveAreaEx(id, rect, extraFlags); }; auto checkInteractionsInArea = [this, &emitInteractiveArea, &hotObject, &activeObject, &clickedObject, &doubleClickedObject](ObjectId id, const ImRect& rect, Object* object) -> bool { auto buttonIndex = emitInteractiveArea(id, rect); bool isClick = (buttonIndex >= 0); bool isDragging = ImGui::IsMouseDragging(0, 1) || ImGui::IsMouseDragging(1, 1) || ImGui::IsMouseDragging(2, 1); if (isClick && !isDragging) { const char* objType = "Unknown"; void* objId = nullptr; if (auto node = object->AsNode()) { objType = "Node"; objId = node->m_ID.AsPointer(); } else if (auto pin = object->AsPin()) { objType = "Pin"; objId = pin->m_ID.AsPointer(); } else if (auto link = object->AsLink()) { objType = "Link"; objId = link->m_ID.AsPointer(); } if (buttonIndex >= 0) { Log("BuildControl: Setting clickedObject to %s %p (prev was %s)", objType, objId, clickedObject ? (clickedObject->AsNode() ? "Node" : clickedObject->AsPin() ? "Pin" : clickedObject->AsLink() ? "Link" : "Unknown") : "null"); } } if (buttonIndex >= 0) clickedObject = object; if (!doubleClickedObject && ImGui::IsMouseDoubleClicked(m_Config.DragButtonIndex) && ImGui::IsItemHovered()) doubleClickedObject = object; if (!hotObject && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) hotObject = object; bool isActive = ImGui::IsItemActive(); if (isActive) { if (auto pin = object->AsPin()) { auto activeNode = activeObject ? activeObject->AsNode() : nullptr; if (activeNode && pin->m_Node != activeNode) { return false; } } activeObject = object; return true; } return false; }; std::vector nodesToProcess; if (m_Config.GetContainerNodeIds && m_Config.UserPointer) { int nodeCount = m_Config.GetContainerNodeIds(m_Config.UserPointer, nullptr, 0); if (nodeCount > 0) { std::vector nodeIds(nodeCount); int actualCount = m_Config.GetContainerNodeIds(m_Config.UserPointer, nodeIds.data(), nodeCount); for (int i = 0; i < actualCount; ++i) { NodeId nodeId = nodeIds[i]; if (!nodeId) continue; Node* editorNode = GetNode(nodeId); if (!editorNode) continue; if (!editorNode->m_IsLive) { continue; } UpdateNodeState(editorNode); const ImRect& bounds = editorNode->m_Bounds; if (!std::isfinite(bounds.Min.x) || !std::isfinite(bounds.Min.y) || !std::isfinite(bounds.Max.x) || !std::isfinite(bounds.Max.y) || bounds.Min.x > bounds.Max.x || bounds.Min.y > bounds.Max.y) { continue; } nodesToProcess.push_back(editorNode); } std::stable_sort(nodesToProcess.begin(), nodesToProcess.end(), [](const Node* lhs, const Node* rhs) { return lhs->m_ZPosition < rhs->m_ZPosition; }); } } if (nodesToProcess.empty()) { for (auto nodeIt = m_Nodes.rbegin(), nodeItEnd = m_Nodes.rend(); nodeIt != nodeItEnd; ++nodeIt) { auto node = *nodeIt; if (node->m_IsLive) nodesToProcess.push_back(node); } } for (auto nodeIt = nodesToProcess.rbegin(), nodeItEnd = nodesToProcess.rend(); nodeIt != nodeItEnd; ++nodeIt) { auto node = *nodeIt; if (!node->m_IsLive) continue; for (auto pin = node->m_LastPin; pin; pin = pin->m_PreviousPin) { if (!pin->m_IsLive) continue; checkInteractionsInArea(pin->m_ID, pin->m_Bounds, pin); } if (node->m_Type == NodeType::Group) { ImGui::PushID(node->m_ID.AsPointer()); static const NodeRegion c_Regions[] = { NodeRegion::TopLeft, NodeRegion::TopRight, NodeRegion::BottomLeft, NodeRegion::BottomRight, NodeRegion::Top, NodeRegion::Bottom, NodeRegion::Left, NodeRegion::Right, NodeRegion::Header, }; for (auto region : c_Regions) { auto bounds = node->GetRegionBounds(region); if (ImRect_IsEmpty(bounds)) continue; checkInteractionsInArea(NodeId(static_cast(region)), bounds, node); } ImGui::PopID(); } else { checkInteractionsInArea(node->m_ID, node->m_Bounds, node); } } if (nullptr == hotObject) hotObject = FindLinkAt(mousePos); ImGuiButtonFlags backgroundExtraFlags = ImGuiButtonFlags_None; if (m_Config.DragButtonIndex == 0 || m_Config.SelectButtonIndex == 0 || m_Config.NavigateButtonIndex == 0) backgroundExtraFlags |= ImGuiButtonFlags_MouseButtonLeft; if (m_Config.DragButtonIndex == 1 || m_Config.SelectButtonIndex == 1 || m_Config.NavigateButtonIndex == 1) backgroundExtraFlags |= ImGuiButtonFlags_MouseButtonRight; if (m_Config.DragButtonIndex == 2 || m_Config.SelectButtonIndex == 2 || m_Config.NavigateButtonIndex == 2) backgroundExtraFlags |= ImGuiButtonFlags_MouseButtonMiddle; auto isMouseDoubleClickOverBackground = [doubleClickedObject, backgroundExtraFlags]() -> int { if (doubleClickedObject) return -1; if (!ImGui::IsItemHovered()) return -1; if ((backgroundExtraFlags & ImGuiButtonFlags_MouseButtonLeft) && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) return ImGuiButtonFlags_MouseButtonLeft; if ((backgroundExtraFlags & ImGuiButtonFlags_MouseButtonRight) && ImGui::IsMouseDoubleClicked(ImGuiButtonFlags_MouseButtonRight)) return ImGuiButtonFlags_MouseButtonRight; if ((backgroundExtraFlags & ImGuiButtonFlags_MouseButtonMiddle) && ImGui::IsMouseDoubleClicked(ImGuiButtonFlags_MouseButtonMiddle)) return ImGuiButtonFlags_MouseButtonMiddle; return -1; }; auto backgroundClickButonIndex = emitInteractiveAreaEx(NodeId(0), editorRect, backgroundExtraFlags); auto backgroundDoubleClickButtonIndex = isMouseDoubleClickOverBackground(); auto isBackgroundActive = ImGui::IsItemActive(); auto isBackgroundHot = !hotObject; auto isDragging = ImGui::IsMouseDragging(0, 1) || ImGui::IsMouseDragging(1, 1) || ImGui::IsMouseDragging(2, 1); if (backgroundDoubleClickButtonIndex >= 0) backgroundClickButonIndex = -1; if (isMouseOffscreen) ImGui::PopClipRect(); auto hotLink = hotObject ? hotObject->AsLink() : nullptr; if (!isDragging && isBackgroundActive && hotLink && !m_LastActiveLink) m_LastActiveLink = hotLink; if (isBackgroundActive && m_LastActiveLink) { activeObject = m_LastActiveLink; isBackgroundActive = false; } else if (!isBackgroundActive && m_LastActiveLink) m_LastActiveLink = nullptr; if (!isDragging && backgroundClickButonIndex >= 0 && hotLink) { clickedObject = hotLink; backgroundClickButonIndex = -1; } if (!isDragging && backgroundDoubleClickButtonIndex >= 0 && hotLink) { doubleClickedObject = hotLink; backgroundDoubleClickButtonIndex = -1; } if (activeId) m_EditorActiveId = activeId; if (ImGui::IsAnyItemActive() && ImGui::GetActiveID() != m_EditorActiveId) return Control(); m_IsHovered = ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly); m_IsHoveredWithoutOverlapp = ImGui::IsItemHovered(); if (!allowOffscreen && !m_IsHovered) return Control(); # if IMGUI_VERSION_NUM >= 18836 if (m_IsHoveredWithoutOverlapp) ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY); # elif IMGUI_VERSION_NUM >= 17909 if (m_IsHoveredWithoutOverlapp) ImGui::SetItemUsingMouseWheel(); # endif if (!isDragging) { const char* clickedType = "null"; void* clickedId = nullptr; Node* clickedNode = nullptr; if (clickedObject) { if (auto node = clickedObject->AsNode()) { clickedType = "Node"; clickedId = node->m_ID.AsPointer(); clickedNode = node; } else if (auto pin = clickedObject->AsPin()) { clickedType = "Pin"; clickedId = pin->m_ID.AsPointer(); clickedNode = pin->m_Node; Log("BuildControl: Final clickedObject is Pin %p (node %p)", clickedId, clickedNode ? clickedNode->m_ID.AsPointer() : nullptr); } else if (auto link = clickedObject->AsLink()) { clickedType = "Link"; clickedId = link->m_ID.AsPointer(); } } if (clickedNode) { Log("BuildControl: ClickedNode should be %p (via clickedObject->AsNode())", clickedNode->m_ID.AsPointer()); } else if (clickedObject && !clickedObject->AsNode()) { // Log("BuildControl: WARNING - clickedObject is %s but AsNode() returns null (this is why ClickedNode is null!)", clickedType); } } return Control(hotObject, activeObject, clickedObject, doubleClickedObject, isBackgroundHot, isBackgroundActive, backgroundClickButonIndex, backgroundDoubleClickButtonIndex); } void ed::EditorContext::ShowMetrics(const Control& control) { auto& io = ImGui::GetIO(); auto getObjectName = [](Object* object) { if (!object) return ""; else if (object->AsNode()) return "Node"; else if (object->AsPin()) return "Pin"; else if (object->AsLink()) return "Link"; else return ""; }; auto getHotObjectName = [&control, &getObjectName]() { if (control.HotObject) return getObjectName(control.HotObject); else if (control.BackgroundHot) return "Background"; else return ""; }; auto getActiveObjectName = [&control, &getObjectName]() { if (control.ActiveObject) return getObjectName(control.ActiveObject); else if (control.BackgroundActive) return "Background"; else return ""; }; auto liveNodeCount = CountLiveNodes(); auto livePinCount = CountLivePins(); auto liveLinkCount = CountLiveLinks(); auto canvasRect = m_Canvas.Rect(); auto viewRect = m_Canvas.ViewRect(); auto localMousePos = m_Canvas.ToLocal(io.MousePos); auto globalMousePos = io.MousePos; ImGui::SetCursorScreenPos(canvasRect.Min + ImVec2(5, 5)); ImGui::BeginGroup(); ImGui::Text("Is Focused: %s", m_IsFocused ? "true" : "false"); ImGui::Text("Is Hovered: %s", m_IsHovered ? "true" : "false"); ImGui::Text("Is Hovered (without overlapp): %s", m_IsHoveredWithoutOverlapp ? "true" : "false"); ImGui::Text("Accept Input: %s", CanAcceptUserInput() ? "true" : "false"); ImGui::Text("View Position: { x=%g y=%g }", viewRect.Min.x, viewRect.Min.y); ImGui::Text("View Size: { w=%g h=%g }", viewRect.GetWidth(), viewRect.GetHeight()); ImGui::Text("Canvas Size: { w=%g h=%g }", canvasRect.GetWidth(), canvasRect.GetHeight()); ImGui::Text("Mouse: { x=%.0f y=%.0f } global: { x=%g y=%g }", localMousePos.x, localMousePos.y, globalMousePos.x, globalMousePos.y); ImGui::Text("Live Nodes: %d", liveNodeCount); ImGui::Text("Live Pins: %d", livePinCount); ImGui::Text("Live Links: %d", liveLinkCount); ImGui::Text("Hot Object: %s (%p)", getHotObjectName(), control.HotObject ? control.HotObject->ID().AsPointer() : nullptr); if (auto node = control.HotObject ? control.HotObject->AsNode() : nullptr) { ImGui::SameLine(); ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), node->m_Bounds.GetHeight()); } ImGui::Text("Active Object: %s (%p)", getActiveObjectName(), control.ActiveObject ? control.ActiveObject->ID().AsPointer() : nullptr); if (auto node = control.ActiveObject ? control.ActiveObject->AsNode() : nullptr) { ImGui::SameLine(); ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), node->m_Bounds.GetHeight()); } ImGui::Text("Action: %s", m_CurrentAction ? m_CurrentAction->GetName() : ""); ImGui::Text("Action Is Dragging: %s", m_CurrentAction && m_CurrentAction->IsDragging() ? "Yes" : "No"); m_NavigateAction.ShowMetrics(); m_SizeAction.ShowMetrics(); m_DragAction.ShowMetrics(); m_SelectAction.ShowMetrics(); m_ContextMenuAction.ShowMetrics(); m_CreateItemAction.ShowMetrics(); m_DeleteItemsAction.ShowMetrics(); ImGui::EndGroup(); } //------------------------------------------------------------------------------ // From imgui_node_editor_render.cpp //------------------------------------------------------------------------------ void ed::EditorContext::DrawNodes() { for (auto node : m_Nodes) if (node->m_IsLive && node->IsVisible()) node->Draw(m_DrawList); } void ed::EditorContext::DrawLinks() { for (auto link : m_Links) if (link->m_IsLive && link->IsVisible()) link->Draw(m_DrawList); } void ed::EditorContext::DrawSelectionHighlights(const Control& control) { auto selectedObjects = &m_SelectedObjects; if (auto selectAction = m_CurrentAction ? m_CurrentAction->AsSelect() : nullptr) selectedObjects = &selectAction->m_CandidateObjects; for (auto selectedObject : *selectedObjects) { if (selectedObject->IsVisible()) selectedObject->Draw(m_DrawList, Object::Selected); } static auto isLinkHighlightedForPin = [](const Pin& pin) { return pin.m_Node->m_HighlightConnectedLinks && pin.m_Node->m_IsSelected; }; for (auto& link : m_Links) { if (!link->m_IsLive || !link->IsVisible()) continue; auto isLinkHighlighted = isLinkHighlightedForPin(*link->m_StartPin) || isLinkHighlightedForPin(*link->m_EndPin); if (!isLinkHighlighted) continue; link->Draw(m_DrawList, Object::Highlighted); } } void ed::EditorContext::DrawHoverHighlight(const Control& control, bool isSelecting) { if (isSelecting) return; auto hoveredObject = control.HotObject; if (auto dragAction = m_CurrentAction ? m_CurrentAction->AsDrag() : nullptr) hoveredObject = dragAction->m_DraggedObject; if (auto sizeAction = m_CurrentAction ? m_CurrentAction->AsSize() : nullptr) hoveredObject = sizeAction->m_SizedNode; if (hoveredObject && !IsSelected(hoveredObject) && hoveredObject->IsVisible()) hoveredObject->Draw(m_DrawList, Object::Hovered); } void ed::EditorContext::ProcessCurrentAction(const Control& control) { if (m_CurrentAction && !m_CurrentAction->Process(control)) m_CurrentAction = nullptr; if (m_CurrentAction) ImGui::SetMouseCursor(m_CurrentAction->GetCursor()); } void ed::EditorContext::ProcessNavigateAction(const Control& control) { if (m_NavigateAction.m_IsActive) m_NavigateAction.Process(control); else m_NavigateAction.Accept(control); } void ed::EditorContext::SelectNextAction(const Control& control) { if (m_CurrentAction) return; EditorAction* possibleAction = nullptr; auto accept = [&possibleAction, &control](EditorAction& action) { auto result = action.Accept(control); if (result == EditorAction::True) return true; else if (result == EditorAction::Possible) { possibleAction = &action; } else if (result == EditorAction::Possible) action.Reject(); return false; }; if (accept(m_ContextMenuAction)) m_CurrentAction = &m_ContextMenuAction; else if (accept(m_ShortcutAction)) m_CurrentAction = &m_ShortcutAction; else if (accept(m_SizeAction)) m_CurrentAction = &m_SizeAction; else if (accept(m_DragAction)) m_CurrentAction = &m_DragAction; else if (accept(m_CreateItemAction)) m_CurrentAction = &m_CreateItemAction; else if (accept(m_DeleteItemsAction)) m_CurrentAction = &m_DeleteItemsAction; else if (accept(m_SelectAction)) m_CurrentAction = &m_SelectAction; if (control.ActiveObject!= nullptr && m_CurrentAction==nullptr) { // m_CurrentAction = &m_SelectAction; } if (possibleAction) ImGui::SetMouseCursor(possibleAction->GetCursor()); if (m_CurrentAction && possibleAction) possibleAction->Reject(); } bool ed::EditorContext::BringActiveNodeToFront(const Control& control, bool isDragging) { return false; if (!control.ActiveNode) return false; if (!IsGroup(control.ActiveNode)) { auto activeNodeIt = std::find(m_Nodes.begin(), m_Nodes.end(), control.ActiveNode); std::rotate(activeNodeIt, activeNodeIt + 1, m_Nodes.end()); return false; } else if (!isDragging && m_CurrentAction && m_CurrentAction->AsDrag()) { std::vector nodes; control.ActiveNode->GetGroupedNodes(nodes); std::stable_partition(m_Nodes.begin(), m_Nodes.end(), [&nodes](Node* node) { return std::find(nodes.begin(), nodes.end(), node) == nodes.end(); }); return true; } return false; } void ed::EditorContext::SortNodesByGroupAndZOrder(bool sortGroups) { if (sortGroups || ((m_Settings.m_DirtyReason & (SaveReasonFlags::Position | SaveReasonFlags::Size)) != SaveReasonFlags::None)) { auto groupsItEnd = std::stable_partition(m_Nodes.begin(), m_Nodes.end(), IsGroup); std::sort(m_Nodes.begin(), groupsItEnd, [this](Node* lhs, Node* rhs) { const auto& lhsSize = lhs == m_SizeAction.m_SizedNode ? m_SizeAction.GetStartGroupBounds().GetSize() : lhs->m_GroupBounds.GetSize(); const auto& rhsSize = rhs == m_SizeAction.m_SizedNode ? m_SizeAction.GetStartGroupBounds().GetSize() : rhs->m_GroupBounds.GetSize(); const auto lhsArea = lhsSize.x * lhsSize.y; const auto rhsArea = rhsSize.x * rhsSize.y; return lhsArea > rhsArea; }); } std::stable_sort(m_Nodes.begin(), m_Nodes.end(), [](const auto& lhs, const auto& rhs) { return lhs->m_ZPosition < rhs->m_ZPosition; }); } void ed::EditorContext::ArrangeNodeChannels() { auto liveNodeCount = static_cast(std::count_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return node->m_IsLive; })); auto nodeChannelCount = m_DrawList->_Splitter._Count; ImDrawList_ChannelsGrow(m_DrawList, m_DrawList->_Splitter._Count + c_ChannelsPerNode * liveNodeCount + c_LinkChannelCount); int targetChannel = nodeChannelCount; auto copyNode = [this, &targetChannel](Node* node) { if (!node->m_IsLive) return; for (int i = 0; i < c_ChannelsPerNode; ++i) ImDrawList_SwapChannels(m_DrawList, node->m_Channel + i, targetChannel + i); node->m_Channel = targetChannel; targetChannel += c_ChannelsPerNode; }; auto groupsItEnd = std::find_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return !IsGroup(node); }); std::for_each(m_Nodes.begin(), groupsItEnd, copyNode); for (int i = 0; i < c_LinkChannelCount; ++i, ++targetChannel) ImDrawList_SwapChannels(m_DrawList, c_LinkStartChannel + i, targetChannel); std::for_each(groupsItEnd, m_Nodes.end(), copyNode); } void ed::EditorContext::DrawGrid() { m_DrawList->ChannelsSetCurrent(c_UserChannel_Grid); ImVec2 offset = m_Canvas.ViewOrigin() * (1.0f / m_Canvas.ViewScale()); ImU32 GRID_COLOR = GetColor(StyleColor_Grid, ImClamp(m_Canvas.ViewScale() * m_Canvas.ViewScale(), 0.0f, 1.0f)); float GRID_SX = 32.0f; float GRID_SY = 32.0f; ImVec2 VIEW_POS = m_Canvas.ViewRect().Min; ImVec2 VIEW_SIZE = m_Canvas.ViewRect().GetSize(); m_DrawList->AddRectFilled(VIEW_POS, VIEW_POS + VIEW_SIZE, GetColor(StyleColor_Bg)); for (float x = fmodf(offset.x, GRID_SX); x < VIEW_SIZE.x; x += GRID_SX) m_DrawList->AddLine(ImVec2(x, 0.0f) + VIEW_POS, ImVec2(x, VIEW_SIZE.y) + VIEW_POS, GRID_COLOR); for (float y = fmodf(offset.y, GRID_SY); y < VIEW_SIZE.y; y += GRID_SY) m_DrawList->AddLine(ImVec2(0.0f, y) + VIEW_POS, ImVec2(VIEW_SIZE.x, y) + VIEW_POS, GRID_COLOR); } void ed::EditorContext::FinalizeDrawChannels() { auto preTransformClipRect = [this](int channelIndex) { ImDrawChannel& channel = m_DrawList->_Splitter._Channels[channelIndex]; for (ImDrawCmd& cmd : channel._CmdBuffer) { auto a = ToCanvas(ImVec2(cmd.ClipRect.x, cmd.ClipRect.y)); auto b = ToCanvas(ImVec2(cmd.ClipRect.z, cmd.ClipRect.w)); cmd.ClipRect = ImVec4(a.x, a.y, b.x, b.y); } }; m_DrawList->ChannelsSetCurrent(0); auto channelCount = m_DrawList->_Splitter._Count; ImDrawList_ChannelsGrow(m_DrawList, channelCount + 3); ImDrawList_SwapChannels(m_DrawList, c_UserChannel_HintsBackground, channelCount + 0); ImDrawList_SwapChannels(m_DrawList, c_UserChannel_Hints, channelCount + 1); ImDrawList_SwapChannels(m_DrawList, c_UserChannel_Content, channelCount + 2); preTransformClipRect(channelCount + 0); preTransformClipRect(channelCount + 1); preTransformClipRect(channelCount + 2); } void ed::EditorContext::MergeChannelsAndFinishCanvas() { m_DrawList->ChannelsMerge(); if (m_IsCanvasVisible) m_Canvas.End(); ImDrawList_SwapSplitter(m_DrawList, m_Splitter); auto& style = ImGui::GetStyle(); auto borderShadowColor = style.Colors[ImGuiCol_BorderShadow]; auto borderColor = style.Colors[ImGuiCol_Border]; m_DrawList->AddRect(m_Canvas.Rect().Min + ImVec2(1, 1), m_Canvas.Rect().Max - ImVec2(1, 1), ImColor(borderShadowColor)); m_DrawList->AddRect(m_Canvas.Rect().Min, m_Canvas.Rect().Max, ImColor(borderColor)); } void ed::EditorContext::PostFrameCleanup() { ImGui::PopID(); if (!m_CurrentAction && m_IsFirstFrame && !m_Settings.m_Selection.empty()) { ClearSelection(); for (auto id : m_Settings.m_Selection) if (auto object = FindObject(id)) SelectObject(object); } if (HasSelectionChanged()) MakeDirty(SaveReasonFlags::Selection); if (m_Settings.m_IsDirty && !m_CurrentAction) SaveSettings(); m_DrawList = nullptr; m_IsFirstFrame = false; } void ed::EditorContext::UpdateControlState(const Control& control) { m_HoveredNode = control.HotNode && m_CurrentAction == nullptr ? control.HotNode->m_ID : 0; m_HoveredPin = control.HotPin && m_CurrentAction == nullptr ? control.HotPin->m_ID : 0; m_HoveredLink = control.HotLink && m_CurrentAction == nullptr ? control.HotLink->m_ID : 0; m_DoubleClickedNode = control.DoubleClickedNode ? control.DoubleClickedNode->m_ID : 0; m_DoubleClickedPin = control.DoubleClickedPin ? control.DoubleClickedPin->m_ID : 0; m_DoubleClickedLink = control.DoubleClickedLink ? control.DoubleClickedLink->m_ID : 0; m_BackgroundClickButtonIndex = control.BackgroundClickButtonIndex; m_BackgroundDoubleClickButtonIndex = control.BackgroundDoubleClickButtonIndex; } void ed::EditorContext::End() { auto control = BuildControl(m_CurrentAction && m_CurrentAction->IsDragging()); UpdateControlState(control); const bool isSelecting = m_CurrentAction && m_CurrentAction->AsSelect() != nullptr; const bool isDragging = m_CurrentAction && m_CurrentAction->AsDrag() != nullptr; DrawNodes(); DrawLinks(); DrawSelectionHighlights(control); DrawHoverHighlight(control, isSelecting); HandleControlPointDragging(); HandleGuidedLinkInteractions(control); ShowControlPointHoverCursor(control); DrawAnimations(); ProcessCurrentAction(control); ProcessNavigateAction(control); SelectNextAction(control); m_SelectAction.Draw(m_DrawList); bool sortGroups = BringActiveNodeToFront(control, isDragging); SortNodesByGroupAndZOrder(sortGroups); ArrangeNodeChannels(); DrawGrid(); FinalizeDrawChannels(); UpdateAnimations(); MergeChannelsAndFinishCanvas(); PostFrameCleanup(); ExecuteRuntime(); } //------------------------------------------------------------------------------ // From imgui_node_editor_tools.cpp //------------------------------------------------------------------------------ void ed::EditorContext::SetNodePosition(NodeId nodeId, const ImVec2& position) { auto node = FindNode(nodeId); if (!node) { node = CreateNode(nodeId); node->m_IsLive = false; } if (node->m_Bounds.Min != position) { node->m_Bounds.Translate(position - node->m_Bounds.Min); node->m_Bounds.Floor(); auto settings = m_Settings.FindNode(nodeId); if (settings) { settings->m_Location = node->m_Bounds.Min; ImVec2 currentSize = node->m_Bounds.GetSize(); if (currentSize.x > 0 && currentSize.y > 0) { settings->m_Size = currentSize; } } MakeDirty(NodeEditor::SaveReasonFlags::Position, node); } } void ed::EditorContext::SetGroupSize(NodeId nodeId, const ImVec2& size) { auto node = FindNode(nodeId); if (!node) { node = CreateNode(nodeId); node->m_IsLive = false; } node->m_Type = NodeType::Group; if (node->m_GroupBounds.GetSize() != size) { node->m_GroupBounds.Min = node->m_Bounds.Min; node->m_GroupBounds.Max = node->m_Bounds.Min + size; node->m_GroupBounds.Floor(); MakeDirty(NodeEditor::SaveReasonFlags::Size, node); } } ImVec2 ed::EditorContext::GetNodePosition(NodeId nodeId) { auto node = FindNode(nodeId); if (!node) return ImVec2(FLT_MAX, FLT_MAX); return node->m_Bounds.Min; } ImVec2 ed::EditorContext::GetNodeSize(NodeId nodeId) { auto node = FindNode(nodeId); if (!node) return ImVec2(0, 0); return node->m_Bounds.GetSize(); } void ed::EditorContext::SetNodeZPosition(NodeId nodeId, float z) { auto node = FindNode(nodeId); if (!node) { node = CreateNode(nodeId); node->m_IsLive = false; } node->m_ZPosition = z; } float ed::EditorContext::GetNodeZPosition(NodeId nodeId) { auto node = FindNode(nodeId); if (!node) return 0.0f; return node->m_ZPosition; } ed::Node* ed::EditorContext::FindNodeAt(const ImVec2& p) { for (auto node : m_Nodes) if (node->TestHit(p)) return node; return nullptr; } void ed::EditorContext::FindNodesInRect(const ImRect& r, vector& result, bool append, bool includeIntersecting) { if (!append) result.resize(0); if (ImRect_IsEmpty(r)) return; for (auto node : m_Nodes) if (node->TestHit(r, includeIntersecting)) result.push_back(node); } ed::Node* ed::EditorContext::FindNode(NodeId id) { return FindItemInLinear(m_Nodes, id); } ed::Pin* ed::EditorContext::FindPin(PinId id) { return FindItemIn(m_Pins, id); } ed::Link* ed::EditorContext::FindLink(LinkId id) { return FindItemIn(m_Links, id); } ed::Object* ed::EditorContext::FindObject(ObjectId id) { if (id.IsNodeId()) return FindNode(id.AsNodeId()); else if (id.IsLinkId()) return FindLink(id.AsLinkId()); else if (id.IsPinId()) return FindPin(id.AsPinId()); else return nullptr; } ed::Node* ed::EditorContext::GetNode(NodeId id) { auto node = FindNode(id); if (!node) node = CreateNode(id); return node; } ed::Pin* ed::EditorContext::GetPin(PinId id, PinKind kind) { if (auto pin = FindPin(id)) { pin->m_Kind = kind; return pin; } else return CreatePin(id, kind); } ed::Link* ed::EditorContext::GetLink(LinkId id) { if (auto link = FindLink(id)) return link; else return CreateLink(id); } void ed::EditorContext::MakeDirty(SaveReasonFlags reason) { m_Settings.MakeDirty(reason); } void ed::EditorContext::MakeDirty(SaveReasonFlags reason, Node* node) { m_Settings.MakeDirty(reason, node); } ed::Link* ed::EditorContext::FindLinkAt(const ImVec2& p) { for (auto& link : m_Links) { if (link->IsGuided()) { int cpIndex = link->TestHitControlPoint(p, 10.0f / m_Canvas.View().Scale); if (cpIndex >= 0) { if (ImGui::IsMouseClicked(0) && !m_CurrentAction) { m_EditingLink = link; m_DraggingControlPointIndex = cpIndex; m_ControlPointDragStart = link->m_GuidedData->ControlPoints[cpIndex].Position; } return link; } } } for (auto& link : m_Links) if (link->TestHit(p, c_LinkSelectThickness)) return link; return nullptr; } int ed::EditorContext::GetNodeIds(NodeId* nodes, int size) const { if (size <= 0) return 0; int result = 0; for (auto node : m_Nodes) { if (!node->m_IsLive) continue; *nodes++ = node->m_ID; ++result; if (--size == 0) break; } return result; } //------------------------------------------------------------------------------ // From imgui_node_editor_selection.cpp //------------------------------------------------------------------------------ void ed::EditorContext::ClearSelection() { for (auto& object : m_SelectedObjects) object->m_IsSelected = false; m_SelectedObjects.clear(); } void ed::EditorContext::SelectObject(Object* object) { m_SelectedObjects.push_back(object); object->m_IsSelected = true; } void ed::EditorContext::DeselectObject(Object* object) { auto objectIt = std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), object); if (objectIt == m_SelectedObjects.end()) return; object->m_IsSelected = false; m_SelectedObjects.erase(objectIt); } void ed::EditorContext::SetSelectedObject(Object* object) { ClearSelection(); SelectObject(object); } void ed::EditorContext::ToggleObjectSelection(Object* object) { if (IsSelected(object)) DeselectObject(object); else SelectObject(object); } bool ed::EditorContext::IsSelected(Object* object) { return object && object->m_IsSelected; } const ed::vector& ed::EditorContext::GetSelectedObjects() { return m_SelectedObjects; } bool ed::EditorContext::IsAnyNodeSelected() { for (auto object : m_SelectedObjects) if (object->AsNode()) return true; return false; } bool ed::EditorContext::IsAnyLinkSelected() { for (auto object : m_SelectedObjects) if (object->AsLink()) return true; return false; } bool ed::EditorContext::HasSelectionChanged() { return m_LastSelectedObjects != m_SelectedObjects; } //------------------------------------------------------------------------------ // From imgui_node_editor_links.cpp //------------------------------------------------------------------------------ void ed::EditorContext::FindLinksInRect(const ImRect& r, vector& result, bool append) { if (!append) result.resize(0); if (ImRect_IsEmpty(r)) return; for (auto link : m_Links) if (link->TestHit(r)) result.push_back(link); } bool ed::EditorContext::HasAnyLinks(NodeId nodeId) const { for (auto link : m_Links) { if (!link->m_IsLive) continue; if (link->m_StartPin->m_Node->m_ID == nodeId || link->m_EndPin->m_Node->m_ID == nodeId) return true; } return false; } bool ed::EditorContext::HasAnyLinks(PinId pinId) const { for (auto link : m_Links) { if (!link->m_IsLive) continue; if (link->m_StartPin->m_ID == pinId || link->m_EndPin->m_ID == pinId) return true; } return false; } int ed::EditorContext::BreakLinks(NodeId nodeId) { int result = 0; for (auto link : m_Links) { if (!link->m_IsLive) continue; if (link->m_StartPin->m_Node->m_ID == nodeId || link->m_EndPin->m_Node->m_ID == nodeId) { if (GetItemDeleter().Add(link)) ++result; } } return result; } int ed::EditorContext::BreakLinks(PinId pinId) { int result = 0; for (auto link : m_Links) { if (!link->m_IsLive) continue; if (link->m_StartPin->m_ID == pinId || link->m_EndPin->m_ID == pinId) { if (GetItemDeleter().Add(link)) ++result; } } return result; } void ed::EditorContext::FindLinksForNode(NodeId nodeId, vector& result, bool add) { if (!add) result.clear(); for (auto link : m_Links) { if (!link->m_IsLive) continue; if (link->m_StartPin->m_Node->m_ID == nodeId || link->m_EndPin->m_Node->m_ID == nodeId) result.push_back(link); } } bool ed::EditorContext::PinHadAnyLinks(PinId pinId) { auto pin = FindPin(pinId); if (!pin || !pin->m_IsLive) return false; return pin->m_HasConnection || pin->m_HadConnection; } void ed::EditorContext::NotifyLinkDeleted(Link* link) { if (m_LastActiveLink == link) m_LastActiveLink = nullptr; } ed::Link* ed::EditorContext::CreateLink(LinkId id) { IM_ASSERT(nullptr == FindObject(id)); auto link = new Link(this, id); m_Links.push_back({id, link}); std::sort(m_Links.begin(), m_Links.end()); return link; } ed::GuidedLink* ed::EditorContext::GetGuidedLinkData(LinkId id) { auto it = m_GuidedLinks.find(id); return (it != m_GuidedLinks.end()) ? it->second : nullptr; } ed::GuidedLink* ed::EditorContext::CreateGuidedLinkData(LinkId id) { auto existing = GetGuidedLinkData(id); if (existing) { return existing; } auto guidedLink = new ed::GuidedLink(id); m_GuidedLinks[id] = guidedLink; if (!m_Settings.FindLink(id)) m_Settings.AddLink(id); return guidedLink; } void ed::EditorContext::DestroyGuidedLinkData(LinkId id) { auto it = m_GuidedLinks.find(id); if (it != m_GuidedLinks.end()) { delete it->second; m_GuidedLinks.erase(it); } m_Settings.RemoveLink(id); } bool ed::EditorContext::IsEditingLink(const Link* link) const { return link && m_EditingLink == link; } void ed::EditorContext::HandleControlPointDragging() { if (!m_EditingLink || m_DraggingControlPointIndex < 0) return; if (ImGui::IsMouseDragging(0, 0.0f)) { auto dragOffset = ImGui::GetMouseDragDelta(0, 0.0f); auto newPos = m_ControlPointDragStart + dragOffset; if (m_DraggingControlPointIndex < static_cast(m_EditingLink->m_GuidedData->ControlPoints.size())) { m_EditingLink->m_GuidedData->ControlPoints[m_DraggingControlPointIndex].Position = m_EditingLink->m_GuidedData->SnapToGrid(newPos); MakeDirty(SaveReasonFlags::User); // Mark link as user-manipulated (preserve waypoints, disable auto-adjustment) if (m_Config.MarkLinkUserManipulated) { m_Config.MarkLinkUserManipulated(m_EditingLink->m_ID, m_Config.UserPointer); } } ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); } else if (!ImGui::IsMouseDown(0)) { // Validate that the control point still exists before accessing it if (!m_EditingLink || !m_EditingLink->m_GuidedData || m_DraggingControlPointIndex < 0 || m_DraggingControlPointIndex >= static_cast(m_EditingLink->m_GuidedData->ControlPoints.size())) { // Control point was removed or index is invalid - clear dragging state m_EditingLink = nullptr; m_DraggingControlPointIndex = -1; return; } auto& controlPoints = m_EditingLink->m_GuidedData->ControlPoints; auto& currentPoint = controlPoints[m_DraggingControlPointIndex]; const float snapThreshold = 50.0f; std::vector candidatePoints; m_EditingLink->UpdateEndpoints(); candidatePoints.push_back(m_EditingLink->m_Start); candidatePoints.push_back(m_EditingLink->m_End); if (m_DraggingControlPointIndex > 0) { candidatePoints.push_back(controlPoints[m_DraggingControlPointIndex - 1].Position); if (m_DraggingControlPointIndex > 1) candidatePoints.push_back(controlPoints[m_DraggingControlPointIndex - 2].Position); } if (m_DraggingControlPointIndex < static_cast(controlPoints.size()) - 1) { candidatePoints.push_back(controlPoints[m_DraggingControlPointIndex + 1].Position); if (m_DraggingControlPointIndex < static_cast(controlPoints.size()) - 2) candidatePoints.push_back(controlPoints[m_DraggingControlPointIndex + 2].Position); } ImVec2 originalPos = currentPoint.Position; ImVec2 bestAlignment = originalPos; float bestXDist = FLT_MAX; float bestYDist = FLT_MAX; float bestCandidateX = originalPos.x; float bestCandidateY = originalPos.y; for (const auto& candidate : candidatePoints) { float xDist = fabs(originalPos.x - candidate.x); float yDist = fabs(originalPos.y - candidate.y); if (xDist < snapThreshold && xDist < bestXDist) { bestXDist = xDist; bestCandidateX = candidate.x; } if (yDist < snapThreshold && yDist < bestYDist) { bestYDist = yDist; bestCandidateY = candidate.y; } } if (fabs(bestCandidateX - originalPos.x) > 0.01f) bestAlignment.x = bestCandidateX; if (fabs(bestCandidateY - originalPos.y) > 0.01f) bestAlignment.y = bestCandidateY; currentPoint.Position = bestAlignment; if (bestAlignment.x != originalPos.x || bestAlignment.y != originalPos.y) { MakeDirty(SaveReasonFlags::User); // Mark link as user-manipulated (preserve waypoints, disable auto-adjustment) if (m_Config.MarkLinkUserManipulated) { m_Config.MarkLinkUserManipulated(m_EditingLink->m_ID, m_Config.UserPointer); } } m_EditingLink = nullptr; m_DraggingControlPointIndex = -1; } } void ed::EditorContext::HandleGuidedLinkInteractions(const Control& control) { if (m_CurrentAction) return; if (!control.DoubleClickedLink) return; auto link = control.DoubleClickedLink; auto mouseCanvasPos = ImGui::GetMousePos(); link->UpdateEndpoints(); if (link->IsGuided()) { int cpIndex = link->TestHitControlPoint(mouseCanvasPos, 10.0f / m_Canvas.View().Scale); if (cpIndex >= 0) { link->m_GuidedData->RemoveControlPoint(cpIndex); MakeDirty(SaveReasonFlags::User); // Clear dragging state if the removed control point was being dragged if (m_EditingLink == link && m_DraggingControlPointIndex == cpIndex) { m_EditingLink = nullptr; m_DraggingControlPointIndex = -1; } // Adjust dragging index if a control point before the dragged one was removed else if (m_EditingLink == link && m_DraggingControlPointIndex >= 0 && m_DraggingControlPointIndex > cpIndex) { m_DraggingControlPointIndex--; } // Mark link as user-manipulated (preserve waypoints, disable auto-adjustment) if (m_Config.MarkLinkUserManipulated) { m_Config.MarkLinkUserManipulated(link->m_ID, m_Config.UserPointer); } if (link->m_GuidedData->ControlPoints.empty()) link->m_GuidedData->Mode = LinkMode::Auto; } else { AddControlPointToGuidedLink(link, mouseCanvasPos); // Mark link as user-manipulated (preserve waypoints, disable auto-adjustment) if (m_Config.MarkLinkUserManipulated) { m_Config.MarkLinkUserManipulated(link->m_ID, m_Config.UserPointer); } } } else { ConvertLinkToGuidedMode(link, mouseCanvasPos); // Mark link as user-manipulated (preserve waypoints, disable auto-adjustment) if (m_Config.MarkLinkUserManipulated) { m_Config.MarkLinkUserManipulated(link->m_ID, m_Config.UserPointer); } } } void ed::EditorContext::AddControlPointToGuidedLink(Link* link, const ImVec2& mousePos) { ImVec2 pointOnPath = mousePos; std::vector points; points.push_back(link->m_Start); for (const auto& cp : link->m_GuidedData->ControlPoints) points.push_back(cp.Position); points.push_back(link->m_End); float minDistSq = FLT_MAX; for (size_t i = 0; i < points.size() - 1; ++i) { const ImVec2& p0 = points[i]; const ImVec2& p1 = points[i + 1]; const ImVec2 delta = p1 - p0; const float lengthSq = delta.x * delta.x + delta.y * delta.y; if (lengthSq > 0.0001f) { const ImVec2 pointDelta = mousePos - p0; float t = (pointDelta.x * delta.x + pointDelta.y * delta.y) / lengthSq; t = ImClamp(t, 0.0f, 1.0f); const ImVec2 projected = p0 + delta * t; const ImVec2 diff = mousePos - projected; const float distSq = diff.x * diff.x + diff.y * diff.y; if (distSq < minDistSq) { minDistSq = distSq; pointOnPath = projected; } } } link->m_GuidedData->AddControlPoint(pointOnPath); MakeDirty(SaveReasonFlags::User); } void ed::EditorContext::ConvertLinkToGuidedMode(Link* link, const ImVec2& mousePos) { ImVec2 pointOnPath = mousePos; const auto bezier = link->GetCurve(); const auto projection = ImProjectOnCubicBezier(mousePos, bezier.P0, bezier.P1, bezier.P2, bezier.P3, 50); pointOnPath = projection.Point; auto guidedData = CreateGuidedLinkData(link->m_ID); if (guidedData) { guidedData->Mode = LinkMode::Guided; guidedData->AddControlPoint(pointOnPath); link->m_GuidedData = guidedData; MakeDirty(SaveReasonFlags::User); } } void ed::EditorContext::ShowControlPointHoverCursor(const Control& control) { if (!control.HotLink || !control.HotLink->IsGuided()) return; if (control.HotNode || control.HotPin || control.ActiveNode) return; auto link = control.HotLink; auto mousePos = ToCanvas(ImGui::GetMousePos()); int cpIndex = link->TestHitControlPoint(mousePos, 10.0f / m_Canvas.View().Scale); if (cpIndex >= 0) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); } //------------------------------------------------------------------------------ // From imgui_node_editor_animation.cpp //------------------------------------------------------------------------------ void ed::EditorContext::DrawAnimations() { for (auto controller : m_AnimationControllers) controller->Draw(m_DrawList); } void ed::EditorContext::RegisterAnimation(Animation* animation) { m_LiveAnimations.push_back(animation); } void ed::EditorContext::UnregisterAnimation(Animation* animation) { auto it = std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), animation); if (it != m_LiveAnimations.end()) m_LiveAnimations.erase(it); } void ed::EditorContext::UpdateAnimations() { m_LastLiveAnimations = m_LiveAnimations; for (auto animation : m_LastLiveAnimations) { const bool isLive = (std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), animation) != m_LiveAnimations.end()); if (isLive) animation->Update(); } } void ed::EditorContext::Flow(Link* link, FlowDirection direction) { m_FlowAnimationController.Flow(link, direction); } //------------------------------------------------------------------------------ // From imgui_node_editor_runtime.cpp //------------------------------------------------------------------------------ void ed::EditorContext::ExecuteRuntime() { if (!m_Config.UserPointer) return; typedef void (*ExecuteRuntimeCallback)(void* app); static ExecuteRuntimeCallback s_RuntimeCallback = nullptr; static void* s_RuntimeApp = nullptr; if (s_RuntimeCallback && s_RuntimeApp == m_Config.UserPointer) { s_RuntimeCallback(s_RuntimeApp); } } void ed::EditorContext::RegisterRuntimeCallback(void* app, void (*callback)(void*)) { typedef void (*ExecuteRuntimeCallback)(void* app); static ExecuteRuntimeCallback s_RuntimeCallback = nullptr; static void* s_RuntimeApp = nullptr; s_RuntimeApp = app; s_RuntimeCallback = callback; } //------------------------------------------------------------------------------ // From imgui_node_editor_store.cpp //------------------------------------------------------------------------------ void ed::EditorContext::LoadSettings() { auto settingsData = m_Config.Load(); ed::Settings::Parse(settingsData, m_Settings); if (ImRect_IsEmpty(m_Settings.m_VisibleRect)) { m_NavigateAction.m_Scroll = m_Settings.m_ViewScroll; m_NavigateAction.m_Zoom = m_Settings.m_ViewZoom; } } void ed::EditorContext::SaveSettings() { m_Config.BeginSave(); int savedNodeCount = 0; int liveNodeCount = 0; for (auto& node : m_Nodes) { if (node->m_IsLive) liveNodeCount++; auto settings = m_Settings.FindNode(node->m_ID); settings->m_Location = node->m_Bounds.Min; settings->m_Size = node->m_Bounds.GetSize(); if (IsGroup(node)) settings->m_GroupSize = node->m_GroupBounds.GetSize(); if (!node->m_RestoreState && settings->m_IsDirty && m_Config.SaveNodeSettings) { if (m_Config.SaveNode(node->m_ID, settings->Serialize().dump(), settings->m_DirtyReason)) settings->ClearDirty(); } if (settings->m_WasUsed) savedNodeCount++; } for (auto& linkPair : m_GuidedLinks) { auto linkId = linkPair.first; auto guidedData = linkPair.second; if (guidedData->Mode != LinkMode::Auto) { auto settings = m_Settings.FindLink(linkId); if (!settings) settings = m_Settings.AddLink(linkId); settings->m_Mode = guidedData->Mode; settings->m_EnableSnapping = guidedData->EnableSnapping; settings->m_SnapGridSize = guidedData->SnapGridSize; settings->m_ControlPoints.clear(); if (guidedData->Mode == LinkMode::Guided) { for (const auto& cp : guidedData->ControlPoints) settings->m_ControlPoints.push_back(cp.Position); } } } m_Settings.m_Selection.resize(0); for (auto& object : m_SelectedObjects) m_Settings.m_Selection.push_back(object->ID()); m_Settings.m_ViewScroll = m_NavigateAction.m_Scroll; m_Settings.m_ViewZoom = m_NavigateAction.m_Zoom; m_Settings.m_VisibleRect = m_NavigateAction.m_VisibleRect; if (m_Config.Save(m_Settings.Serialize(), m_Settings.m_DirtyReason)) m_Settings.ClearDirty(); m_Config.EndSave(); }