// =====================================================================================================================
// imnode-graph, and open source extension for Dear ImGui that adds functionality for drawing a node graph.
// Copyright (C) 2024 Medusa Slockbower
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// =====================================================================================================================
#include "imnode_graph.h"
#include "imnode_graph_internal.h"
#include
#include
//#define IMNODE_GRAPH_DEBUG_PIN_BOUNDS
struct ImNodeFontConfig
{
char* Path;
float Size;
const ImWchar* GlyphRanges;
};
ImNodeGraphContext* GImNodeGraph = nullptr; // Global Node Graph Context
ImVector GFonts; // Fonts added to ImNodeGraph
float GFontUpscale = 4.0f;
ImVec4 operator*(const ImVec4& v, float s) { return { v.x * s, v.y * s, v.z * s, v.w * s }; }
// =====================================================================================================================
// Internal Extensions
// =====================================================================================================================
// Helper to check if any key mods are active
bool ImGui::IsAnyModKeyDown()
{
ImGuiContext& G = *GImGui;
ImGuiIO& IO = G.IO;
return IO.KeyMods != ImGuiMod_None;
}
// =====================================================================================================================
// Internal Functionality
// =====================================================================================================================
// Math ----------------------------------------------------------------------------------------------------------------
// AABB Collision
bool ImAABB(const ImRect &a, const ImRect &b)
{
return a.Max.x > b.Min.x
&& a.Min.x < b.Max.x
&& a.Max.y > b.Min.y
&& a.Min.y < b.Max.y;
}
// Context -------------------------------------------------------------------------------------------------------------
ImNodeGraphContext::ImNodeGraphContext()
: Initialized(false)
, Scope(ImNodeGraphScope_None)
, CurrentGraph(nullptr)
{
}
void ImNodeGraph::Initialize()
{
ImGuiContext& Ctx = *ImGui::GetCurrentContext();
ImNodeGraphContext& G = *GImNodeGraph;
IM_ASSERT(!G.Initialized);
// If no fonts were set up, add the default font
if(G.Fonts.empty())
{
if(Ctx.IO.Fonts->Fonts.Size == 0) Ctx.IO.Fonts->AddFontDefault();
LoadFonts();
}
G.Initialized = true;
}
void ImNodeGraph::Shutdown()
{
ImNodeGraphContext& G = *GImNodeGraph;
IM_ASSERT(G.Initialized);
G.Graphs.clear_delete();
GFonts.clear();
}
void ImNodeGraph::LoadFonts()
{
if(GFonts.empty())
{
LoadDefaultFont();
return;
}
ImGuiContext& Ctx = *ImGui::GetCurrentContext();
ImNodeGraphContext& G = *GImNodeGraph;
bool first = true;
for(const auto& font : GFonts)
{
ImFontConfig cfg = ImFontConfig();
cfg.OversampleH = cfg.OversampleV = 1;
cfg.SizePixels = font.Size * GFontUpscale;
cfg.MergeMode = !first;
cfg.PixelSnapH = false;
G.Fonts.push_back(Ctx.IO.Fonts->AddFontFromFileTTF(font.Path, 0, &cfg, font.GlyphRanges));
first = false;
}
}
void ImNodeGraph::LoadDefaultFont()
{
ImGuiContext& Ctx = *ImGui::GetCurrentContext();
ImNodeGraphContext& G = *GImNodeGraph;
ImFontConfig cfg = ImFontConfig();
cfg.OversampleH = cfg.OversampleV = 1;
cfg.SizePixels = 20.0f * GFontUpscale;
cfg.MergeMode = false;
cfg.PixelSnapH = false;
G.Fonts.push_back(Ctx.IO.Fonts->AddFontDefault(&cfg));
}
// Graph ---------------------------------------------------------------------------------------------------------------
ImNodeGraphData::ImNodeGraphData(ImNodeGraphContext* ctx, const char* name)
: Ctx(ctx)
, Flags(ImNodeGraphFlags_None)
, Name(nullptr)
, ID(ImHashStr(name))
, TargetZoom(1.0f)
, IsPanning(false)
, CurrentNode(nullptr)
, CurrentPin(nullptr)
, SubmitCount(0)
, Validation(nullptr)
, Dragging(false)
, LockSelectRegion(false)
{ Name = ImStrdup(name); }
void ImNodeGraphData::Reset()
{
// Flags, Style, and Settings are not reset
// Reset Camera
Camera = ImGraphCamera();
TargetZoom = Camera.Scale;
IsPanning = false;
// Reset Selection Vars
SelectRegionStart.Reset();
SelectRegion.Clear();
Selected.Clear();
DragOffset = { 0, 0 };
Dragging = false;
LockSelectRegion = false;
// Reset Nodes
Nodes.Clear();
HoveredNode.Reset();
FocusedNode.Reset();
HoveredPin.Reset();
FocusedPin.Reset();
CurrentNode = nullptr;
CurrentPin = nullptr;
SubmitCount = 0;
// Reset Connections
Connections.Clear();
NewConnection.Reset();
// Validation Function is not Reset
}
ImPinData* ImNodeGraphData::FindPin(ImPinPtr pin)
{
ImNodeData& Node = Nodes[pin.Node];
ImObjectPool& Pins = pin.Direction ? Node.OutputPins : Node.InputPins;
return Pins(pin.Pin) ? &Pins[pin.Pin] : nullptr;
}
ImNodeData* ImNodeGraphData::FindNode(int id)
{
for(int i = 0; i < Nodes.Size(); ++i)
{
if(Nodes[i].UserID == id) return &Nodes[i];
}
return nullptr;
}
ImPinData* ImNodeGraphData::FindPin(ImUserPinPtr pin)
{
ImNodeData* Node = FindNode(pin.Node);
if(Node == nullptr) return nullptr;
ImObjectPool& Pins = pin.Direction ? Node->OutputPins : Node->InputPins;
for(int i = 0; i < Pins.Size(); ++i)
{
if(Pins[i].UserID == pin.Pin) return &Pins[i];
}
return nullptr;
}
ImRect ImNodeGraphData::GetSelection()
{
if(SelectRegionStart() == false) return { { -1, -1 }, { -1, -1 } };
ImVec2 mouse = ImGui::GetMousePos();
return { ImMin(mouse, SelectRegionStart), ImMax(mouse, SelectRegionStart) };
}
void ImNodeGraphData::UpdateSelection(ImGuiID Node, bool allow_clear, bool removal)
{
ImGuiContext& Ctx = *ImGui::GetCurrentContext();
ImGuiIO& IO = Ctx.IO;
bool selected = Selected.Contains(Node);
switch(IO.KeyMods)
{
case ImGuiMod_Ctrl:
if(selected) Selected.Erase(Node);
else Selected.Insert(Node);
break;
default:
if(allow_clear) Selected.Clear();
case ImGuiMod_Shift:
if(removal) Selected.Erase(Node);
else Selected.Insert(Node);
}
}
ImNodeGraphData* ImNodeGraph::FindGraphByID(ImGuiID id)
{
ImNodeGraphContext& G = *GImNodeGraph;
return static_cast(G.GraphsById.GetVoidPtr(id));
}
ImNodeGraphData* ImNodeGraph::FindGraphByTitle(const char *title)
{
ImGuiID id = ImHashStr(title);
return FindGraphByID(id);
}
ImNodeGraphData* ImNodeGraph::CreateNewGraph(const char *title)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = IM_NEW(ImNodeGraphData)(&G, title);
G.GraphsById.SetVoidPtr(Graph->ID, Graph);
G.Graphs.push_back(Graph);
return Graph;
}
void ImNodeGraph::DrawGrid(const ImRect &grid_bounds)
{
// Draw the grid
ImGuiWindow& DrawWindow = *ImGui::GetCurrentWindow();
ImDrawList& DrawList = *DrawWindow.DrawList;
ImNodeGraphData& Graph = *GImNodeGraph->CurrentGraph;
ImNodeGraphStyle& Style = Graph.Style;
ImGraphCamera& Camera = Graph.Camera;
const float GridSecondarySize = ImGui::GetFontSize() / Camera.Scale;
const float GridPrimarySize = GridSecondarySize * Style.GridPrimaryStep;
const float GridSecondaryStep = GridSecondarySize * Camera.Scale;
const float GridPrimaryStep = GridPrimarySize * Camera.Scale;
ImVec2 GridStart = ScreenToGrid(grid_bounds.Min);
GridStart = ImFloor(GridStart / GridPrimarySize) * GridPrimarySize;
GridStart = GridToScreen(GridStart);
ImVec2 GridEnd = ScreenToGrid(grid_bounds.Max);
GridEnd = ImFloor(GridEnd / GridPrimarySize) * GridPrimarySize;
GridEnd = GridEnd + ImVec2{ GridPrimarySize, GridPrimarySize };
GridEnd = GridToScreen(GridEnd);
// Secondary Grid
for(float x = GridStart.x; x < GridEnd.x; x += GridSecondaryStep)
{
DrawList.AddLine(
{ x, 0 }, { x, GridEnd.y }
, Style.Colors[ImNodeGraphColor_GridSecondaryLines], Style.GridSecondaryThickness * Camera.Scale
);
}
for(float y = GridStart.y; y < GridEnd.y; y += GridSecondaryStep)
{
DrawList.AddLine(
{ 0, y }, { GridEnd.x, y }
, Style.Colors[ImNodeGraphColor_GridSecondaryLines], Style.GridSecondaryThickness * Camera.Scale
);
}
// Primary Grid
for(float x = GridStart.x; x < GridEnd.x; x += GridPrimaryStep)
{
DrawList.AddLine(
{ x, 0 }, { x, GridEnd.y }
, Style.Colors[ImNodeGraphColor_GridPrimaryLines], Style.GridPrimaryThickness * Camera.Scale
);
}
for(float y = GridStart.y; y < GridEnd.y; y += GridPrimaryStep)
{
DrawList.AddLine(
{ 0, y }, { GridEnd.x, y }
, Style.Colors[ImNodeGraphColor_GridPrimaryLines], Style.GridPrimaryThickness * Camera.Scale
);
}
}
void ImNodeGraph::GraphBehaviour(const ImRect& grid_bounds)
{
// Context
ImGuiContext& Ctx = *ImGui::GetCurrentContext();
ImGuiIO& IO = Ctx.IO;
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImObjectPool& Nodes = Graph.Nodes;
ImNodeGraphSettings& Settings = Graph.Settings;
ImGraphCamera& Camera = Graph.Camera;
// Check Focus
if(!ImGui::IsWindowFocused() || Graph.NewConnection())
{
if(ImGui::IsMouseReleased(ImGuiMouseButton_Left) && Graph.NewConnection())
{
Graph.NewConnection.Reset();
ImGui::SetActiveID(0, ImGui::GetCurrentWindow());
}
return;
}
// Vars
const bool Hovered = ImGui::IsMouseHoveringRect(grid_bounds.Min, grid_bounds.Max);
// Zooming
if(Hovered) Graph.TargetZoom += Ctx.IO.MouseWheel * Settings.ZoomRate * Camera.Scale;
Graph.TargetZoom = ImClamp(Graph.TargetZoom, Settings.ZoomBounds.x, Settings.ZoomBounds.y);
Camera.Scale = ImLerp(Camera.Scale, Graph.TargetZoom, Ctx.IO.DeltaTime * Settings.ZoomSmoothing);
// Select Region
if(ImGui::IsMouseClicked(ImGuiMouseButton_Left))
{
if(not Hovered) return;
if(Graph.FocusedNode() == false)
{
if(IO.KeyMods == ImGuiMod_None) Graph.Selected.Clear();
}
else
{
ImVec2 mouse = ScreenToGrid(ImGui::GetMousePos());
for(ImGuiID node : Graph.Selected) Nodes[node].DragOffset = mouse - Nodes[node].Root;
Nodes[Graph.FocusedNode].DragOffset = mouse - Nodes[Graph.FocusedNode].Root;
}
}
// Item Focus
if(ImGui::IsAnyItemFocused()) return;
// Pin Drag Connection & Node Focus
if(ImGui::IsMouseReleased(ImGuiMouseButton_Left))
{
if(Graph.FocusedNode() && !Graph.Dragging)
{
Graph.UpdateSelection(Graph.FocusedNode, true);
}
Graph.FocusedNode.Reset();
Graph.SelectRegionStart.Reset();
Graph.SelectRegion.Clear();
Graph.Dragging = false;
}
// Dragging Nodes & Region Select
if(ImGui::IsMouseDragging(ImGuiMouseButton_Left))
{
if(Graph.FocusedNode())
{
if(!Graph.Selected.Contains(Graph.FocusedNode))
{
Graph.UpdateSelection(Graph.FocusedNode, true);
}
ImVec2 mouse = ScreenToGrid(ImGui::GetMousePos());
for(ImGuiID node : Graph.Selected)
{
Nodes[node].Root = mouse - Nodes[node].DragOffset;
if(IO.KeyMods == ImGuiMod_Alt) Nodes[node].Root = SnapToGrid(Nodes[node].Root);
}
Graph.Dragging = true;
}
else if(Graph.SelectRegionStart() == false && !Graph.LockSelectRegion)
{
Graph.SelectRegionStart = ImGui::GetMousePos();
}
}
// Panning
if(Hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle))
Graph.IsPanning = true;
if(ImGui::IsMouseReleased(ImGuiMouseButton_Middle))
Graph.IsPanning = false;
if(Graph.IsPanning)
{
Camera.Position -= Ctx.IO.MouseDelta / Camera.Scale;
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll);
}
if(ImGui::IsKeyPressed(ImGuiKey_T))
{
Nodes.PushToTop(Graph.Nodes.IdxToID[0]);
}
}
void ImNodeGraph::DrawGraph(ImNodeGraphData* Graph)
{
ImDrawList& DrawList = *ImGui::GetWindowDrawList();
ImDrawListSplitter& Splitter = DrawList._Splitter;
ImObjectPool& Nodes = Graph->Nodes;
ImNodeGraphStyle& Style = Graph->Style;
ImGraphCamera& Camera = Graph->Camera;
ImOptional prevFocus = Graph->FocusedNode;
Graph->HoveredNode.Reset();
if(ImGui::IsWindowFocused() && !Graph->NewConnection())
{
for(auto it = Nodes.rbegin(); it != Nodes.rend(); ++it)
{
if(NodeBehaviour(&*it)) break;
}
}
if(prevFocus != Graph->FocusedNode)
{
Graph->Nodes.PushToTop(Graph->FocusedNode);
}
// Draw Nodes
for(ImNodeData& Node : Nodes)
{
SetChannel(Node.BgChannelIndex);
DrawNode(&Node);
}
SortChannels();
Splitter.Merge(&DrawList);
Splitter.Clear();
if(Graph->NewConnection())
{
ImPinData* pin = Graph->FindPin(Graph->NewConnection);
DrawConnection(pin, ImGui::GetMousePos());
}
for(int i = 0; i < Graph->Connections.Size(); ++i)
{
if(Graph->Connections(i) == false) continue;
ImPinConnection& connection = Graph->Connections[i];
if(CheckConnectionValidity(i, connection)) continue;
DrawConnection(Graph->FindPin(connection.A), Graph->FindPin(connection.B));
}
if(Graph->SelectRegionStart())
{
ImRect Selection = Graph->GetSelection();
DrawList.AddRectFilled(
Selection.Min, Selection.Max
, Style.GetColorU32(ImNodeGraphColor_SelectRegionBackground)
, Style.SelectRegionRounding
);
DrawList.AddRect(
Selection.Min, Selection.Max
, Style.GetColorU32(ImNodeGraphColor_SelectRegionOutline)
, Style.SelectRegionRounding, 0
, Style.SelectRegionOutlineThickness
);
}
}
ImVec2 ImNodeGraph::GridToWindow(const ImVec2 &pos)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
return GridToScreen(pos) - Graph.ScreenPos;
}
ImVec2 ImNodeGraph::WindowToScreen(const ImVec2 &pos)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
return Graph.ScreenPos + pos;
}
ImVec2 ImNodeGraph::GridToScreen(const ImVec2& pos)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImGraphCamera& Camera = Graph.Camera;
return (pos - Camera.Position) * Camera.Scale + Graph.GetCenter();
}
ImVec2 ImNodeGraph::ScreenToGrid(const ImVec2& pos)
{
ImNodeGraphContext& G = *GImNodeGraph;
IM_ASSERT(G.CurrentGraph);
ImNodeGraphData& Graph = *G.CurrentGraph;
ImGraphCamera& Camera = Graph.Camera;
return Camera.Position + (pos - Graph.GetCenter()) / Camera.Scale;
}
ImVec2 ImNodeGraph::ScreenToWindow(const ImVec2 &pos)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
return pos - Graph.ScreenPos;
}
ImVec2 ImNodeGraph::WindowToGrid(const ImVec2 &pos)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
return ScreenToGrid(Graph.ScreenPos + pos);
}
ImVec2 ImNodeGraph::SnapToGrid(const ImVec2 &pos)
{
// Draw the grid
ImGuiWindow& DrawWindow = *ImGui::GetCurrentWindow();
ImDrawList& DrawList = *DrawWindow.DrawList;
ImNodeGraphData& Graph = *GImNodeGraph->CurrentGraph;
ImNodeGraphStyle& Style = Graph.Style;
ImGraphCamera& Camera = Graph.Camera;
const float GridSecondarySize = ImGui::GetFontSize() / Camera.Scale;
return ImFloor(pos / GridSecondarySize) * GridSecondarySize;
}
void ImNodeGraph::PushItemWidth(float width)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImGui::PushItemWidth(Graph.Camera.Scale * width);
}
const ImObjectList& ImNodeGraph::GetConnections()
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
return Graph.Connections;
}
void ImNodeGraph::ResetGraph()
{
ImNodeGraphContext& G = *GImNodeGraph;
if(G.CurrentGraph == nullptr) return;
ImNodeGraphData& Graph = *G.CurrentGraph;
Graph.Reset();
}
void ImNodeGraph::ResetGraph(const char* graph)
{
ImNodeGraphData* Graph = FindGraphByTitle(graph);
if(Graph == nullptr) return;
Graph->Reset();
}
int ImNodeGraph::PushChannels(int count)
{
ImDrawList& DrawList = *ImGui::GetWindowDrawList();
ImDrawListSplitter& Splitter = DrawList._Splitter;
// NOTE: this logic has been lifted from ImDrawListSplitter::Split with slight modifications
// to allow nested splits. The main modification is that we only create new ImDrawChannel
// instances after splitter._Count, instead of over the whole splitter._Channels array like
// the regular ImDrawListSplitter::Split method does.
const int old_channel_capacity = Splitter._Channels.Size;
// NOTE: _Channels is not resized down, and therefore _Count <= _Channels.size()!
const int old_channel_count = Splitter._Count;
const int requested_channel_count = old_channel_count + count;
if (old_channel_capacity < old_channel_count + count)
{
Splitter._Channels.resize(requested_channel_count);
}
Splitter._Count = requested_channel_count;
for (int i = old_channel_count; i < requested_channel_count; ++i)
{
ImDrawChannel& channel = Splitter._Channels[i];
// If we're inside the old capacity region of the array, we need to reuse the existing
// memory of the command and index buffers.
if (i < old_channel_capacity)
{
channel._CmdBuffer.resize(0);
channel._IdxBuffer.resize(0);
}
// Else, we need to construct new draw channels.
else
{
IM_PLACEMENT_NEW(&channel) ImDrawChannel();
}
{
ImDrawCmd draw_cmd;
draw_cmd.ClipRect = DrawList._ClipRectStack.back();
draw_cmd.TextureId = DrawList._TextureIdStack.back();
channel._CmdBuffer.push_back(draw_cmd);
}
}
return Splitter._Count - count;
}
void ImNodeGraph::SetChannel(ImGuiID id)
{
ImDrawList* DrawList = ImGui::GetWindowDrawList();
ImDrawListSplitter& Splitter = DrawList->_Splitter;
Splitter.SetCurrentChannel(DrawList, static_cast(id));
}
void ImNodeGraph::SwapChannel(ImDrawChannel& a, ImDrawChannel& b)
{
a._CmdBuffer.swap(b._CmdBuffer);
a._IdxBuffer.swap(b._IdxBuffer);
}
void ImNodeGraph::SortChannels()
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImDrawList& DrawList = *ImGui::GetWindowDrawList();
ImDrawListSplitter& Splitter = DrawList._Splitter;
Splitter.SetCurrentChannel(&DrawList, 0);
auto& Nodes = Graph.Nodes;
auto& Channels = Splitter._Channels;
int Start = 1;
for(auto it : Nodes) { Start = ImMin(Start, it.BgChannelIndex); }
int ChnlCnt = Splitter._Count - Start;
ImVector Buffer; Buffer.resize(ChnlCnt, { });
for(int i = 0, c = 0; i < Nodes.Size(); ++i)
{
if(not Nodes(i)) continue;
const int swap_idx = Start + (c++ * 2);
ImNodeData& node = Nodes[i];
if(node.Graph == nullptr) continue;
SwapChannel(Buffer[node.BgChannelIndex - Start], Channels[swap_idx]);
SwapChannel(Buffer[node.FgChannelIndex - Start], Channels[swap_idx + 1]);
}
for(int i = 0; i < ChnlCnt; ++i)
{
SwapChannel(Channels[Start + i], Buffer[i]);
}
}
bool ImNodeGraph::CheckConnectionValidity(ImGuiID id, ImPinConnection& connection)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImNodeData* node_a = Graph.Nodes(connection.A.Node) ? &Graph.Nodes[connection.A.Node] : nullptr;
ImNodeData* node_b = Graph.Nodes(connection.B.Node) ? &Graph.Nodes[connection.B.Node] : nullptr;
if(node_a == nullptr) { CleanupConnection(id, connection); return true; }
if(node_b == nullptr) { CleanupConnection(id, connection); return true; }
if(connection.A.Direction && node_a->OutputPins(connection.A.Pin) == false) { CleanupConnection(id, connection); return true; }
if(!connection.A.Direction && node_a->InputPins(connection.A.Pin) == false) { CleanupConnection(id, connection); return true; }
if(connection.B.Direction && node_b->OutputPins(connection.B.Pin) == false) { CleanupConnection(id, connection); return true; }
if(!connection.B.Direction && node_b->InputPins(connection.B.Pin) == false) { CleanupConnection(id, connection); return true; }
return false;
}
void ImNodeGraph::CleanupConnection(ImGuiID id, ImPinConnection &connection)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImNodeData* node_a = Graph.Nodes(connection.A.Node) ? &Graph.Nodes[connection.A.Node] : nullptr;
ImNodeData* node_b = Graph.Nodes(connection.B.Node) ? &Graph.Nodes[connection.B.Node] : nullptr;
if(node_a)
{
auto& pins = connection.A.Direction ? node_a->OutputPins : node_a->InputPins;
pins[connection.A.Pin].Connections.find_erase(id);
}
if(node_b)
{
auto& pins = connection.B.Direction ? node_b->OutputPins : node_b->InputPins;
pins[connection.B.Pin].Connections.find_erase(id);
}
Graph.Connections.Erase(id);
}
// Nodes ---------------------------------------------------------------------------------------------------------------
ImNodeData::ImNodeData()
: Graph(nullptr)
, ID(0)
, Root(0, 0)
, FgChannelIndex(0), BgChannelIndex(0)
, Hovered(false), Active(false)
{
}
ImNodeData::ImNodeData(const ImNodeData& other)
: Graph(other.Graph)
, ID(other.ID)
, Root(other.Root)
, ScreenBounds(other.ScreenBounds)
, BgChannelIndex(other.BgChannelIndex)
, FgChannelIndex(other.FgChannelIndex)
, Hovered(other.Hovered), Active(other.Active)
, Header(other.Header)
, InputPins(other.InputPins)
, OutputPins(other.OutputPins)
{
}
ImNodeData &ImNodeData::operator=(const ImNodeData& other)
{
if(&other == this) return *this;
Graph = other.Graph;
ID = other.ID;
Root = other.Root;
ScreenBounds = other.ScreenBounds;
BgChannelIndex = other.BgChannelIndex;
FgChannelIndex = other.FgChannelIndex;
Hovered = other.Hovered;
Active = other.Active;
Header = other.Header;
InputPins = other.InputPins;
OutputPins = other.OutputPins;
return *this;
}
void ImNodeGraph::DrawNode(ImNodeData* Node)
{
if(Node == nullptr) return;
ImNodeGraphData* Graph = Node->Graph;
ImNodeGraphStyle& Style = Graph->Style;
ImGraphCamera& Camera = Graph->Camera;
ImDrawList& DrawList = *ImGui::GetCurrentWindow()->DrawList;
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, Style.NodeOutlineThickness * Camera.Scale);
ImGui::PushStyleColor(ImGuiCol_Border, Style.GetColorU32(ImNodeGraphColor_NodeOutline));
ImU32 color = Style.GetColorU32(ImNodeGraphColor_NodeBackground);
if(Node->Hovered) color = Style.GetColorU32(ImNodeGraphColor_NodeHoveredBackground);
if(Node->Active) color = Style.GetColorU32(ImNodeGraphColor_NodeActiveBackground);
// Render Base Frame
ImGui::RenderFrame(
Node->ScreenBounds.Min, Node->ScreenBounds.Max
, color, true, Style.NodeRounding * Camera.Scale
);
// Render Header
if(Node->Header())
{
// Same as base, but clipped
ImGui::PushClipRect(Node->Header->ScreenBounds.Min, Node->Header->ScreenBounds.Max, true);
ImGui::RenderFrame(
Node->ScreenBounds.Min, Node->ScreenBounds.Max
, Node->Header->Color, true, Style.NodeRounding * Camera.Scale
);
ImGui::PopClipRect();
// Border line between header and content
DrawList.AddLine(
{ Node->Header->ScreenBounds.Min.x, Node->Header->ScreenBounds.Max.y }
, { Node->Header->ScreenBounds.Max.x, Node->Header->ScreenBounds.Max.y }
, Style.GetColorU32(ImNodeGraphColor_NodeOutline), Style.NodeOutlineThickness * Camera.Scale
);
}
if(Graph->Selected.Contains(*Node))
{
DrawList.AddRect(
Node->ScreenBounds.Min, Node->ScreenBounds.Max
, Style.GetColorU32(ImNodeGraphColor_NodeOutlineSelected)
, Style.NodeRounding * Camera.Scale, 0, Style.NodeOutlineSelectedThickness * Camera.Scale
);
}
#ifdef IMNODE_GRAPH_DEBUG_PIN_BOUNDS
for(ImPinData& pin : Node.InputPins)
{
DrawList.AddRect(
pin.ScreenBounds.Min, pin.ScreenBounds.Max
, ImGui::ColorConvertFloat4ToU32({ 1, 0, 0, 1 })
, 0, 0, 2
);
}
for(ImPinData& pin : Node.OutputPins)
{
DrawList.AddRect(
pin.ScreenBounds.Min, pin.ScreenBounds.Max
, ImGui::ColorConvertFloat4ToU32({ 1, 0, 0, 1 })
, 0, 0, 2
);
}
#endif
ImGui::PopStyleColor();
ImGui::PopStyleVar();
}
bool ImNodeGraph::NodeBehaviour(ImNodeData* Node)
{
if(Node == nullptr) return false;
ImNodeGraphData& Graph = *Node->Graph;
bool is_focus = Graph.FocusedNode == *Node;
if(Node->Hovered) Graph.HoveredNode = *Node;
if(Node->Hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left))
{
Graph.FocusedNode = *Node;
}
// Select Region
if(Graph.SelectRegionStart())
{
bool intersect = ImAABB(Graph.GetSelection(), Node->ScreenBounds);
bool checked = Graph.SelectRegion.Contains(*Node);
if(intersect && !checked)
{
Graph.SelectRegion.Insert(*Node);
Graph.UpdateSelection(*Node);
}
if(!intersect && checked)
{
Graph.SelectRegion.Erase(*Node);
Graph.UpdateSelection(*Node, false, true);
}
}
Node->Active = is_focus;
if(Node->Active) ImGui::SetActiveID(Node->ID, ImGui::GetCurrentWindow());
return false;
}
// Pins ----------------------------------------------------------------------------------------------------------------
ImPinData::ImPinData()
: Node(0)
, ID(0)
, Type(0)
, Direction(ImPinDirection_Input)
, Flags(ImPinFlags_None)
, Hovered(false)
{ }
void ImNodeGraph::PinHead(ImGuiID id, ImPinData* Pin)
{
ImNodeGraphData& Graph = *GImNodeGraph->CurrentGraph;
const ImGraphCamera& Camera = Graph.Camera;
const ImNodeGraphStyle& Style = Graph.Style;
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, Style.PinOutlineThickness * Camera.Scale);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{ Style.ItemSpacing, Style.ItemSpacing } * Camera.Scale);
ImGuiWindow& Window = *ImGui::GetCurrentWindow();
ImDrawList& DrawList = *Window.DrawList;
ImGuiStyle& ImStyle = ImGui::GetStyle();
const ImVec2 label_size = ImGui::CalcTextSize("##", NULL, true);
// Modified Radio Button to get proper framing
const float square_sz = ImGui::GetFrameHeight();
const ImVec2 pos = Window.DC.CursorPos;
const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? ImStyle.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + ImStyle.FramePadding.y * 2.0f));
Pin->Center = check_bb.GetCenter();
const float radius = Style.PinRadius * Camera.Scale;
const float outline = Style.PinOutlineThickness * Camera.Scale;
// Behaviour
bool pressed = false, filled = !Pin->Connections.empty();
if(ImGui::IsWindowFocused())
{
Pin->Hovered = ImGui::IsMouseHoveringRect(check_bb.Min, check_bb.Max);
pressed = (Pin->Hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left));
filled |= (Pin->Hovered || Graph.NewConnection == *Pin);
// Start new connection when left clicked
if(Pin->Hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsAnyModKeyDown())
{
BeginConnection(*Pin);
ImGui::SetActiveID(id, ImGui::GetCurrentWindow());
}
// Dropping new connection
if(Pin->Hovered && Graph.NewConnection() && ImGui::IsMouseReleased(ImGuiMouseButton_Left))
{
ImPinData* other = Graph.FindPin(Graph.NewConnection);
MakeConnection(*Pin, *other);
}
// Break connections with Alt-Left-Click
if(Pin->Hovered && ImGui::IsMouseReleased(ImGuiMouseButton_Left) && ImGui::IsKeyDown(ImGuiMod_Alt) && !Graph.NewConnection())
BreakConnections(*Pin);
}
// Item for ImGui
ImGui::ItemSize(total_bb, ImStyle.FramePadding.y);
ImGui::ItemAdd(total_bb, id, &check_bb);
ImGui::ItemHoverable(check_bb, id, ImGuiHoveredFlags_None);
// Drawing
ImVec4 PinColor = Style.PinColors[Pin->Type].Value;
PinColor = PinColor * (pressed ? 0.8f : 1.0f);
ImVec4 FillColor = filled ? PinColor : Style.GetColorVec4(ImNodeGraphColor_PinBackground);
if(pressed || filled)
{
DrawList.AddCircleFilled(Pin->Center, radius + outline * 0.5f, ImGui::ColorConvertFloat4ToU32(FillColor));
}
else
{
DrawList.AddCircleFilled(Pin->Center, radius, ImGui::ColorConvertFloat4ToU32(FillColor));
DrawList.AddCircle(Pin->Center, radius, ImGui::ColorConvertFloat4ToU32(PinColor), 0, outline);
}
ImGui::SameLine();
ImGui::PopStyleVar(2);
}
void ImNodeGraph::DummyPinHead(ImPinData* Pin)
{
if(Pin == nullptr) return;
ImNodeGraphData* Graph = GImNodeGraph->CurrentGraph;
const ImGraphCamera& Camera = Graph->Camera;
const ImNodeGraphStyle& Style = Graph->Style;
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, Style.PinOutlineThickness * Camera.Scale);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{ Style.ItemSpacing, Style.ItemSpacing } * Camera.Scale);
ImGuiWindow& Window = *ImGui::GetCurrentWindow();
ImGuiStyle& ImStyle = ImGui::GetStyle();
const ImVec2 label_size = ImGui::CalcTextSize("##", NULL, true);
const float square_sz = ImGui::GetFrameHeight();
const ImVec2 pos = Window.DC.CursorPos;
const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? ImStyle.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + ImStyle.FramePadding.y * 2.0f));
ImGui::ItemSize(total_bb, ImStyle.FramePadding.y);
ImGui::ItemAdd(total_bb, -1);
ImGui::SameLine();
ImGui::PopStyleVar(2);
}
// Connections ---------------------------------------------------------------------------------------------------------
void ImNodeGraph::BeginConnection(const ImPinPtr &pin)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
Graph.NewConnection = pin;
}
bool ImNodeGraph::Exists(ImPinConnection connection)
{
const ImPinPtr &a = connection.A, &b = connection.B;
if(a.Direction == b.Direction) return false;
if(a.Node == b.Node) return false;
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImPinData* A = Graph.FindPin(a);
if(A == nullptr) return false;
bool result = false;
for(ImGuiID cid : A->Connections)
{
const ImPinConnection& conn = Graph.Connections[cid];
result |= (conn.A == a && conn.B == b);
result |= (conn.A == b && conn.B == a);
if(result) return result;
}
return false;
}
bool ImNodeGraph::MakeConnection(const ImPinPtr &a, const ImPinPtr &b)
{
if(a.Direction == b.Direction) return false;
if(a.Node == b.Node) return false;
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
if(Exists({ a, b })) return false;
ImPinData* A = Graph.FindPin(a);
ImPinData* B = Graph.FindPin(b);
if(A == nullptr || B == nullptr) return false;
if(Graph.Validation && not Graph.Validation(*A, *B)) return false;
if(A->Direction == ImPinDirection_Input && !A->Connections.empty()) BreakConnections(*A);
if(B->Direction == ImPinDirection_Input && !B->Connections.empty()) BreakConnections(*B);
ImGuiID connId = Graph.Connections.Insert({ a, b });
A->Connections.push_back(connId);
B->Connections.push_back(connId);
A->NewConnections.push_back(b); A->BNewConnections = true;
B->NewConnections.push_back(a); B->BNewConnections = true;
return true;
}
bool ImNodeGraph::MakeConnection(const ImUserPinPtr& a, const ImUserPinPtr& b)
{
if(a.Direction == b.Direction) return false;
if(a.Node == b.Node) return false;
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImPinData* A = Graph.FindPin(a);
ImPinData* B = Graph.FindPin(b);
if(A == nullptr || B == nullptr) return false;
if(Exists({ *A, *B })) return false;
if(Graph.Validation && not Graph.Validation(*A, *B)) return false;
if(A->Direction == ImPinDirection_Input && !A->Connections.empty()) BreakConnections(*A);
if(B->Direction == ImPinDirection_Input && !B->Connections.empty()) BreakConnections(*B);
ImGuiID connId = Graph.Connections.Insert({ *A, *B });
A->Connections.push_back(connId);
B->Connections.push_back(connId);
A->NewConnections.push_back(*B); A->BNewConnections = true;
B->NewConnections.push_back(*A); B->BNewConnections = true;
return true;
}
void ImNodeGraph::BreakConnection(ImGuiID id)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImPinConnection connection = Graph.Connections[id]; Graph.Connections.Erase(id);
ImPinData* A = Graph.FindPin(connection.A);
ImPinData* B = Graph.FindPin(connection.B);
if(A == nullptr || B == nullptr) return;
A->Connections.find_erase_unsorted(id);
B->Connections.find_erase_unsorted(id);
A->ErasedConnections.push_back(connection.B); A->BErasedConnections = true;
B->ErasedConnections.push_back(connection.A); B->BErasedConnections = true;
ImPinPtr* it;
if((it = A->NewConnections.find(*B)) != A->NewConnections.end()) A->NewConnections.erase(it);
if((it = B->NewConnections.find(*A)) != B->NewConnections.end()) B->NewConnections.erase(it);
}
void ImNodeGraph::BreakConnections(const ImPinPtr &ptr)
{
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData& Graph = *G.CurrentGraph;
ImPinData* Pin = Graph.FindPin(ptr);
if(Pin == nullptr) return;
for(ImGuiID id : Pin->Connections)
{
ImPinConnection connection = Graph.Connections[id]; Graph.Connections.Erase(id);
ImPinData* other = Graph.FindPin((connection.A == ptr) ? connection.B : connection.A);
Pin->ErasedConnections.push_back(*other); Pin->BErasedConnections = true;
other->ErasedConnections.push_back(ptr); other->BErasedConnections = true;
ImPinPtr* it;
if((it = Pin->NewConnections.find(*other)) != Pin->NewConnections.end()) Pin->NewConnections.erase(it);
if((it = other->NewConnections.find(*Pin)) != other->NewConnections.end()) other->NewConnections.erase(it);
other->Connections.find_erase_unsorted(id);
}
Pin->Connections.clear();
}
ImPinConnection ImNodeGraph::GetConnection(ImGuiID connection)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
return Graph->Connections[connection];
}
void ImNodeGraph::DrawConnection(const ImVec2& out, const ImVec4& out_col, const ImVec2& in, const ImVec4& in_col)
{
ImGuiWindow& Window = *ImGui::GetCurrentWindow();
ImDrawList& DrawList = *Window.DrawList;
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
const ImGraphCamera& Camera = Graph->Camera;
const ImNodeGraphStyle& Style = Graph->Style;
// Calculate Bezier Derivatives
const float FrameHeight = ImGui::GetFrameHeight();
const float diff_x = out.x - in.x;
const float diff_y = out.y - in.y;
const float y_weight = ImAbs(diff_y);
const float xy_ratio = 1.0f + ImMax(diff_x, 0.0f) / (FrameHeight + ImAbs(diff_y));
const float offset = y_weight * xy_ratio;
const ImVec2 out_v = ImVec2(out.x + offset, out.y);
const ImVec2 in_v = ImVec2(in.x - offset, in.y);
AddBezierCubicMultiColored(DrawList, in, in_v, out_v, out, in_col, out_col, Style.ConnectionThickness * Camera.Scale);
}
void ImNodeGraph::DrawConnection(const ImPinData* pin, const ImVec2& point)
{
if(pin == nullptr) return;
ImNodeGraphData* Graph = GImNodeGraph->CurrentGraph;
const ImNodeGraphStyle& Style = Graph->Style;
if(pin->Direction) DrawConnection(PinConnectionAnchor(pin), Style.PinColors[pin->Type], point, Style.PinColors[pin->Type]);
else DrawConnection(point, Style.PinColors[pin->Type], PinConnectionAnchor(pin), Style.PinColors[pin->Type]);
}
void ImNodeGraph::DrawConnection(const ImPinData *a, const ImPinData *b)
{
ImNodeGraphData* Graph = GImNodeGraph->CurrentGraph;
const ImNodeGraphStyle& Style = Graph->Style;
const ImVec2& a_anchor = PinConnectionAnchor(a);
const ImVec4& a_col = Style.PinColors[a->Type];
const ImVec2& b_anchor = PinConnectionAnchor(b);
const ImVec4& b_col = Style.PinColors[b->Type];
const ImVec2& out = a->Direction ? a_anchor : b_anchor;
const ImVec2& in = a->Direction ? b_anchor : a_anchor;
const ImVec4& out_col = a->Direction ? a_col : b_col;
const ImVec4& in_col = a->Direction ? b_col : a_col;
DrawConnection(out, out_col, in, in_col);
}
ImVec2 ImNodeGraph::PinConnectionAnchor(const ImPinData *Pin)
{
ImNodeGraphData* Graph = GImNodeGraph->CurrentGraph;
const ImGraphCamera& Camera = Graph->Camera;
const ImNodeGraphStyle& Style = Graph->Style;
const float radius = Style.PinRadius * Camera.Scale;
ImVec2 loc = Pin->Center;
loc += ImVec2(radius, 0) * (Pin->Direction ? 1.0f : -1.0f);
return loc;
}
// On AddPolyline() and AddConvexPolyFilled() we intentionally avoid using ImVec2 and superfluous function calls to optimize debug/non-inlined builds.
// - Those macros expects l-values and need to be used as their own statement.
// - Those macros are intentionally not surrounded by the 'do {} while (0)' idiom because even that translates to runtime with debug compilers.
#define IM_NORMALIZE2F_OVER_ZERO(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.0f) { float inv_len = ImRsqrt(d2); VX *= inv_len; VY *= inv_len; } } (void)0
#define IM_FIXNORMAL2F_MAX_INVLEN2 100.0f // 500.0f (see #4053, #3366)
#define IM_FIXNORMAL2F(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.000001f) { float inv_len2 = 1.0f / d2; if (inv_len2 > IM_FIXNORMAL2F_MAX_INVLEN2) inv_len2 = IM_FIXNORMAL2F_MAX_INVLEN2; VX *= inv_len2; VY *= inv_len2; } } (void)0
void ImNodeGraph::AddPolylineMultiColored(ImDrawList &draw_list, const ImVec2 *points, int num_points, const ImVec4 &c1,
const ImVec4 &c2, ImDrawFlags flags, float thickness)
{
if (num_points < 2) return;
const int count = num_points - 1; // The number of line segments we need to draw
const bool thick_line = (thickness > draw_list._FringeScale);
const ImVec2 opaque_uv = draw_list._Data->TexUvWhitePixel;
draw_list._Data->TempBuffer.reserve_discard(num_points * 2);
ImVec2* normals = draw_list._Data->TempBuffer.Data;
ImU32* colors = reinterpret_cast(normals + num_points);
for(int i = 0; i < count; ++i)
{
const ImVec2 &a = points[i], &b = points[(i + 1) % num_points];
normals[i] = b - a;
IM_NORMALIZE2F_OVER_ZERO(normals[i].x, normals[i].y);
normals[i] = { normals[i].y, -normals[i].x };
colors[i] = ImGui::ColorConvertFloat4ToU32({ ImLerp(c1, c2, i / static_cast(num_points)) });
}
colors[num_points - 1] = ImGui::ColorConvertFloat4ToU32(c2);
normals[num_points - 1] = normals[num_points - 2];
for(int i = 1; i < count; ++i)
{
normals[i] = (normals[i] + normals[i + 1]) * 0.5f;
IM_FIXNORMAL2F(normals[i].x, normals[i].y);
}
const float AA_SIZE = draw_list._FringeScale;
// Thicknesses <1.0 should behave like thickness 1.0
thickness = ImMax(thickness, 1.0f);
const int integer_thickness = (int)thickness;
const float half_inner_thickness = (thickness - AA_SIZE) * 0.5f;
const int idx_count = thick_line ? count * 18 : count * 12;
const int vtx_count = thick_line ? num_points * 4 : num_points * 3;
draw_list.PrimReserve(idx_count, vtx_count);
ImDrawIdx* _IdxWritePtr = draw_list._IdxWritePtr;
ImDrawVert* _VtxWritePtr = draw_list._VtxWritePtr;
for(int i = 0; i <= count; ++i)
{
if(thick_line)
{
const int v1 = draw_list._VtxCurrentIdx + i * 4;
const int v2 = draw_list._VtxCurrentIdx + i * 4 + 4;
const int i1 = i;
const ImVec2 n1 = normals[i1] * (half_inner_thickness + AA_SIZE);
const ImVec2 n2 = normals[i1] * (half_inner_thickness);
// first points
_VtxWritePtr[0].pos = points[i1] + n1; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = colors[i1] & ~IM_COL32_A_MASK;
_VtxWritePtr[1].pos = points[i1] + n2; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = colors[i1];
_VtxWritePtr[2].pos = points[i1] - n2; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = colors[i1];
_VtxWritePtr[3].pos = points[i1] - n1; _VtxWritePtr[3].uv = opaque_uv; _VtxWritePtr[3].col = colors[i1] & ~IM_COL32_A_MASK;
_VtxWritePtr += 4;
if(i == count) continue;
// top
_IdxWritePtr[0] = v1 + 0; _IdxWritePtr[1] = v2 + 0; _IdxWritePtr[2] = v1 + 1;
_IdxWritePtr[3] = v1 + 1; _IdxWritePtr[4] = v2 + 0; _IdxWritePtr[5] = v2 + 1;
// middle
_IdxWritePtr[6] = v1 + 1; _IdxWritePtr[7] = v2 + 1; _IdxWritePtr[8] = v1 + 2;
_IdxWritePtr[9] = v1 + 2; _IdxWritePtr[10] = v2 + 1; _IdxWritePtr[11] = v2 + 2;
// bottom
_IdxWritePtr[12] = v1 + 2; _IdxWritePtr[13] = v2 + 2; _IdxWritePtr[14] = v1 + 3;
_IdxWritePtr[15] = v1 + 3; _IdxWritePtr[16] = v2 + 2; _IdxWritePtr[17] = v2 + 3;
_IdxWritePtr += 18;
}
else
{
const int v1 = draw_list._VtxCurrentIdx + i * 4;
const int v2 = draw_list._VtxCurrentIdx + i * 4 + 4;
const int i1 = i;
const ImVec2 n = normals[i1] * AA_SIZE;
// first points
_VtxWritePtr[0].pos = points[i1] + n; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = colors[i1] & ~IM_COL32_A_MASK;
_VtxWritePtr[1].pos = points[i1]; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = colors[i1];
_VtxWritePtr[2].pos = points[i1] - n; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = colors[i1] & ~IM_COL32_A_MASK;
_VtxWritePtr += 3;
if(i == count) continue;
// top
_IdxWritePtr[0] = v1 + 0; _IdxWritePtr[1] = v2 + 0; _IdxWritePtr[2] = v1 + 1;
_IdxWritePtr[3] = v1 + 1; _IdxWritePtr[4] = v2 + 0; _IdxWritePtr[5] = v2 + 1;
// bottom
_IdxWritePtr[6] = v1 + 1; _IdxWritePtr[7] = v2 + 1; _IdxWritePtr[8] = v1 + 2;
_IdxWritePtr[9] = v1 + 2; _IdxWritePtr[10] = v2 + 1; _IdxWritePtr[11] = v2 + 2;
_IdxWritePtr += 12;
}
}
draw_list._VtxCurrentIdx += static_cast(_VtxWritePtr - draw_list._VtxWritePtr);
draw_list._VtxWritePtr = _VtxWritePtr;
draw_list._IdxWritePtr = _IdxWritePtr;
}
#define IM_FIXNORMAL2F_MAX_INVLEN2 100.0f // 500.0f (see #4053, #3366)
#define IM_FIXNORMAL2F(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.000001f) { float inv_len2 = 1.0f / d2; if (inv_len2 > IM_FIXNORMAL2F_MAX_INVLEN2) inv_len2 = IM_FIXNORMAL2F_MAX_INVLEN2; VX *= inv_len2; VY *= inv_len2; } } (void)0
void ImNodeGraph::AddBezierCubicMultiColored(ImDrawList& draw_list, const ImVec2 &p1, const ImVec2 &p2, const ImVec2 &p3, const ImVec2 &p4,
const ImVec4 &c1, const ImVec4 &c2, float thickness, int num_segments)
{
draw_list.PathLineTo(p1);
draw_list.PathBezierCubicCurveTo(p2, p3, p4, num_segments);
PathStrokeMultiColored(draw_list, c1, c2, 0, thickness);
}
// =====================================================================================================================
// Public Functionality
// =====================================================================================================================
// Context -------------------------------------------------------------------------------------------------------------
ImNodeGraphContext* ImNodeGraph::CreateContext()
{
// Get current context
ImNodeGraphContext* prev_ctx = GetCurrentContext();
// Create new context
ImNodeGraphContext* ctx = IM_NEW(ImNodeGraphContext)();
SetCurrentContext(ctx);
Initialize();
// If there is a previous context, restore it
if(prev_ctx != nullptr) SetCurrentContext(prev_ctx);
// Return the new context
return ctx;
}
void ImNodeGraph::DestroyContext(ImNodeGraphContext* ctx)
{
// Get current context
ImNodeGraphContext* prev_ctx = GetCurrentContext();
// If the provided context is null, use the current context
if(ctx == nullptr) ctx = prev_ctx;
// Shutdown the context to destroy
SetCurrentContext(ctx);
Shutdown();
// Restore or clear the context
SetCurrentContext((prev_ctx == ctx) ? nullptr : prev_ctx);
// Free context memory
IM_DELETE(ctx);
}
ImNodeGraphContext * ImNodeGraph::GetCurrentContext()
{
return GImNodeGraph;
}
void ImNodeGraph::SetCurrentContext(ImNodeGraphContext *ctx)
{
GImNodeGraph = ctx;
}
void ImNodeGraph::AddFont(const char * path, float size, const ImWchar* glyph_ranges)
{
ImNodeFontConfig cfg{ ImStrdup(path), size, glyph_ranges };
GFonts.push_back(cfg);
}
// Graph ---------------------------------------------------------------------------------------------------------------
ImGraphCamera::ImGraphCamera() : Position{ 0, 0 }, Scale(1.0f) {}
ImNodeGraphStyle::ImNodeGraphStyle()
: GridPrimaryStep(5)
, GridPrimaryThickness(2.0f)
, GridSecondaryThickness(1.0f)
, NodePadding(8.0f)
, NodeRounding(8.0f)
, NodeOutlineThickness(2.0f)
, NodeOutlineSelectedThickness(4.0f)
, SelectRegionRounding(2.0f)
, SelectRegionOutlineThickness(2.0f)
, ItemSpacing(4.0f)
, PinRadius(8.0f)
, PinOutlineThickness(3.0f)
, ConnectionThickness(2.0f)
, Colors{ ImColor(0x000000FF) }
, PinColors(nullptr)
{
Colors[ImNodeGraphColor_GridBackground] = ImColor(0x11, 0x11, 0x11);
Colors[ImNodeGraphColor_GridPrimaryLines] = ImColor(0x88, 0x88, 0x88);
Colors[ImNodeGraphColor_GridSecondaryLines] = ImColor(0x44, 0x44, 0x44);
Colors[ImNodeGraphColor_NodeBackground] = ImColor(0x88, 0x88, 0x88);
Colors[ImNodeGraphColor_NodeHoveredBackground] = ImColor(0x9C, 0x9C, 0x9C);
Colors[ImNodeGraphColor_NodeActiveBackground] = ImColor(0x7A, 0x7A, 0x7A);
Colors[ImNodeGraphColor_NodeOutline] = ImColor(0x33, 0x33, 0x33);
Colors[ImNodeGraphColor_NodeOutlineSelected] = ImColor(0xEF, 0xAE, 0x4B);
Colors[ImNodeGraphColor_PinBackground] = ImColor(0x22, 0x22, 0x22);
Colors[ImNodeGraphColor_SelectRegionBackground] = ImColor(0xC9, 0x8E, 0x36, 0x44);
Colors[ImNodeGraphColor_SelectRegionOutline] = ImColor(0xEF, 0xAE, 0x4B, 0xBB);
}
ImNodeGraphSettings::ImNodeGraphSettings()
: ZoomRate(0.1f)
, ZoomSmoothing(8.0f)
, ZoomBounds(0.6f, 2.5f)
{
}
void ImNodeGraph::BeginGraph(const char* title, const ImVec2& size_arg)
{
// Validate Global State
IM_ASSERT(GImNodeGraph != nullptr);
ImNodeGraphContext& G = *GImNodeGraph;
// Ensure we are in the scope of a window
ImGuiWindow* Window = ImGui::GetCurrentWindow();
IM_ASSERT(Window != nullptr); // Ensure we are within a window
// Validate parameters and graph state
IM_ASSERT(title != nullptr && title[0] != '\0'); // Graph name required
IM_ASSERT(G.Scope == ImNodeGraphScope_None); // Ensure we are not in the scope of another graph
// Get Graph
ImNodeGraphData* Graph = FindGraphByTitle(title);
const bool FirstFrame = (Graph == nullptr);
if(FirstFrame) { Graph = CreateNewGraph(title); }
ImGraphCamera& Camera = Graph->Camera;
// Update State
G.CurrentGraph = Graph;
G.Scope = ImNodeGraphScope_Graph;
// Style & Settings
ImNodeGraphStyle& Style = Graph->Style;
// Fonts
G.Fonts.front()->Scale = Camera.Scale / GFontUpscale;
ImGui::PushFont(G.Fonts.front());
// Calculate Size
const ImVec2 SizeAvail = ImGui::GetContentRegionAvail();
const ImVec2 Size = ImGui::CalcItemSize(size_arg, SizeAvail.x, SizeAvail.y);
Graph->ScreenSize = Size;
Graph->ScreenPos = ImGui::GetCursorScreenPos();
Graph->SubmitCount = 0;
Graph->LockSelectRegion = false;
// Cleanup erased nodes
int cnt = Graph->Nodes.Cleanup();
for(int i = ImMax(Graph->Nodes.Freed.Size - cnt - 1, 0); i < Graph->Nodes.Freed.Size; ++i)
{
Graph->Selected.Erase(Graph->Nodes.IdxToID[Graph->Nodes.Freed[i]]);
}
// Reset nodes
Graph->Nodes.Reset();
// Begin the Graph Child
ImGui::PushStyleColor(ImGuiCol_ChildBg, static_cast(Style.Colors[ImNodeGraphColor_GridBackground]));
ImGui::BeginChild(Graph->ID, Size, 0, ImGuiWindowFlags_NoScrollbar);
ImGui::PopStyleColor();
ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2{ Style.ItemSpacing, Style.ItemSpacing } * Camera.Scale);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{ Style.ItemSpacing, Style.ItemSpacing } * Camera.Scale);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{ Style.NodePadding, Style.NodePadding } * Camera.Scale);
DrawGrid({ Graph->ScreenPos, Graph->ScreenPos + Graph->ScreenSize });
}
void ImNodeGraph::EndGraph()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
ImNodeGraphContext& G = *GImNodeGraph;
// Validate graph state
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(G.Scope == ImNodeGraphScope_Graph && Graph != nullptr); // Ensure we are in the scope of a graph
DrawGraph(Graph);
if(ImGui::IsWindowFocused()) GraphBehaviour({ Graph->ScreenPos, Graph->ScreenPos + Graph->ScreenSize });
ImGui::PopStyleVar(3);
ImGui::PopFont();
ImGui::EndChild();
// Update State
G.CurrentGraph = nullptr;
G.Scope = ImNodeGraphScope_None;
}
bool ImNodeGraph::BeginGraphPostOp(const char *title)
{
// Validate Global State
IM_ASSERT(GImNodeGraph != nullptr);
ImNodeGraphContext& G = *GImNodeGraph;
// Ensure we are in the scope of a window
ImGuiWindow* Window = ImGui::GetCurrentWindow();
IM_ASSERT(Window != nullptr); // Ensure we are within a window
// Validate parameters and graph state
IM_ASSERT(title != nullptr && title[0] != '\0'); // Graph name required
IM_ASSERT(G.Scope == ImNodeGraphScope_None); // Ensure we are not in the scope of another graph
// Get Graph
ImNodeGraphData* Graph = FindGraphByTitle(title);
if(Graph == nullptr) return false;
// Update State
G.CurrentGraph = Graph;
G.Scope = ImNodeGraphScope_Graph;
return true;
}
void ImNodeGraph::EndGraphPostOp()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
ImNodeGraphContext& G = *GImNodeGraph;
// Validate graph state
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(G.Scope == ImNodeGraphScope_Graph && Graph != nullptr); // Ensure we are in the scope of a graph
// Update State
G.CurrentGraph = nullptr;
G.Scope = ImNodeGraphScope_None;
}
void ImNodeGraph::SetGraphValidation(ImConnectionValidation validation)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
ImNodeGraphContext& G = *GImNodeGraph;
// Validate graph state
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(G.Scope != ImNodeGraphScope_None && Graph != nullptr); // Ensure we are in the scope of a graph
Graph->Validation = validation;
}
float ImNodeGraph::GetCameraScale()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
ImNodeGraphContext& G = *GImNodeGraph;
// Validate graph state
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(G.Scope != ImNodeGraphScope_None && Graph != nullptr); // Ensure we are in the scope of a graph
return Graph->Camera.Scale;
}
void ImNodeGraph::BeginNode(int iid, ImVec2& pos)
{
IM_ASSERT(GImNodeGraph != nullptr);
// Validate State
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(G.Scope == ImNodeGraphScope_Graph && Graph != nullptr); // Ensure we are in the scope of a graph
// Get Node
ImGuiID id = ImGui::GetCurrentWindow()->GetID(iid);
ImNodeData& Node = Graph->Nodes[id];
if(Node.Graph == nullptr)
{
Node.Graph = Graph;
Node.Root = pos;
Node.ID = id;
Node.UserID = iid;
}
// Style
const ImNodeGraphStyle& Style = Graph->Style;
// Update node vars
Node.InputPins.Cleanup(); Node.InputPins.Reset();
Node.OutputPins.Cleanup(); Node.OutputPins.Reset();
Node.Header.Reset();
pos = Node.Root;
// Push Scope
Graph->CurrentNode = &Node;
Graph->SubmitCount ++;
G.Scope = ImNodeGraphScope_Node;
// Push new draw channels
Node.BgChannelIndex = PushChannels(2);
Node.FgChannelIndex = Node.BgChannelIndex + 1;
SetChannel(Node.FgChannelIndex);
// Setup Node Group
ImGui::SetCursorScreenPos(GridToScreen(pos + ImVec2(Style.NodePadding, Style.NodePadding)));
ImGui::BeginGroup();
ImGui::PushID(static_cast(id));
ImGuiContext& Ctx = *ImGui::GetCurrentContext();
Node.PrevActiveItem = Ctx.ActiveId;
}
void ImNodeGraph::EndNode()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
IM_ASSERT(G.Scope == ImNodeGraphScope_Node && Graph->CurrentNode != nullptr); // Ensure we are in the scope of a node
ImNodeData& Node = *Graph->CurrentNode;
ImGuiContext& Ctx = *ImGui::GetCurrentContext();
if(Ctx.ActiveId != Node.PrevActiveItem || Ctx.ActiveId == 0) Node.ActiveItem = Ctx.ActiveId;
bool is_node_item_active = Ctx.ActiveId == Node.ActiveItem && Ctx.ActiveId != 0;
bool other_hovered = ImGui::IsAnyItemHovered() || is_node_item_active;
if(other_hovered) Graph->LockSelectRegion = true;
ImGui::PopID();
ImGui::EndGroup();
const ImNodeGraphStyle& Style = Graph->Style;
const ImGraphCamera& Camera = Graph->Camera;
Node.ScreenBounds = { ImGui::GetItemRectMin(), ImGui::GetItemRectMax() };
Node.ScreenBounds.Expand(Style.NodePadding * Camera.Scale);
bool hovering = ImGui::IsMouseHoveringRect(Node.ScreenBounds.Min, Node.ScreenBounds.Max) && !other_hovered;
bool is_focus = Graph->FocusedNode == Node;
bool is_hovered = Graph->HoveredNode == Node;
// Fixup pins
float Width = Node.Header->ScreenBounds.GetWidth() - Style.NodePadding * Camera.Scale;
auto Input = Node.InputPins.begin();
auto Output = Node.OutputPins.begin();
const auto InputEnd = Node.InputPins.end();
const auto OutputEnd = Node.OutputPins.end();
while(Input != InputEnd || Output != OutputEnd)
{
float iWidth = Input != InputEnd ? Input->ScreenBounds.GetWidth() : 0;
float oWidth = Output != OutputEnd ? Output->ScreenBounds.GetWidth() : 0;
Width = ImMax(Width, iWidth + oWidth);
if(Input != InputEnd) { if(Input->Hovered) hovering = false; ++Input; }
if(Output != OutputEnd) { if(Output->Hovered) hovering = false; ++Output; }
}
Node.Hovered = hovering; // Whether mouse is over node
Node.Hovered &= !Graph->HoveredNode() || is_hovered; // Check if a node later in the draw order is hovered
Node.Hovered &= !Graph->FocusedNode() || is_focus; // Chech if another node is focused
Node.Hovered &= !Graph->SelectRegionStart(); // Check for drag selection
// Pop Scope
G.Scope = ImNodeGraphScope_Graph;
Graph->CurrentNode = nullptr;
// fix up header width
if(Node.Header())
{
Node.Header->ScreenBounds.Min.x = Node.ScreenBounds.Min.x;
Node.Header->ScreenBounds.Max.x = Node.ScreenBounds.Max.x;
}
Input = Node.InputPins.begin();
Output = Node.OutputPins.begin();
float Y = Node.Header->ScreenBounds.Max.y + (Node.PinOffset + Style.NodePadding) * Camera.Scale;
float InX = Node.ScreenBounds.Min.x + Style.NodePadding * Camera.Scale;
while(Input != InputEnd || Output != OutputEnd)
{
float Step = 0.0f;
if(Input != InputEnd)
{
Input->Pos = { InX, Y };
Step = ImMax(Step, Input->ScreenBounds.GetHeight());
++Input;
}
if(Output != OutputEnd)
{
float OutX = InX + Width - Output->ScreenBounds.GetWidth();
Output->Pos = { OutX, Y };
Step = ImMax(Step, Output->ScreenBounds.GetHeight());
++Output;
}
Y += Step + Style.ItemSpacing;
}
}
void ImNodeGraph::BeginNodeHeader(const char *title, ImColor color, ImColor hovered, ImColor active)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate node scope
ImNodeData* Node = Graph->CurrentNode;
IM_ASSERT(G.Scope == ImNodeGraphScope_Node && Node != nullptr); // Ensure we are in the scope of a node
IM_ASSERT(Node->Header() == false); // Ensure there is only one header
if(Node->Hovered) color = hovered;
if(Node->Active) color = active;
// Setup header
Node->Header = ImNodeHeaderData{
Node
, color
, ImRect()
};
// Create group
ImGui::BeginGroup();
ImGui::PushID(title);
// Push scope
G.Scope = ImNodeGraphScope_NodeHeader;
}
void ImNodeGraph::BeginNodeHeader(int id, ImColor color, ImColor hovered, ImColor active)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate node scope
ImNodeData* Node = Graph->CurrentNode;
IM_ASSERT(G.Scope == ImNodeGraphScope_Node && Node != nullptr); // Ensure we are in the scope of a node
IM_ASSERT(Node->Header() == false); // Ensure there is only one header
if(Node->Hovered) color = hovered;
if(Node->Active) color = active;
// Setup header
Node->Header = ImNodeHeaderData{
Node
, color
, ImRect()
};
// Create group
ImGui::BeginGroup();
ImGui::PushID(id);
// Push scope
G.Scope = ImNodeGraphScope_NodeHeader;
}
void ImNodeGraph::EndNodeHeader()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate node scope
ImNodeData* Node = Graph->CurrentNode;
IM_ASSERT(G.Scope == ImNodeGraphScope_NodeHeader && Node != nullptr); // Ensure we are in the scope of a node
IM_ASSERT(Node->Header()); // Ensure the header is valid
// End Group
ImGui::PopID();
ImGui::EndGroup();
const ImNodeGraphStyle& Style = Graph->Style;
const ImGraphCamera& Camera = Graph->Camera;
Node->Header->ScreenBounds = { ImGui::GetItemRectMin(), ImGui::GetItemRectMax() };
Node->Header->ScreenBounds.Expand(Style.NodePadding * Camera.Scale);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + Style.NodePadding * Camera.Scale);
G.Scope = ImNodeGraphScope_Node;
}
ImSet* ImNodeGraph::GetSelected()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
return &Graph->Selected;
}
ImSet* ImNodeGraph::GetSelected(const char *title)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = FindGraphByTitle(title);
return &Graph->Selected;
}
int ImNodeGraph::GetUserID(ImGuiID id)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
IM_ASSERT(GImNodeGraph->CurrentGraph != nullptr);
ImNodeData& Node = GImNodeGraph->CurrentGraph->Nodes[id];
if(Node.Graph == nullptr) return -1;
return Node.UserID;
}
int ImNodeGraph::GetUserID(const char* graph, ImGuiID id)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = FindGraphByTitle(graph);
return Graph->Nodes[id].UserID;
}
void ImNodeGraph::SetPinColors(const ImColor *colors)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
Graph->Style.PinColors = colors;
}
bool ImNodeGraph::BeginPin(int iid, ImPinType type, ImPinDirection direction, ImPinFlags flags)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate node scope
ImNodeData* Node = Graph->CurrentNode;
IM_ASSERT(G.Scope == ImNodeGraphScope_Node && Node != nullptr); // Ensure we are in the scope of a node
const ImNodeGraphStyle& Style = Graph->Style;
const ImGraphCamera& Camera = Graph->Camera;
// Get Pin Offset
if(not Node->PinOffset())
{
float screen_y = WindowToScreen(ImGui::GetCursorPos()).y;
if(Node->Header()) Node->PinOffset = (screen_y - Node->Header->ScreenBounds.Max.y);
else Node->PinOffset = (screen_y - Node->ScreenBounds.Min.y);
}
// Push the pin
ImGuiID id = ImGui::GetCurrentWindow()->GetID(iid);
ImPinData* Pin = &(direction ? Node->OutputPins[id] : Node->InputPins[id]);
Graph->CurrentPin = Pin;
bool changed = false;
changed |= !Pin->NewConnections.empty();
changed |= !Pin->ErasedConnections.empty();
Pin->BNewConnections = false;
Pin->BErasedConnections = false;
// Setup pin
Pin->Node = Node->ID;
Pin->ID = id;
Pin->UserID = iid;
Pin->Type = type;
Pin->Direction = direction;
Pin->Flags = flags;
// Setup ImGui Group
ImGui::SetCursorScreenPos(Pin->Pos); // The first frame the node will be completely garbled
ImGui::BeginGroup();
ImGui::PushID(static_cast(id));
// Push Scope
G.Scope = ImNodeGraphScope_Pin;
if(!Pin->Direction)
{
PinHead(id, Pin);
ImGui::SameLine();
}
else if(!(flags & ImPinFlags_NoPadding))
{
DummyPinHead(Pin); // Guess this counts as padding
ImGui::SameLine();
}
return changed;
}
void ImNodeGraph::EndPin()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate pin scope
ImPinData* Pin = Graph->CurrentPin;
IM_ASSERT(G.Scope == ImNodeGraphScope_Pin && Pin != nullptr); // Ensure we are in the scope of a pin
if(Pin->Direction)
{
ImGui::SameLine();
PinHead(Pin->ID, Pin);
}
ImGui::PopID();
ImGui::EndGroup();
Pin->ScreenBounds = { ImGui::GetItemRectMin(), ImGui::GetItemRectMax() };
// Pop Scope
G.Scope = ImNodeGraphScope_Node;
if(Pin->BNewConnections == false) Pin->NewConnections.clear();
if(Pin->BErasedConnections == false) Pin->ErasedConnections.clear();
}
bool ImNodeGraph::IsPinConnected()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate pin scope
ImPinData* Pin = Graph->CurrentPin;
IM_ASSERT(G.Scope == ImNodeGraphScope_Pin && Pin != nullptr); // Ensure we are in the scope of a pin
return Pin->Connections.empty() == false;
}
bool ImNodeGraph::IsPinConnected(ImPinPtr pin)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate pin scope
ImPinData* Pin = Graph->FindPin(pin);
if(Pin == nullptr) return false;
return Pin->Connections.empty() == false;
}
const ImVector& ImNodeGraph::GetPinConnections()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate pin scope
ImPinData* Pin = Graph->CurrentPin;
IM_ASSERT(G.Scope == ImNodeGraphScope_Pin && Pin != nullptr); // Ensure we are in the scope of a pin
return Pin->Connections;
}
const ImVector& ImNodeGraph::GetPinConnections(ImPinPtr pin)
{
static const ImVector Default = ImVector();
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
ImPinData* Pin = Graph->FindPin(pin);
if(Pin == nullptr) return Default;
return Pin->Connections;
}
const ImVector& ImNodeGraph::GetNewConnections()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate pin scope
ImPinData* Pin = Graph->CurrentPin;
IM_ASSERT(G.Scope == ImNodeGraphScope_Pin && Pin != nullptr); // Ensure we are in the scope of a pin
return Pin->NewConnections;
}
const ImVector& ImNodeGraph::GetErasedConnections()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate pin scope
ImPinData* Pin = Graph->CurrentPin;
IM_ASSERT(G.Scope == ImNodeGraphScope_Pin && Pin != nullptr); // Ensure we are in the scope of a pin
return Pin->ErasedConnections;
}
int ImNodeGraph::GetUserID(ImPinPtr ptr)
{
ImNodeData& Node = GImNodeGraph->CurrentGraph->Nodes[ptr.Node];
return ptr.Direction ? Node.OutputPins[ptr.Pin].UserID : Node.InputPins[ptr.Pin].UserID;
}
int ImNodeGraph::GetUserID(const char *graph, ImPinPtr ptr)
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = FindGraphByTitle(graph);
ImNodeData& Node = Graph->Nodes[ptr.Node];
return ptr.Direction ? Node.OutputPins[ptr.Pin].UserID : Node.InputPins[ptr.Pin].UserID;
}
ImPinPtr ImNodeGraph::GetPinPtr()
{
// Validate global state
IM_ASSERT(GImNodeGraph != nullptr);
// Validate Graph state
ImNodeGraphContext& G = *GImNodeGraph;
ImNodeGraphData* Graph = G.CurrentGraph;
IM_ASSERT(Graph != nullptr);
// Validate pin scope
return *Graph->CurrentPin;
}