// ===================================================================================================================== // 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; }