deargui-vpl/external/imgui_node/EditorContext.cpp
2026-02-03 18:25:25 +01:00

2131 lines
68 KiB
C++

//------------------------------------------------------------------------------
// 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 <cstdio>
# include <string>
# include <fstream>
# include <bitset>
# include <climits>
# include <algorithm>
# include <sstream>
# include <streambuf>
# include <type_traits>
# include <cstdarg>
# include <vector>
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<Node*> nodesToProcess;
if (m_Config.GetContainerNodeIds && m_Config.UserPointer)
{
int nodeCount = m_Config.GetContainerNodeIds(m_Config.UserPointer, nullptr, 0);
if (nodeCount > 0)
{
std::vector<NodeId> 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<int>(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 "<unknown>";
};
auto getActiveObjectName = [&control, &getObjectName]()
{
if (control.ActiveObject)
return getObjectName(control.ActiveObject);
else if (control.BackgroundActive)
return "Background";
else
return "<unknown>";
};
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() : "<none>");
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<Node*> 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<int>(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<Node*>& 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::Object*>& 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<Link*>& 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<Link*>& 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<int>(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<int>(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<ImVec2> 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<int>(controlPoints.size()) - 1)
{
candidatePoints.push_back(controlPoints[m_DraggingControlPointIndex + 1].Position);
if (m_DraggingControlPointIndex < static_cast<int>(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<ImVec2> 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();
}