Skip to content

Instantly share code, notes, and snippets.

@ChemistAion
Created January 25, 2018 15:02
Show Gist options
  • Save ChemistAion/0b93553b06beac9fd3824cfeb989d50e to your computer and use it in GitHub Desktop.
Save ChemistAion/0b93553b06beac9fd3824cfeb989d50e to your computer and use it in GitHub Desktop.
Prototype of standalone node graph editor for ImGui
// Prototype of standalone node graph editor for ImGui
// Thread: https://github.com/ocornut/imgui/issues/306
//
// This is based on code by:
// @emoon https://gist.github.com/emoon/b8ff4b4ce4f1b43e79f2
// @ocornut https://gist.github.com/ocornut/7e9b3ec566a333d725d4
// @flix01 https://github.com/Flix01/imgui/blob/b248df2df98af13d4b7dbb70c92430afc47a038a/addons/imguinodegrapheditor/imguinodegrapheditor.cpp#L432
#include "Nodes.h"
namespace ImGui
{
Nodes::Nodes()
{
id_ = 0;
element_.Reset();
canvas_scale_ = 1.0f;
}
Nodes::~Nodes()
{
}
Nodes::Node* Nodes::GetHoverNode(ImVec2 offset, ImVec2 pos)
{
for (auto& node : nodes_)
{
ImRect rect((node->position_ * canvas_scale_) + offset, ((node->position_ + node->size_) * canvas_scale_) + offset);
rect.Expand(2.0f);
if (rect.Contains(pos))
{
return node->Get();
}
}
return nullptr;
}
void Nodes::RenderLines(ImDrawList* draw_list, ImVec2 offset)
{
for (auto& node : nodes_)
{
for (auto& connection : node->inputs_)
{
if (connection->connections_ == 0)
{
continue;
}
ImVec2 p1 = offset;
ImVec2 p4 = offset;
if (connection->target_->state_ > 0)
{ // we are connected to output of not a collapsed node
p1 += ((connection->target_->position_ + connection->input_->position_) * canvas_scale_);
}
else
{ // we are connected to output of a collapsed node
p1 += ((connection->target_->position_ + ImVec2(connection->target_->size_.x, connection->target_->size_.y / 2.0f)) * canvas_scale_);
}
if (node->state_ > 0)
{ // we are not a collapsed node
p4 += ((node->position_ + connection->position_) * canvas_scale_);
}
else
{ // we are a collapsed node
p4 += ((node->position_ + ImVec2(0.0f, node->size_.y / 2.0f)) * canvas_scale_);
}
// default bezier control points
ImVec2 p2 = p1 + (ImVec2(+50.0f, 0.0f) * canvas_scale_);
ImVec2 p3 = p4 + (ImVec2(-50.0f, 0.0f) * canvas_scale_);
if (element_.state_ == NodesState_Default)
{
const float distance_squared = GetSquaredDistanceToBezierCurve(ImGui::GetIO().MousePos, p1, p2, p3, p4);
if (distance_squared < (10.0f * 10.0f))
{
element_.Reset(NodesState_HoverConnection);
element_.rect_ = ImRect
(
(connection->target_->position_ + connection->input_->position_),
(node->position_ + connection->position_)
);
element_.node_ = node->Get();
element_.connection_ = connection.get();
}
}
bool selected = false;
selected |= element_.state_ == NodesState_SelectedConnection;
selected |= element_.state_ == NodesState_DragingConnection;
selected &= element_.connection_ == connection.get();
draw_list->AddBezierCurve(p1, p2, p3, p4, ImColor(0.5f, 0.5f, 0.5f, 1.0f), 2.0f * canvas_scale_);
if (selected)
{
draw_list->AddBezierCurve(p1, p2, p3, p4, ImColor(1.0f, 1.0f, 1.0f, 0.25f), 4.0f * canvas_scale_);
}
}
}
}
void Nodes::DisplayNodes(ImDrawList* drawList, ImVec2 offset)
{
ImGui::SetWindowFontScale(canvas_scale_);
for (auto& node : nodes_)
{
DisplayNode(drawList, offset, *node);
}
ImGui::SetWindowFontScale(1.0f);
}
Nodes::Node* Nodes::CreateNodeFromType(ImVec2 pos, const NodeType& type)
{
auto node = std::make_unique<Node>();
////////////////////////////////////////////////////////////////////////////////
node->id_ = -++id_;
node->name_ = type.name_ + std::to_string(id_).c_str();
node->position_ = pos;
{
auto &inputs = node->inputs_;
std::for_each
(
type.inputs_.begin(),
type.inputs_.end(),
[&inputs](auto& element)
{
auto connection = std::make_unique<Connection>();
connection->name_ = element.first;
connection->type_ = element.second;
inputs.push_back(std::move(connection));
}
);
auto &outputs = node->outputs_;
std::for_each
(
type.outputs_.begin(),
type.outputs_.end(),
[&outputs](auto& element)
{
auto connection = std::make_unique<Connection>();
connection->name_ = element.first;
connection->type_ = element.second;
outputs.push_back(std::move(connection));
}
);
}
////////////////////////////////////////////////////////////////////////////////
ImVec2 title_size = ImGui::CalcTextSize(node->name_.c_str());
const float vertical_padding = 1.5f;
////////////////////////////////////////////////////////////////////////////////
ImVec2 inputs_size;
for (auto& connection : node->inputs_)
{
ImVec2 name_size = ImGui::CalcTextSize(connection->name_.c_str());
inputs_size.x = ImMax(inputs_size.x, name_size.x);
inputs_size.y += name_size.y * vertical_padding;
}
ImVec2 outputs_size;
for (auto& connection : node->outputs_)
{
ImVec2 name_size = ImGui::CalcTextSize(connection->name_.c_str());
outputs_size.x = ImMax(outputs_size.x, name_size.x);
outputs_size.y += name_size.y * vertical_padding;
}
////////////////////////////////////////////////////////////////////////////////
node->size_.x = ImMax((inputs_size.x + outputs_size.x), title_size.x);
node->size_.x += title_size.y * 6.0f;
node->collapsed_height = (title_size.y * 2.0f);
node->full_height = (title_size.y * 3.0f) + ImMax(inputs_size.y, outputs_size.y);
node->size_.y = node->full_height;
node->position_ -= node->size_ / 2.0f;
////////////////////////////////////////////////////////////////////////////////
inputs_size = ImVec2(title_size.y * 0.75f, title_size.y * 2.5f);
for (auto& connection : node->inputs_)
{
const float half = ((ImGui::CalcTextSize(connection->name_.c_str()).y * vertical_padding) / 2.0f);
inputs_size.y += half;
connection->position_ = ImVec2(inputs_size.x, inputs_size.y);
inputs_size.y += half;
}
outputs_size = ImVec2(node->size_.x - (title_size.y * 0.75f), title_size.y * 2.5f);
for (auto& connection : node->outputs_)
{
const float half = ((ImGui::CalcTextSize(connection->name_.c_str()).y * vertical_padding) / 2.0f);
outputs_size.y += half;
connection->position_ = ImVec2(outputs_size.x, outputs_size.y);
outputs_size.y += half;
}
////////////////////////////////////////////////////////////////////////////////
nodes_.push_back(std::move(node));
return nodes_.back().get();
}
void Nodes::UpdateScroll()
{
////////////////////////////////////////////////////////////////////////////////
{
ImVec2 scroll;
if (ImGui::GetIO().KeyShift && !ImGui::GetIO().KeyCtrl && !ImGui::IsMouseDown(0) && !ImGui::IsMouseDown(1) && !ImGui::IsMouseDown(2))
{
scroll.x = ImGui::GetIO().MouseWheel * 24.0f;
}
if (!ImGui::GetIO().KeyShift && !ImGui::GetIO().KeyCtrl && !ImGui::IsMouseDown(0) && !ImGui::IsMouseDown(1) && !ImGui::IsMouseDown(2))
{
scroll.y = ImGui::GetIO().MouseWheel * 24.0f;
}
if (ImGui::IsMouseDragging(1, 6.0f) && !ImGui::IsMouseDown(0) && !ImGui::IsMouseDown(2))
{
scroll += ImGui::GetIO().MouseDelta;
}
canvas_scroll_ += scroll;
}
////////////////////////////////////////////////////////////////////////////////
{
ImVec2 mouse = canvas_mouse_;
float zoom = 0.0f;
if (!ImGui::GetIO().KeyShift && !ImGui::IsMouseDown(0) && !ImGui::IsMouseDown(1))
{
if (ImGui::GetIO().KeyCtrl)
{
zoom += ImGui::GetIO().MouseWheel;
}
if (ImGui::IsMouseDragging(2, 6.0f))
{
zoom -= ImGui::GetIO().MouseDelta.y;
mouse -= ImGui::GetMouseDragDelta(2, 6.0f);
}
}
ImVec2 focus = (mouse - canvas_scroll_) / canvas_scale_;
if (zoom < 0.0f)
{
canvas_scale_ /= 1.05f;
}
if (zoom > 0.0f)
{
canvas_scale_ *= 1.05f;
}
if (canvas_scale_ < 0.3f)
{
canvas_scale_ = 0.3f;
}
if (canvas_scale_ > 3.0f)
{
canvas_scale_ = 3.0f;
}
focus = canvas_scroll_ + (focus * canvas_scale_);
canvas_scroll_ += (mouse - focus);
}
////////////////////////////////////////////////////////////////////////////////
}
void Nodes::UpdateState(ImVec2 offset)
{
if (element_.state_ == NodesState_HoverNode && ImGui::IsMouseDoubleClicked(0))
{
if (element_.node_->state_ < 0)
{ // collapsed node goes to full
element_.node_->size_.y = element_.node_->full_height;
}
else
{ // full node goes to collapsed
element_.node_->size_.y = element_.node_->collapsed_height;
}
element_.node_->state_ = -element_.node_->state_;
}
switch (element_.state_)
{
case NodesState_Default:
{
if (ImGui::IsMouseDown(0) && !ImGui::IsMouseDown(1) && !ImGui::IsMouseDown(2))
{
ImRect canvas = ImRect(ImVec2(0.0f, 0.0f), canvas_size_);
if (!canvas.Contains(canvas_mouse_))
{
break;
}
element_.Reset(NodesState_SelectingEmpty);
element_.position_ = ImGui::GetIO().MousePos;
element_.rect_.Min = ImGui::GetIO().MousePos;
element_.rect_.Max = ImGui::GetIO().MousePos;
}
} break;
// helper: just block all states till next update
case NodesState_Block:
{
element_.Reset();
} break;
case NodesState_HoverConnection:
{
const float distance_squared = GetSquaredDistanceToBezierCurve
(
ImGui::GetIO().MousePos,
offset + (element_.rect_.Min * canvas_scale_),
offset + (element_.rect_.Min * canvas_scale_) + (ImVec2(+50.0f, 0.0f) * canvas_scale_),
offset + (element_.rect_.Max * canvas_scale_) + (ImVec2(-50.0f, 0.0f) * canvas_scale_),
offset + (element_.rect_.Max * canvas_scale_)
);
if (distance_squared > (10.0f * 10.0f))
{
element_.Reset();
break;
}
if (ImGui::IsMouseDown(0))
{
element_.state_ = NodesState_SelectedConnection;
}
} break;
case NodesState_DragingInput:
{
if (!ImGui::IsMouseDown(0) || ImGui::IsMouseClicked(1))
{
element_.Reset(NodesState_Block);
break;
}
ImVec2 p1 = offset + (element_.position_ * canvas_scale_);
ImVec2 p2 = p1 + (ImVec2(-50.0f, 0.0f) * canvas_scale_);
ImVec2 p3 = ImGui::GetIO().MousePos + (ImVec2(+50.0f, 0.0f) * canvas_scale_);
ImVec2 p4 = ImGui::GetIO().MousePos;
ImGui::GetWindowDrawList()->AddBezierCurve(p1, p2, p3, p4, ImColor(0.0f, 1.0f, 0.0f, 1.0f), 2.0f * canvas_scale_);
} break;
case NodesState_DragingInputValid:
{
element_.state_ = NodesState_DragingInput;
if (ImGui::IsMouseClicked(1))
{
element_.Reset(NodesState_Block);
break;
}
ImVec2 p1 = offset + (element_.position_ * canvas_scale_);
ImVec2 p2 = p1 + (ImVec2(-50.0f, 0.0f) * canvas_scale_);
ImVec2 p3 = ImGui::GetIO().MousePos + (ImVec2(+50.0f, 0.0f) * canvas_scale_);
ImVec2 p4 = ImGui::GetIO().MousePos;
ImGui::GetWindowDrawList()->AddBezierCurve(p1, p2, p3, p4, ImColor(0.0f, 1.0f, 0.0f, 1.0f), 2.0f * canvas_scale_);
} break;
case NodesState_DragingOutput:
{
if (!ImGui::IsMouseDown(0) || ImGui::IsMouseClicked(1))
{
element_.Reset(NodesState_Block);
break;
}
ImVec2 p1 = offset + (element_.position_ * canvas_scale_);
ImVec2 p2 = p1 + (ImVec2(+50.0f, 0.0f) * canvas_scale_);
ImVec2 p3 = ImGui::GetIO().MousePos + (ImVec2(-50.0f, 0.0f) * canvas_scale_);
ImVec2 p4 = ImGui::GetIO().MousePos;
ImGui::GetWindowDrawList()->AddBezierCurve(p1, p2, p3, p4, ImColor(0.0f, 1.0f, 0.0f, 1.0f), 2.0f * canvas_scale_);
} break;
case NodesState_DragingOutputValid:
{
element_.state_ = NodesState_DragingOutput;
if (ImGui::IsMouseClicked(1))
{
element_.Reset(NodesState_Block);
break;
}
ImVec2 p1 = offset + (element_.position_ * canvas_scale_);
ImVec2 p2 = p1 + (ImVec2(+50.0f, 0.0f) * canvas_scale_);
ImVec2 p3 = ImGui::GetIO().MousePos + (ImVec2(-50.0f, 0.0f) * canvas_scale_);
ImVec2 p4 = ImGui::GetIO().MousePos;
ImGui::GetWindowDrawList()->AddBezierCurve(p1, p2, p3, p4, ImColor(0.0f, 1.0f, 0.0f, 1.0f), 2.0f * canvas_scale_);
} break;
case NodesState_SelectingEmpty:
{
if (!ImGui::IsMouseDown(0))
{
element_.Reset(NodesState_Block);
break;
}
element_.rect_.Min = ImMin(element_.position_, ImGui::GetIO().MousePos);
element_.rect_.Max = ImMax(element_.position_, ImGui::GetIO().MousePos);
} break;
case NodesState_SelectingValid:
{
if (!ImGui::IsMouseDown(0))
{
element_.Reset(NodesState_Selected);
break;
}
element_.rect_.Min = ImMin(element_.position_, ImGui::GetIO().MousePos);
element_.rect_.Max = ImMax(element_.position_, ImGui::GetIO().MousePos);
element_.state_ = NodesState_SelectingEmpty;
} break;
case NodesState_SelectingMore:
{
element_.rect_.Min = ImMin(element_.position_, ImGui::GetIO().MousePos);
element_.rect_.Max = ImMax(element_.position_, ImGui::GetIO().MousePos);
if (ImGui::IsMouseDown(0) && ImGui::GetIO().KeyShift)
{
break;
}
for (auto& node : nodes_)
{
ImVec2 node_rect_min = offset + (node->position_ * canvas_scale_);
ImVec2 node_rect_max = node_rect_min + (node->size_ * canvas_scale_);
ImRect node_rect(node_rect_min, node_rect_max);
if (ImGui::GetIO().KeyCtrl && element_.rect_.Overlaps(node_rect))
{
node->id_ = -abs(node->id_); // add "selected" flag
continue;
}
if (!ImGui::GetIO().KeyCtrl && element_.rect_.Contains(node_rect))
{
node->id_ = -abs(node->id_); // add "selected" flag
continue;
}
}
element_.Reset(NodesState_Selected);
} break;
case NodesState_Selected:
{
// delete all selected nodes
if (ImGui::IsKeyPressed(ImGui::GetIO().KeyMap[ImGuiKey_Delete]))
{
std::vector<std::unique_ptr<Node>> replacement;
replacement.reserve(nodes_.size());
// delete connections
for (auto& node : nodes_)
{
for (auto& connection : node->inputs_)
{
if (connection->connections_ == 0)
{
continue;
}
if (node->id_ < 0)
{
connection->input_->connections_--;
}
if (connection->target_->id_ <0)
{
connection->target_ = nullptr;
connection->input_ = nullptr;
connection->connections_ = 0;
}
}
}
// save not selected nodes
for (auto& node : nodes_)
{
if (node->id_ > 0)
{
replacement.push_back(std::move(node));
}
}
nodes_ = std::move(replacement);
element_.Reset();
break;
}
// no action taken
if (!ImGui::IsMouseClicked(0))
{
break;
}
element_.Reset();
auto hovered = GetHoverNode(offset, ImGui::GetIO().MousePos);
// empty area under the mouse
if (!hovered)
{
element_.position_ = ImGui::GetIO().MousePos;
element_.rect_.Min = ImGui::GetIO().MousePos;
element_.rect_.Max = ImGui::GetIO().MousePos;
if (ImGui::GetIO().KeyShift)
{
element_.state_ = NodesState_SelectingMore;
}
else
{
element_.state_ = NodesState_SelectingEmpty;
}
break;
}
// lets select node under the mouse
if (ImGui::GetIO().KeyShift)
{
hovered->id_ = -abs(hovered->id_);
element_.state_ = NodesState_DragingSelected;
break;
}
// lets toggle selection of a node under the mouse
if (!ImGui::GetIO().KeyShift && ImGui::GetIO().KeyCtrl)
{
if (hovered->id_ > 0)
{
hovered->id_ = -abs(hovered->id_);
element_.state_ = NodesState_DragingSelected;
}
else
{
hovered->id_ = abs(hovered->id_);
element_.state_ = NodesState_Selected;
}
break;
}
// lets start dragging
if (hovered->id_ < 0)
{
element_.state_ = NodesState_DragingSelected;
break;
}
// not selected node clicked, lets jump selection to it
for (auto& node : nodes_)
{
if (node.get() != hovered)
{
node->id_ = abs(node->id_);
}
}
} break;
case NodesState_DragingSelected:
{
if (!ImGui::IsMouseDown(0))
{
if (element_.node_)
{
if (ImGui::GetIO().KeyShift || ImGui::GetIO().KeyCtrl)
{
element_.Reset(NodesState_Selected);
break;
}
element_.state_ = NodesState_HoverNode;
}
else
{
element_.Reset(NodesState_Selected);
}
}
else
{
for (auto& node : nodes_)
{
if (node->id_ < 0)
{
node->position_ += ImGui::GetIO().MouseDelta / canvas_scale_;
}
}
}
} break;
case NodesState_SelectedConnection:
{
if (ImGui::IsMouseClicked(1))
{
element_.Reset(NodesState_Block);
break;
}
if (ImGui::IsMouseDown(0))
{
const float distance_squared = GetSquaredDistanceToBezierCurve
(
ImGui::GetIO().MousePos,
offset + (element_.rect_.Min * canvas_scale_),
offset + (element_.rect_.Min * canvas_scale_) + (ImVec2(+50.0f, 0.0f) * canvas_scale_),
offset + (element_.rect_.Max * canvas_scale_) + (ImVec2(-50.0f, 0.0f) * canvas_scale_),
offset + (element_.rect_.Max * canvas_scale_)
);
if (distance_squared > (10.0f * 10.0f))
{
element_.Reset();
break;
}
element_.state_ = NodesState_DragingConnection;
}
} break;
case NodesState_DragingConnection:
{
if (!ImGui::IsMouseDown(0))
{
element_.state_ = NodesState_SelectedConnection;
break;
}
if (ImGui::IsMouseClicked(1))
{
element_.Reset(NodesState_Block);
break;
}
element_.node_->position_ += ImGui::GetIO().MouseDelta / canvas_scale_;
element_.connection_->target_->position_ += ImGui::GetIO().MouseDelta / canvas_scale_;
} break;
}
}
void Nodes::DisplayNode(ImDrawList* drawList, ImVec2 offset, Node& node)
{
ImGui::PushID(abs(node.id_));
ImGui::BeginGroup();
ImVec2 node_rect_min = offset + (node.position_ * canvas_scale_);
ImVec2 node_rect_max = node_rect_min + (node.size_ * canvas_scale_);
ImGui::SetCursorScreenPos(node_rect_min);
ImGui::InvisibleButton("Node", node.size_ * canvas_scale_);
////////////////////////////////////////////////////////////////////////////////
// state machine for node hover/drag
{
bool node_hovered = ImGui::IsItemHovered();
bool node_active = ImGui::IsItemActive();
if (node_hovered && element_.state_ == NodesState_HoverNode)
{
element_.node_ = node.Get();
if (node_active)
{
node.id_ = -abs(node.id_); // add "selected" flag
element_.state_ = NodesState_DragingSelected;
}
}
if (node_hovered && element_.state_ == NodesState_Default)
{
element_.node_ = node.Get();
if (node_active)
{
node.id_ = -abs(node.id_); // add "selected" flag
element_.state_ = NodesState_DragingSelected;
}
else
{
element_.state_ = NodesState_HoverNode;
}
}
if (!node_hovered && element_.state_ == NodesState_HoverNode)
{
if (element_.node_ == node.Get())
{
element_.Reset();
}
}
}
////////////////////////////////////////////////////////////////////////////////
bool consider_hover = element_.node_ ? element_.node_ == node.Get() : false;
////////////////////////////////////////////////////////////////////////////////
if (element_.state_ != NodesState_Selected && element_.state_ != NodesState_DragingSelected && element_.state_ != NodesState_SelectingMore)
{
node.id_ = abs(node.id_); // remove "selected" flag
}
////////////////////////////////////////////////////////////////////////////////
bool consider_select = false;
consider_select |= element_.state_ == NodesState_SelectingEmpty;
consider_select |= element_.state_ == NodesState_SelectingValid;
consider_select |= element_.state_ == NodesState_SelectingMore;
if (consider_select)
{
bool select_it = false;
ImRect node_rect(node_rect_min, node_rect_max);
if (ImGui::GetIO().KeyCtrl)
{
select_it |= element_.rect_.Overlaps(node_rect);
}
else
{
select_it |= element_.rect_.Contains(node_rect);
}
consider_hover |= select_it;
if (select_it && element_.state_ != NodesState_SelectingMore)
{
node.id_ = -abs(node.id_); // add "selected" flag
element_.state_ = NodesState_SelectingValid;
}
}
////////////////////////////////////////////////////////////////////////////////
ImVec2 title_name_size = ImGui::CalcTextSize(node.name_.c_str());
const float corner = title_name_size.y / 2.0f;
{
ImVec2 title_area;
title_area.x = node_rect_max.x;
title_area.y = node_rect_min.y + (title_name_size.y * 2.0f);
ImVec2 title_pos;
title_pos.x = node_rect_min.x + ((title_area.x - node_rect_min.x) / 2.0f) - (title_name_size.x / 2.0f);
if (node.state_ > 0)
{
drawList->AddRectFilled(node_rect_min, node_rect_max, ImColor(0.25f, 0.25f, 0.25f, 0.9f), corner, ImDrawCornerFlags_All);
drawList->AddRectFilled(node_rect_min, title_area, ImColor(0.25f, 0.0f, 0.125f, 0.9f), corner, ImDrawCornerFlags_Top);
title_pos.y = node_rect_min.y + ((title_name_size.y * 2.0f) / 2.0f) - (title_name_size.y / 2.0f);
}
else
{
drawList->AddRectFilled(node_rect_min, node_rect_max, ImColor(0.25f, 0.0f, 0.125f, 0.9f), corner, ImDrawCornerFlags_All);
title_pos.y = node_rect_min.y + ((node_rect_max.y - node_rect_min.y) / 2.0f) - (title_name_size.y / 2.0f);
}
ImGui::SetCursorScreenPos(title_pos);
ImGui::Text("%s", node.name_.c_str());
}
////////////////////////////////////////////////////////////////////////////////
if (node.state_ > 0)
{
////////////////////////////////////////////////////////////////////////////////
for (auto& connection : node.inputs_)
{
if (connection->type_ == ConnectionType_None)
{
continue;
}
bool consider_io = false;
ImVec2 input_name_size = ImGui::CalcTextSize(connection->name_.c_str());
ImVec2 connection_pos = node_rect_min + (connection->position_ * canvas_scale_);
{
ImVec2 pos = connection_pos;
pos += ImVec2(input_name_size.y * 0.75f, -input_name_size.y / 2.0f);
ImGui::SetCursorScreenPos(pos);
ImGui::Text("%s", connection->name_.c_str());
}
if (IsConnectorHovered(connection_pos, (input_name_size.y / 2.0f)))
{
consider_io |= element_.state_ == NodesState_Default;
consider_io |= element_.state_ == NodesState_HoverConnection;
consider_io |= element_.state_ == NodesState_HoverNode;
// from nothing to hovered input
if (consider_io)
{
element_.Reset(NodesState_HoverIO);
element_.node_ = node.Get();
element_.connection_ = connection.get();
element_.position_ = node.position_ + connection->position_;
}
// we could start draging input now
if (ImGui::IsMouseClicked(0) && element_.connection_ == connection.get())
{
element_.state_ = NodesState_DragingInput;
// remove connection from this input
if (connection->input_)
{
connection->input_->connections_--;
}
connection->target_ = nullptr;
connection->input_ = nullptr;
connection->connections_ = 0;
}
consider_io = true;
}
else if (element_.state_ == NodesState_HoverIO && element_.connection_ == connection.get())
{
element_.Reset(); // we are not hovering this last hovered input anymore
}
////////////////////////////////////////////////////////////////////////////////
ImColor color = ImColor(0.5f, 0.5f, 0.5f, 1.0f);
if (connection->connections_ > 0)
{
drawList->AddCircleFilled(connection_pos, (input_name_size.y / 3.0f), color);
}
// currently we are dragin some output, check if there is a possibilty to connect here (this input)
if (element_.state_ == NodesState_DragingOutput || element_.state_ == NodesState_DragingOutputValid)
{
// check is draging output are not from the same node
if (element_.node_ != node.Get() && element_.connection_->type_ == connection->type_)
{
color = ImColor(0.0f, 1.0f, 0.0f, 1.0f);
if (consider_io)
{
element_.state_ = NodesState_DragingOutputValid;
drawList->AddCircleFilled(connection_pos, (input_name_size.y / 3.0f), color);
if (!ImGui::IsMouseDown(0))
{
if (connection->input_)
{
connection->input_->connections_--;
}
connection->target_ = element_.node_;
connection->input_ = element_.connection_;
connection->connections_ = 1;
element_.connection_->connections_++;
element_.Reset(NodesState_HoverIO);
element_.node_ = node.Get();
element_.connection_ = connection.get();
element_.position_ = node_rect_min + connection->position_;
}
}
}
}
consider_io |= element_.state_ == NodesState_HoverIO;
consider_io |= element_.state_ == NodesState_DragingInput;
consider_io |= element_.state_ == NodesState_DragingInputValid;
consider_io &= element_.connection_ == connection.get();
if (consider_io)
{
color = ImColor(0.0f, 1.0f, 0.0f, 1.0f);
if (element_.state_ != NodesState_HoverIO)
{
drawList->AddCircleFilled(connection_pos, (input_name_size.y / 3.0f), color);
}
}
drawList->AddCircle(connection_pos, (input_name_size.y / 3.0f), color, ((int)(6.0f * canvas_scale_) + 10), (1.5f * canvas_scale_));
}
////////////////////////////////////////////////////////////////////////////////
for (auto& connection : node.outputs_)
{
if (connection->type_ == ConnectionType_None)
{
continue;
}
bool consider_io = false;
ImVec2 output_name_size = ImGui::CalcTextSize(connection->name_.c_str());
ImVec2 connection_pos = node_rect_min + (connection->position_ * canvas_scale_);
{
ImVec2 pos = connection_pos;
pos += ImVec2(-output_name_size.x - (output_name_size.y * 0.75f), -output_name_size.y / 2.0f);
ImGui::SetCursorScreenPos(pos);
ImGui::Text("%s", connection->name_.c_str());
}
if (IsConnectorHovered(connection_pos, (output_name_size.y / 2.0f)))
{
consider_io |= element_.state_ == NodesState_Default;
consider_io |= element_.state_ == NodesState_HoverConnection;
consider_io |= element_.state_ == NodesState_HoverNode;
// from nothing to hovered output
if (consider_io)
{
element_.Reset(NodesState_HoverIO);
element_.node_ = node.Get();
element_.connection_ = connection.get();
element_.position_ = node.position_ + connection->position_;
}
// we could start draging output now
if (ImGui::IsMouseClicked(0) && element_.connection_ == connection.get())
{
element_.state_ = NodesState_DragingOutput;
}
consider_io = true;
}
else if (element_.state_ == NodesState_HoverIO && element_.connection_ == connection.get())
{
element_.Reset(); // we are not hovering this last hovered output anymore
}
////////////////////////////////////////////////////////////////////////////////
ImColor color = ImColor(0.5f, 0.5f, 0.5f, 1.0f);
if (connection->connections_ > 0)
{
drawList->AddCircleFilled(connection_pos, (output_name_size.y / 3.0f), ImColor(0.5f, 0.5f, 0.5f, 1.0f));
}
// currently we are dragin some input, check if there is a possibilty to connect here (this output)
if (element_.state_ == NodesState_DragingInput || element_.state_ == NodesState_DragingInputValid)
{
// check is draging input are not from the same node
if (element_.node_ != node.Get() && element_.connection_->type_ == connection->type_)
{
color = ImColor(0.0f, 1.0f, 0.0f, 1.0f);
if (consider_io)
{
element_.state_ = NodesState_DragingInputValid;
drawList->AddCircleFilled(connection_pos, (output_name_size.y / 3.0f), color);
if (!ImGui::IsMouseDown(0))
{
element_.connection_->target_ = node.Get();
element_.connection_->input_ = connection.get();
element_.connection_->connections_ = 1;
connection->connections_++;
element_.Reset(NodesState_HoverIO);
element_.node_ = node.Get();
element_.connection_ = connection.get();
element_.position_ = node_rect_min + connection->position_;
}
}
}
}
consider_io |= element_.state_ == NodesState_HoverIO;
consider_io |= element_.state_ == NodesState_DragingOutput;
consider_io |= element_.state_ == NodesState_DragingOutputValid;
consider_io &= element_.connection_ == connection.get();
if (consider_io)
{
color = ImColor(0.0f, 1.0f, 0.0f, 1.0f);
if (element_.state_ != NodesState_HoverIO)
{
drawList->AddCircleFilled(connection_pos, (output_name_size.y / 3.0f), color);
}
}
drawList->AddCircle(connection_pos, (output_name_size.y / 3.0f), color, ((int)(6.0f * canvas_scale_) + 10), (1.5f * canvas_scale_));
}
////////////////////////////////////////////////////////////////////////////////
}
////////////////////////////////////////////////////////////////////////////////
if ((consider_select && consider_hover) || (node.id_ < 0))
{
drawList->AddRectFilled(node_rect_min, node_rect_max, ImColor(1.0f, 1.0f, 1.0f, 0.25f), corner, ImDrawCornerFlags_All);
}
ImGui::EndGroup();
ImGui::PopID();
}
void Nodes::ProcessNodes()
{
////////////////////////////////////////////////////////////////////////////////
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.2f, 0.2f, 0.2f, 1.0f));
ImGui::BeginChild("NodesScrollingRegion", ImVec2(0.0f, 0.0f), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoMove);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
////////////////////////////////////////////////////////////////////////////////
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows))
{
canvas_mouse_ = ImGui::GetIO().MousePos - ImGui::GetCursorScreenPos();
canvas_position_ = ImGui::GetCursorScreenPos();
canvas_size_ = ImGui::GetWindowSize();
UpdateScroll();
}
////////////////////////////////////////////////////////////////////////////////
{
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImU32 color = ImColor(0.5f, 0.5f, 0.5f, 0.1f);
const float size = 64.0f * canvas_scale_;
for (float x = fmodf(canvas_scroll_.x, size); x < canvas_size_.x; x += size)
{
draw_list->AddLine(ImVec2(x, 0.0f) + canvas_position_, ImVec2(x, canvas_size_.y) + canvas_position_, color);
}
for (float y = fmodf(canvas_scroll_.y, size); y < canvas_size_.y; y += size)
{
draw_list->AddLine(ImVec2(0.0f, y) + canvas_position_, ImVec2(canvas_size_.x, y) + canvas_position_, color);
}
}
////////////////////////////////////////////////////////////////////////////////
ImVec2 offset = canvas_position_ + canvas_scroll_;
UpdateState(offset);
RenderLines(draw_list, offset);
DisplayNodes(draw_list, offset);
if (element_.state_ == NodesState_SelectingEmpty || element_.state_ == NodesState_SelectingValid || element_.state_ == NodesState_SelectingMore)
{
draw_list->AddRectFilled(element_.rect_.Min, element_.rect_.Max, ImColor(200.0f, 200.0f, 0.0f, 0.1f));
draw_list->AddRect(element_.rect_.Min, element_.rect_.Max, ImColor(200.0f, 200.0f, 0.0f, 0.5f));
}
////////////////////////////////////////////////////////////////////////////////
{
ImGui::SetCursorScreenPos(canvas_position_);
bool consider_menu = !ImGui::IsAnyItemHovered();
consider_menu &= ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
consider_menu &= element_.state_ == NodesState_Default || element_.state_ == NodesState_Selected;
consider_menu &= ImGui::IsMouseReleased(1);
if (consider_menu)
{
ImGuiContext* context = ImGui::GetCurrentContext();
if (context->IO.MouseDragMaxDistanceSqr[1] < 36.0f)
{
ImGui::OpenPopup("NodesContextMenu");
}
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
if (ImGui::BeginPopup("NodesContextMenu"))
{
element_.Reset(NodesState_Block);
for (auto& node : nodes_types_)
{
if (ImGui::MenuItem(node.name_.c_str()))
{
element_.Reset();
element_.node_ = CreateNodeFromType((canvas_mouse_ - canvas_scroll_) / canvas_scale_, node);
}
}
ImGui::EndPopup();
}
ImGui::PopStyleVar();
}
////////////////////////////////////////////////////////////////////////////////
// {
// ImGui::SetCursorScreenPos(canvas_position_);
//
// switch (element_.state_)
// {
// case NodesState_Default: ImGui::Text("NodesState_Default"); break;
// case NodesState_Block: ImGui::Text("NodesState_Block"); break;
// case NodesState_HoverIO: ImGui::Text("NodesState_HoverIO"); break;
// case NodesState_HoverConnection: ImGui::Text("NodesState_HoverConnection"); break;
// case NodesState_HoverNode: ImGui::Text("NodesState_HoverNode"); break;
// case NodesState_DragingInput: ImGui::Text("NodesState_DragingInput"); break;
// case NodesState_DragingInputValid: ImGui::Text("NodesState_DragingInputValid"); break;
// case NodesState_DragingOutput: ImGui::Text("NodesState_DragingOutput"); break;
// case NodesState_DragingOutputValid: ImGui::Text("NodesState_DragingOutputValid"); break;
// case NodesState_DragingConnection: ImGui::Text("NodesState_DragingConnection"); break;
// case NodesState_DragingSelected: ImGui::Text("NodesState_DragingSelected"); break;
// case NodesState_SelectingEmpty: ImGui::Text("NodesState_SelectingEmpty"); break;
// case NodesState_SelectingValid: ImGui::Text("NodesState_SelectingValid"); break;
// case NodesState_SelectingMore: ImGui::Text("NodesState_SelectingMore"); break;
// case NodesState_Selected: ImGui::Text("NodesState_Selected"); break;
// case NodesState_SelectedConnection: ImGui::Text("NodesState_SelectedConnection"); break;
// default: ImGui::Text("UNKNOWN"); break;
// }
//
// ImGui::Text("");
//
// ImGui::Text("Mouse: %.2f, %.2f", ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y);
// ImGui::Text("Mouse delta: %.2f, %.2f", ImGui::GetIO().MouseDelta.x, ImGui::GetIO().MouseDelta.y);
// ImGui::Text("Offset: %.2f, %.2f", offset.x, offset.y);
//
// ImGui::Text("");
//
// ImGui::Text("Canvas_mouse: %.2f, %.2f", canvas_mouse_.x, canvas_mouse_.y);
// ImGui::Text("Canvas_position: %.2f, %.2f", canvas_position_.x, canvas_position_.y);
// ImGui::Text("Canvas_size: %.2f, %.2f", canvas_size_.x, canvas_size_.y);
// ImGui::Text("Canvas_scroll: %.2f, %.2f", canvas_scroll_.x, canvas_scroll_.y);
// ImGui::Text("Canvas_scale: %.2f", canvas_scale_);
// }
////////////////////////////////////////////////////////////////////////////////
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::PopStyleVar(2);
}
}
// Prototype of standalone node graph editor for ImGui
// Thread: https://github.com/ocornut/imgui/issues/306
//
// This is based on code by:
// @emoon https://gist.github.com/emoon/b8ff4b4ce4f1b43e79f2
// @ocornut https://gist.github.com/ocornut/7e9b3ec566a333d725d4
// @flix01 https://github.com/Flix01/imgui/blob/b248df2df98af13d4b7dbb70c92430afc47a038a/addons/imguinodegrapheditor/imguinodegrapheditor.cpp#L432
#pragma once
#define IMGUI_DEFINE_MATH_OPERATORS
#include "imgui.h"
#include "imgui_internal.h"
#include <memory>
#include <string>
#include <vector>
#include <algorithm>
namespace ImGui
{
enum ConnectionType : uint32_t
{
ConnectionType_None = 0,
ConnectionType_Vec3,
ConnectionType_Float,
ConnectionType_Int,
};
////////////////////////////////////////////////////////////////////////////////
struct NodeType
{
std::string name_;
std::vector<std::pair<std::string, ConnectionType>> inputs_;
std::vector<std::pair<std::string, ConnectionType>> outputs_;
};
template<int n>
struct BezierWeights
{
constexpr BezierWeights() : x_(), y_(), z_(), w_()
{
for (int i = 1; i <= n; ++i)
{
float t = (float)i / (float)(n + 1);
float u = 1.0f - t;
x_[i - 1] = u * u * u;
y_[i - 1] = 3 * u * u * t;
z_[i - 1] = 3 * u * t * t;
w_[i - 1] = t * t * t;
}
}
float x_[n];
float y_[n];
float z_[n];
float w_[n];
};
static constexpr auto bezier_weights_ = BezierWeights<16>();
////////////////////////////////////////////////////////////////////////////////
static const std::vector<ImGui::NodeType> nodes_types_=
{
////////////////////////////////////////////////////////////////////////////////
{
{ std::string("Test") },
{
{ std::string("FloatTEST"), ImGui::ConnectionType_Float },
{ std::string("Int"), ImGui::ConnectionType_Int }
},
{
{ std::string("Float"), ImGui::ConnectionType_Float }
}
},
////////////////////////////////////////////////////////////////////////////////
{
{ std::string("BigInput") },
{
{ std::string("Float1"), ImGui::ConnectionType_Float },
{ std::string("Float2"), ImGui::ConnectionType_Float },
{ std::string("Int1"), ImGui::ConnectionType_Int },
{ std::string("Int2"), ImGui::ConnectionType_Int },
{ {}, ImGui::ConnectionType_None },
{ std::string("Float3"), ImGui::ConnectionType_Float },
{ std::string("Float4"), ImGui::ConnectionType_Float },
{ std::string("Float5"), ImGui::ConnectionType_Float }
},
{
{ std::string("Float1"), ImGui::ConnectionType_Float },
{ std::string("Int1"), ImGui::ConnectionType_Int },
{ {}, ImGui::ConnectionType_None },
{ std::string("Vec1"), ImGui::ConnectionType_Vec3 }
}
},
////////////////////////////////////////////////////////////////////////////////
{
{ std::string("BigOutput") },
{
{ std::string("VecTEST"), ImGui::ConnectionType_Vec3 },
{ {}, ImGui::ConnectionType_None },
{ std::string("Int"), ImGui::ConnectionType_Int },
{ std::string("Float"), ImGui::ConnectionType_Float }
},
{
{ std::string("FloatTEST"), ImGui::ConnectionType_Float },
{ std::string("Float"), ImGui::ConnectionType_Float },
{ {}, ImGui::ConnectionType_None },
{ std::string("Int1"), ImGui::ConnectionType_Int },
{ std::string("Int2"), ImGui::ConnectionType_Int },
{ {}, ImGui::ConnectionType_None },
{ std::string("VecABC"), ImGui::ConnectionType_Vec3 },
{ std::string("VecXYZ"), ImGui::ConnectionType_Vec3 }
}
}
////////////////////////////////////////////////////////////////////////////////
};
////////////////////////////////////////////////////////////////////////////////
class Nodes final
{
private:
////////////////////////////////////////////////////////////////////////////////
struct Node;
struct Connection
{
ImVec2 position_;
std::string name_;
ConnectionType type_;
Node* target_;
Connection* input_;
uint32_t connections_;
Connection()
{
position_ = ImVec2(0.0f, 0.0f);
type_ = ConnectionType_None;
target_ = nullptr;
input_ = nullptr;
connections_ = 0;
}
Connection* Get()
{
return this;
}
};
////////////////////////////////////////////////////////////////////////////////
enum NodeStateFlag : int32_t
{
NodeStateFlag_Default = 1,
};
struct Node
{
int32_t id_; // 0 = empty, positive/negative = not selected/selected
int32_t state_;
ImVec2 position_;
ImVec2 size_;
float collapsed_height;
float full_height;
std::string name_;
std::vector<std::unique_ptr<Connection>> inputs_;
std::vector<std::unique_ptr<Connection>> outputs_;
Node()
{
id_ = 0;
state_ = NodeStateFlag_Default;
position_ = ImVec2(0.0f, 0.0f);
size_ = ImVec2(0.0f, 0.0f);
collapsed_height = 0.0f;
full_height = 0.0f;
}
Node* Get()
{
return this;
}
};
////////////////////////////////////////////////////////////////////////////////
enum NodesState : uint32_t
{
NodesState_Default = 0,
NodesState_Block, // helper: just block all states till next update (frame)
NodesState_HoverIO,
NodesState_HoverConnection,
NodesState_HoverNode,
NodesState_DragingInput,
NodesState_DragingInputValid,
NodesState_DragingOutput,
NodesState_DragingOutputValid,
NodesState_DragingConnection,
NodesState_DragingSelected,
NodesState_SelectingEmpty,
NodesState_SelectingValid,
NodesState_SelectingMore,
NodesState_Selected,
NodesState_SelectedConnection
};
struct NodesElement
{
NodesState state_;
ImVec2 position_;
ImRect rect_;
Node* node_;
Connection* connection_;
void Reset(NodesState state = NodesState_Default)
{
state_ = state;
position_ = ImVec2(0.0f, 0.0f);
rect_ = ImRect(0.0f, 0.0f, 0.0f, 0.0f);
node_ = nullptr;
connection_ = nullptr;
}
};
////////////////////////////////////////////////////////////////////////////////
std::vector<std::unique_ptr<Node>> nodes_;
int32_t id_;
NodesElement element_;
ImVec2 canvas_mouse_;
ImVec2 canvas_position_;
ImVec2 canvas_size_;
ImVec2 canvas_scroll_;
float canvas_scale_;
////////////////////////////////////////////////////////////////////////////////
float ImVec2Dot(const ImVec2& S1, const ImVec2& S2)
{
return (S1.x * S2.x + S1.y * S2.y);
}
float GetSquaredDistancePointSegment(const ImVec2& P, const ImVec2& S1, const ImVec2& S2)
{
const float l2 = (S1.x - S2.x) * (S1.x - S2.x) + (S1.y - S2.y) * (S1.y - S2.y);
if (l2 < 1.0f)
{
return (P.x - S2.x) * (P.x - S2.x) + (P.y - S2.y) * (P.y - S2.y);
}
ImVec2 PS1(P.x - S1.x, P.y - S1.y);
ImVec2 T(S2.x - S1.x, S2.y - S2.y);
const float tf = ImVec2Dot(PS1, T) / l2;
const float minTf = 1.0f < tf ? 1.0f : tf;
const float t = 0.0f > minTf ? 0.0f : minTf;
T.x = S1.x + T.x * t;
T.y = S1.y + T.y * t;
return (P.x - T.x) * (P.x - T.x) + (P.y - T.y) * (P.y - T.y);
}
float GetSquaredDistanceToBezierCurve(const ImVec2& point, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4)
{
float minSquaredDistance = FLT_MAX;
float tmp;
ImVec2 L = p1;
ImVec2 temp;
for (int i = 1; i < 16 - 1; ++i)
{
const ImVec4& W = ImVec4(bezier_weights_.x_[i], bezier_weights_.y_[i], bezier_weights_.z_[i], bezier_weights_.w_[i]);
temp.x = W.x * p1.x + W.y * p2.x + W.z * p3.x + W.w * p4.x;
temp.y = W.x * p1.y + W.y * p2.y + W.z * p3.y + W.w * p4.y;
tmp = GetSquaredDistancePointSegment(point, L, temp);
if (minSquaredDistance > tmp)
{
minSquaredDistance = tmp;
}
L = temp;
}
tmp = GetSquaredDistancePointSegment(point, L, p4);
if (minSquaredDistance > tmp)
{
minSquaredDistance = tmp;
}
return minSquaredDistance;
}
////////////////////////////////////////////////////////////////////////////////
bool IsConnectorHovered(ImVec2 connection, float radius)
{
ImVec2 delta = ImGui::GetIO().MousePos - connection;
return ((delta.x * delta.x) + (delta.y * delta.y)) < (radius * radius);
}
Node* GetHoverNode(ImVec2 offset, ImVec2 pos);
void DisplayNode(ImDrawList* drawList, ImVec2 offset, Node& node);
////////////////////////////////////////////////////////////////////////////////
void UpdateScroll();
void UpdateState(ImVec2 offset);
void RenderLines(ImDrawList* draw_list, ImVec2 offset);
void DisplayNodes(ImDrawList* drawList, ImVec2 offset);
////////////////////////////////////////////////////////////////////////////////
Nodes::Node* CreateNodeFromType(ImVec2 pos, const NodeType& type);
public:
explicit Nodes();
~Nodes();
void ProcessNodes();
};
}
@moebiussurfing
Copy link

moebiussurfing commented May 15, 2020

hey @ChemiAion, thanks for sharing.
I am almost made work your code into an openFrameworks project but I have a little question...
How can I know what 'node and pins' are being connected by a wire/link?
sphaero/ofNodeEditor#1 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment