327 lines
13 KiB
C++
327 lines
13 KiB
C++
# define IMGUI_DEFINE_MATH_OPERATORS
|
|
# include <imgui.h>
|
|
# include <imgui_internal.h>
|
|
# include <imgui_node_editor.h>
|
|
# include <base.h>
|
|
# include <vector>
|
|
# include "NodeEx.h"
|
|
|
|
namespace ed = ax::NodeEditor;
|
|
|
|
struct Example:
|
|
public Application
|
|
{
|
|
// Struct to hold basic information about connection between
|
|
// pins. Note that connection (aka. link) has its own ID.
|
|
// This is useful later with dealing with selections, deletion
|
|
// or other operations.
|
|
struct LinkInfo
|
|
{
|
|
ed::LinkId Id;
|
|
ed::PinId InputId;
|
|
ed::PinId OutputId;
|
|
};
|
|
|
|
using Application::Application;
|
|
|
|
void OnStart() override
|
|
{
|
|
ed::Config config;
|
|
config.SettingsFile = "BasicInteraction.json";
|
|
m_Context = ed::CreateEditor(&config);
|
|
}
|
|
|
|
void OnStop() override
|
|
{
|
|
ed::DestroyEditor(m_Context);
|
|
}
|
|
|
|
void ImGuiEx_BeginColumn()
|
|
{
|
|
ImGui::BeginGroup();
|
|
}
|
|
|
|
void ImGuiEx_NextColumn()
|
|
{
|
|
ImGui::EndGroup();
|
|
ImGui::SameLine();
|
|
ImGui::BeginGroup();
|
|
}
|
|
|
|
void ImGuiEx_EndColumn()
|
|
{
|
|
ImGui::EndGroup();
|
|
}
|
|
|
|
void OnFrame(float deltaTime) override
|
|
{
|
|
auto& io = ImGui::GetIO();
|
|
|
|
ImGui::Text("FPS: %.2f (%.2gms)", io.Framerate, io.Framerate ? 1000.0f / io.Framerate : 0.0f);
|
|
|
|
ImGui::Separator();
|
|
|
|
ed::SetCurrentEditor(m_Context);
|
|
|
|
// Start interaction with editor.
|
|
ed::Begin("My Editor", ImVec2(0.0, 0.0f));
|
|
|
|
// Clear pin tooltips at start of frame
|
|
ed::ClearPinTooltips();
|
|
|
|
int uniqueId = 1;
|
|
|
|
//
|
|
// 1) Commit known data to editor
|
|
//
|
|
|
|
// Submit Node A
|
|
ed::NodeId nodeA_Id = uniqueId++;
|
|
ed::PinId nodeA_InputPinId = uniqueId++;
|
|
ed::PinId nodeA_OutputPinId = uniqueId++;
|
|
|
|
if (m_FirstFrame)
|
|
ed::SetNodePosition(nodeA_Id, ImVec2(10, 10));
|
|
ed::BeginNode(nodeA_Id);
|
|
ImGui::Text("Node A");
|
|
ed::BeginPin(nodeA_InputPinId, ed::PinKind::Input);
|
|
ImGui::Text("-> In");
|
|
ed::EndPin();
|
|
ImGui::SameLine();
|
|
ed::BeginPin(nodeA_OutputPinId, ed::PinKind::Output);
|
|
ImGui::Text("Out ->");
|
|
ed::EndPin();
|
|
ed::EndNode();
|
|
|
|
// Submit Node B
|
|
ed::NodeId nodeB_Id = uniqueId++;
|
|
ed::PinId nodeB_InputPinId1 = uniqueId++;
|
|
ed::PinId nodeB_InputPinId2 = uniqueId++;
|
|
ed::PinId nodeB_OutputPinId = uniqueId++;
|
|
|
|
if (m_FirstFrame)
|
|
ed::SetNodePosition(nodeB_Id, ImVec2(210, 60));
|
|
ed::BeginNode(nodeB_Id);
|
|
ImGui::Text("Node B");
|
|
|
|
// Store cursor position before content to get node bounds
|
|
ImVec2 nodeContentStart = ImGui::GetCursorScreenPos();
|
|
|
|
ImGuiEx_BeginColumn();
|
|
ed::BeginPin(nodeB_InputPinId1, ed::PinKind::Input);
|
|
ImGui::Text("-> In1");
|
|
ed::EndPin();
|
|
ed::BeginPin(nodeB_InputPinId2, ed::PinKind::Input);
|
|
ImGui::Text("-> In2");
|
|
ed::EndPin();
|
|
ImGuiEx_NextColumn();
|
|
ed::BeginPin(nodeB_OutputPinId, ed::PinKind::Output);
|
|
ImGui::Text("Out ->");
|
|
ed::EndPin();
|
|
ImGuiEx_EndColumn();
|
|
|
|
// After content, get actual node bounds
|
|
ImRect nodeRect = ImRect(
|
|
nodeContentStart,
|
|
ImGui::GetItemRectMax()
|
|
);
|
|
|
|
// Now place pins at edges using the placer
|
|
// Note: This is a demonstration - in practice you'd place these during node building
|
|
ed::EndNode();
|
|
|
|
// Submit Node C with edge-placed pins using PinEx
|
|
ed::NodeId nodeC_Id = uniqueId++;
|
|
ed::PinId nodeC_TopPinId = uniqueId++;
|
|
ed::PinId nodeC_BottomPinId = uniqueId++;
|
|
ed::PinId nodeC_LeftPinId = uniqueId++;
|
|
ed::PinId nodeC_RightPinId = uniqueId++;
|
|
|
|
if (m_FirstFrame)
|
|
ed::SetNodePosition(nodeC_Id, ImVec2(410, 60));
|
|
|
|
ed::BeginNode(nodeC_Id);
|
|
ImGui::Text("Node C (Edge Pins)");
|
|
|
|
// Add some widgets to verify pins remain properly centered
|
|
static bool check_box = true;
|
|
ImGui::Checkbox("Checkbox", &check_box);
|
|
|
|
static int radio_val = 0;
|
|
ImGui::RadioButton("Option A", &radio_val, 0); ImGui::SameLine();
|
|
ImGui::RadioButton("Option B", &radio_val, 1);
|
|
|
|
static float slider_val = 0.5f;
|
|
ImGui::SliderFloat("Slider", &slider_val, 0.0f, 1.0f);
|
|
|
|
static int counter = 0;
|
|
char buttonLabel[64];
|
|
snprintf(buttonLabel, sizeof(buttonLabel), "Count: %d", counter);
|
|
if (ImGui::Button(buttonLabel))
|
|
counter++;
|
|
|
|
// Save cursor position - this marks the end of our content
|
|
// Pins will be placed after this, so they won't affect node size
|
|
ImVec2 contentEndPos = ImGui::GetCursorScreenPos();
|
|
|
|
// Get node bounds properly
|
|
// The node position is set via SetNodePosition, and content determines size
|
|
ImVec2 nodePos = ed::GetNodePosition(nodeC_Id);
|
|
ImVec2 nodeSize = ed::GetNodeSize(nodeC_Id);
|
|
|
|
// If node size is not yet determined (first frame), calculate from content
|
|
if (nodeSize.x <= 0 || nodeSize.y <= 0)
|
|
{
|
|
// Get content bounds (just the widgets, before pins)
|
|
ImVec2 contentMin = ImGui::GetItemRectMin();
|
|
ImVec2 contentMax = ImGui::GetItemRectMax();
|
|
|
|
// Calculate size: from content extent
|
|
ImVec2 contentSize = contentMax - contentMin;
|
|
nodeSize = contentSize;
|
|
|
|
// Ensure minimum size for widgets
|
|
if (nodeSize.x < 180) nodeSize.x = 180;
|
|
if (nodeSize.y < 150) nodeSize.y = 150;
|
|
}
|
|
|
|
// Calculate final node bounds (this should remain stable)
|
|
ImRect nodeCRect = ImRect(nodePos, nodePos + nodeSize);
|
|
|
|
// Place pins at edges using PinEx - centered properly
|
|
// offset: 0.5 = center, direction: -8.0 = 8 pixels inside edge
|
|
// These pins won't affect node size because we restore cursor position
|
|
// Place multiple pins on each edge to demonstrate precision, states, and tooltips
|
|
// Top edge: 3 input pins with different states and tooltips
|
|
PinEx(nodeC_TopPinId, ed::PinKind::Input, ed::PinEdge::Top, 0.25f, -8.0f, nodeCRect, ed::PinState::Normal, "Normal Input Pin");
|
|
PinEx(uniqueId++, ed::PinKind::Input, ed::PinEdge::Top, 0.5f, -8.0f, nodeCRect, ed::PinState::Running, "Running Input Pin");
|
|
PinEx(uniqueId++, ed::PinKind::Input, ed::PinEdge::Top, 0.75f, -8.0f, nodeCRect, ed::PinState::Deactivated, "Deactivated Input Pin");
|
|
|
|
// Bottom edge: 3 output pins with different states and tooltips
|
|
PinEx(nodeC_BottomPinId, ed::PinKind::Output, ed::PinEdge::Bottom, 0.25f, -8.0f, nodeCRect, ed::PinState::Normal, "Normal Output Pin");
|
|
PinEx(uniqueId++, ed::PinKind::Output, ed::PinEdge::Bottom, 0.5f, -8.0f, nodeCRect, ed::PinState::Running, "Running Output Pin");
|
|
PinEx(uniqueId++, ed::PinKind::Output, ed::PinEdge::Bottom, 0.75f, -8.0f, nodeCRect, ed::PinState::Error, "Error Output Pin");
|
|
|
|
// Left edge: 3 input pins with different states and tooltips
|
|
PinEx(nodeC_LeftPinId, ed::PinKind::Input, ed::PinEdge::Left, 0.25f, -8.0f, nodeCRect, ed::PinState::Normal, "Normal Input Pin");
|
|
PinEx(uniqueId++, ed::PinKind::Input, ed::PinEdge::Left, 0.5f, -8.0f, nodeCRect, ed::PinState::Running, "Running Input Pin");
|
|
PinEx(uniqueId++, ed::PinKind::Input, ed::PinEdge::Left, 0.75f, -8.0f, nodeCRect, ed::PinState::Warning, "Warning Input Pin");
|
|
|
|
// Right edge: 3 output pins with different states and tooltips
|
|
PinEx(nodeC_RightPinId, ed::PinKind::Output, ed::PinEdge::Right, 0.25f, -8.0f, nodeCRect, ed::PinState::Normal, "Normal Output Pin");
|
|
PinEx(uniqueId++, ed::PinKind::Output, ed::PinEdge::Right, 0.5f, -8.0f, nodeCRect, ed::PinState::Running, "Running Output Pin");
|
|
PinEx(uniqueId++, ed::PinKind::Output, ed::PinEdge::Right, 0.75f, -8.0f, nodeCRect, ed::PinState::Deactivated, "Deactivated Output Pin");
|
|
|
|
// Restore cursor to end of content (pins don't affect layout)
|
|
ImGui::SetCursorScreenPos(contentEndPos);
|
|
ed::EndNode();
|
|
|
|
// Submit Links
|
|
for (auto& linkInfo : m_Links)
|
|
ed::Link(linkInfo.Id, linkInfo.InputId, linkInfo.OutputId);
|
|
|
|
// Process pin tooltips (deferred like in widgets-example)
|
|
ed::Suspend();
|
|
ed::ProcessPinTooltips();
|
|
ed::Resume();
|
|
|
|
//
|
|
// 2) Handle interactions
|
|
//
|
|
|
|
// Handle creation action, returns true if editor want to create new object (node or link)
|
|
if (ed::BeginCreate())
|
|
{
|
|
ed::PinId inputPinId, outputPinId;
|
|
if (ed::QueryNewLink(&inputPinId, &outputPinId))
|
|
{
|
|
// QueryNewLink returns true if editor want to create new link between pins.
|
|
//
|
|
// Link can be created only for two valid pins, it is up to you to
|
|
// validate if connection make sense. Editor is happy to make any.
|
|
//
|
|
// Link always goes from input to output. User may choose to drag
|
|
// link from output pin or input pin. This determine which pin ids
|
|
// are valid and which are not:
|
|
// * input valid, output invalid - user started to drag new ling from input pin
|
|
// * input invalid, output valid - user started to drag new ling from output pin
|
|
// * input valid, output valid - user dragged link over other pin, can be validated
|
|
|
|
if (inputPinId && outputPinId) // both are valid, let's accept link
|
|
{
|
|
// ed::AcceptNewItem() return true when user release mouse button.
|
|
if (ed::AcceptNewItem())
|
|
{
|
|
// Since we accepted new link, lets add one to our list of links.
|
|
m_Links.push_back({ ed::LinkId(m_NextLinkId++), inputPinId, outputPinId });
|
|
|
|
// Draw new link.
|
|
ed::Link(m_Links.back().Id, m_Links.back().InputId, m_Links.back().OutputId);
|
|
}
|
|
|
|
// You may choose to reject connection between these nodes
|
|
// by calling ed::RejectNewItem(). This will allow editor to give
|
|
// visual feedback by changing link thickness and color.
|
|
}
|
|
}
|
|
}
|
|
ed::EndCreate(); // Wraps up object creation action handling.
|
|
|
|
|
|
// Handle deletion action
|
|
if (ed::BeginDelete())
|
|
{
|
|
// There may be many links marked for deletion, let's loop over them.
|
|
ed::LinkId deletedLinkId;
|
|
while (ed::QueryDeletedLink(&deletedLinkId))
|
|
{
|
|
// If you agree that link can be deleted, accept deletion.
|
|
if (ed::AcceptDeletedItem())
|
|
{
|
|
// Then remove link from your data.
|
|
for (auto& link : m_Links)
|
|
{
|
|
if (link.Id == deletedLinkId)
|
|
{
|
|
m_Links.erase(&link);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// You may reject link deletion by calling:
|
|
// ed::RejectDeletedItem();
|
|
}
|
|
}
|
|
ed::EndDelete(); // Wrap up deletion action
|
|
|
|
|
|
|
|
// End of interaction with editor.
|
|
ed::End();
|
|
|
|
if (m_FirstFrame)
|
|
ed::NavigateToContent(0.0f);
|
|
|
|
ed::SetCurrentEditor(nullptr);
|
|
|
|
m_FirstFrame = false;
|
|
|
|
// ImGui::ShowMetricsWindow();
|
|
}
|
|
|
|
ed::EditorContext* m_Context = nullptr; // Editor context, required to trace a editor state.
|
|
bool m_FirstFrame = true; // Flag set for first frame only, some action need to be executed once.
|
|
ImVector<LinkInfo> m_Links; // List of live links. It is dynamic unless you want to create read-only view over nodes.
|
|
int m_NextLinkId = 100; // Counter to help generate link ids. In real application this will probably based on pointer to user data structure.
|
|
};
|
|
|
|
int Main(int argc, char** argv)
|
|
{
|
|
Example exampe("Basic Interaction", argc, argv);
|
|
|
|
if (exampe.Create())
|
|
return exampe.Run();
|
|
|
|
return 0;
|
|
} |