aboutsummaryrefslogtreecommitdiff
path: root/3rdparty/imgui-node-editor/imgui_node_editor.cpp
diff options
context:
space:
mode:
Diffstat (limited to '3rdparty/imgui-node-editor/imgui_node_editor.cpp')
-rw-r--r--3rdparty/imgui-node-editor/imgui_node_editor.cpp5316
1 files changed, 5316 insertions, 0 deletions
diff --git a/3rdparty/imgui-node-editor/imgui_node_editor.cpp b/3rdparty/imgui-node-editor/imgui_node_editor.cpp
new file mode 100644
index 0000000..c1911c3
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_node_editor.cpp
@@ -0,0 +1,5316 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# include "imgui_node_editor_internal.h"
+# include <cstdio> // snprintf
+# include <string>
+# include <fstream>
+# include <bitset>
+# include <climits>
+# include <algorithm>
+# include <sstream>
+# include <streambuf>
+# include <type_traits>
+
+// https://stackoverflow.com/a/8597498
+# define DECLARE_HAS_NESTED(Name, Member) \
+ \
+ template<class T> \
+ struct has_nested_ ## Name \
+ { \
+ typedef char yes; \
+ typedef yes(&no)[2]; \
+ \
+ template<class U> static yes test(decltype(U::Member)*); \
+ template<class U> static no test(...); \
+ \
+ static bool const value = sizeof(test<T>(0)) == sizeof(yes); \
+ };
+
+
+namespace ax {
+namespace NodeEditor {
+namespace Detail {
+
+# define DECLARE_KEY_TESTER(Key) \
+ DECLARE_HAS_NESTED(Key, Key) \
+ struct KeyTester_ ## Key \
+ { \
+ template <typename T> \
+ static int Get(typename std::enable_if<has_nested_ ## Key<ImGuiKey_>::value, T>::type*) \
+ { \
+ return ImGui::GetKeyIndex(T::Key); \
+ } \
+ \
+ template <typename T> \
+ static int Get(typename std::enable_if<!has_nested_ ## Key<ImGuiKey_>::value, T>::type*) \
+ { \
+ return -1; \
+ } \
+ }
+
+DECLARE_KEY_TESTER(ImGuiKey_F);
+DECLARE_KEY_TESTER(ImGuiKey_D);
+
+static inline int GetKeyIndexForF()
+{
+ return KeyTester_ImGuiKey_F::Get<ImGuiKey_>(nullptr);
+}
+
+static inline int GetKeyIndexForD()
+{
+ return KeyTester_ImGuiKey_D::Get<ImGuiKey_>(nullptr);
+}
+
+} // namespace Detail
+} // namespace NodeEditor
+} // namespace ax
+
+
+//------------------------------------------------------------------------------
+namespace ed = ax::NodeEditor::Detail;
+
+
+//------------------------------------------------------------------------------
+static const int c_BackgroundChannelCount = 1;
+static const int c_LinkChannelCount = 4;
+static const int c_UserLayersCount = 5;
+
+static const int c_UserLayerChannelStart = 0;
+static const int c_BackgroundChannelStart = c_UserLayerChannelStart + c_UserLayersCount;
+static const int c_LinkStartChannel = c_BackgroundChannelStart + c_BackgroundChannelCount;
+static const int c_NodeStartChannel = c_LinkStartChannel + c_LinkChannelCount;
+
+static const int c_BackgroundChannel_SelectionRect = c_BackgroundChannelStart + 0;
+
+static const int c_UserChannel_Content = c_UserLayerChannelStart + 1;
+static const int c_UserChannel_Grid = c_UserLayerChannelStart + 2;
+static const int c_UserChannel_HintsBackground = c_UserLayerChannelStart + 3;
+static const int c_UserChannel_Hints = c_UserLayerChannelStart + 4;
+
+static const int c_LinkChannel_Selection = c_LinkStartChannel + 0;
+static const int c_LinkChannel_Links = c_LinkStartChannel + 1;
+static const int c_LinkChannel_Flow = c_LinkStartChannel + 2;
+static const int c_LinkChannel_NewLink = c_LinkStartChannel + 3;
+
+static const int c_ChannelsPerNode = 5;
+static const int c_NodeBaseChannel = 0;
+static const int c_NodeBackgroundChannel = 1;
+static const int c_NodeUserBackgroundChannel = 2;
+static const int c_NodePinChannel = 3;
+static const int c_NodeContentChannel = 4;
+
+static const float c_GroupSelectThickness = 6.0f; // canvas pixels
+static const float c_LinkSelectThickness = 5.0f; // canvas pixels
+static const float c_NavigationZoomMargin = 0.1f; // percentage of visible bounds
+static const float c_MouseZoomDuration = 0.15f; // seconds
+static const float c_SelectionFadeOutDuration = 0.15f; // seconds
+static const auto c_ScrollButtonIndex = 1;
+
+
+//------------------------------------------------------------------------------
+# if defined(_DEBUG) && defined(_WIN32)
+extern "C" __declspec(dllimport) void __stdcall OutputDebugStringA(const char* string);
+
+static void LogV(const char* fmt, va_list args)
+{
+ const int buffer_size = 1024;
+ static char buffer[1024];
+
+ vsnprintf(buffer, buffer_size - 1, fmt, args);
+ buffer[buffer_size - 1] = 0;
+
+ ImGui::LogText("\nNode Editor: %s", buffer);
+
+ OutputDebugStringA("NodeEditor: ");
+ OutputDebugStringA(buffer);
+ OutputDebugStringA("\n");
+}
+# endif
+
+void ed::Log(const char* fmt, ...)
+{
+# if defined(_DEBUG) && defined(_WIN32)
+ va_list args;
+ va_start(args, fmt);
+ LogV(fmt, args);
+ va_end(args);
+# endif
+}
+
+
+//------------------------------------------------------------------------------
+static bool IsGroup(const ed::Node* node)
+{
+ if (node && node->m_Type == ed::NodeType::Group)
+ return true;
+ else
+ return false;
+}
+
+
+//------------------------------------------------------------------------------
+static void ImDrawListSplitter_Grow(ImDrawList* draw_list, ImDrawListSplitter* splitter, int channels_count)
+{
+ IM_ASSERT(splitter != nullptr);
+ IM_ASSERT(splitter->_Count <= channels_count);
+
+ if (splitter->_Count == 1)
+ {
+ splitter->Split(draw_list, channels_count);
+ return;
+ }
+
+ int old_channels_count = splitter->_Channels.Size;
+ if (old_channels_count < channels_count)
+ splitter->_Channels.resize(channels_count);
+ int old_used_channels_count = splitter->_Count;
+ splitter->_Count = channels_count;
+
+ for (int i = old_used_channels_count; i < channels_count; i++)
+ {
+ if (i >= old_channels_count)
+ {
+ IM_PLACEMENT_NEW(&splitter->_Channels[i]) ImDrawChannel();
+ }
+ else
+ {
+ splitter->_Channels[i]._CmdBuffer.resize(0);
+ splitter->_Channels[i]._IdxBuffer.resize(0);
+ }
+ if (splitter->_Channels[i]._CmdBuffer.Size == 0)
+ {
+ ImDrawCmd draw_cmd;
+ draw_cmd.ClipRect = draw_list->_ClipRectStack.back();
+ draw_cmd.TextureId = draw_list->_TextureIdStack.back();
+ splitter->_Channels[i]._CmdBuffer.push_back(draw_cmd);
+ }
+ }
+}
+
+static void ImDrawList_ChannelsGrow(ImDrawList* draw_list, int channels_count)
+{
+ ImDrawListSplitter_Grow(draw_list, &draw_list->_Splitter, channels_count);
+}
+
+static void ImDrawListSplitter_SwapChannels(ImDrawListSplitter* splitter, int left, int right)
+{
+ IM_ASSERT(left < splitter->_Count && right < splitter->_Count);
+ if (left == right)
+ return;
+
+ auto currentChannel = splitter->_Current;
+
+ auto* leftCmdBuffer = &splitter->_Channels[left]._CmdBuffer;
+ auto* leftIdxBuffer = &splitter->_Channels[left]._IdxBuffer;
+ auto* rightCmdBuffer = &splitter->_Channels[right]._CmdBuffer;
+ auto* rightIdxBuffer = &splitter->_Channels[right]._IdxBuffer;
+
+ leftCmdBuffer->swap(*rightCmdBuffer);
+ leftIdxBuffer->swap(*rightIdxBuffer);
+
+ if (currentChannel == left)
+ splitter->_Current = right;
+ else if (currentChannel == right)
+ splitter->_Current = left;
+}
+
+static void ImDrawList_SwapChannels(ImDrawList* drawList, int left, int right)
+{
+ ImDrawListSplitter_SwapChannels(&drawList->_Splitter, left, right);
+}
+
+static void ImDrawList_SwapSplitter(ImDrawList* drawList, ImDrawListSplitter& splitter)
+{
+ auto& currentSplitter = drawList->_Splitter;
+
+ std::swap(currentSplitter._Current, splitter._Current);
+ std::swap(currentSplitter._Count, splitter._Count);
+ currentSplitter._Channels.swap(splitter._Channels);
+}
+
+//static void ImDrawList_TransformChannel_Inner(ImVector<ImDrawVert>& vtxBuffer, const ImVector<ImDrawIdx>& idxBuffer, const ImVector<ImDrawCmd>& cmdBuffer, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& postOffset)
+//{
+// auto idxRead = idxBuffer.Data;
+//
+// int indexOffset = 0;
+// for (auto& cmd : cmdBuffer)
+// {
+// auto idxCount = cmd.ElemCount;
+//
+// if (idxCount == 0) continue;
+//
+// auto minIndex = idxRead[indexOffset];
+// auto maxIndex = idxRead[indexOffset];
+//
+// for (auto i = 1u; i < idxCount; ++i)
+// {
+// auto idx = idxRead[indexOffset + i];
+// minIndex = std::min(minIndex, idx);
+// maxIndex = ImMax(maxIndex, idx);
+// }
+//
+// for (auto vtx = vtxBuffer.Data + minIndex, vtxEnd = vtxBuffer.Data + maxIndex + 1; vtx < vtxEnd; ++vtx)
+// {
+// vtx->pos.x = (vtx->pos.x + preOffset.x) * scale.x + postOffset.x;
+// vtx->pos.y = (vtx->pos.y + preOffset.y) * scale.y + postOffset.y;
+// }
+//
+// indexOffset += idxCount;
+// }
+//}
+
+//static void ImDrawList_TransformChannels(ImDrawList* drawList, int begin, int end, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& postOffset)
+//{
+// int lastCurrentChannel = drawList->_ChannelsCurrent;
+// if (lastCurrentChannel != 0)
+// drawList->ChannelsSetCurrent(0);
+//
+// auto& vtxBuffer = drawList->VtxBuffer;
+//
+// if (begin == 0 && begin != end)
+// {
+// ImDrawList_TransformChannel_Inner(vtxBuffer, drawList->IdxBuffer, drawList->CmdBuffer, preOffset, scale, postOffset);
+// ++begin;
+// }
+//
+// for (int channelIndex = begin; channelIndex < end; ++channelIndex)
+// {
+// auto& channel = drawList->_Channels[channelIndex];
+// ImDrawList_TransformChannel_Inner(vtxBuffer, channel.IdxBuffer, channel.CmdBuffer, preOffset, scale, postOffset);
+// }
+//
+// if (lastCurrentChannel != 0)
+// drawList->ChannelsSetCurrent(lastCurrentChannel);
+//}
+
+//static void ImDrawList_ClampClipRects_Inner(ImVector<ImDrawCmd>& cmdBuffer, const ImVec4& clipRect, const ImVec2& offset)
+//{
+// for (auto& cmd : cmdBuffer)
+// {
+// cmd.ClipRect.x = ImMax(cmd.ClipRect.x + offset.x, clipRect.x);
+// cmd.ClipRect.y = ImMax(cmd.ClipRect.y + offset.y, clipRect.y);
+// cmd.ClipRect.z = std::min(cmd.ClipRect.z + offset.x, clipRect.z);
+// cmd.ClipRect.w = std::min(cmd.ClipRect.w + offset.y, clipRect.w);
+// }
+//}
+
+//static void ImDrawList_TranslateAndClampClipRects(ImDrawList* drawList, int begin, int end, const ImVec2& offset)
+//{
+// int lastCurrentChannel = drawList->_ChannelsCurrent;
+// if (lastCurrentChannel != 0)
+// drawList->ChannelsSetCurrent(0);
+//
+// auto clipRect = drawList->_ClipRectStack.back();
+//
+// if (begin == 0 && begin != end)
+// {
+// ImDrawList_ClampClipRects_Inner(drawList->CmdBuffer, clipRect, offset);
+// ++begin;
+// }
+//
+// for (int channelIndex = begin; channelIndex < end; ++channelIndex)
+// {
+// auto& channel = drawList->_Channels[channelIndex];
+// ImDrawList_ClampClipRects_Inner(channel.CmdBuffer, clipRect, offset);
+// }
+//
+// if (lastCurrentChannel != 0)
+// drawList->ChannelsSetCurrent(lastCurrentChannel);
+//}
+
+static void ImDrawList_PathBezierOffset(ImDrawList* drawList, float offset, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3)
+{
+ using namespace ed;
+
+ auto acceptPoint = [drawList, offset](const ImCubicBezierSubdivideSample& r)
+ {
+ drawList->PathLineTo(r.Point + ImNormalized(ImVec2(-r.Tangent.y, r.Tangent.x)) * offset);
+ };
+
+ ImCubicBezierSubdivide(acceptPoint, p0, p1, p2, p3);
+}
+
+/*
+static void ImDrawList_PolyFillScanFlood(ImDrawList *draw, std::vector<ImVec2>* poly, ImColor color, int gap = 1, float strokeWidth = 1.0f)
+{
+ std::vector<ImVec2> scanHits;
+ ImVec2 min, max; // polygon min/max points
+ auto io = ImGui::GetIO();
+ float y;
+ bool isMinMaxDone = false;
+ unsigned int polysize = poly->size();
+
+ // find the orthagonal bounding box
+ // probably can put this as a predefined
+ if (!isMinMaxDone)
+ {
+ min.x = min.y = FLT_MAX;
+ max.x = max.y = FLT_MIN;
+ for (auto p : *poly)
+ {
+ if (p.x < min.x) min.x = p.x;
+ if (p.y < min.y) min.y = p.y;
+ if (p.x > max.x) max.x = p.x;
+ if (p.y > max.y) max.y = p.y;
+ }
+ isMinMaxDone = true;
+ }
+
+ // Bounds check
+ if ((max.x < 0) || (min.x > io.DisplaySize.x) || (max.y < 0) || (min.y > io.DisplaySize.y)) return;
+
+ // Vertically clip
+ if (min.y < 0) min.y = 0;
+ if (max.y > io.DisplaySize.y) max.y = io.DisplaySize.y;
+
+ // so we know we start on the outside of the object we step out by 1.
+ min.x -= 1;
+ max.x += 1;
+
+ // Initialise our starting conditions
+ y = min.y;
+
+ // Go through each scan line iteratively, jumping by 'gap' pixels each time
+ while (y < max.y)
+ {
+ scanHits.clear();
+
+ {
+ int jump = 1;
+ ImVec2 fp = poly->at(0);
+
+ for (size_t i = 0; i < polysize - 1; i++)
+ {
+ ImVec2 pa = poly->at(i);
+ ImVec2 pb = poly->at(i + 1);
+
+ // jump double/dud points
+ if (pa.x == pb.x && pa.y == pb.y) continue;
+
+ // if we encounter our hull/poly start point, then we've now created the
+ // closed
+ // hull, jump the next segment and reset the first-point
+ if ((!jump) && (fp.x == pb.x) && (fp.y == pb.y))
+ {
+ if (i < polysize - 2)
+ {
+ fp = poly->at(i + 2);
+ jump = 1;
+ i++;
+ }
+ }
+ else
+ {
+ jump = 0;
+ }
+
+ // test to see if this segment makes the scan-cut.
+ if ((pa.y > pb.y && y < pa.y && y > pb.y) || (pa.y < pb.y && y > pa.y && y < pb.y))
+ {
+ ImVec2 intersect;
+
+ intersect.y = y;
+ if (pa.x == pb.x)
+ {
+ intersect.x = pa.x;
+ }
+ else
+ {
+ intersect.x = (pb.x - pa.x) / (pb.y - pa.y) * (y - pa.y) + pa.x;
+ }
+ scanHits.push_back(intersect);
+ }
+ }
+
+ // Sort the scan hits by X, so we have a proper left->right ordering
+ sort(scanHits.begin(), scanHits.end(), [](ImVec2 const &a, ImVec2 const &b) { return a.x < b.x; });
+
+ // generate the line segments.
+ {
+ int i = 0;
+ int l = scanHits.size() - 1; // we need pairs of points, this prevents segfault.
+ for (i = 0; i < l; i += 2)
+ {
+ draw->AddLine(scanHits[i], scanHits[i + 1], color, strokeWidth);
+ }
+ }
+ }
+ y += gap;
+ } // for each scan line
+ scanHits.clear();
+}
+*/
+
+static void ImDrawList_AddBezierWithArrows(ImDrawList* drawList, const ImCubicBezierPoints& curve, float thickness,
+ float startArrowSize, float startArrowWidth, float endArrowSize, float endArrowWidth,
+ bool fill, ImU32 color, float strokeThickness)
+{
+ using namespace ax;
+
+ if ((color >> 24) == 0)
+ return;
+
+ const auto half_thickness = thickness * 0.5f;
+
+ if (fill)
+ {
+ drawList->AddBezierCurve(curve.P0, curve.P1, curve.P2, curve.P3, color, thickness);
+
+ if (startArrowSize > 0.0f)
+ {
+ const auto start_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f));
+ const auto start_n = ImVec2(-start_dir.y, start_dir.x);
+ const auto half_width = startArrowWidth * 0.5f;
+ const auto tip = curve.P0 - start_dir * startArrowSize;
+
+ drawList->PathLineTo(curve.P0 - start_n * ImMax(half_width, half_thickness));
+ drawList->PathLineTo(curve.P0 + start_n * ImMax(half_width, half_thickness));
+ drawList->PathLineTo(tip);
+ drawList->PathFillConvex(color);
+ }
+
+ if (endArrowSize > 0.0f)
+ {
+ const auto end_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f));
+ const auto end_n = ImVec2( -end_dir.y, end_dir.x);
+ const auto half_width = endArrowWidth * 0.5f;
+ const auto tip = curve.P3 + end_dir * endArrowSize;
+
+ drawList->PathLineTo(curve.P3 + end_n * ImMax(half_width, half_thickness));
+ drawList->PathLineTo(curve.P3 - end_n * ImMax(half_width, half_thickness));
+ drawList->PathLineTo(tip);
+ drawList->PathFillConvex(color);
+ }
+ }
+ else
+ {
+ if (startArrowSize > 0.0f)
+ {
+ const auto start_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f));
+ const auto start_n = ImVec2(-start_dir.y, start_dir.x);
+ const auto half_width = startArrowWidth * 0.5f;
+ const auto tip = curve.P0 - start_dir * startArrowSize;
+
+ if (half_width > half_thickness)
+ drawList->PathLineTo(curve.P0 - start_n * half_width);
+ drawList->PathLineTo(tip);
+ if (half_width > half_thickness)
+ drawList->PathLineTo(curve.P0 + start_n * half_width);
+ }
+
+ ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P0, curve.P1, curve.P2, curve.P3);
+
+ if (endArrowSize > 0.0f)
+ {
+ const auto end_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f));
+ const auto end_n = ImVec2( -end_dir.y, end_dir.x);
+ const auto half_width = endArrowWidth * 0.5f;
+ const auto tip = curve.P3 + end_dir * endArrowSize;
+
+ if (half_width > half_thickness)
+ drawList->PathLineTo(curve.P3 + end_n * half_width);
+ drawList->PathLineTo(tip);
+ if (half_width > half_thickness)
+ drawList->PathLineTo(curve.P3 - end_n * half_width);
+ }
+
+ ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P3, curve.P2, curve.P1, curve.P0);
+
+ drawList->PathStroke(color, true, strokeThickness);
+ }
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Pin
+//
+//------------------------------------------------------------------------------
+void ed::Pin::Draw(ImDrawList* drawList, DrawFlags flags)
+{
+ if (flags & Hovered)
+ {
+ drawList->ChannelsSetCurrent(m_Node->m_Channel + c_NodePinChannel);
+
+ drawList->AddRectFilled(m_Bounds.Min, m_Bounds.Max,
+ m_Color, m_Rounding, m_Corners);
+
+ if (m_BorderWidth > 0.0f)
+ {
+ FringeScaleScope fringe(1.0f);
+ drawList->AddRect(m_Bounds.Min, m_Bounds.Max,
+ m_BorderColor, m_Rounding, m_Corners, m_BorderWidth);
+ }
+
+ if (!Editor->IsSelected(m_Node))
+ m_Node->Draw(drawList, flags);
+ }
+}
+
+ImVec2 ed::Pin::GetClosestPoint(const ImVec2& p) const
+{
+ return ImRect_ClosestPoint(m_Pivot, p, true, m_Radius + m_ArrowSize);
+}
+
+ImLine ed::Pin::GetClosestLine(const Pin* pin) const
+{
+ return ImRect_ClosestLine(m_Pivot, pin->m_Pivot, m_Radius + m_ArrowSize, pin->m_Radius + pin->m_ArrowSize);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Node
+//
+//------------------------------------------------------------------------------
+bool ed::Node::AcceptDrag()
+{
+ m_DragStart = m_Bounds.Min;
+ return true;
+}
+
+void ed::Node::UpdateDrag(const ImVec2& offset)
+{
+ auto size = m_Bounds.GetSize();
+ m_Bounds.Min = ImFloor(m_DragStart + offset);
+ m_Bounds.Max = m_Bounds.Min + size;
+}
+
+bool ed::Node::EndDrag()
+{
+ return m_Bounds.Min != m_DragStart;
+}
+
+void ed::Node::Draw(ImDrawList* drawList, DrawFlags flags)
+{
+ if (flags == Detail::Object::None)
+ {
+ drawList->ChannelsSetCurrent(m_Channel + c_NodeBackgroundChannel);
+
+ drawList->AddRectFilled(
+ m_Bounds.Min,
+ m_Bounds.Max,
+ m_Color, m_Rounding);
+
+ if (IsGroup(this))
+ {
+ drawList->AddRectFilled(
+ m_GroupBounds.Min,
+ m_GroupBounds.Max,
+ m_GroupColor, m_GroupRounding);
+
+ if (m_GroupBorderWidth > 0.0f)
+ {
+ FringeScaleScope fringe(1.0f);
+
+ drawList->AddRect(
+ m_GroupBounds.Min,
+ m_GroupBounds.Max,
+ m_GroupBorderColor, m_GroupRounding, 15, m_GroupBorderWidth);
+ }
+ }
+
+# if 0
+ // #debug: highlight group regions
+ auto drawRect = [drawList](const ImRect& rect, ImU32 color)
+ {
+ if (ImRect_IsEmpty(rect)) return;
+ drawList->AddRectFilled(rect.Min, rect.Max, color);
+ };
+
+ drawRect(GetRegionBounds(NodeRegion::Top), IM_COL32(255, 0, 0, 64));
+ drawRect(GetRegionBounds(NodeRegion::Bottom), IM_COL32(255, 0, 0, 64));
+ drawRect(GetRegionBounds(NodeRegion::Left), IM_COL32(0, 255, 0, 64));
+ drawRect(GetRegionBounds(NodeRegion::Right), IM_COL32(0, 255, 0, 64));
+ drawRect(GetRegionBounds(NodeRegion::TopLeft), IM_COL32(255, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::TopRight), IM_COL32(255, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::BottomLeft), IM_COL32(255, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::BottomRight), IM_COL32(255, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::Center), IM_COL32(0, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::Header), IM_COL32(0, 255, 255, 64));
+# endif
+
+ DrawBorder(drawList, m_BorderColor, m_BorderWidth);
+ }
+ else if (flags & Selected)
+ {
+ const auto borderColor = Editor->GetColor(StyleColor_SelNodeBorder);
+ const auto& editorStyle = Editor->GetStyle();
+
+ drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel);
+
+ DrawBorder(drawList, borderColor, editorStyle.SelectedNodeBorderWidth);
+ }
+ else if (!IsGroup(this) && (flags & Hovered))
+ {
+ const auto borderColor = Editor->GetColor(StyleColor_HovNodeBorder);
+ const auto& editorStyle = Editor->GetStyle();
+
+ drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel);
+
+ DrawBorder(drawList, borderColor, editorStyle.HoveredNodeBorderWidth);
+ }
+}
+
+void ed::Node::DrawBorder(ImDrawList* drawList, ImU32 color, float thickness)
+{
+ if (thickness > 0.0f)
+ {
+ drawList->AddRect(m_Bounds.Min, m_Bounds.Max,
+ color, m_Rounding, 15, thickness);
+ }
+}
+
+void ed::Node::GetGroupedNodes(std::vector<Node*>& result, bool append)
+{
+ if (!append)
+ result.resize(0);
+
+ if (!IsGroup(this))
+ return;
+
+ const auto firstNodeIndex = result.size();
+ Editor->FindNodesInRect(m_GroupBounds, result, true, false);
+
+ for (auto index = firstNodeIndex; index < result.size(); ++index)
+ result[index]->GetGroupedNodes(result, true);
+}
+
+ImRect ed::Node::GetRegionBounds(NodeRegion region) const
+{
+ if (m_Type == NodeType::Node)
+ {
+ if (region == NodeRegion::Header)
+ return m_Bounds;
+ }
+ else if (m_Type == NodeType::Group)
+ {
+ const float activeAreaMinimumSize = ImMax(ImMax(
+ Editor->GetView().InvScale * c_GroupSelectThickness,
+ m_GroupBorderWidth), c_GroupSelectThickness);
+ const float minimumSize = activeAreaMinimumSize * 5;
+
+ auto bounds = m_Bounds;
+ if (bounds.GetWidth() < minimumSize)
+ bounds.Expand(ImVec2(minimumSize - bounds.GetWidth(), 0.0f));
+ if (bounds.GetHeight() < minimumSize)
+ bounds.Expand(ImVec2(0.0f, minimumSize - bounds.GetHeight()));
+
+ if (region == NodeRegion::Top)
+ {
+ bounds.Max.y = bounds.Min.y + activeAreaMinimumSize;
+ bounds.Min.x += activeAreaMinimumSize;
+ bounds.Max.x -= activeAreaMinimumSize;
+ return bounds;
+ }
+ else if (region == NodeRegion::Bottom)
+ {
+ bounds.Min.y = bounds.Max.y - activeAreaMinimumSize;
+ bounds.Min.x += activeAreaMinimumSize;
+ bounds.Max.x -= activeAreaMinimumSize;
+ return bounds;
+ }
+ else if (region == NodeRegion::Left)
+ {
+ bounds.Max.x = bounds.Min.x + activeAreaMinimumSize;
+ bounds.Min.y += activeAreaMinimumSize;
+ bounds.Max.y -= activeAreaMinimumSize;
+ return bounds;
+ }
+ else if (region == NodeRegion::Right)
+ {
+ bounds.Min.x = bounds.Max.x - activeAreaMinimumSize;
+ bounds.Min.y += activeAreaMinimumSize;
+ bounds.Max.y -= activeAreaMinimumSize;
+ return bounds;
+ }
+ else if (region == NodeRegion::TopLeft)
+ {
+ bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2;
+ bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2;
+ return bounds;
+ }
+ else if (region == NodeRegion::TopRight)
+ {
+ bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2;
+ bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2;
+ return bounds;
+ }
+ else if (region == NodeRegion::BottomRight)
+ {
+ bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2;
+ bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2;
+ return bounds;
+ }
+ else if (region == NodeRegion::BottomLeft)
+ {
+ bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2;
+ bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2;
+ return bounds;
+ }
+ else if (region == NodeRegion::Header)
+ {
+ bounds.Min.x += activeAreaMinimumSize;
+ bounds.Max.x -= activeAreaMinimumSize;
+ bounds.Min.y += activeAreaMinimumSize;
+ bounds.Max.y = ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y);
+ return bounds;
+ }
+ else if (region == NodeRegion::Center)
+ {
+ bounds.Max.x -= activeAreaMinimumSize;
+ bounds.Min.y = ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y);
+ bounds.Min.x += activeAreaMinimumSize;
+ bounds.Max.y -= activeAreaMinimumSize;
+ return bounds;
+ }
+ }
+
+ return ImRect();
+}
+
+ed::NodeRegion ed::Node::GetRegion(const ImVec2& point) const
+{
+ if (m_Type == NodeType::Node)
+ {
+ if (m_Bounds.Contains(point))
+ return NodeRegion::Header;
+ else
+ return NodeRegion::None;
+ }
+ else if (m_Type == NodeType::Group)
+ {
+ static const NodeRegion c_Regions[] =
+ {
+ // Corners first, they may overlap other regions.
+ NodeRegion::TopLeft,
+ NodeRegion::TopRight,
+ NodeRegion::BottomLeft,
+ NodeRegion::BottomRight,
+ NodeRegion::Header,
+ NodeRegion::Top,
+ NodeRegion::Bottom,
+ NodeRegion::Left,
+ NodeRegion::Right,
+ NodeRegion::Center
+ };
+
+ for (auto region : c_Regions)
+ {
+ auto bounds = GetRegionBounds(region);
+ if (bounds.Contains(point))
+ return region;
+ }
+ }
+
+ return NodeRegion::None;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Link
+//
+//------------------------------------------------------------------------------
+void ed::Link::Draw(ImDrawList* drawList, DrawFlags flags)
+{
+ if (flags == None)
+ {
+ drawList->ChannelsSetCurrent(c_LinkChannel_Links);
+
+ Draw(drawList, m_Color, 0.0f);
+ }
+ else if (flags & Selected)
+ {
+ const auto borderColor = Editor->GetColor(StyleColor_SelLinkBorder);
+
+ drawList->ChannelsSetCurrent(c_LinkChannel_Selection);
+
+ Draw(drawList, borderColor, 4.5f);
+ }
+ else if (flags & Hovered)
+ {
+ const auto borderColor = Editor->GetColor(StyleColor_HovLinkBorder);
+
+ drawList->ChannelsSetCurrent(c_LinkChannel_Selection);
+
+ Draw(drawList, borderColor, 2.0f);
+ }
+}
+
+void ed::Link::Draw(ImDrawList* drawList, ImU32 color, float extraThickness) const
+{
+ if (!m_IsLive)
+ return;
+
+ const auto curve = GetCurve();
+
+ ImDrawList_AddBezierWithArrows(drawList, curve, m_Thickness + extraThickness,
+ m_StartPin && m_StartPin->m_ArrowSize > 0.0f ? m_StartPin->m_ArrowSize + extraThickness : 0.0f,
+ m_StartPin && m_StartPin->m_ArrowWidth > 0.0f ? m_StartPin->m_ArrowWidth + extraThickness : 0.0f,
+ m_EndPin && m_EndPin->m_ArrowSize > 0.0f ? m_EndPin->m_ArrowSize + extraThickness : 0.0f,
+ m_EndPin && m_EndPin->m_ArrowWidth > 0.0f ? m_EndPin->m_ArrowWidth + extraThickness : 0.0f,
+ true, color, 1.0f);
+}
+
+void ed::Link::UpdateEndpoints()
+{
+ const auto line = m_StartPin->GetClosestLine(m_EndPin);
+ m_Start = line.A;
+ m_End = line.B;
+}
+
+ImCubicBezierPoints ed::Link::GetCurve() const
+{
+ auto easeLinkStrength = [](const ImVec2& a, const ImVec2& b, float strength)
+ {
+ const auto distanceX = b.x - a.x;
+ const auto distanceY = b.y - a.y;
+ const auto distance = ImSqrt(distanceX * distanceX + distanceY * distanceY);
+ const auto halfDistance = distance * 0.5f;
+
+ if (halfDistance < strength)
+ strength = strength * ImSin(IM_PI * 0.5f * halfDistance / strength);
+
+ return strength;
+ };
+
+ const auto startStrength = easeLinkStrength(m_Start, m_End, m_StartPin->m_Strength);
+ const auto endStrength = easeLinkStrength(m_Start, m_End, m_EndPin->m_Strength);
+ const auto cp0 = m_Start + m_StartPin->m_Dir * startStrength;
+ const auto cp1 = m_End + m_EndPin->m_Dir * endStrength;
+
+ ImCubicBezierPoints result;
+ result.P0 = m_Start;
+ result.P1 = cp0;
+ result.P2 = cp1;
+ result.P3 = m_End;
+
+ return result;
+}
+
+bool ed::Link::TestHit(const ImVec2& point, float extraThickness) const
+{
+ if (!m_IsLive)
+ return false;
+
+ auto bounds = GetBounds();
+ if (extraThickness > 0.0f)
+ bounds.Expand(extraThickness);
+
+ if (!bounds.Contains(point))
+ return false;
+
+ const auto bezier = GetCurve();
+ const auto result = ImProjectOnCubicBezier(point, bezier.P0, bezier.P1, bezier.P2, bezier.P3, 50);
+
+ return result.Distance <= m_Thickness + extraThickness;
+}
+
+bool ed::Link::TestHit(const ImRect& rect, bool allowIntersect) const
+{
+ if (!m_IsLive)
+ return false;
+
+ const auto bounds = GetBounds();
+
+ if (rect.Contains(bounds))
+ return true;
+
+ if (!allowIntersect || !rect.Overlaps(bounds))
+ return false;
+
+ const auto bezier = GetCurve();
+
+ const auto p0 = rect.GetTL();
+ const auto p1 = rect.GetTR();
+ const auto p2 = rect.GetBR();
+ const auto p3 = rect.GetBL();
+
+ if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p0, p1).Count > 0)
+ return true;
+ if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p1, p2).Count > 0)
+ return true;
+ if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p2, p3).Count > 0)
+ return true;
+ if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p3, p0).Count > 0)
+ return true;
+
+ return false;
+}
+
+ImRect ed::Link::GetBounds() const
+{
+ if (m_IsLive)
+ {
+ const auto curve = GetCurve();
+ auto bounds = ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3);
+
+ if (bounds.GetWidth() == 0.0f)
+ {
+ bounds.Min.x -= 0.5f;
+ bounds.Max.x += 0.5f;
+ }
+
+ if (bounds.GetHeight() == 0.0f)
+ {
+ bounds.Min.y -= 0.5f;
+ bounds.Max.y += 0.5f;
+ }
+
+ if (m_StartPin->m_ArrowSize)
+ {
+ const auto start_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f));
+ const auto p0 = curve.P0;
+ const auto p1 = curve.P0 - start_dir * m_StartPin->m_ArrowSize;
+ const auto min = ImMin(p0, p1);
+ const auto max = ImMax(p0, p1);
+ auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1)));
+ bounds.Add(arrowBounds);
+ }
+
+ if (m_EndPin->m_ArrowSize)
+ {
+ const auto end_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f));
+ const auto p0 = curve.P3;
+ const auto p1 = curve.P3 + end_dir * m_EndPin->m_ArrowSize;
+ const auto min = ImMin(p0, p1);
+ const auto max = ImMax(p0, p1);
+ auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1)));
+ bounds.Add(arrowBounds);
+ }
+
+ return bounds;
+ }
+ else
+ return ImRect();
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Editor Context
+//
+//------------------------------------------------------------------------------
+ed::EditorContext::EditorContext(const ax::NodeEditor::Config* config)
+ : m_IsFirstFrame(true)
+ , m_IsWindowActive(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_DoubleClickedNode(0)
+ , m_DoubleClickedPin(0)
+ , m_DoubleClickedLink(0)
+ , m_BackgroundClicked(false)
+ , m_BackgroundDoubleClicked(false)
+ , m_IsInitialized(false)
+ , m_Settings()
+ , m_Config(config)
+ , m_ExternalChannel(0)
+{
+}
+
+ed::EditorContext::~EditorContext()
+{
+ if (m_IsInitialized)
+ SaveSettings();
+
+ 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)
+{
+ if (!m_IsInitialized)
+ {
+ LoadSettings();
+ m_IsInitialized = true;
+ }
+
+ //ImGui::LogToClipboard();
+ //Log("---- begin ----");
+
+ for (auto node : m_Nodes) node->Reset();
+ for (auto pin : m_Pins) pin->Reset();
+ for (auto link : m_Links) link->Reset();
+
+ auto drawList = ImGui::GetWindowDrawList();
+
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+ m_ExternalChannel = drawList->_Splitter._Current;
+
+ 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);
+
+ m_IsCanvasVisible = m_Canvas.Begin(id, canvasSize);
+
+ //ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
+ //ImGui::BeginChild(id, size, false,
+ // ImGuiWindowFlags_NoMove |
+ // ImGuiWindowFlags_NoScrollbar |
+ // ImGuiWindowFlags_NoScrollWithMouse);
+
+ ImGui::CaptureKeyboardFromApp();
+
+ m_IsWindowActive = ImGui::IsWindowFocused();
+
+ //
+ m_NavigateAction.SetWindow(m_Canvas.ViewRect().Min, m_Canvas.ViewRect().GetSize());
+
+ if (m_CurrentAction && m_CurrentAction->IsDragging() && m_NavigateAction.MoveOverEdge())
+ {
+ auto& io = ImGui::GetIO();
+ auto offset = m_NavigateAction.GetMoveOffset();
+ for (int i = 0; i < 5; ++i)
+ io.MouseClickedPos[i] = io.MouseClickedPos[i] - offset;
+ }
+ else
+ m_NavigateAction.StopMoveOverEdge();
+
+ m_Canvas.SetView(m_NavigateAction.GetView());
+
+ // #debug #clip
+ //ImGui::Text("CLIP = { x=%g y=%g w=%g h=%g r=%g b=%g }",
+ // clipMin.x, clipMin.y, clipMax.x - clipMin.x, clipMax.y - clipMin.y, clipMax.x, clipMax.y);
+
+ // Reserve channels for background and links
+ ImDrawList_ChannelsGrow(drawList, c_NodeStartChannel);
+
+ if (HasSelectionChanged())
+ ++m_SelectionId;
+
+ m_LastSelectedObjects = m_SelectedObjects;
+}
+
+void ed::EditorContext::End()
+{
+ //auto& io = ImGui::GetIO();
+ auto control = BuildControl(m_CurrentAction && m_CurrentAction->IsDragging()); // NavigateAction.IsMovingOverEdge()
+ auto drawList = ImGui::GetWindowDrawList();
+ //auto& editorStyle = GetStyle();
+
+ 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_BackgroundClicked = control.BackgroundClicked;
+ m_BackgroundDoubleClicked = control.BackgroundDoubleClicked;
+
+ //if (DoubleClickedNode) LOG_TRACE(0, "DOUBLE CLICK NODE: %d", DoubleClickedNode);
+ //if (DoubleClickedPin) LOG_TRACE(0, "DOUBLE CLICK PIN: %d", DoubleClickedPin);
+ //if (DoubleClickedLink) LOG_TRACE(0, "DOUBLE CLICK LINK: %d", DoubleClickedLink);
+ //if (BackgroundDoubleClicked) LOG_TRACE(0, "DOUBLE CLICK BACKGROUND", DoubleClickedLink);
+
+ const bool isSelecting = m_CurrentAction && m_CurrentAction->AsSelect() != nullptr;
+ const bool isDragging = m_CurrentAction && m_CurrentAction->AsDrag() != nullptr;
+ //const bool isSizing = CurrentAction && CurrentAction->AsSize() != nullptr;
+
+ // Draw nodes
+ for (auto node : m_Nodes)
+ if (node->m_IsLive && node->IsVisible())
+ node->Draw(drawList);
+
+ // Draw links
+ for (auto link : m_Links)
+ if (link->m_IsLive && link->IsVisible())
+ link->Draw(drawList);
+
+ // Highlight selected objects
+ {
+ 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(drawList, Object::Selected);
+ }
+
+ if (!isSelecting)
+ {
+ 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(drawList, Object::Hovered);
+ }
+
+ // Draw animations
+ for (auto controller : m_AnimationControllers)
+ controller->Draw(drawList);
+
+ if (m_CurrentAction && !m_CurrentAction->Process(control))
+ m_CurrentAction = nullptr;
+
+ if (m_NavigateAction.m_IsActive)
+ m_NavigateAction.Process(control);
+ else
+ m_NavigateAction.Accept(control);
+
+ if (nullptr == m_CurrentAction)
+ {
+ EditorAction* possibleAction = nullptr;
+
+ auto accept = [&possibleAction, &control](EditorAction& action)
+ {
+ auto result = action.Accept(control);
+
+ if (result == EditorAction::True)
+ return true;
+ else if (/*!possibleAction &&*/ 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_SelectAction))
+ m_CurrentAction = &m_SelectAction;
+ else if (accept(m_CreateItemAction))
+ m_CurrentAction = &m_CreateItemAction;
+ else if (accept(m_DeleteItemsAction))
+ m_CurrentAction = &m_DeleteItemsAction;
+
+ if (possibleAction)
+ ImGui::SetMouseCursor(possibleAction->GetCursor());
+
+ if (m_CurrentAction && possibleAction)
+ possibleAction->Reject();
+ }
+
+ if (m_CurrentAction)
+ ImGui::SetMouseCursor(m_CurrentAction->GetCursor());
+
+ // Draw selection rectangle
+ m_SelectAction.Draw(drawList);
+
+ bool sortGroups = false;
+ if (control.ActiveNode)
+ {
+ if (!IsGroup(control.ActiveNode))
+ {
+ // Bring active node to front
+ auto activeNodeIt = std::find(m_Nodes.begin(), m_Nodes.end(), control.ActiveNode);
+ std::rotate(activeNodeIt, activeNodeIt + 1, m_Nodes.end());
+ }
+ else if (!isDragging && m_CurrentAction && m_CurrentAction->AsDrag())
+ {
+ // Bring content of dragged group to front
+ 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();
+ });
+
+ sortGroups = true;
+ }
+ }
+
+ // Sort nodes if bounds of node changed
+ if (sortGroups || ((m_Settings.m_DirtyReason & (SaveReasonFlags::Position | SaveReasonFlags::Size)) != SaveReasonFlags::None))
+ {
+ // Bring all groups before regular nodes
+ auto groupsItEnd = std::stable_partition(m_Nodes.begin(), m_Nodes.end(), IsGroup);
+
+ // Sort groups by area
+ 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;
+ });
+ }
+
+# if 1
+ // Every node has few channels assigned. Grow channel list
+ // to hold twice as much of channels and place them in
+ // node drawing order.
+ {
+ // Copy group nodes
+ auto liveNodeCount = static_cast<int>(std::count_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return node->m_IsLive; }));
+
+ // Reserve two additional channels for sorted list of channels
+ auto nodeChannelCount = drawList->_Splitter._Count;
+ ImDrawList_ChannelsGrow(drawList, drawList->_Splitter._Count + c_ChannelsPerNode * liveNodeCount + c_LinkChannelCount);
+
+ int targetChannel = nodeChannelCount;
+
+ auto copyNode = [&targetChannel, drawList](Node* node)
+ {
+ if (!node->m_IsLive)
+ return;
+
+ for (int i = 0; i < c_ChannelsPerNode; ++i)
+ ImDrawList_SwapChannels(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); });
+
+ // Copy group nodes
+ std::for_each(m_Nodes.begin(), groupsItEnd, copyNode);
+
+ // Copy links
+ for (int i = 0; i < c_LinkChannelCount; ++i, ++targetChannel)
+ ImDrawList_SwapChannels(drawList, c_LinkStartChannel + i, targetChannel);
+
+ // Copy normal nodes
+ std::for_each(groupsItEnd, m_Nodes.end(), copyNode);
+ }
+# endif
+
+ // ImGui::PopClipRect();
+
+ // Draw grid
+# if 1 // #FIXME
+ {
+ //auto& style = ImGui::GetStyle();
+
+ 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;// * m_Canvas.ViewScale();
+ float GRID_SY = 32.0f;// * m_Canvas.ViewScale();
+ ImVec2 VIEW_POS = m_Canvas.ViewRect().Min;
+ ImVec2 VIEW_SIZE = m_Canvas.ViewRect().GetSize();
+
+ 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)
+ 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)
+ drawList->AddLine(ImVec2(0.0f, y) + VIEW_POS, ImVec2(VIEW_SIZE.x, y) + VIEW_POS, GRID_COLOR);
+ }
+# endif
+
+# if 0
+ {
+ auto userChannel = drawList->_Splitter._Count;
+ auto channelsToCopy = c_UserLayersCount;
+ ImDrawList_ChannelsGrow(drawList, userChannel + channelsToCopy);
+ for (int i = 0; i < channelsToCopy; ++i)
+ ImDrawList_SwapChannels(drawList, userChannel + i, c_UserLayerChannelStart + i);
+ }
+# endif
+
+# if 0
+ {
+ auto preOffset = ImVec2(0, 0);
+ auto postOffset = m_OldCanvas.WindowScreenPos + m_OldCanvas.ClientOrigin;
+ auto scale = m_OldCanvas.Zoom;
+
+ ImDrawList_TransformChannels(drawList, 0, 1, preOffset, scale, postOffset);
+ ImDrawList_TransformChannels(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, preOffset, scale, postOffset);
+
+ auto clipTranslation = m_OldCanvas.WindowScreenPos - m_OldCanvas.FromScreen(m_OldCanvas.WindowScreenPos);
+ ImGui::PushClipRect(m_OldCanvas.WindowScreenPos + ImVec2(1, 1), m_OldCanvas.WindowScreenPos + m_OldCanvas.WindowScreenSize - ImVec2(1, 1), false);
+ ImDrawList_TranslateAndClampClipRects(drawList, 0, 1, clipTranslation);
+ ImDrawList_TranslateAndClampClipRects(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, clipTranslation);
+ ImGui::PopClipRect();
+
+ // #debug: Static grid in local space
+ //for (float x = 0; x < Canvas.WindowScreenSize.x; x += 100)
+ // drawList->AddLine(ImVec2(x, 0.0f) + Canvas.WindowScreenPos, ImVec2(x, Canvas.WindowScreenSize.y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128));
+ //for (float y = 0; y < Canvas.WindowScreenSize.y; y += 100)
+ // drawList->AddLine(ImVec2(0.0f, y) + Canvas.WindowScreenPos, ImVec2(Canvas.WindowScreenSize.x, y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128));
+ }
+# endif
+
+# if 1
+ // Move user and hint channels to top
+ {
+ // Clip plane is transformed to global space.
+ // These channels already have clip planes in global space, so
+ // we move them to clip plane. Batch transformation in canvas
+ // will bring them back to global space.
+ auto preTransformClipRect = [this, drawList](int channelIndex)
+ {
+ ImDrawChannel& channel = 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);
+ }
+ };
+
+ drawList->ChannelsSetCurrent(0);
+
+ auto channelCount = drawList->_Splitter._Count;
+ ImDrawList_ChannelsGrow(drawList, channelCount + 3);
+ ImDrawList_SwapChannels(drawList, c_UserChannel_HintsBackground, channelCount + 0);
+ ImDrawList_SwapChannels(drawList, c_UserChannel_Hints, channelCount + 1);
+ ImDrawList_SwapChannels(drawList, c_UserChannel_Content, channelCount + 2);
+
+ preTransformClipRect(channelCount + 0);
+ preTransformClipRect(channelCount + 1);
+ preTransformClipRect(channelCount + 2);
+ }
+# endif
+
+ UpdateAnimations();
+
+ drawList->ChannelsMerge();
+
+ // #debug
+ // drawList->AddRectFilled(ImVec2(-10.0f, -10.0f), ImVec2(10.0f, 10.0f), IM_COL32(255, 0, 255, 255));
+
+ // ImGui::EndChild();
+ // ImGui::PopStyleColor();
+ if (m_IsCanvasVisible)
+ m_Canvas.End();
+
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+
+ // Draw border
+ {
+ auto& style = ImGui::GetStyle();
+ auto borderShadoColor = style.Colors[ImGuiCol_BorderShadow];
+ auto borderColor = style.Colors[ImGuiCol_Border];
+ drawList->AddRect(m_Canvas.Rect().Min + ImVec2(1, 1), m_Canvas.Rect().Max - ImVec2(1, 1), ImColor(borderShadoColor));
+ drawList->AddRect(m_Canvas.Rect().Min, m_Canvas.Rect().Max, ImColor(borderColor));
+ }
+
+ // ShowMetrics(control);
+
+ 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_IsFirstFrame = false;
+}
+
+bool ed::EditorContext::DoLink(LinkId id, PinId startPinId, PinId endPinId, ImU32 color, float thickness)
+{
+ //auto& editorStyle = GetStyle();
+
+ 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_Thickness = thickness;
+ link->m_IsLive = true;
+
+ link->UpdateEndpoints();
+
+ return true;
+}
+
+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();
+ MakeDirty(NodeEditor::SaveReasonFlags::Position, 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::MarkNodeToRestoreState(Node* node)
+{
+ node->m_RestoreState = true;
+}
+
+void ed::EditorContext::RestoreNodeState(Node* node)
+{
+ auto settings = m_Settings.FindNode(node->m_ID);
+ if (!settings)
+ return;
+
+ // Load state from config (if possible)
+ if (!NodeSettings::Parse(m_Config.LoadNode(node->m_ID), *settings))
+ return;
+
+ 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::ClearSelection()
+{
+ m_SelectedObjects.clear();
+}
+
+void ed::EditorContext::SelectObject(Object* object)
+{
+ m_SelectedObjects.push_back(object);
+}
+
+void ed::EditorContext::DeselectObject(Object* object)
+{
+ auto objectIt = std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), object);
+ if (objectIt != m_SelectedObjects.end())
+ 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 std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), object) != m_SelectedObjects.end();
+}
+
+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;
+}
+
+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);
+}
+
+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);
+}
+
+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;
+}
+
+void ed::EditorContext::Suspend(SuspendFlags flags)
+{
+ auto drawList = ImGui::GetWindowDrawList();
+ auto lastChannel = drawList->_Splitter._Current;
+ drawList->ChannelsSetCurrent(m_ExternalChannel);
+ m_Canvas.Suspend();
+ drawList->ChannelsSetCurrent(lastChannel);
+ if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter)
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+}
+
+void ed::EditorContext::Resume(SuspendFlags flags)
+{
+ auto drawList = ImGui::GetWindowDrawList();
+ if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter)
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+ auto lastChannel = drawList->_Splitter._Current;
+ drawList->ChannelsSetCurrent(m_ExternalChannel);
+ m_Canvas.Resume();
+ drawList->ChannelsSetCurrent(lastChannel);
+}
+
+bool ed::EditorContext::IsSuspended()
+{
+ return m_Canvas.IsSuspended();
+}
+
+bool ed::EditorContext::IsActive()
+{
+ return m_IsWindowActive;
+}
+
+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});
+ //std::sort(Nodes.begin(), Nodes.end());
+
+ auto settings = m_Settings.FindNode(id);
+ if (!settings)
+ settings = m_Settings.AddNode(id);
+
+ if (!settings->m_WasUsed)
+ {
+ settings->m_WasUsed = true;
+ RestoreNodeState(node);
+ }
+
+ node->m_Bounds.Min = settings->m_Location;
+ node->m_Bounds.Max = node->m_Bounds.Min;
+ node->m_Bounds.Floor();
+
+ if (settings->m_GroupSize.x > 0 || settings->m_GroupSize.y > 0)
+ {
+ node->m_Type = NodeType::Group;
+ node->m_GroupBounds.Min = settings->m_Location;
+ node->m_GroupBounds.Max = node->m_GroupBounds.Min + settings->m_GroupSize;
+ node->m_GroupBounds.Floor();
+ }
+
+ node->m_IsLive = false;
+
+ return node;
+}
+
+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;
+}
+
+template <typename C, typename Id>
+static inline auto FindItemInLinear(C& container, Id id)
+{
+# if defined(_DEBUG)
+ auto start = container.data();
+ auto end = container.data() + container.size();
+ for (auto it = start; it < end; ++it)
+ if ((*it).m_ID == id)
+ return it->m_Object;
+# else
+ for (auto item : container)
+ if (item.m_ID == id)
+ return item.m_Object;
+# endif
+
+ return static_cast<decltype(container[0].m_Object)>(nullptr);
+}
+
+template <typename C, typename Id>
+static inline auto FindItemIn(C& container, Id id)
+{
+//# if defined(_DEBUG)
+// auto start = container.data();
+// auto end = container.data() + container.size();
+// for (auto it = start; it < end; ++it)
+// if ((*it)->ID == id)
+// return *it;
+//# else
+// for (auto item : container)
+// if (item->ID == id)
+// return item;
+//# endif
+ auto key = typename C::value_type{ id, nullptr };
+ auto first = container.cbegin();
+ auto last = container.cend();
+ auto it = std::lower_bound(first, last, key);
+ if (it != last && (key.m_ID == it->m_ID))
+ return it->m_Object;
+ else
+ return static_cast<decltype(it->m_Object)>(nullptr);
+}
+
+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::LoadSettings()
+{
+ ed::Settings::Parse(m_Config.Load(), m_Settings);
+
+ m_NavigateAction.m_Scroll = m_Settings.m_ViewScroll;
+ m_NavigateAction.m_Zoom = m_Settings.m_ViewZoom;
+}
+
+void ed::EditorContext::SaveSettings()
+{
+ m_Config.BeginSave();
+
+ for (auto& node : m_Nodes)
+ {
+ 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();
+ }
+ }
+
+ 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;
+
+ if (m_Config.Save(m_Settings.Serialize(), m_Settings.m_DirtyReason))
+ m_Settings.ClearDirty();
+
+ m_Config.EndSave();
+}
+
+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->TestHit(p, c_LinkSelectThickness))
+ return link;
+
+ return nullptr;
+}
+
+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::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)
+{
+ m_FlowAnimationController.Flow(link);
+}
+
+void ed::EditorContext::SetUserContext(bool globalSpace)
+{
+ const auto mousePos = ImGui::GetMousePos();
+
+ // Move drawing cursor to mouse location and prepare layer for
+ // content added by user.
+ if (globalSpace)
+ ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos));
+ else
+ ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos));
+ //ImGui::SetCursorScreenPos(ImFloor(mousePos));
+ //ImGui::SetCursorScreenPos(ImVec2(floorf(mousePos.x), floorf(mousePos.y)));
+
+ if (!IsSuspended())
+ {
+ auto drawList = ImGui::GetWindowDrawList();
+ drawList->ChannelsSetCurrent(c_UserChannel_Content);
+ }
+
+ // #debug
+ // drawList->AddCircleFilled(ImGui::GetMousePos(), 4, IM_COL32(0, 255, 0, 255));
+}
+
+void ed::EditorContext::EnableShortcuts(bool enable)
+{
+ m_ShortcutsEnabled = enable;
+}
+
+bool ed::EditorContext::AreShortcutsEnabled()
+{
+ return m_ShortcutsEnabled;
+}
+
+ed::Control ed::EditorContext::BuildControl(bool allowOffscreen)
+{
+ if (!allowOffscreen && !ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
+ return Control(nullptr, nullptr, nullptr, nullptr, false, false, false, false);
+
+ const auto mousePos = ImGui::GetMousePos();
+
+ // Expand clip rectangle to always contain cursor
+ auto editorRect = m_Canvas.ViewRect();
+ auto isMouseOffscreen = allowOffscreen && !editorRect.Contains(mousePos);
+ if (isMouseOffscreen)
+ {
+ // Extend clip rect to capture off-screen mouse cursor
+ editorRect.Add(ImFloor(mousePos));
+ editorRect.Add(ImVec2(ImCeil(mousePos.x), ImCeil(mousePos.y)));
+
+ ImGui::PushClipRect(editorRect.Min, editorRect.Max, false);
+ }
+
+ Object* hotObject = nullptr;
+ Object* activeObject = nullptr;
+ Object* clickedObject = nullptr;
+ Object* doubleClickedObject = nullptr;
+
+ // Emits invisible button and returns true if it is clicked.
+ auto emitInteractiveArea = [](ObjectId id, const ImRect& rect)
+ {
+ char idString[33] = { 0 }; // itoa can output 33 bytes maximum
+ snprintf(idString, 32, "%p", id.AsPointer());
+ ImGui::SetCursorScreenPos(rect.Min);
+
+ // debug
+ //if (id < 0) return ImGui::Button(idString, to_imvec(rect.size));
+
+ auto result = ImGui::InvisibleButton(idString, rect.GetSize());
+
+ // #debug
+ //ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(0, 255, 0, 64));
+
+ return result;
+ };
+
+ // Check input interactions over area.
+ auto checkInteractionsInArea = [&emitInteractiveArea, &hotObject, &activeObject, &clickedObject, &doubleClickedObject](ObjectId id, const ImRect& rect, Object* object)
+ {
+ if (emitInteractiveArea(id, rect))
+ clickedObject = object;
+ if (!doubleClickedObject && ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered())
+ doubleClickedObject = object;
+
+ if (!hotObject && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
+ hotObject = object;
+
+ if (ImGui::IsItemActive())
+ activeObject = object;
+ };
+
+ // Process live nodes and pins.
+ for (auto nodeIt = m_Nodes.rbegin(), nodeItEnd = m_Nodes.rend(); nodeIt != nodeItEnd; ++nodeIt)
+ {
+ auto node = *nodeIt;
+
+ if (!node->m_IsLive) continue;
+
+ // Check for interactions with live pins in node before
+ // processing node itself. Pins does not overlap each other
+ // and all are within node bounds.
+ for (auto pin = node->m_LastPin; pin; pin = pin->m_PreviousPin)
+ {
+ if (!pin->m_IsLive) continue;
+
+ checkInteractionsInArea(pin->m_ID, pin->m_Bounds, pin);
+ }
+
+ // Check for interactions with node.
+ if (node->m_Type == NodeType::Group)
+ {
+ // Node with a hole
+ 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);
+ }
+
+ // Links are not regular widgets and must be done manually since
+ // ImGui does not support interactive elements with custom hit maps.
+ //
+ // Links can steal input from background.
+
+ // Links are just over background. So if anything else
+ // is hovered we can skip them.
+ if (nullptr == hotObject)
+ hotObject = FindLinkAt(mousePos);
+
+ // Check for interaction with background.
+ auto backgroundClicked = emitInteractiveArea(NodeId(0), editorRect);
+ auto backgroundDoubleClicked = !doubleClickedObject && ImGui::IsItemHovered() ? ImGui::IsMouseDoubleClicked(0) : false;
+ auto isBackgroundActive = ImGui::IsItemActive();
+ auto isBackgroundHot = !hotObject;
+ auto isDragging = ImGui::IsMouseDragging(0, 1) || ImGui::IsMouseDragging(1, 1) || ImGui::IsMouseDragging(2, 1);
+
+ if (backgroundDoubleClicked)
+ backgroundClicked = false;
+
+ if (isMouseOffscreen)
+ ImGui::PopClipRect();
+
+ // Process link input using background interactions.
+ auto hotLink = hotObject ? hotObject->AsLink() : nullptr;
+
+ // ImGui take care of tracking active items. With link
+ // we must do this ourself.
+ 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;
+
+ // Steal click from backgrounds if link is hovered.
+ if (!isDragging && backgroundClicked && hotLink)
+ {
+ clickedObject = hotLink;
+ backgroundClicked = false;
+ }
+
+ // Steal double-click from backgrounds if link is hovered.
+ if (!isDragging && backgroundDoubleClicked && hotLink)
+ {
+ doubleClickedObject = hotLink;
+ backgroundDoubleClicked = false;
+ }
+
+ return Control(hotObject, activeObject, clickedObject, doubleClickedObject,
+ isBackgroundHot, isBackgroundActive, backgroundClicked, backgroundDoubleClicked);
+}
+
+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 = (int)std::count_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return node->m_IsLive; });
+ auto livePinCount = (int)std::count_if(m_Pins.begin(), m_Pins.end(), [](Pin* pin) { return pin->m_IsLive; });
+ auto liveLinkCount = (int)std::count_if(m_Links.begin(), m_Links.end(), [](Link* link) { return link->m_IsLive; });
+
+ 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 Editor Active: %s", ImGui::IsWindowHovered() ? "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();
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Node Settings
+//
+//------------------------------------------------------------------------------
+void ed::NodeSettings::ClearDirty()
+{
+ m_IsDirty = false;
+ m_DirtyReason = SaveReasonFlags::None;
+}
+
+void ed::NodeSettings::MakeDirty(SaveReasonFlags reason)
+{
+ m_IsDirty = true;
+ m_DirtyReason = m_DirtyReason | reason;
+}
+
+ed::json::value ed::NodeSettings::Serialize()
+{
+ json::value result;
+ result["location"]["x"] = m_Location.x;
+ result["location"]["y"] = m_Location.y;
+
+ if (m_GroupSize.x > 0 || m_GroupSize.y > 0)
+ {
+ result["group_size"]["x"] = m_GroupSize.x;
+ result["group_size"]["y"] = m_GroupSize.y;
+ }
+
+ return result;
+}
+
+bool ed::NodeSettings::Parse(const std::string& string, NodeSettings& settings)
+{
+ auto settingsValue = json::value::parse(string);
+ if (settingsValue.is_discarded())
+ return false;
+
+ return Parse(settingsValue, settings);
+}
+
+bool ed::NodeSettings::Parse(const json::value& data, NodeSettings& result)
+{
+ if (!data.is_object())
+ return false;
+
+ auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool
+ {
+ if (v.is_object())
+ {
+ auto xValue = v["x"];
+ auto yValue = v["y"];
+
+ if (xValue.is_number() && yValue.is_number())
+ {
+ result.x = static_cast<float>(xValue.get<double>());
+ result.y = static_cast<float>(yValue.get<double>());
+
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ if (!tryParseVector(data["location"], result.m_Location))
+ return false;
+
+ if (data.contains("group_size") && !tryParseVector(data["group_size"], result.m_GroupSize))
+ return false;
+
+ return true;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Settings
+//
+//------------------------------------------------------------------------------
+ed::NodeSettings* ed::Settings::AddNode(NodeId id)
+{
+ m_Nodes.push_back(NodeSettings(id));
+ return &m_Nodes.back();
+}
+
+ed::NodeSettings* ed::Settings::FindNode(NodeId id)
+{
+ for (auto& settings : m_Nodes)
+ if (settings.m_ID == id)
+ return &settings;
+
+ return nullptr;
+}
+
+void ed::Settings::ClearDirty(Node* node)
+{
+ if (node)
+ {
+ auto settings = FindNode(node->m_ID);
+ IM_ASSERT(settings);
+ settings->ClearDirty();
+ }
+ else
+ {
+ m_IsDirty = false;
+ m_DirtyReason = SaveReasonFlags::None;
+
+ for (auto& knownNode : m_Nodes)
+ knownNode.ClearDirty();
+ }
+}
+
+void ed::Settings::MakeDirty(SaveReasonFlags reason, Node* node)
+{
+ m_IsDirty = true;
+ m_DirtyReason = m_DirtyReason | reason;
+
+ if (node)
+ {
+ auto settings = FindNode(node->m_ID);
+ IM_ASSERT(settings);
+
+ settings->MakeDirty(reason);
+ }
+}
+
+std::string ed::Settings::Serialize()
+{
+ json::value result;
+
+ auto serializeObjectId = [](ObjectId id)
+ {
+ auto value = std::to_string(reinterpret_cast<uintptr_t>(id.AsPointer()));
+ switch (id.Type())
+ {
+ default:
+ case NodeEditor::Detail::ObjectType::None: return value;
+ case NodeEditor::Detail::ObjectType::Node: return "node:" + value;
+ case NodeEditor::Detail::ObjectType::Link: return "link:" + value;
+ case NodeEditor::Detail::ObjectType::Pin: return "pin:" + value;
+ }
+ };
+
+ auto& nodes = result["nodes"];
+ for (auto& node : m_Nodes)
+ {
+ if (node.m_WasUsed)
+ nodes[serializeObjectId(node.m_ID)] = node.Serialize();
+ }
+
+ auto& selection = result["selection"];
+ for (auto& id : m_Selection)
+ selection.push_back(serializeObjectId(id));
+
+ auto& view = result["view"];
+ view["scroll"]["x"] = m_ViewScroll.x;
+ view["scroll"]["y"] = m_ViewScroll.y;
+ view["zoom"] = m_ViewZoom;
+
+ return result.dump();
+}
+
+bool ed::Settings::Parse(const std::string& string, Settings& settings)
+{
+ Settings result = settings;
+
+ auto settingsValue = json::value::parse(string);
+ if (settingsValue.is_discarded())
+ return false;
+
+ if (!settingsValue.is_object())
+ return false;
+
+ auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool
+ {
+ if (v.is_object() && v.contains("x") && v.contains("y"))
+ {
+ auto xValue = v["x"];
+ auto yValue = v["y"];
+
+ if (xValue.is_number() && yValue.is_number())
+ {
+ result.x = static_cast<float>(xValue.get<double>());
+ result.y = static_cast<float>(yValue.get<double>());
+
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ auto deserializeObjectId = [](const std::string& str)
+ {
+ auto separator = str.find_first_of(':');
+ auto idStart = str.c_str() + ((separator != std::string::npos) ? separator + 1 : 0);
+ auto id = reinterpret_cast<void*>(strtoull(idStart, nullptr, 10));
+ if (str.compare(0, separator, "node") == 0)
+ return ObjectId(NodeId(id));
+ else if (str.compare(0, separator, "link") == 0)
+ return ObjectId(LinkId(id));
+ else if (str.compare(0, separator, "pin") == 0)
+ return ObjectId(PinId(id));
+ else
+ // fallback to old format
+ return ObjectId(NodeId(id)); //return ObjectId();
+ };
+
+ //auto& settingsObject = settingsValue.get<json::object>();
+
+ auto& nodesValue = settingsValue["nodes"];
+ if (nodesValue.is_object())
+ {
+ for (auto& node : nodesValue.get<json::object>())
+ {
+ auto id = deserializeObjectId(node.first.c_str()).AsNodeId();
+
+ auto nodeSettings = result.FindNode(id);
+ if (!nodeSettings)
+ nodeSettings = result.AddNode(id);
+
+ NodeSettings::Parse(node.second, *nodeSettings);
+ }
+ }
+
+ auto& selectionValue = settingsValue["selection"];
+ if (selectionValue.is_array())
+ {
+ const auto selectionArray = selectionValue.get<json::array>();
+
+ result.m_Selection.reserve(selectionArray.size());
+ result.m_Selection.resize(0);
+ for (auto& selection : selectionArray)
+ {
+ if (selection.is_string())
+ result.m_Selection.push_back(deserializeObjectId(selection.get<json::string>()));
+ }
+ }
+
+ auto& viewValue = settingsValue["view"];
+ if (viewValue.is_object())
+ {
+ auto& viewScrollValue = viewValue["scroll"];
+ auto& viewZoomValue = viewValue["zoom"];
+
+ if (!tryParseVector(viewScrollValue, result.m_ViewScroll))
+ result.m_ViewScroll = ImVec2(0, 0);
+
+ result.m_ViewZoom = viewZoomValue.is_number() ? static_cast<float>(viewZoomValue.get<double>()) : 1.0f;
+ }
+
+ settings = std::move(result);
+
+ return true;
+}
+
+
+
+//------------------------------------------------------------------------------
+//
+// Animation
+//
+//------------------------------------------------------------------------------
+ed::Animation::Animation(EditorContext* editor):
+ Editor(editor),
+ m_State(Stopped),
+ m_Time(0.0f),
+ m_Duration(0.0f)
+{
+}
+
+ed::Animation::~Animation()
+{
+ Stop();
+}
+
+void ed::Animation::Play(float duration)
+{
+ if (IsPlaying())
+ Stop();
+
+ m_State = Playing;
+ if (duration < 0)
+ duration = 0.0f;
+
+ m_Time = 0.0f;
+ m_Duration = duration;
+
+ OnPlay();
+
+ Editor->RegisterAnimation(this);
+
+ if (duration == 0.0f)
+ Stop();
+}
+
+void ed::Animation::Stop()
+{
+ if (!IsPlaying())
+ return;
+
+ m_State = Stopped;
+
+ Editor->UnregisterAnimation(this);
+
+ OnStop();
+}
+
+void ed::Animation::Finish()
+{
+ if (!IsPlaying())
+ return;
+
+ OnFinish();
+
+ Stop();
+}
+
+void ed::Animation::Update()
+{
+ if (!IsPlaying())
+ return;
+
+ m_Time += ImMax(0.0f, ImGui::GetIO().DeltaTime);
+ if (m_Time < m_Duration)
+ {
+ const float progress = GetProgress();
+ OnUpdate(progress);
+ }
+ else
+ {
+ OnFinish();
+ Stop();
+ }
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Navigate Animation
+//
+//------------------------------------------------------------------------------
+ed::NavigateAnimation::NavigateAnimation(EditorContext* editor, NavigateAction& scrollAction):
+ Animation(editor),
+ Action(scrollAction)
+{
+}
+
+void ed::NavigateAnimation::NavigateTo(const ImRect& target, float duration)
+{
+ Stop();
+
+ m_Start = Action.GetViewRect();
+ m_Target = target;
+
+ // Skip tiny animations
+ auto minoffset = m_Target.Min - m_Start.Min;
+ auto maxOffset = m_Target.Max - m_Start.Max;
+ auto epsilon = 1e-4f;
+ if (ImFabs(minoffset.x) < epsilon && ImFabs(minoffset.y) < epsilon &&
+ ImFabs(maxOffset.x) < epsilon && ImFabs(maxOffset.y) < epsilon)
+ {
+ duration = 0;
+ }
+
+ Play(duration);
+}
+
+void ed::NavigateAnimation::OnUpdate(float progress)
+{
+ ImRect current;
+ current.Min = ImEasing::EaseOutQuad(m_Start.Min, m_Target.Min - m_Start.Min, progress);
+ current.Max = ImEasing::EaseOutQuad(m_Start.Max, m_Target.Max - m_Start.Max, progress);
+ Action.SetViewRect(current);
+}
+
+void ed::NavigateAnimation::OnStop()
+{
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+}
+
+void ed::NavigateAnimation::OnFinish()
+{
+ Action.SetViewRect(m_Target);
+
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Flow Animation
+//
+//------------------------------------------------------------------------------
+ed::FlowAnimation::FlowAnimation(FlowAnimationController* controller):
+ Animation(controller->Editor),
+ Controller(controller),
+ m_Link(nullptr),
+ m_Offset(0.0f),
+ m_PathLength(0.0f)
+{
+}
+
+void ed::FlowAnimation::Flow(ed::Link* link, float markerDistance, float speed, float duration)
+{
+ Stop();
+
+ if (m_Link != link)
+ {
+ m_Offset = 0.0f;
+ ClearPath();
+ }
+
+ if (m_MarkerDistance != markerDistance)
+ ClearPath();
+
+ m_MarkerDistance = markerDistance;
+ m_Speed = speed;
+ m_Link = link;
+
+ Play(duration);
+}
+
+void ed::FlowAnimation::Draw(ImDrawList* drawList)
+{
+ if (!IsPlaying() || !IsLinkValid() || !m_Link->IsVisible())
+ return;
+
+ if (!IsPathValid())
+ UpdatePath();
+
+ m_Offset = fmodf(m_Offset, m_MarkerDistance);
+
+ const auto progress = GetProgress();
+
+ const auto flowAlpha = 1.0f - progress * progress;
+ const auto flowColor = Editor->GetColor(StyleColor_Flow, flowAlpha);
+ //const auto flowPath = Link->GetCurve();
+
+ m_Link->Draw(drawList, flowColor, 2.0f);
+
+ if (IsPathValid())
+ {
+ //Offset = 0;
+
+ const auto markerAlpha = powf(1.0f - progress, 0.35f);
+ const auto markerRadius = 4.0f * (1.0f - progress) + 2.0f;
+ const auto markerColor = Editor->GetColor(StyleColor_FlowMarker, markerAlpha);
+
+ for (float d = m_Offset; d < m_PathLength; d += m_MarkerDistance)
+ drawList->AddCircleFilled(SamplePath(d), markerRadius, markerColor);
+ }
+}
+
+bool ed::FlowAnimation::IsLinkValid() const
+{
+ return m_Link && m_Link->m_IsLive;
+}
+
+bool ed::FlowAnimation::IsPathValid() const
+{
+ return m_Path.size() > 1 && m_PathLength > 0.0f && m_Link->m_Start == m_LastStart && m_Link->m_End == m_LastEnd;
+}
+
+void ed::FlowAnimation::UpdatePath()
+{
+ if (!IsLinkValid())
+ {
+ ClearPath();
+ return;
+ }
+
+ const auto curve = m_Link->GetCurve();
+
+ m_LastStart = m_Link->m_Start;
+ m_LastEnd = m_Link->m_End;
+ m_PathLength = ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3);
+
+ auto collectPointsCallback = [this](ImCubicBezierFixedStepSample& result)
+ {
+ m_Path.push_back(CurvePoint{ result.Length, result.Point });
+ };
+
+ const auto step = ImMax(m_MarkerDistance * 0.5f, 15.0f);
+
+ m_Path.resize(0);
+ ImCubicBezierFixedStep(collectPointsCallback, curve, step, false, 0.5f, 0.001f);
+}
+
+void ed::FlowAnimation::ClearPath()
+{
+ vector<CurvePoint>().swap(m_Path);
+ m_PathLength = 0.0f;
+}
+
+ImVec2 ed::FlowAnimation::SamplePath(float distance)
+{
+ //distance = ImMax(0.0f, std::min(distance, PathLength));
+
+ auto endPointIt = std::find_if(m_Path.begin(), m_Path.end(), [distance](const CurvePoint& p) { return distance < p.Distance; });
+ if (endPointIt == m_Path.end())
+ endPointIt = m_Path.end() - 1;
+ else if (endPointIt == m_Path.begin())
+ endPointIt = m_Path.begin() + 1;
+
+ const auto& start = endPointIt[-1];
+ const auto& end = *endPointIt;
+ const auto t = (distance - start.Distance) / (end.Distance - start.Distance);
+
+ return start.Point + (end.Point - start.Point) * t;
+}
+
+void ed::FlowAnimation::OnUpdate(float progress)
+{
+ IM_UNUSED(progress);
+
+ m_Offset += m_Speed * ImGui::GetIO().DeltaTime;
+}
+
+void ed::FlowAnimation::OnStop()
+{
+ Controller->Release(this);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Flow Animation Controller
+//
+//------------------------------------------------------------------------------
+ed::FlowAnimationController::FlowAnimationController(EditorContext* editor):
+ AnimationController(editor)
+{
+}
+
+ed::FlowAnimationController::~FlowAnimationController()
+{
+ for (auto animation : m_Animations)
+ delete animation;
+}
+
+void ed::FlowAnimationController::Flow(Link* link)
+{
+ if (!link || !link->m_IsLive)
+ return;
+
+ auto& editorStyle = GetStyle();
+
+ auto animation = GetOrCreate(link);
+
+ animation->Flow(link, editorStyle.FlowMarkerDistance, editorStyle.FlowSpeed, editorStyle.FlowDuration);
+}
+
+void ed::FlowAnimationController::Draw(ImDrawList* drawList)
+{
+ if (m_Animations.empty())
+ return;
+
+ drawList->ChannelsSetCurrent(c_LinkChannel_Flow);
+
+ for (auto animation : m_Animations)
+ animation->Draw(drawList);
+}
+
+ed::FlowAnimation* ed::FlowAnimationController::GetOrCreate(Link* link)
+{
+ // Return live animation which match target link
+ {
+ auto animationIt = std::find_if(m_Animations.begin(), m_Animations.end(), [link](FlowAnimation* animation) { return animation->m_Link == link; });
+ if (animationIt != m_Animations.end())
+ return *animationIt;
+ }
+
+ // There are no live animations for target link, try to reuse inactive old one
+ if (!m_FreePool.empty())
+ {
+ auto animation = m_FreePool.back();
+ m_FreePool.pop_back();
+ return animation;
+ }
+
+ // Cache miss, allocate new one
+ auto animation = new FlowAnimation(this);
+ m_Animations.push_back(animation);
+
+ return animation;
+}
+
+void ed::FlowAnimationController::Release(FlowAnimation* animation)
+{
+ IM_UNUSED(animation);
+}
+
+
+
+//------------------------------------------------------------------------------
+//
+// Navigate Action
+//
+//------------------------------------------------------------------------------
+const float ed::NavigateAction::s_ZoomLevels[] =
+{
+ 0.1f, 0.15f, 0.20f, 0.25f, 0.33f, 0.5f, 0.75f, 1.0f, 1.25f, 1.50f, 2.0f, 2.5f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f
+};
+
+const int ed::NavigateAction::s_ZoomLevelCount = sizeof(s_ZoomLevels) / sizeof(*s_ZoomLevels);
+
+ed::NavigateAction::NavigateAction(EditorContext* editor, ImGuiEx::Canvas& canvas):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_Zoom(1),
+ m_Scroll(0, 0),
+ m_ScrollStart(0, 0),
+ m_ScrollDelta(0, 0),
+ m_Canvas(canvas),
+ m_WindowScreenPos(0, 0),
+ m_WindowScreenSize(0, 0),
+ m_Animation(editor, *this),
+ m_Reason(NavigationReason::Unknown),
+ m_LastSelectionId(0),
+ m_LastObject(nullptr),
+ m_MovingOverEdge(false),
+ m_MoveOffset(0, 0)
+{
+}
+
+ed::EditorAction::AcceptResult ed::NavigateAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ if (ImGui::IsWindowHovered() /*&& !ImGui::IsAnyItemActive()*/ && ImGui::IsMouseDragging(c_ScrollButtonIndex, 0.0f))
+ {
+ m_IsActive = true;
+ m_ScrollStart = m_Scroll;
+ m_ScrollDelta = ImGui::GetMouseDragDelta(c_ScrollButtonIndex);
+ m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom;
+ }
+
+ auto& io = ImGui::GetIO();
+
+ if (ImGui::IsWindowFocused() && ImGui::IsKeyPressed(GetKeyIndexForF()) && Editor->AreShortcutsEnabled())
+ {
+ const auto allowZoomIn = io.KeyShift;
+
+ auto findHotObjectToZoom = [this, &control, &io]() -> Object*
+ {
+ if (control.HotObject)
+ {
+ if (auto pin = control.HotObject->AsPin())
+ return pin->m_Node;
+ else
+ return control.HotObject;
+ }
+ else if (control.BackgroundHot)
+ {
+ auto node = Editor->FindNodeAt(io.MousePos);
+ if (IsGroup(node))
+ return node;
+ }
+
+ return nullptr;
+ };
+
+ bool navigateToContent = false;
+ if (!Editor->GetSelectedObjects().empty())
+ {
+ if (m_Reason != NavigationReason::Selection || m_LastSelectionId != Editor->GetSelectionId() || allowZoomIn)
+ {
+ m_LastSelectionId = Editor->GetSelectionId();
+ NavigateTo(Editor->GetSelectionBounds(), allowZoomIn, -1.0f, NavigationReason::Selection);
+ }
+ else
+ navigateToContent = true;
+ }
+ else if(auto hotObject = findHotObjectToZoom())
+ {
+ if (m_Reason != NavigationReason::Object || m_LastObject != hotObject || allowZoomIn)
+ {
+ m_LastObject = hotObject;
+ auto bounds = hotObject->GetBounds();
+ NavigateTo(bounds, allowZoomIn, -1.0f, NavigationReason::Object);
+ }
+ else
+ navigateToContent = true;
+ }
+ else
+ navigateToContent = true;
+
+ if (navigateToContent)
+ NavigateTo(Editor->GetContentBounds(), true, -1.0f, NavigationReason::Content);
+ }
+
+ // // #debug
+ // if (auto drawList = ImGui::GetWindowDrawList())
+ // drawList->AddCircleFilled(io.MousePos, 4.0f, IM_COL32(255, 0, 255, 255));
+
+ if (HandleZoom(control))
+ return True;
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::NavigateAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ if (!m_IsActive)
+ return false;
+
+ if (ImGui::IsMouseDragging(c_ScrollButtonIndex, 0.0f))
+ {
+ m_ScrollDelta = ImGui::GetMouseDragDelta(c_ScrollButtonIndex);
+ m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom;
+
+// if (IsActive && Animation.IsPlaying())
+// Animation.Target = Animation.Target - ScrollDelta * Animation.TargetZoom;
+ }
+ else
+ {
+ if (m_Scroll != m_ScrollStart)
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+
+ m_IsActive = false;
+ }
+
+ // #TODO: Handle zoom while scrolling
+ // HandleZoom(control);
+
+ return m_IsActive;
+}
+
+bool ed::NavigateAction::HandleZoom(const Control& control)
+{
+ IM_UNUSED(control);
+
+ const auto currentAction = Editor->GetCurrentAction();
+ const auto allowOffscreen = currentAction && currentAction->IsDragging();
+
+ auto& io = ImGui::GetIO();
+
+ if (!io.MouseWheel || (!allowOffscreen && !ImGui::IsWindowHovered()))// && !ImGui::IsAnyItemActive())
+ return false;
+
+ auto savedScroll = m_Scroll;
+ auto savedZoom = m_Zoom;
+
+ m_Animation.Finish();
+
+ auto mousePos = io.MousePos;
+ auto steps = (int)io.MouseWheel;
+ auto newZoom = MatchZoom(steps, s_ZoomLevels[steps < 0 ? 0 : s_ZoomLevelCount - 1]);
+
+ auto oldView = GetView();
+ m_Zoom = newZoom;
+ auto newView = GetView();
+
+ auto screenPos = m_Canvas.FromLocal(mousePos, oldView);
+ auto canvasPos = m_Canvas.ToLocal(screenPos, newView);
+
+ auto offset = (canvasPos - mousePos) * m_Zoom;
+ auto targetScroll = m_Scroll - offset;
+
+ if (m_Scroll != savedScroll || m_Zoom != savedZoom)
+ {
+ m_Scroll = savedScroll;
+ m_Zoom = savedZoom;
+
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+ }
+
+ auto targetRect = m_Canvas.CalcViewRect(ImGuiEx::CanvasView(-targetScroll, newZoom));
+
+ NavigateTo(targetRect, c_MouseZoomDuration, NavigationReason::MouseZoom);
+
+ return true;
+}
+
+void ed::NavigateAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+ ImGui::Text(" Scroll: { x=%g y=%g }", m_Scroll.x, m_Scroll.y);
+ ImGui::Text(" Zoom: %g", m_Zoom);
+}
+
+void ed::NavigateAction::NavigateTo(const ImRect& bounds, bool zoomIn, float duration, NavigationReason reason)
+{
+ if (ImRect_IsEmpty(bounds))
+ return;
+
+ if (duration < 0.0f)
+ duration = GetStyle().ScrollDuration;
+
+ if (!zoomIn)
+ {
+ auto viewRect = m_Canvas.ViewRect();
+ auto viewRectCenter = viewRect.GetCenter();
+ auto targetCenter = bounds.GetCenter();
+
+ viewRect.Translate(targetCenter - viewRectCenter);
+
+ NavigateTo(viewRect, duration, reason);
+ }
+ else
+ {
+ // Grow rect by 5% to leave some reasonable margin
+ // from the edges of the canvas.
+ auto rect = bounds;
+ auto extend = ImMax(rect.GetWidth(), rect.GetHeight());
+ rect.Expand(extend * c_NavigationZoomMargin * 0.5f);
+
+ NavigateTo(rect, duration, reason);
+ }
+}
+
+void ed::NavigateAction::NavigateTo(const ImRect& target, float duration, NavigationReason reason)
+{
+ m_Reason = reason;
+
+ m_Animation.NavigateTo(target, duration);
+}
+
+void ed::NavigateAction::StopNavigation()
+{
+ m_Animation.Stop();
+}
+
+void ed::NavigateAction::FinishNavigation()
+{
+ m_Animation.Finish();
+}
+
+bool ed::NavigateAction::MoveOverEdge()
+{
+ // Don't interrupt non-edge animations
+ if (m_Animation.IsPlaying())
+ return false;
+
+ auto& io = ImGui::GetIO();
+ const auto screenRect = m_Canvas.ViewRect();
+ const auto screenMousePos = io.MousePos;
+
+ // Mouse is over screen, do nothing
+ if (screenRect.Contains(screenMousePos))
+ return false;
+
+ const auto screenPointOnEdge = ImRect_ClosestPoint(screenRect, screenMousePos, true);
+ const auto direction = screenPointOnEdge - screenMousePos;
+ const auto offset = -direction * io.DeltaTime * 10.0f;
+
+ m_Scroll = m_Scroll + offset;
+
+ m_MoveOffset = offset;
+ m_MovingOverEdge = true;
+
+ return true;
+}
+
+void ed::NavigateAction::StopMoveOverEdge()
+{
+ if (m_MovingOverEdge)
+ {
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+
+ m_MoveOffset = ImVec2(0, 0);
+ m_MovingOverEdge = false;
+ }
+}
+
+void ed::NavigateAction::SetWindow(ImVec2 position, ImVec2 size)
+{
+ m_WindowScreenPos = position;
+ m_WindowScreenSize = size;
+}
+
+ImGuiEx::CanvasView ed::NavigateAction::GetView() const
+{
+ return ImGuiEx::CanvasView(-m_Scroll, m_Zoom);
+}
+
+ImVec2 ed::NavigateAction::GetViewOrigin() const
+{
+ return -m_Scroll;
+}
+
+float ed::NavigateAction::GetViewScale() const
+{
+ return m_Zoom;
+}
+
+void ed::NavigateAction::SetViewRect(const ImRect& rect)
+{
+ auto view = m_Canvas.CalcCenterView(rect);
+ m_Scroll = -view.Origin;
+ m_Zoom = view.Scale;
+}
+
+ImRect ed::NavigateAction::GetViewRect() const
+{
+ return m_Canvas.CalcViewRect(GetView());
+}
+
+float ed::NavigateAction::MatchZoom(int steps, float fallbackZoom)
+{
+ auto currentZoomIndex = MatchZoomIndex(steps);
+ if (currentZoomIndex < 0)
+ return fallbackZoom;
+
+ auto currentZoom = s_ZoomLevels[currentZoomIndex];
+ if (fabsf(currentZoom - m_Zoom) > 0.001f)
+ return currentZoom;
+
+ auto newIndex = currentZoomIndex + steps;
+ if (newIndex >= 0 && newIndex < s_ZoomLevelCount)
+ return s_ZoomLevels[newIndex];
+ else
+ return fallbackZoom;
+}
+
+int ed::NavigateAction::MatchZoomIndex(int direction)
+{
+ int bestIndex = -1;
+ float bestDistance = 0.0f;
+
+ for (int i = 0; i < s_ZoomLevelCount; ++i)
+ {
+ auto distance = fabsf(s_ZoomLevels[i] - m_Zoom);
+ if (distance < bestDistance || bestIndex < 0)
+ {
+ bestDistance = distance;
+ bestIndex = i;
+ }
+ }
+
+ if (bestDistance > 0.001f)
+ {
+ if (direction > 0)
+ {
+ ++bestIndex;
+
+ if (bestIndex >= s_ZoomLevelCount)
+ bestIndex = s_ZoomLevelCount - 1;
+ }
+ else if (direction < 0)
+ {
+ --bestIndex;
+
+ if (bestIndex < 0)
+ bestIndex = 0;
+ }
+ }
+
+ return bestIndex;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Size Action
+//
+//------------------------------------------------------------------------------
+ed::SizeAction::SizeAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_Clean(false),
+ m_SizedNode(nullptr),
+ m_Pivot(NodeRegion::None),
+ m_Cursor(ImGuiMouseCursor_Arrow)
+{
+}
+
+ed::EditorAction::AcceptResult ed::SizeAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ if (control.ActiveNode && IsGroup(control.ActiveNode) && ImGui::IsMouseDragging(0, 0))
+ {
+ //const auto mousePos = to_point(ImGui::GetMousePos());
+ //const auto closestPoint = control.ActiveNode->Bounds.get_closest_point_hollow(mousePos, static_cast<int>(control.ActiveNode->Rounding));
+
+ auto pivot = GetRegion(control.ActiveNode);
+ if (pivot != NodeRegion::Header && pivot != NodeRegion::Center)
+ {
+ m_StartBounds = control.ActiveNode->m_Bounds;
+ m_StartGroupBounds = control.ActiveNode->m_GroupBounds;
+ m_LastSize = control.ActiveNode->m_Bounds.GetSize();
+ m_MinimumSize = ImVec2(0, 0);
+ m_LastDragOffset = ImVec2(0, 0);
+ m_Pivot = pivot;
+ m_Cursor = ChooseCursor(m_Pivot);
+ m_SizedNode = control.ActiveNode;
+ m_IsActive = true;
+ }
+ }
+ else if (control.HotNode && IsGroup(control.HotNode))
+ {
+ m_Cursor = ChooseCursor(GetRegion(control.HotNode));
+ return Possible;
+ }
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::SizeAction::Process(const Control& control)
+{
+ if (m_Clean)
+ {
+ m_Clean = false;
+
+ if (m_SizedNode->m_Bounds.Min != m_StartBounds.Min || m_SizedNode->m_GroupBounds.Min != m_StartGroupBounds.Min)
+ Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, m_SizedNode);
+
+ if (m_SizedNode->m_Bounds.GetSize() != m_StartBounds.GetSize() || m_SizedNode->m_GroupBounds.GetSize() != m_StartGroupBounds.GetSize())
+ Editor->MakeDirty(SaveReasonFlags::Size | SaveReasonFlags::User, m_SizedNode);
+
+ m_SizedNode = nullptr;
+ }
+
+ if (!m_IsActive)
+ return false;
+
+ if (control.ActiveNode == m_SizedNode)
+ {
+ const auto dragOffset = (control.ActiveNode == m_SizedNode) ? ImGui::GetMouseDragDelta(0, 0.0f) : m_LastDragOffset;
+ m_LastDragOffset = dragOffset;
+
+ if (m_MinimumSize.x == 0.0f && m_LastSize.x != m_SizedNode->m_Bounds.GetWidth())
+ m_MinimumSize.x = m_SizedNode->m_Bounds.GetWidth();
+ if (m_MinimumSize.y == 0.0f && m_LastSize.y != m_SizedNode->m_Bounds.GetHeight())
+ m_MinimumSize.y = m_SizedNode->m_Bounds.GetHeight();
+
+ auto minimumSize = ImMax(m_MinimumSize, m_StartBounds.GetSize() - m_StartGroupBounds.GetSize());
+
+
+ auto newBounds = m_StartBounds;
+
+ if ((m_Pivot & NodeRegion::Top) == NodeRegion::Top)
+ newBounds.Min.y = ImMin(newBounds.Max.y - minimumSize.y, Editor->AlignPointToGrid(newBounds.Min.y + dragOffset.y));
+ if ((m_Pivot & NodeRegion::Bottom) == NodeRegion::Bottom)
+ newBounds.Max.y = ImMax(newBounds.Min.y + minimumSize.y, Editor->AlignPointToGrid(newBounds.Max.y + dragOffset.y));
+ if ((m_Pivot & NodeRegion::Left) == NodeRegion::Left)
+ newBounds.Min.x = ImMin(newBounds.Max.x - minimumSize.x, Editor->AlignPointToGrid(newBounds.Min.x + dragOffset.x));
+ if ((m_Pivot & NodeRegion::Right) == NodeRegion::Right)
+ newBounds.Max.x = ImMax(newBounds.Min.x + minimumSize.x, Editor->AlignPointToGrid(newBounds.Max.x + dragOffset.x));
+
+ newBounds.Floor();
+
+ m_LastSize = newBounds.GetSize();
+
+ m_SizedNode->m_Bounds = newBounds;
+ m_SizedNode->m_GroupBounds = newBounds;
+ m_SizedNode->m_GroupBounds.Min.x -= m_StartBounds.Min.x - m_StartGroupBounds.Min.x;
+ m_SizedNode->m_GroupBounds.Min.y -= m_StartBounds.Min.y - m_StartGroupBounds.Min.y;
+ m_SizedNode->m_GroupBounds.Max.x -= m_StartBounds.Max.x - m_StartGroupBounds.Max.x;
+ m_SizedNode->m_GroupBounds.Max.y -= m_StartBounds.Max.y - m_StartGroupBounds.Max.y;
+ }
+ else if (!control.ActiveNode)
+ {
+ m_Clean = true;
+ m_IsActive = false;
+ return true;
+ }
+
+ return m_IsActive;
+}
+
+void ed::SizeAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ 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 "";
+ };
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+ ImGui::Text(" Node: %s (%p)", getObjectName(m_SizedNode), m_SizedNode ? m_SizedNode->m_ID.AsPointer() : nullptr);
+ if (m_SizedNode && m_IsActive)
+ {
+ ImGui::Text(" Bounds: { x=%g y=%g w=%g h=%g }", m_SizedNode->m_Bounds.Min.x, m_SizedNode->m_Bounds.Min.y, m_SizedNode->m_Bounds.GetWidth(), m_SizedNode->m_Bounds.GetHeight());
+ ImGui::Text(" Group Bounds: { x=%g y=%g w=%g h=%g }", m_SizedNode->m_GroupBounds.Min.x, m_SizedNode->m_GroupBounds.Min.y, m_SizedNode->m_GroupBounds.GetWidth(), m_SizedNode->m_GroupBounds.GetHeight());
+ ImGui::Text(" Start Bounds: { x=%g y=%g w=%g h=%g }", m_StartBounds.Min.x, m_StartBounds.Min.y, m_StartBounds.GetWidth(), m_StartBounds.GetHeight());
+ ImGui::Text(" Start Group Bounds: { x=%g y=%g w=%g h=%g }", m_StartGroupBounds.Min.x, m_StartGroupBounds.Min.y, m_StartGroupBounds.GetWidth(), m_StartGroupBounds.GetHeight());
+ ImGui::Text(" Minimum Size: { w=%g h=%g }", m_MinimumSize.x, m_MinimumSize.y);
+ ImGui::Text(" Last Size: { w=%g h=%g }", m_LastSize.x, m_LastSize.y);
+ }
+}
+
+ed::NodeRegion ed::SizeAction::GetRegion(Node* node)
+{
+ return node->GetRegion(ImGui::GetMousePos());
+}
+
+ImGuiMouseCursor ed::SizeAction::ChooseCursor(NodeRegion region)
+{
+ switch (region)
+ {
+ default:
+ case NodeRegion::Center:
+ return ImGuiMouseCursor_Arrow;
+
+ case NodeRegion::Top:
+ case NodeRegion::Bottom:
+ return ImGuiMouseCursor_ResizeNS;
+
+ case NodeRegion::Left:
+ case NodeRegion::Right:
+ return ImGuiMouseCursor_ResizeEW;
+
+ case NodeRegion::TopLeft:
+ case NodeRegion::BottomRight:
+ return ImGuiMouseCursor_ResizeNWSE;
+
+ case NodeRegion::TopRight:
+ case NodeRegion::BottomLeft:
+ return ImGuiMouseCursor_ResizeNESW;
+ }
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Drag Action
+//
+//------------------------------------------------------------------------------
+ed::DragAction::DragAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_Clear(false),
+ m_DraggedObject(nullptr)
+{
+}
+
+ed::EditorAction::AcceptResult ed::DragAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ if (control.ActiveObject && ImGui::IsMouseDragging(0))
+ {
+ if (!control.ActiveObject->AcceptDrag())
+ return False;
+
+ m_DraggedObject = control.ActiveObject;
+
+ m_Objects.resize(0);
+ m_Objects.push_back(m_DraggedObject);
+
+ if (Editor->IsSelected(m_DraggedObject))
+ {
+ for (auto selectedObject : Editor->GetSelectedObjects())
+ if (auto selectedNode = selectedObject->AsNode())
+ if (selectedNode != m_DraggedObject && selectedNode->AcceptDrag())
+ m_Objects.push_back(selectedNode);
+ }
+
+ auto& io = ImGui::GetIO();
+ if (!io.KeyShift)
+ {
+ std::vector<Node*> groupedNodes;
+ for (auto object : m_Objects)
+ if (auto node = object->AsNode())
+ node->GetGroupedNodes(groupedNodes, true);
+
+ auto isAlreadyPicked = [this](Node* node)
+ {
+ return std::find(m_Objects.begin(), m_Objects.end(), node) != m_Objects.end();
+ };
+
+ for (auto candidate : groupedNodes)
+ if (!isAlreadyPicked(candidate) && candidate->AcceptDrag())
+ m_Objects.push_back(candidate);
+ }
+
+ m_IsActive = true;
+ }
+ else if (control.HotNode && IsGroup(control.HotNode) && control.HotNode->GetRegion(ImGui::GetMousePos()) == NodeRegion::Header)
+ {
+ return Possible;
+ }
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::DragAction::Process(const Control& control)
+{
+ if (m_Clear)
+ {
+ m_Clear = false;
+
+ for (auto object : m_Objects)
+ {
+ if (object->EndDrag())
+ Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, object->AsNode());
+ }
+
+ m_Objects.resize(0);
+
+ m_DraggedObject = nullptr;
+ }
+
+ if (!m_IsActive)
+ return false;
+
+ if (control.ActiveObject == m_DraggedObject)
+ {
+ auto dragOffset = ImGui::GetMouseDragDelta(0, 0.0f);
+
+ auto draggedOrigin = m_DraggedObject->DragStartLocation();
+ auto alignPivot = ImVec2(0, 0);
+
+ // TODO: Move this experimental alignment to closes pivot out of internals to node API
+ if (auto draggedNode = m_DraggedObject->AsNode())
+ {
+ float x = FLT_MAX;
+ float y = FLT_MAX;
+
+ auto testPivot = [this, &x, &y, &draggedOrigin, &dragOffset, &alignPivot](const ImVec2& pivot)
+ {
+ auto initial = draggedOrigin + dragOffset + pivot;
+ auto candidate = Editor->AlignPointToGrid(initial) - draggedOrigin - pivot;
+
+ if (ImFabs(candidate.x) < ImFabs(ImMin(x, FLT_MAX)))
+ {
+ x = candidate.x;
+ alignPivot.x = pivot.x;
+ }
+
+ if (ImFabs(candidate.y) < ImFabs(ImMin(y, FLT_MAX)))
+ {
+ y = candidate.y;
+ alignPivot.y = pivot.y;
+ }
+ };
+
+ for (auto pin = draggedNode->m_LastPin; pin; pin = pin->m_PreviousPin)
+ {
+ auto pivot = pin->m_Pivot.GetCenter() - draggedNode->m_Bounds.Min;
+ testPivot(pivot);
+ }
+
+ //testPivot(point(0, 0));
+ }
+
+ auto alignedOffset = Editor->AlignPointToGrid(draggedOrigin + dragOffset + alignPivot) - draggedOrigin - alignPivot;
+
+ if (!ImGui::GetIO().KeyAlt)
+ dragOffset = alignedOffset;
+
+ for (auto object : m_Objects)
+ object->UpdateDrag(dragOffset);
+ }
+ else if (!control.ActiveObject)
+ {
+ m_Clear = true;
+
+ m_IsActive = false;
+ return true;
+ }
+
+ return m_IsActive;
+}
+
+void ed::DragAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ 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 "";
+ };
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+ ImGui::Text(" Node: %s (%p)", getObjectName(m_DraggedObject), m_DraggedObject ? m_DraggedObject->ID().AsPointer() : nullptr);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Select Action
+//
+//------------------------------------------------------------------------------
+ed::SelectAction::SelectAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_SelectGroups(false),
+ m_SelectLinkMode(false),
+ m_CommitSelection(false),
+ m_StartPoint(),
+ m_Animation(editor)
+{
+}
+
+ed::EditorAction::AcceptResult ed::SelectAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ auto& io = ImGui::GetIO();
+ m_SelectGroups = io.KeyShift;
+ m_SelectLinkMode = io.KeyAlt;
+
+ m_SelectedObjectsAtStart.clear();
+
+ if (control.BackgroundActive && ImGui::IsMouseDragging(0, 1))
+ {
+ m_IsActive = true;
+ m_StartPoint = ImGui::GetMousePos();
+ m_EndPoint = m_StartPoint;
+
+ // Links and nodes cannot be selected together
+ if ((m_SelectLinkMode && Editor->IsAnyNodeSelected()) ||
+ (!m_SelectLinkMode && Editor->IsAnyLinkSelected()))
+ {
+ Editor->ClearSelection();
+ }
+
+ if (io.KeyCtrl)
+ m_SelectedObjectsAtStart = Editor->GetSelectedObjects();
+ }
+ else if (control.BackgroundClicked)
+ {
+ Editor->ClearSelection();
+ }
+ else
+ {
+ Object* clickedObject = control.ClickedNode ? static_cast<Object*>(control.ClickedNode) : static_cast<Object*>(control.ClickedLink);
+
+ if (clickedObject)
+ {
+ // Links and nodes cannot be selected together
+ if ((clickedObject->AsLink() && Editor->IsAnyNodeSelected()) ||
+ (clickedObject->AsNode() && Editor->IsAnyLinkSelected()))
+ {
+ Editor->ClearSelection();
+ }
+
+ if (io.KeyCtrl)
+ Editor->ToggleObjectSelection(clickedObject);
+ else
+ Editor->SetSelectedObject(clickedObject);
+ }
+ }
+
+ if (m_IsActive)
+ m_Animation.Stop();
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::SelectAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ if (m_CommitSelection)
+ {
+ Editor->ClearSelection();
+ for (auto object : m_CandidateObjects)
+ Editor->SelectObject(object);
+
+ m_CandidateObjects.clear();
+
+ m_CommitSelection = false;
+ }
+
+ if (!m_IsActive)
+ return false;
+
+ if (ImGui::IsMouseDragging(0, 0))
+ {
+ m_EndPoint = ImGui::GetMousePos();
+
+ auto topLeft = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), std::min(m_StartPoint.y, m_EndPoint.y));
+ auto bottomRight = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), ImMax(m_StartPoint.y, m_EndPoint.y));
+ auto rect = ImRect(topLeft, bottomRight);
+ if (rect.GetWidth() <= 0)
+ rect.Max.x = rect.Min.x + 1;
+ if (rect.GetHeight() <= 0)
+ rect.Max.y = rect.Min.y + 1;
+
+ vector<Node*> nodes;
+ vector<Link*> links;
+
+ if (m_SelectLinkMode)
+ {
+ Editor->FindLinksInRect(rect, links);
+ m_CandidateObjects.assign(links.begin(), links.end());
+ }
+ else
+ {
+ Editor->FindNodesInRect(rect, nodes);
+ m_CandidateObjects.assign(nodes.begin(), nodes.end());
+
+ if (m_SelectGroups)
+ {
+ auto endIt = std::remove_if(m_CandidateObjects.begin(), m_CandidateObjects.end(), [](Object* object) { return !IsGroup(object->AsNode()); });
+ m_CandidateObjects.erase(endIt, m_CandidateObjects.end());
+ }
+ else
+ {
+ auto endIt = std::remove_if(m_CandidateObjects.begin(), m_CandidateObjects.end(), [](Object* object) { return IsGroup(object->AsNode()); });
+ m_CandidateObjects.erase(endIt, m_CandidateObjects.end());
+ }
+ }
+
+ m_CandidateObjects.insert(m_CandidateObjects.end(), m_SelectedObjectsAtStart.begin(), m_SelectedObjectsAtStart.end());
+ std::sort(m_CandidateObjects.begin(), m_CandidateObjects.end());
+ m_CandidateObjects.erase(std::unique(m_CandidateObjects.begin(), m_CandidateObjects.end()), m_CandidateObjects.end());
+ }
+ else
+ {
+ m_IsActive = false;
+
+ m_Animation.Play(c_SelectionFadeOutDuration);
+
+ m_CommitSelection = true;
+
+ return true;
+ }
+
+ return m_IsActive;
+}
+
+void ed::SelectAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+}
+
+void ed::SelectAction::Draw(ImDrawList* drawList)
+{
+ if (!m_IsActive && !m_Animation.IsPlaying())
+ return;
+
+ const auto alpha = m_Animation.IsPlaying() ? ImEasing::EaseOutQuad(1.0f, -1.0f, m_Animation.GetProgress()) : 1.0f;
+
+ const auto fillColor = Editor->GetColor(m_SelectLinkMode ? StyleColor_LinkSelRect : StyleColor_NodeSelRect, alpha);
+ const auto outlineColor = Editor->GetColor(m_SelectLinkMode ? StyleColor_LinkSelRectBorder : StyleColor_NodeSelRectBorder, alpha);
+
+ drawList->ChannelsSetCurrent(c_BackgroundChannel_SelectionRect);
+
+ auto min = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), std::min(m_StartPoint.y, m_EndPoint.y));
+ auto max = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), ImMax(m_StartPoint.y, m_EndPoint.y));
+
+ drawList->AddRectFilled(min, max, fillColor);
+ FringeScaleScope fringe(1.0f);
+ drawList->AddRect(min, max, outlineColor);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Context Menu Action
+//
+//------------------------------------------------------------------------------
+ed::ContextMenuAction::ContextMenuAction(EditorContext* editor):
+ EditorAction(editor),
+ m_CandidateMenu(Menu::None),
+ m_CurrentMenu(Menu::None),
+ m_ContextId()
+{
+}
+
+ed::EditorAction::AcceptResult ed::ContextMenuAction::Accept(const Control& control)
+{
+ const auto isPressed = ImGui::IsMouseClicked(1);
+ const auto isReleased = ImGui::IsMouseReleased(1);
+ const auto isDragging = ImGui::IsMouseDragging(1);
+
+ if (isPressed || isReleased || isDragging)
+ {
+ Menu candidateMenu = ContextMenuAction::None;
+ ObjectId contextId;
+
+ if (auto hotObejct = control.HotObject)
+ {
+ if (hotObejct->AsNode())
+ candidateMenu = Node;
+ else if (hotObejct->AsPin())
+ candidateMenu = Pin;
+ else if (hotObejct->AsLink())
+ candidateMenu = Link;
+
+ if (candidateMenu != None)
+ contextId = hotObejct->ID();
+ }
+ else if (control.BackgroundHot)
+ candidateMenu = Background;
+
+ if (isPressed)
+ {
+ m_CandidateMenu = candidateMenu;
+ m_ContextId = contextId;
+ return Possible;
+ }
+ else if (isReleased && m_CandidateMenu == candidateMenu && m_ContextId == contextId)
+ {
+ m_CurrentMenu = m_CandidateMenu;
+ m_CandidateMenu = ContextMenuAction::None;
+ return True;
+ }
+ else
+ {
+ m_CandidateMenu = None;
+ m_CurrentMenu = None;
+ m_ContextId = ObjectId();
+ return False;
+ }
+ }
+
+ return False;
+}
+
+bool ed::ContextMenuAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ m_CandidateMenu = None;
+ m_CurrentMenu = None;
+ m_ContextId = ObjectId();
+ return false;
+}
+
+void ed::ContextMenuAction::Reject()
+{
+ m_CandidateMenu = None;
+ m_CurrentMenu = None;
+ m_ContextId = ObjectId();
+}
+
+void ed::ContextMenuAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ auto getMenuName = [](Menu menu)
+ {
+ switch (menu)
+ {
+ default:
+ case None: return "None";
+ case Node: return "Node";
+ case Pin: return "Pin";
+ case Link: return "Link";
+ case Background: return "Background";
+ }
+ };
+
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Menu: %s", getMenuName(m_CurrentMenu));
+}
+
+bool ed::ContextMenuAction::ShowNodeContextMenu(NodeId* nodeId)
+{
+ if (m_CurrentMenu != Node)
+ return false;
+
+ *nodeId = m_ContextId.AsNodeId();
+ Editor->SetUserContext();
+ return true;
+}
+
+bool ed::ContextMenuAction::ShowPinContextMenu(PinId* pinId)
+{
+ if (m_CurrentMenu != Pin)
+ return false;
+
+ *pinId = m_ContextId.AsPinId();
+ Editor->SetUserContext();
+ return true;
+}
+
+bool ed::ContextMenuAction::ShowLinkContextMenu(LinkId* linkId)
+{
+ if (m_CurrentMenu != Link)
+ return false;
+
+ *linkId = m_ContextId.AsLinkId();
+ Editor->SetUserContext();
+ return true;
+}
+
+bool ed::ContextMenuAction::ShowBackgroundContextMenu()
+{
+ if (m_CurrentMenu != Background)
+ return false;
+
+ Editor->SetUserContext();
+ return true;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Cut/Copy/Paste Action
+//
+//------------------------------------------------------------------------------
+ed::ShortcutAction::ShortcutAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_InAction(false),
+ m_CurrentAction(Action::None),
+ m_Context()
+{
+}
+
+ed::EditorAction::AcceptResult ed::ShortcutAction::Accept(const Control& control)
+{
+ if (!Editor->IsActive() || !Editor->AreShortcutsEnabled())
+ return False;
+
+ Action candidateAction = None;
+
+ auto& io = ImGui::GetIO();
+ if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_X)))
+ candidateAction = Cut;
+ if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C)))
+ candidateAction = Copy;
+ if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_V)))
+ candidateAction = Paste;
+ if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(GetKeyIndexForD()))
+ candidateAction = Duplicate;
+ if (!io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Space)))
+ candidateAction = CreateNode;
+
+ if (candidateAction != None)
+ {
+ if (candidateAction != Paste && candidateAction != CreateNode)
+ {
+ auto& selection = Editor->GetSelectedObjects();
+ if (!selection.empty())
+ {
+ // #TODO: Find a way to simplify logic.
+
+ m_Context.assign(selection.begin(), selection.end());
+
+ // Expand groups
+ vector<Node*> extra;
+ for (auto object : m_Context)
+ {
+ auto node = object->AsNode();
+ if (IsGroup(node))
+ node->GetGroupedNodes(extra, true);
+ }
+
+ // Apply groups and remove duplicates
+ if (!extra.empty())
+ {
+ m_Context.insert(m_Context.end(), extra.begin(), extra.end());
+ std::sort(m_Context.begin(), m_Context.end());
+ m_Context.erase(std::unique(m_Context.begin(), m_Context.end()), m_Context.end());
+ }
+ }
+ else if (control.HotObject && control.HotObject->IsSelectable() && !IsGroup(control.HotObject->AsNode()))
+ {
+ m_Context.push_back(control.HotObject);
+ }
+
+ if (m_Context.empty())
+ return False;
+
+ // Does copying only links make sense?
+ //const auto hasOnlyLinks = std::all_of(Context.begin(), Context.end(), [](Object* object) { return object->AsLink() != nullptr; });
+ //if (hasOnlyLinks)
+ // return False;
+
+ // If no links are selected, pick all links between nodes within context
+ const auto hasAnyLinks = std::any_of(m_Context.begin(), m_Context.end(), [](Object* object) { return object->AsLink() != nullptr; });
+ if (!hasAnyLinks && m_Context.size() > 1) // one node cannot make connection to anything
+ {
+ // Collect nodes in sorted vector viable for binary search
+ std::vector<ObjectWrapper<Node>> nodes;
+
+ nodes.reserve(m_Context.size());
+ std::for_each(m_Context.begin(), m_Context.end(), [&nodes](Object* object) { if (auto node = object->AsNode()) nodes.push_back({node->m_ID, node}); });
+
+ std::sort(nodes.begin(), nodes.end());
+
+ auto isNodeInContext = [&nodes](NodeId nodeId)
+ {
+ return std::binary_search(nodes.begin(), nodes.end(), ObjectWrapper<Node>{nodeId, nullptr});
+ };
+
+ // Collect links connected to nodes and drop those reaching out of context
+ std::vector<Link*> links;
+
+ for (auto node : nodes)
+ Editor->FindLinksForNode(node.m_ID, links, true);
+
+ // Remove duplicates
+ std::sort(links.begin(), links.end());
+ links.erase(std::unique(links.begin(), links.end()), links.end());
+
+ // Drop out of context links
+ links.erase(std::remove_if(links.begin(), links.end(), [&isNodeInContext](Link* link)
+ {
+ return !isNodeInContext(link->m_StartPin->m_Node->m_ID) || !isNodeInContext(link->m_EndPin->m_Node->m_ID);
+ }), links.end());
+
+ // Append links and remove duplicates
+ m_Context.insert(m_Context.end(), links.begin(), links.end());
+ }
+ }
+ else
+ m_Context.resize(0);
+
+ m_IsActive = true;
+ m_CurrentAction = candidateAction;
+
+ return True;
+ }
+
+ return False;
+}
+
+bool ed::ShortcutAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ m_IsActive = false;
+ m_CurrentAction = None;
+ m_Context.resize(0);
+ return false;
+}
+
+void ed::ShortcutAction::Reject()
+{
+ m_IsActive = false;
+ m_CurrentAction = None;
+ m_Context.resize(0);
+}
+
+void ed::ShortcutAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ auto getActionName = [](Action action)
+ {
+ switch (action)
+ {
+ default:
+ case None: return "None";
+ case Cut: return "Cut";
+ case Copy: return "Copy";
+ case Paste: return "Paste";
+ case Duplicate: return "Duplicate";
+ case CreateNode: return "CreateNode";
+ }
+ };
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Action: %s", getActionName(m_CurrentAction));
+}
+
+bool ed::ShortcutAction::Begin()
+{
+ if (m_IsActive)
+ m_InAction = true;
+ return m_IsActive;
+}
+
+void ed::ShortcutAction::End()
+{
+ if (m_IsActive)
+ m_InAction = false;
+}
+
+bool ed::ShortcutAction::AcceptCut()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == Cut;
+}
+
+bool ed::ShortcutAction::AcceptCopy()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == Copy;
+}
+
+bool ed::ShortcutAction::AcceptPaste()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == Paste;
+}
+
+bool ed::ShortcutAction::AcceptDuplicate()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == Duplicate;
+}
+
+bool ed::ShortcutAction::AcceptCreateNode()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == CreateNode;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Create Item Action
+//
+//------------------------------------------------------------------------------
+ed::CreateItemAction::CreateItemAction(EditorContext* editor):
+ EditorAction(editor),
+ m_InActive(false),
+ m_NextStage(None),
+ m_CurrentStage(None),
+ m_ItemType(NoItem),
+ m_UserAction(Unknown),
+ m_LinkColor(IM_COL32_WHITE),
+ m_LinkThickness(1.0f),
+ m_LinkStart(nullptr),
+ m_LinkEnd(nullptr),
+
+ m_IsActive(false),
+ m_DraggedPin(nullptr),
+
+ m_IsInGlobalSpace(false)
+{
+}
+
+ed::EditorAction::AcceptResult ed::CreateItemAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return EditorAction::False;
+
+ if (control.ActivePin && ImGui::IsMouseDragging(0))
+ {
+ m_DraggedPin = control.ActivePin;
+ DragStart(m_DraggedPin);
+
+ Editor->ClearSelection();
+ }
+ else if (control.HotPin)
+ {
+ return EditorAction::Possible;
+ }
+ else
+ return EditorAction::False;
+
+ m_IsActive = true;
+
+ return EditorAction::True;
+}
+
+bool ed::CreateItemAction::Process(const Control& control)
+{
+ IM_ASSERT(m_IsActive);
+
+ if (!m_IsActive)
+ return false;
+
+ if (m_DraggedPin && control.ActivePin == m_DraggedPin && (m_CurrentStage == Possible))
+ {
+ const auto draggingFromSource = (m_DraggedPin->m_Kind == PinKind::Output);
+
+ ed::Pin cursorPin(Editor, 0, draggingFromSource ? PinKind::Input : PinKind::Output);
+ cursorPin.m_Pivot = ImRect(ImGui::GetMousePos(), ImGui::GetMousePos());
+ cursorPin.m_Dir = -m_DraggedPin->m_Dir;
+ cursorPin.m_Strength = m_DraggedPin->m_Strength;
+
+ ed::Link candidate(Editor, 0);
+ candidate.m_Color = m_LinkColor;
+ candidate.m_StartPin = draggingFromSource ? m_DraggedPin : &cursorPin;
+ candidate.m_EndPin = draggingFromSource ? &cursorPin : m_DraggedPin;
+
+ ed::Pin*& freePin = draggingFromSource ? candidate.m_EndPin : candidate.m_StartPin;
+
+ if (control.HotPin)
+ {
+ DropPin(control.HotPin);
+
+ if (m_UserAction == UserAccept)
+ freePin = control.HotPin;
+ }
+ else if (control.BackgroundHot)
+ DropNode();
+ else
+ DropNothing();
+
+ auto drawList = ImGui::GetWindowDrawList();
+ drawList->ChannelsSetCurrent(c_LinkChannel_NewLink);
+
+ candidate.UpdateEndpoints();
+ candidate.Draw(drawList, m_LinkColor, m_LinkThickness);
+ }
+ else if (m_CurrentStage == Possible || !control.ActivePin)
+ {
+ if (!ImGui::IsWindowHovered())
+ {
+ m_DraggedPin = nullptr;
+ DropNothing();
+ }
+
+ DragEnd();
+ m_IsActive = false;
+ }
+
+ return m_IsActive;
+}
+
+void ed::CreateItemAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ auto getStageName = [](Stage stage)
+ {
+ switch (stage)
+ {
+ case None: return "None";
+ case Possible: return "Possible";
+ case Create: return "Create";
+ default: return "<unknown>";
+ }
+ };
+
+ auto getActionName = [](Action action)
+ {
+ switch (action)
+ {
+ default:
+ case Unknown: return "Unknown";
+ case UserReject: return "Reject";
+ case UserAccept: return "Accept";
+ }
+ };
+
+ auto getItemName = [](Type item)
+ {
+ switch (item)
+ {
+ default:
+ case NoItem: return "None";
+ case Node: return "Node";
+ case Link: return "Link";
+ }
+ };
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Stage: %s", getStageName(m_CurrentStage));
+ ImGui::Text(" User Action: %s", getActionName(m_UserAction));
+ ImGui::Text(" Item Type: %s", getItemName(m_ItemType));
+}
+
+void ed::CreateItemAction::SetStyle(ImU32 color, float thickness)
+{
+ m_LinkColor = color;
+ m_LinkThickness = thickness;
+}
+
+bool ed::CreateItemAction::Begin()
+{
+ IM_ASSERT(false == m_InActive);
+
+ m_InActive = true;
+ m_CurrentStage = m_NextStage;
+ m_UserAction = Unknown;
+ m_LinkColor = IM_COL32_WHITE;
+ m_LinkThickness = 1.0f;
+
+ if (m_CurrentStage == None)
+ return false;
+
+ m_LastChannel = ImGui::GetWindowDrawList()->_Splitter._Current;
+
+ return true;
+}
+
+void ed::CreateItemAction::End()
+{
+ IM_ASSERT(m_InActive);
+
+ if (m_IsInGlobalSpace)
+ {
+ ImGui::PopClipRect();
+ Editor->Resume(SuspendFlags::KeepSplitter);
+
+ auto currentChannel = ImGui::GetWindowDrawList()->_Splitter._Current;
+ if (currentChannel != m_LastChannel)
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(m_LastChannel);
+
+ m_IsInGlobalSpace = false;
+ }
+
+ m_InActive = false;
+}
+
+void ed::CreateItemAction::DragStart(Pin* startPin)
+{
+ IM_ASSERT(!m_InActive);
+
+ m_NextStage = Possible;
+ m_LinkStart = startPin;
+ m_LinkEnd = nullptr;
+}
+
+void ed::CreateItemAction::DragEnd()
+{
+ IM_ASSERT(!m_InActive);
+
+ if (m_CurrentStage == Possible && m_UserAction == UserAccept)
+ {
+ m_NextStage = Create;
+ }
+ else
+ {
+ m_NextStage = None;
+ m_ItemType = NoItem;
+ m_LinkStart = nullptr;
+ m_LinkEnd = nullptr;
+ }
+}
+
+void ed::CreateItemAction::DropPin(Pin* endPin)
+{
+ IM_ASSERT(!m_InActive);
+
+ m_ItemType = Link;
+ m_LinkEnd = endPin;
+}
+
+void ed::CreateItemAction::DropNode()
+{
+ IM_ASSERT(!m_InActive);
+
+ m_ItemType = Node;
+ m_LinkEnd = nullptr;
+}
+
+void ed::CreateItemAction::DropNothing()
+{
+ IM_ASSERT(!m_InActive);
+
+ m_ItemType = NoItem;
+ m_LinkEnd = nullptr;
+}
+
+ed::CreateItemAction::Result ed::CreateItemAction::RejectItem()
+{
+ IM_ASSERT(m_InActive);
+
+ if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem)
+ return Indeterminate;
+
+ m_UserAction = UserReject;
+
+ return True;
+}
+
+ed::CreateItemAction::Result ed::CreateItemAction::AcceptItem()
+{
+ IM_ASSERT(m_InActive);
+
+ if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem)
+ return Indeterminate;
+
+ m_UserAction = UserAccept;
+
+ if (m_CurrentStage == Create)
+ {
+ m_NextStage = None;
+ m_ItemType = NoItem;
+ m_LinkStart = nullptr;
+ m_LinkEnd = nullptr;
+ return True;
+ }
+ else
+ return False;
+}
+
+ed::CreateItemAction::Result ed::CreateItemAction::QueryLink(PinId* startId, PinId* endId)
+{
+ IM_ASSERT(m_InActive);
+
+ if (!m_InActive || m_CurrentStage == None || m_ItemType != Link)
+ return Indeterminate;
+
+ auto linkStartId = m_LinkStart->m_ID;
+ auto linkEndId = m_LinkEnd->m_ID;
+
+ *startId = linkStartId;
+ *endId = linkEndId;
+
+ Editor->SetUserContext(true);
+
+ if (!m_IsInGlobalSpace)
+ {
+ Editor->Suspend(SuspendFlags::KeepSplitter);
+
+ auto rect = Editor->GetRect();
+ ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false);
+ m_IsInGlobalSpace = true;
+ }
+
+ return True;
+}
+
+ed::CreateItemAction::Result ed::CreateItemAction::QueryNode(PinId* pinId)
+{
+ IM_ASSERT(m_InActive);
+
+ if (!m_InActive || m_CurrentStage == None || m_ItemType != Node)
+ return Indeterminate;
+
+ *pinId = m_LinkStart ? m_LinkStart->m_ID : 0;
+
+ Editor->SetUserContext(true);
+
+ if (!m_IsInGlobalSpace)
+ {
+ Editor->Suspend(SuspendFlags::KeepSplitter);
+
+ auto rect = Editor->GetRect();
+ ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false);
+ m_IsInGlobalSpace = true;
+ }
+
+ return True;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Delete Items Action
+//
+//------------------------------------------------------------------------------
+ed::DeleteItemsAction::DeleteItemsAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_InInteraction(false),
+ m_CurrentItemType(Unknown),
+ m_UserAction(Undetermined)
+{
+}
+
+ed::EditorAction::AcceptResult ed::DeleteItemsAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ auto addDeadLinks = [this]()
+ {
+ vector<ed::Link*> links;
+ for (auto object : m_CandidateObjects)
+ {
+ auto node = object->AsNode();
+ if (!node)
+ continue;
+
+ Editor->FindLinksForNode(node->m_ID, links, true);
+ }
+ if (!links.empty())
+ {
+ std::sort(links.begin(), links.end());
+ links.erase(std::unique(links.begin(), links.end()), links.end());
+ m_CandidateObjects.insert(m_CandidateObjects.end(), links.begin(), links.end());
+ }
+ };
+
+ auto& io = ImGui::GetIO();
+ if (ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete)) && Editor->AreShortcutsEnabled())
+ {
+ auto& selection = Editor->GetSelectedObjects();
+ if (!selection.empty())
+ {
+ m_CandidateObjects = selection;
+ addDeadLinks();
+ m_IsActive = true;
+ return True;
+ }
+ }
+ else if (control.ClickedLink && io.KeyAlt)
+ {
+ m_CandidateObjects.clear();
+ m_CandidateObjects.push_back(control.ClickedLink);
+ m_IsActive = true;
+ return True;
+ }
+
+ else if (!m_ManuallyDeletedObjects.empty())
+ {
+ m_CandidateObjects = m_ManuallyDeletedObjects;
+ m_ManuallyDeletedObjects.clear();
+ addDeadLinks();
+ m_IsActive = true;
+ return True;
+ }
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::DeleteItemsAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ if (!m_IsActive)
+ return false;
+
+ m_IsActive = false;
+ return true;
+}
+
+void ed::DeleteItemsAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ //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 "";
+ //};
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+ //ImGui::Text(" Node: %s (%d)", getObjectName(DeleteItemsgedNode), DeleteItemsgedNode ? DeleteItemsgedNode->ID : 0);
+}
+
+bool ed::DeleteItemsAction::Add(Object* object)
+{
+ if (Editor->GetCurrentAction() != nullptr)
+ return false;
+
+ m_ManuallyDeletedObjects.push_back(object);
+
+ return true;
+}
+
+bool ed::DeleteItemsAction::Begin()
+{
+ if (!m_IsActive)
+ return false;
+
+ IM_ASSERT(!m_InInteraction);
+ m_InInteraction = true;
+
+ m_CurrentItemType = Unknown;
+ m_UserAction = Undetermined;
+
+ return m_IsActive;
+}
+
+void ed::DeleteItemsAction::End()
+{
+ if (!m_IsActive)
+ return;
+
+ IM_ASSERT(m_InInteraction);
+ m_InInteraction = false;
+}
+
+bool ed::DeleteItemsAction::QueryLink(LinkId* linkId, PinId* startId, PinId* endId)
+{
+ ObjectId objectId;
+ if (!QueryItem(&objectId, Link))
+ return false;
+
+ if (auto id = objectId.AsLinkId())
+ *linkId = id;
+ else
+ return false;
+
+ if (startId || endId)
+ {
+ auto link = Editor->FindLink(*linkId);
+ if (startId)
+ *startId = link->m_StartPin->m_ID;
+ if (endId)
+ *endId = link->m_EndPin->m_ID;
+ }
+
+ return true;
+}
+
+bool ed::DeleteItemsAction::QueryNode(NodeId* nodeId)
+{
+ ObjectId objectId;
+ if (!QueryItem(&objectId, Node))
+ return false;
+
+ if (auto id = objectId.AsNodeId())
+ *nodeId = id;
+ else
+ return false;
+
+ return true;
+}
+
+bool ed::DeleteItemsAction::QueryItem(ObjectId* itemId, IteratorType itemType)
+{
+ if (!m_InInteraction)
+ return false;
+
+ if (m_CurrentItemType != itemType)
+ {
+ m_CurrentItemType = itemType;
+ m_CandidateItemIndex = 0;
+ }
+ else if (m_UserAction == Undetermined)
+ {
+ RejectItem();
+ }
+
+ m_UserAction = Undetermined;
+
+ auto itemCount = (int)m_CandidateObjects.size();
+ while (m_CandidateItemIndex < itemCount)
+ {
+ auto item = m_CandidateObjects[m_CandidateItemIndex];
+ if (itemType == Node)
+ {
+ if (auto node = item->AsNode())
+ {
+ *itemId = node->m_ID;
+ return true;
+ }
+ }
+ else if (itemType == Link)
+ {
+ if (auto link = item->AsLink())
+ {
+ *itemId = link->m_ID;
+ return true;
+ }
+ }
+
+ ++m_CandidateItemIndex;
+ }
+
+ if (m_CandidateItemIndex == itemCount)
+ m_CurrentItemType = Unknown;
+
+ return false;
+}
+
+bool ed::DeleteItemsAction::AcceptItem()
+{
+ if (!m_InInteraction)
+ return false;
+
+ m_UserAction = Accepted;
+
+ RemoveItem();
+
+ return true;
+}
+
+void ed::DeleteItemsAction::RejectItem()
+{
+ if (!m_InInteraction)
+ return;
+
+ m_UserAction = Rejected;
+
+ RemoveItem();
+}
+
+void ed::DeleteItemsAction::RemoveItem()
+{
+ auto item = m_CandidateObjects[m_CandidateItemIndex];
+ m_CandidateObjects.erase(m_CandidateObjects.begin() + m_CandidateItemIndex);
+
+ Editor->DeselectObject(item);
+
+ if (m_CurrentItemType == Link)
+ Editor->NotifyLinkDeleted(item->AsLink());
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Node Builder
+//
+//------------------------------------------------------------------------------
+ed::NodeBuilder::NodeBuilder(EditorContext* editor):
+ Editor(editor),
+ m_CurrentNode(nullptr),
+ m_CurrentPin(nullptr)
+{
+}
+
+ed::NodeBuilder::~NodeBuilder()
+{
+ m_Splitter.ClearFreeMemory();
+ m_PinSplitter.ClearFreeMemory();
+}
+
+void ed::NodeBuilder::Begin(NodeId nodeId)
+{
+ IM_ASSERT(nullptr == m_CurrentNode);
+
+ m_CurrentNode = Editor->GetNode(nodeId);
+
+ if (m_CurrentNode->m_RestoreState)
+ {
+ Editor->RestoreNodeState(m_CurrentNode);
+ m_CurrentNode->m_RestoreState = false;
+ }
+
+ if (m_CurrentNode->m_CenterOnScreen)
+ {
+ auto bounds = Editor->GetViewRect();
+ auto offset = bounds.GetCenter() - m_CurrentNode->m_Bounds.GetCenter();
+
+ if (ImLengthSqr(offset) > 0)
+ {
+ if (::IsGroup(m_CurrentNode))
+ {
+ std::vector<Node*> groupedNodes;
+ m_CurrentNode->GetGroupedNodes(groupedNodes);
+ groupedNodes.push_back(m_CurrentNode);
+
+ for (auto node : groupedNodes)
+ {
+ node->m_Bounds.Translate(ImFloor(offset));
+ node->m_GroupBounds.Translate(ImFloor(offset));
+ Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, node);
+ }
+ }
+ else
+ {
+ m_CurrentNode->m_Bounds.Translate(ImFloor(offset));
+ m_CurrentNode->m_GroupBounds.Translate(ImFloor(offset));
+ Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, m_CurrentNode);
+ }
+ }
+
+ m_CurrentNode->m_CenterOnScreen = false;
+ }
+
+ // Position node on screen
+ ImGui::SetCursorScreenPos(m_CurrentNode->m_Bounds.Min);
+
+ auto& editorStyle = Editor->GetStyle();
+
+ const auto alpha = ImGui::GetStyle().Alpha;
+
+ m_CurrentNode->m_IsLive = true;
+ m_CurrentNode->m_LastPin = nullptr;
+ m_CurrentNode->m_Color = Editor->GetColor(StyleColor_NodeBg, alpha);
+ m_CurrentNode->m_BorderColor = Editor->GetColor(StyleColor_NodeBorder, alpha);
+ m_CurrentNode->m_BorderWidth = editorStyle.NodeBorderWidth;
+ m_CurrentNode->m_Rounding = editorStyle.NodeRounding;
+ m_CurrentNode->m_GroupColor = Editor->GetColor(StyleColor_GroupBg, alpha);
+ m_CurrentNode->m_GroupBorderColor = Editor->GetColor(StyleColor_GroupBorder, alpha);
+ m_CurrentNode->m_GroupBorderWidth = editorStyle.GroupBorderWidth;
+ m_CurrentNode->m_GroupRounding = editorStyle.GroupRounding;
+
+ m_IsGroup = false;
+
+ // Grow channel list and select user channel
+ if (auto drawList = ImGui::GetWindowDrawList())
+ {
+ m_CurrentNode->m_Channel = drawList->_Splitter._Count;
+ ImDrawList_ChannelsGrow(drawList, drawList->_Splitter._Count + c_ChannelsPerNode);
+ drawList->ChannelsSetCurrent(m_CurrentNode->m_Channel + c_NodeContentChannel);
+
+ m_Splitter.Clear();
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+ }
+
+ // Begin outer group
+ ImGui::BeginGroup();
+
+ // Apply frame padding. Begin inner group if necessary.
+ if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0)
+ {
+ ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2(editorStyle.NodePadding.x, editorStyle.NodePadding.y));
+ ImGui::BeginGroup();
+ }
+}
+
+void ed::NodeBuilder::End()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ if (auto drawList = ImGui::GetWindowDrawList())
+ {
+ IM_ASSERT(drawList->_Splitter._Count == 1); // Did you forgot to call drawList->ChannelsMerge()?
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+ }
+
+ // Apply frame padding. This must be done in this convoluted way if outer group
+ // size must contain inner group padding.
+ auto& editorStyle = Editor->GetStyle();
+ if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0)
+ {
+ ImGui::EndGroup();
+ ImGui::SameLine(0, editorStyle.NodePadding.z);
+ ImGui::Dummy(ImVec2(0, 0));
+ ImGui::SetCursorPosY(ImGui::GetCursorPosY() + editorStyle.NodePadding.w);
+ }
+
+ // End outer group.
+ ImGui::EndGroup();
+
+ m_NodeRect = ImGui_GetItemRect();
+ m_NodeRect.Floor();
+
+ if (m_CurrentNode->m_Bounds.GetSize() != m_NodeRect.GetSize())
+ {
+ m_CurrentNode->m_Bounds.Max = m_CurrentNode->m_Bounds.Min + m_NodeRect.GetSize();
+ Editor->MakeDirty(SaveReasonFlags::Size, m_CurrentNode);
+ }
+
+ if (m_IsGroup)
+ {
+ // Groups cannot have pins. Discard them.
+ for (auto pin = m_CurrentNode->m_LastPin; pin; pin = pin->m_PreviousPin)
+ pin->Reset();
+
+ m_CurrentNode->m_Type = NodeType::Group;
+ m_CurrentNode->m_GroupBounds = m_GroupBounds;
+ m_CurrentNode->m_LastPin = nullptr;
+ }
+ else
+ m_CurrentNode->m_Type = NodeType::Node;
+
+ m_CurrentNode = nullptr;
+}
+
+void ed::NodeBuilder::BeginPin(PinId pinId, PinKind kind)
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+ IM_ASSERT(nullptr == m_CurrentPin);
+ IM_ASSERT(false == m_IsGroup);
+
+ auto& editorStyle = Editor->GetStyle();
+
+ m_CurrentPin = Editor->GetPin(pinId, kind);
+ m_CurrentPin->m_Node = m_CurrentNode;
+
+ m_CurrentPin->m_IsLive = true;
+ m_CurrentPin->m_Color = Editor->GetColor(StyleColor_PinRect);
+ m_CurrentPin->m_BorderColor = Editor->GetColor(StyleColor_PinRectBorder);
+ m_CurrentPin->m_BorderWidth = editorStyle.PinBorderWidth;
+ m_CurrentPin->m_Rounding = editorStyle.PinRounding;
+ m_CurrentPin->m_Corners = static_cast<int>(editorStyle.PinCorners);
+ m_CurrentPin->m_Radius = editorStyle.PinRadius;
+ m_CurrentPin->m_ArrowSize = editorStyle.PinArrowSize;
+ m_CurrentPin->m_ArrowWidth = editorStyle.PinArrowWidth;
+ m_CurrentPin->m_Dir = kind == PinKind::Output ? editorStyle.SourceDirection : editorStyle.TargetDirection;
+ m_CurrentPin->m_Strength = editorStyle.LinkStrength;
+
+ m_CurrentPin->m_PreviousPin = m_CurrentNode->m_LastPin;
+ m_CurrentNode->m_LastPin = m_CurrentPin;
+
+ m_PivotAlignment = editorStyle.PivotAlignment;
+ m_PivotSize = editorStyle.PivotSize;
+ m_PivotScale = editorStyle.PivotScale;
+ m_ResolvePinRect = true;
+ m_ResolvePivot = true;
+
+ if (auto drawList = ImGui::GetWindowDrawList())
+ {
+ m_PinSplitter.Clear();
+ ImDrawList_SwapSplitter(drawList, m_PinSplitter);
+ }
+
+ ImGui::BeginGroup();
+}
+
+void ed::NodeBuilder::EndPin()
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ if (auto drawList = ImGui::GetWindowDrawList())
+ {
+ IM_ASSERT(drawList->_Splitter._Count == 1); // Did you forgot to call drawList->ChannelsMerge()?
+ ImDrawList_SwapSplitter(drawList, m_PinSplitter);
+ }
+
+ ImGui::EndGroup();
+
+ if (m_ResolvePinRect)
+ m_CurrentPin->m_Bounds = ImGui_GetItemRect();
+
+ if (m_ResolvePivot)
+ {
+ auto& pinRect = m_CurrentPin->m_Bounds;
+
+ if (m_PivotSize.x < 0)
+ m_PivotSize.x = pinRect.GetWidth();
+ if (m_PivotSize.y < 0)
+ m_PivotSize.y = pinRect.GetHeight();
+
+ m_CurrentPin->m_Pivot.Min = pinRect.Min + ImMul(pinRect.GetSize(), m_PivotAlignment);
+ m_CurrentPin->m_Pivot.Max = m_CurrentPin->m_Pivot.Min + ImMul(m_PivotSize, m_PivotScale);
+ }
+
+ // #debug: Draw pin bounds
+ //ImGui::GetWindowDrawList()->AddRect(m_CurrentPin->m_Bounds.Min, m_CurrentPin->m_Bounds.Max, IM_COL32(255, 255, 0, 255));
+
+ // #debug: Draw pin pivot rectangle
+ //ImGui::GetWindowDrawList()->AddRect(m_CurrentPin->m_Pivot.Min, m_CurrentPin->m_Pivot.Max, IM_COL32(255, 0, 255, 255));
+
+ m_CurrentPin = nullptr;
+}
+
+void ed::NodeBuilder::PinRect(const ImVec2& a, const ImVec2& b)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_CurrentPin->m_Bounds = ImRect(a, b);
+ m_CurrentPin->m_Bounds.Floor();
+ m_ResolvePinRect = false;
+}
+
+void ed::NodeBuilder::PinPivotRect(const ImVec2& a, const ImVec2& b)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_CurrentPin->m_Pivot = ImRect(a, b);
+ m_ResolvePivot = false;
+}
+
+void ed::NodeBuilder::PinPivotSize(const ImVec2& size)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_PivotSize = size;
+ m_ResolvePivot = true;
+}
+
+void ed::NodeBuilder::PinPivotScale(const ImVec2& scale)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_PivotScale = scale;
+ m_ResolvePivot = true;
+}
+
+void ed::NodeBuilder::PinPivotAlignment(const ImVec2& alignment)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_PivotAlignment = alignment;
+ m_ResolvePivot = true;
+}
+
+void ed::NodeBuilder::Group(const ImVec2& size)
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+ IM_ASSERT(nullptr == m_CurrentPin);
+ IM_ASSERT(false == m_IsGroup);
+
+ m_IsGroup = true;
+
+ if (IsGroup(m_CurrentNode))
+ ImGui::Dummy(m_CurrentNode->m_GroupBounds.GetSize());
+ else
+ ImGui::Dummy(size);
+
+ m_GroupBounds = ImGui_GetItemRect();
+ m_GroupBounds.Floor();
+}
+
+ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList() const
+{
+ return GetUserBackgroundDrawList(m_CurrentNode);
+}
+
+ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList(Node* node) const
+{
+ if (node && node->m_IsLive)
+ {
+ auto drawList = ImGui::GetWindowDrawList();
+ drawList->ChannelsSetCurrent(node->m_Channel + c_NodeUserBackgroundChannel);
+ return drawList;
+ }
+ else
+ return nullptr;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Node Builder
+//
+//------------------------------------------------------------------------------
+ed::HintBuilder::HintBuilder(EditorContext* editor):
+ Editor(editor),
+ m_IsActive(false),
+ m_CurrentNode(nullptr)
+{
+}
+
+bool ed::HintBuilder::Begin(NodeId nodeId)
+{
+ IM_ASSERT(nullptr == m_CurrentNode);
+
+ auto& view = Editor->GetView();
+ auto& rect = Editor->GetRect();
+
+ const float c_min_zoom = 0.75f;
+ const float c_max_zoom = 0.50f;
+
+ if (view.Scale > 0.75f)
+ return false;
+
+ auto node = Editor->FindNode(nodeId);
+ if (!IsGroup(node))
+ return false;
+
+ m_CurrentNode = node;
+
+ m_LastChannel = ImGui::GetWindowDrawList()->_Splitter._Current;
+
+ Editor->Suspend(SuspendFlags::KeepSplitter);
+
+ const auto alpha = ImMax(0.0f, std::min(1.0f, (view.Scale - c_min_zoom) / (c_max_zoom - c_min_zoom)));
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground);
+ ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false);
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_Hints);
+ ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false);
+
+ ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
+
+ m_IsActive = true;
+
+ return true;
+}
+
+void ed::HintBuilder::End()
+{
+ if (!m_IsActive)
+ return;
+
+ ImGui::PopStyleVar();
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_Hints);
+ ImGui::PopClipRect();
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground);
+ ImGui::PopClipRect();
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(m_LastChannel);
+
+ Editor->Resume(SuspendFlags::KeepSplitter);
+
+ m_IsActive = false;
+ m_CurrentNode = nullptr;
+}
+
+ImVec2 ed::HintBuilder::GetGroupMin()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ return Editor->ToScreen(m_CurrentNode->m_Bounds.Min);
+}
+
+ImVec2 ed::HintBuilder::GetGroupMax()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ return Editor->ToScreen(m_CurrentNode->m_Bounds.Max);
+}
+
+ImDrawList* ed::HintBuilder::GetForegroundDrawList()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ auto drawList = ImGui::GetWindowDrawList();
+
+ drawList->ChannelsSetCurrent(c_UserChannel_Hints);
+
+ return drawList;
+}
+
+ImDrawList* ed::HintBuilder::GetBackgroundDrawList()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ auto drawList = ImGui::GetWindowDrawList();
+
+ drawList->ChannelsSetCurrent(c_UserChannel_HintsBackground);
+
+ return drawList;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Style
+//
+//------------------------------------------------------------------------------
+void ed::Style::PushColor(StyleColor colorIndex, const ImVec4& color)
+{
+ ColorModifier modifier;
+ modifier.Index = colorIndex;
+ modifier.Value = Colors[colorIndex];
+ m_ColorStack.push_back(modifier);
+ Colors[colorIndex] = color;
+}
+
+void ed::Style::PopColor(int count)
+{
+ while (count > 0)
+ {
+ auto& modifier = m_ColorStack.back();
+ Colors[modifier.Index] = modifier.Value;
+ m_ColorStack.pop_back();
+ --count;
+ }
+}
+
+void ed::Style::PushVar(StyleVar varIndex, float value)
+{
+ auto* var = GetVarFloatAddr(varIndex);
+ IM_ASSERT(var != nullptr);
+ VarModifier modifier;
+ modifier.Index = varIndex;
+ modifier.Value = ImVec4(*var, 0, 0, 0);
+ *var = value;
+ m_VarStack.push_back(modifier);
+}
+
+void ed::Style::PushVar(StyleVar varIndex, const ImVec2& value)
+{
+ auto* var = GetVarVec2Addr(varIndex);
+ IM_ASSERT(var != nullptr);
+ VarModifier modifier;
+ modifier.Index = varIndex;
+ modifier.Value = ImVec4(var->x, var->y, 0, 0);
+ *var = value;
+ m_VarStack.push_back(modifier);
+}
+
+void ed::Style::PushVar(StyleVar varIndex, const ImVec4& value)
+{
+ auto* var = GetVarVec4Addr(varIndex);
+ IM_ASSERT(var != nullptr);
+ VarModifier modifier;
+ modifier.Index = varIndex;
+ modifier.Value = *var;
+ *var = value;
+ m_VarStack.push_back(modifier);
+}
+
+void ed::Style::PopVar(int count)
+{
+ while (count > 0)
+ {
+ auto& modifier = m_VarStack.back();
+ if (auto floatValue = GetVarFloatAddr(modifier.Index))
+ *floatValue = modifier.Value.x;
+ else if (auto vec2Value = GetVarVec2Addr(modifier.Index))
+ *vec2Value = ImVec2(modifier.Value.x, modifier.Value.y);
+ else if (auto vec4Value = GetVarVec4Addr(modifier.Index))
+ *vec4Value = modifier.Value;
+ m_VarStack.pop_back();
+ --count;
+ }
+}
+
+const char* ed::Style::GetColorName(StyleColor colorIndex) const
+{
+ switch (colorIndex)
+ {
+ case StyleColor_Bg: return "Bg";
+ case StyleColor_Grid: return "Grid";
+ case StyleColor_NodeBg: return "NodeBg";
+ case StyleColor_NodeBorder: return "NodeBorder";
+ case StyleColor_HovNodeBorder: return "HoveredNodeBorder";
+ case StyleColor_SelNodeBorder: return "SelNodeBorder";
+ case StyleColor_NodeSelRect: return "NodeSelRect";
+ case StyleColor_NodeSelRectBorder: return "NodeSelRectBorder";
+ case StyleColor_HovLinkBorder: return "HoveredLinkBorder";
+ case StyleColor_SelLinkBorder: return "SelLinkBorder";
+ case StyleColor_LinkSelRect: return "LinkSelRect";
+ case StyleColor_LinkSelRectBorder: return "LinkSelRectBorder";
+ case StyleColor_PinRect: return "PinRect";
+ case StyleColor_PinRectBorder: return "PinRectBorder";
+ case StyleColor_Flow: return "Flow";
+ case StyleColor_FlowMarker: return "FlowMarker";
+ case StyleColor_GroupBg: return "GroupBg";
+ case StyleColor_GroupBorder: return "GroupBorder";
+ case StyleColor_Count: break;
+ }
+
+ IM_ASSERT(0);
+ return "Unknown";
+}
+
+float* ed::Style::GetVarFloatAddr(StyleVar idx)
+{
+ switch (idx)
+ {
+ case StyleVar_NodeRounding: return &NodeRounding;
+ case StyleVar_NodeBorderWidth: return &NodeBorderWidth;
+ case StyleVar_HoveredNodeBorderWidth: return &HoveredNodeBorderWidth;
+ case StyleVar_SelectedNodeBorderWidth: return &SelectedNodeBorderWidth;
+ case StyleVar_PinRounding: return &PinRounding;
+ case StyleVar_PinBorderWidth: return &PinBorderWidth;
+ case StyleVar_LinkStrength: return &LinkStrength;
+ case StyleVar_ScrollDuration: return &ScrollDuration;
+ case StyleVar_FlowMarkerDistance: return &FlowMarkerDistance;
+ case StyleVar_FlowSpeed: return &FlowSpeed;
+ case StyleVar_FlowDuration: return &FlowDuration;
+ case StyleVar_PinCorners: return &PinCorners;
+ case StyleVar_PinRadius: return &PinRadius;
+ case StyleVar_PinArrowSize: return &PinArrowSize;
+ case StyleVar_PinArrowWidth: return &PinArrowWidth;
+ case StyleVar_GroupRounding: return &GroupRounding;
+ case StyleVar_GroupBorderWidth: return &GroupBorderWidth;
+ default: return nullptr;
+ }
+}
+
+ImVec2* ed::Style::GetVarVec2Addr(StyleVar idx)
+{
+ switch (idx)
+ {
+ case StyleVar_SourceDirection: return &SourceDirection;
+ case StyleVar_TargetDirection: return &TargetDirection;
+ case StyleVar_PivotAlignment: return &PivotAlignment;
+ case StyleVar_PivotSize: return &PivotSize;
+ case StyleVar_PivotScale: return &PivotScale;
+ default: return nullptr;
+ }
+}
+
+ImVec4* ed::Style::GetVarVec4Addr(StyleVar idx)
+{
+ switch (idx)
+ {
+ case StyleVar_NodePadding: return &NodePadding;
+ default: return nullptr;
+ }
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Config
+//
+//------------------------------------------------------------------------------
+ed::Config::Config(const ax::NodeEditor::Config* config)
+{
+ if (config)
+ *static_cast<ax::NodeEditor::Config*>(this) = *config;
+}
+
+std::string ed::Config::Load()
+{
+ std::string data;
+
+ if (LoadSettings)
+ {
+ const auto size = LoadSettings(nullptr, UserPointer);
+ if (size > 0)
+ {
+ data.resize(size);
+ LoadSettings(const_cast<char*>(data.data()), UserPointer);
+ }
+ }
+ else if (SettingsFile)
+ {
+ std::ifstream file(SettingsFile);
+ if (file)
+ {
+ file.seekg(0, std::ios_base::end);
+ auto size = static_cast<size_t>(file.tellg());
+ file.seekg(0, std::ios_base::beg);
+
+ data.reserve(size);
+ data.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
+ }
+ }
+
+ return data;
+}
+
+std::string ed::Config::LoadNode(NodeId nodeId)
+{
+ std::string data;
+
+ if (LoadNodeSettings)
+ {
+ const auto size = LoadNodeSettings(nodeId, nullptr, UserPointer);
+ if (size > 0)
+ {
+ data.resize(size);
+ LoadNodeSettings(nodeId, const_cast<char*>(data.data()), UserPointer);
+ }
+ }
+
+ return data;
+}
+
+void ed::Config::BeginSave()
+{
+ if (BeginSaveSession)
+ BeginSaveSession(UserPointer);
+}
+
+bool ed::Config::Save(const std::string& data, SaveReasonFlags flags)
+{
+ if (SaveSettings)
+ {
+ return SaveSettings(data.c_str(), data.size(), flags, UserPointer);
+ }
+ else if (SettingsFile)
+ {
+ std::ofstream settingsFile(SettingsFile);
+ if (settingsFile)
+ settingsFile << data;
+
+ return !!settingsFile;
+ }
+
+ return false;
+}
+
+bool ed::Config::SaveNode(NodeId nodeId, const std::string& data, SaveReasonFlags flags)
+{
+ if (SaveNodeSettings)
+ return SaveNodeSettings(nodeId, data.c_str(), data.size(), flags, UserPointer);
+
+ return false;
+}
+
+void ed::Config::EndSave()
+{
+ if (EndSaveSession)
+ EndSaveSession(UserPointer);
+}