Initial graph editor and graph instantiation from file.

This commit is contained in:
Martin Felis
2022-02-11 16:51:18 +01:00
parent fd4785afcd
commit c5b0d1f976
48 changed files with 87227 additions and 41 deletions
+710
View File
@@ -0,0 +1,710 @@
#include "node_editor.h"
#include "graph.h"
#include <imnodes.h>
#include <imgui.h>
#include <SDL_keycode.h>
#include <SDL_timer.h>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cmath>
#include <vector>
namespace example
{
namespace
{
enum class NodeType
{
add,
multiply,
output,
sine,
time,
value
};
struct Node
{
NodeType type;
float value;
explicit Node(const NodeType t) : type(t), value(0.f) {}
Node(const NodeType t, const float v) : type(t), value(v) {}
};
template<class T>
T clamp(T x, T a, T b)
{
return std::min(b, std::max(x, a));
}
static float current_time_seconds = 0.f;
static bool emulate_three_button_mouse = false;
ImU32 evaluate(const Graph<Node>& graph, const int root_node)
{
std::stack<int> postorder;
dfs_traverse(
graph, root_node, [&postorder](const int node_id) -> void { postorder.push(node_id); });
std::stack<float> value_stack;
while (!postorder.empty())
{
const int id = postorder.top();
postorder.pop();
const Node node = graph.node(id);
switch (node.type)
{
case NodeType::add:
{
const float rhs = value_stack.top();
value_stack.pop();
const float lhs = value_stack.top();
value_stack.pop();
value_stack.push(lhs + rhs);
}
break;
case NodeType::multiply:
{
const float rhs = value_stack.top();
value_stack.pop();
const float lhs = value_stack.top();
value_stack.pop();
value_stack.push(rhs * lhs);
}
break;
case NodeType::sine:
{
const float x = value_stack.top();
value_stack.pop();
const float res = std::abs(std::sin(x));
value_stack.push(res);
}
break;
case NodeType::time:
{
value_stack.push(current_time_seconds);
}
break;
case NodeType::value:
{
// If the edge does not have an edge connecting to another node, then just use the value
// at this node. It means the node's input pin has not been connected to anything and
// the value comes from the node's UI.
if (graph.num_edges_from_node(id) == 0ull)
{
value_stack.push(node.value);
}
}
break;
default:
break;
}
}
// The final output node isn't evaluated in the loop -- instead we just pop
// the three values which should be in the stack.
assert(value_stack.size() == 3ull);
const int b = static_cast<int>(255.f * clamp(value_stack.top(), 0.f, 1.f) + 0.5f);
value_stack.pop();
const int g = static_cast<int>(255.f * clamp(value_stack.top(), 0.f, 1.f) + 0.5f);
value_stack.pop();
const int r = static_cast<int>(255.f * clamp(value_stack.top(), 0.f, 1.f) + 0.5f);
value_stack.pop();
return IM_COL32(r, g, b, 255);
}
class ColorNodeEditor
{
public:
ColorNodeEditor() : graph_(), nodes_(), root_node_id_(-1),
minimap_location_(ImNodesMiniMapLocation_BottomRight) {}
void show()
{
// Update timer context
current_time_seconds = 0.001f * SDL_GetTicks();
auto flags = ImGuiWindowFlags_MenuBar;
// The node editor window
ImGui::Begin("color node editor", NULL, flags);
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("Mini-map"))
{
const char* names[] = {
"Top Left",
"Top Right",
"Bottom Left",
"Bottom Right",
};
int locations[] = {
ImNodesMiniMapLocation_TopLeft,
ImNodesMiniMapLocation_TopRight,
ImNodesMiniMapLocation_BottomLeft,
ImNodesMiniMapLocation_BottomRight,
};
for (int i = 0; i < 4; i++)
{
bool selected = minimap_location_ == locations[i];
if (ImGui::MenuItem(names[i], NULL, &selected))
minimap_location_ = locations[i];
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Style"))
{
if (ImGui::MenuItem("Classic"))
{
ImGui::StyleColorsClassic();
ImNodes::StyleColorsClassic();
}
if (ImGui::MenuItem("Dark"))
{
ImGui::StyleColorsDark();
ImNodes::StyleColorsDark();
}
if (ImGui::MenuItem("Light"))
{
ImGui::StyleColorsLight();
ImNodes::StyleColorsLight();
}
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
ImGui::TextUnformatted("Edit the color of the output color window using nodes.");
ImGui::Columns(2);
ImGui::TextUnformatted("A -- add node");
ImGui::TextUnformatted("X -- delete selected node or link");
ImGui::NextColumn();
if (ImGui::Checkbox("emulate_three_button_mouse", &emulate_three_button_mouse))
{
ImNodes::GetIO().EmulateThreeButtonMouse.Modifier =
emulate_three_button_mouse ? &ImGui::GetIO().KeyAlt : NULL;
}
ImGui::Columns(1);
ImNodes::BeginNodeEditor();
// Handle new nodes
// These are driven by the user, so we place this code before rendering the nodes
{
const bool open_popup = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
ImNodes::IsEditorHovered() &&
ImGui::IsKeyReleased(SDL_SCANCODE_A);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 8.f));
if (!ImGui::IsAnyItemHovered() && open_popup)
{
ImGui::OpenPopup("add node");
}
if (ImGui::BeginPopup("add node"))
{
const ImVec2 click_pos = ImGui::GetMousePosOnOpeningCurrentPopup();
if (ImGui::MenuItem("add"))
{
const Node value(NodeType::value, 0.f);
const Node op(NodeType::add);
UiNode ui_node;
ui_node.type = UiNodeType::add;
ui_node.add.lhs = graph_.insert_node(value);
ui_node.add.rhs = graph_.insert_node(value);
ui_node.id = graph_.insert_node(op);
graph_.insert_edge(ui_node.id, ui_node.add.lhs);
graph_.insert_edge(ui_node.id, ui_node.add.rhs);
nodes_.push_back(ui_node);
ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos);
}
if (ImGui::MenuItem("multiply"))
{
const Node value(NodeType::value, 0.f);
const Node op(NodeType::multiply);
UiNode ui_node;
ui_node.type = UiNodeType::multiply;
ui_node.multiply.lhs = graph_.insert_node(value);
ui_node.multiply.rhs = graph_.insert_node(value);
ui_node.id = graph_.insert_node(op);
graph_.insert_edge(ui_node.id, ui_node.multiply.lhs);
graph_.insert_edge(ui_node.id, ui_node.multiply.rhs);
nodes_.push_back(ui_node);
ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos);
}
if (ImGui::MenuItem("output") && root_node_id_ == -1)
{
const Node value(NodeType::value, 0.f);
const Node out(NodeType::output);
UiNode ui_node;
ui_node.type = UiNodeType::output;
ui_node.output.r = graph_.insert_node(value);
ui_node.output.g = graph_.insert_node(value);
ui_node.output.b = graph_.insert_node(value);
ui_node.id = graph_.insert_node(out);
graph_.insert_edge(ui_node.id, ui_node.output.r);
graph_.insert_edge(ui_node.id, ui_node.output.g);
graph_.insert_edge(ui_node.id, ui_node.output.b);
nodes_.push_back(ui_node);
ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos);
root_node_id_ = ui_node.id;
}
if (ImGui::MenuItem("sine"))
{
const Node value(NodeType::value, 0.f);
const Node op(NodeType::sine);
UiNode ui_node;
ui_node.type = UiNodeType::sine;
ui_node.sine.input = graph_.insert_node(value);
ui_node.id = graph_.insert_node(op);
graph_.insert_edge(ui_node.id, ui_node.sine.input);
nodes_.push_back(ui_node);
ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos);
}
if (ImGui::MenuItem("time"))
{
UiNode ui_node;
ui_node.type = UiNodeType::time;
ui_node.id = graph_.insert_node(Node(NodeType::time));
nodes_.push_back(ui_node);
ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos);
}
ImGui::EndPopup();
}
ImGui::PopStyleVar();
}
for (const UiNode& node : nodes_)
{
switch (node.type)
{
case UiNodeType::add:
{
const float node_width = 100.f;
ImNodes::BeginNode(node.id);
ImNodes::BeginNodeTitleBar();
ImGui::TextUnformatted("add");
ImNodes::EndNodeTitleBar();
{
ImNodes::BeginInputAttribute(node.add.lhs);
const float label_width = ImGui::CalcTextSize("left").x;
ImGui::TextUnformatted("left");
if (graph_.num_edges_from_node(node.add.lhs) == 0ull)
{
ImGui::SameLine();
ImGui::PushItemWidth(node_width - label_width);
ImGui::DragFloat("##hidelabel", &graph_.node(node.add.lhs).value, 0.01f);
ImGui::PopItemWidth();
}
ImNodes::EndInputAttribute();
}
{
ImNodes::BeginInputAttribute(node.add.rhs);
const float label_width = ImGui::CalcTextSize("right").x;
ImGui::TextUnformatted("right");
if (graph_.num_edges_from_node(node.add.rhs) == 0ull)
{
ImGui::SameLine();
ImGui::PushItemWidth(node_width - label_width);
ImGui::DragFloat("##hidelabel", &graph_.node(node.add.rhs).value, 0.01f);
ImGui::PopItemWidth();
}
ImNodes::EndInputAttribute();
}
ImGui::Spacing();
{
ImNodes::BeginOutputAttribute(node.id);
const float label_width = ImGui::CalcTextSize("result").x;
ImGui::Indent(node_width - label_width);
ImGui::TextUnformatted("result");
ImNodes::EndOutputAttribute();
}
ImNodes::EndNode();
}
break;
case UiNodeType::multiply:
{
const float node_width = 100.0f;
ImNodes::BeginNode(node.id);
ImNodes::BeginNodeTitleBar();
ImGui::TextUnformatted("multiply");
ImNodes::EndNodeTitleBar();
{
ImNodes::BeginInputAttribute(node.multiply.lhs);
const float label_width = ImGui::CalcTextSize("left").x;
ImGui::TextUnformatted("left");
if (graph_.num_edges_from_node(node.multiply.lhs) == 0ull)
{
ImGui::SameLine();
ImGui::PushItemWidth(node_width - label_width);
ImGui::DragFloat(
"##hidelabel", &graph_.node(node.multiply.lhs).value, 0.01f);
ImGui::PopItemWidth();
}
ImNodes::EndInputAttribute();
}
{
ImNodes::BeginInputAttribute(node.multiply.rhs);
const float label_width = ImGui::CalcTextSize("right").x;
ImGui::TextUnformatted("right");
if (graph_.num_edges_from_node(node.multiply.rhs) == 0ull)
{
ImGui::SameLine();
ImGui::PushItemWidth(node_width - label_width);
ImGui::DragFloat(
"##hidelabel", &graph_.node(node.multiply.rhs).value, 0.01f);
ImGui::PopItemWidth();
}
ImNodes::EndInputAttribute();
}
ImGui::Spacing();
{
ImNodes::BeginOutputAttribute(node.id);
const float label_width = ImGui::CalcTextSize("result").x;
ImGui::Indent(node_width - label_width);
ImGui::TextUnformatted("result");
ImNodes::EndOutputAttribute();
}
ImNodes::EndNode();
}
break;
case UiNodeType::output:
{
const float node_width = 100.0f;
ImNodes::PushColorStyle(ImNodesCol_TitleBar, IM_COL32(11, 109, 191, 255));
ImNodes::PushColorStyle(ImNodesCol_TitleBarHovered, IM_COL32(45, 126, 194, 255));
ImNodes::PushColorStyle(ImNodesCol_TitleBarSelected, IM_COL32(81, 148, 204, 255));
ImNodes::BeginNode(node.id);
ImNodes::BeginNodeTitleBar();
ImGui::TextUnformatted("output");
ImNodes::EndNodeTitleBar();
ImGui::Dummy(ImVec2(node_width, 0.f));
{
ImNodes::BeginInputAttribute(node.output.r);
const float label_width = ImGui::CalcTextSize("r").x;
ImGui::TextUnformatted("r");
if (graph_.num_edges_from_node(node.output.r) == 0ull)
{
ImGui::SameLine();
ImGui::PushItemWidth(node_width - label_width);
ImGui::DragFloat(
"##hidelabel", &graph_.node(node.output.r).value, 0.01f, 0.f, 1.0f);
ImGui::PopItemWidth();
}
ImNodes::EndInputAttribute();
}
ImGui::Spacing();
{
ImNodes::BeginInputAttribute(node.output.g);
const float label_width = ImGui::CalcTextSize("g").x;
ImGui::TextUnformatted("g");
if (graph_.num_edges_from_node(node.output.g) == 0ull)
{
ImGui::SameLine();
ImGui::PushItemWidth(node_width - label_width);
ImGui::DragFloat(
"##hidelabel", &graph_.node(node.output.g).value, 0.01f, 0.f, 1.f);
ImGui::PopItemWidth();
}
ImNodes::EndInputAttribute();
}
ImGui::Spacing();
{
ImNodes::BeginInputAttribute(node.output.b);
const float label_width = ImGui::CalcTextSize("b").x;
ImGui::TextUnformatted("b");
if (graph_.num_edges_from_node(node.output.b) == 0ull)
{
ImGui::SameLine();
ImGui::PushItemWidth(node_width - label_width);
ImGui::DragFloat(
"##hidelabel", &graph_.node(node.output.b).value, 0.01f, 0.f, 1.0f);
ImGui::PopItemWidth();
}
ImNodes::EndInputAttribute();
}
ImNodes::EndNode();
ImNodes::PopColorStyle();
ImNodes::PopColorStyle();
ImNodes::PopColorStyle();
}
break;
case UiNodeType::sine:
{
const float node_width = 100.0f;
ImNodes::BeginNode(node.id);
ImNodes::BeginNodeTitleBar();
ImGui::TextUnformatted("sine");
ImNodes::EndNodeTitleBar();
{
ImNodes::BeginInputAttribute(node.sine.input);
const float label_width = ImGui::CalcTextSize("number").x;
ImGui::TextUnformatted("number");
if (graph_.num_edges_from_node(node.sine.input) == 0ull)
{
ImGui::SameLine();
ImGui::PushItemWidth(node_width - label_width);
ImGui::DragFloat(
"##hidelabel", &graph_.node(node.sine.input).value, 0.01f, 0.f, 1.0f);
ImGui::PopItemWidth();
}
ImNodes::EndInputAttribute();
}
ImGui::Spacing();
{
ImNodes::BeginOutputAttribute(node.id);
const float label_width = ImGui::CalcTextSize("output").x;
ImGui::Indent(node_width - label_width);
ImGui::TextUnformatted("output");
ImNodes::EndInputAttribute();
}
ImNodes::EndNode();
}
break;
case UiNodeType::time:
{
ImNodes::BeginNode(node.id);
ImNodes::BeginNodeTitleBar();
ImGui::TextUnformatted("time");
ImNodes::EndNodeTitleBar();
ImNodes::BeginOutputAttribute(node.id);
ImGui::Text("output");
ImNodes::EndOutputAttribute();
ImNodes::EndNode();
}
break;
}
}
for (const auto& edge : graph_.edges())
{
// If edge doesn't start at value, then it's an internal edge, i.e.
// an edge which links a node's operation to its input. We don't
// want to render node internals with visible links.
if (graph_.node(edge.from).type != NodeType::value)
continue;
ImNodes::Link(edge.id, edge.from, edge.to);
}
ImNodes::MiniMap(0.2f, minimap_location_);
ImNodes::EndNodeEditor();
// Handle new links
// These are driven by Imnodes, so we place the code after EndNodeEditor().
{
int start_attr, end_attr;
if (ImNodes::IsLinkCreated(&start_attr, &end_attr))
{
const NodeType start_type = graph_.node(start_attr).type;
const NodeType end_type = graph_.node(end_attr).type;
const bool valid_link = start_type != end_type;
if (valid_link)
{
// Ensure the edge is always directed from the value to
// whatever produces the value
if (start_type != NodeType::value)
{
std::swap(start_attr, end_attr);
}
graph_.insert_edge(start_attr, end_attr);
}
}
}
// Handle deleted links
{
int link_id;
if (ImNodes::IsLinkDestroyed(&link_id))
{
graph_.erase_edge(link_id);
}
}
{
const int num_selected = ImNodes::NumSelectedLinks();
if (num_selected > 0 && ImGui::IsKeyReleased(SDL_SCANCODE_X))
{
static std::vector<int> selected_links;
selected_links.resize(static_cast<size_t>(num_selected));
ImNodes::GetSelectedLinks(selected_links.data());
for (const int edge_id : selected_links)
{
graph_.erase_edge(edge_id);
}
}
}
{
const int num_selected = ImNodes::NumSelectedNodes();
if (num_selected > 0 && ImGui::IsKeyReleased(SDL_SCANCODE_X))
{
static std::vector<int> selected_nodes;
selected_nodes.resize(static_cast<size_t>(num_selected));
ImNodes::GetSelectedNodes(selected_nodes.data());
for (const int node_id : selected_nodes)
{
graph_.erase_node(node_id);
auto iter = std::find_if(
nodes_.begin(), nodes_.end(), [node_id](const UiNode& node) -> bool {
return node.id == node_id;
});
// Erase any additional internal nodes
switch (iter->type)
{
case UiNodeType::add:
graph_.erase_node(iter->add.lhs);
graph_.erase_node(iter->add.rhs);
break;
case UiNodeType::multiply:
graph_.erase_node(iter->multiply.lhs);
graph_.erase_node(iter->multiply.rhs);
break;
case UiNodeType::output:
graph_.erase_node(iter->output.r);
graph_.erase_node(iter->output.g);
graph_.erase_node(iter->output.b);
root_node_id_ = -1;
break;
case UiNodeType::sine:
graph_.erase_node(iter->sine.input);
break;
default:
break;
}
nodes_.erase(iter);
}
}
}
ImGui::End();
// The color output window
const ImU32 color =
root_node_id_ != -1 ? evaluate(graph_, root_node_id_) : IM_COL32(255, 20, 147, 255);
ImGui::PushStyleColor(ImGuiCol_WindowBg, color);
ImGui::Begin("output color");
ImGui::End();
ImGui::PopStyleColor();
}
private:
enum class UiNodeType
{
add,
multiply,
output,
sine,
time,
};
struct UiNode
{
UiNodeType type;
// The identifying id of the ui node. For add, multiply, sine, and time
// this is the "operation" node id. The additional input nodes are
// stored in the structs.
int id;
union
{
struct
{
int lhs, rhs;
} add;
struct
{
int lhs, rhs;
} multiply;
struct
{
int r, g, b;
} output;
struct
{
int input;
} sine;
};
};
Graph<Node> graph_;
std::vector<UiNode> nodes_;
int root_node_id_;
ImNodesMiniMapLocation minimap_location_;
};
static ColorNodeEditor color_editor;
} // namespace
void NodeEditorInitialize()
{
ImNodesIO& io = ImNodes::GetIO();
io.LinkDetachWithModifierClick.Modifier = &ImGui::GetIO().KeyCtrl;
}
void NodeEditorShow() { color_editor.show(); }
void NodeEditorShutdown() {}
} // namespace example
+357
View File
@@ -0,0 +1,357 @@
#pragma once
#include <algorithm>
#include <cassert>
#include <iterator>
#include <stack>
#include <stddef.h>
#include <utility>
#include <vector>
namespace example
{
template<typename ElementType>
struct Span
{
using iterator = ElementType*;
template<typename Container>
Span(Container& c) : begin_(c.data()), end_(begin_ + c.size())
{
}
iterator begin() const { return begin_; }
iterator end() const { return end_; }
private:
iterator begin_;
iterator end_;
};
template<typename ElementType>
class IdMap
{
public:
using iterator = typename std::vector<ElementType>::iterator;
using const_iterator = typename std::vector<ElementType>::const_iterator;
// Iterators
const_iterator begin() const { return elements_.begin(); }
const_iterator end() const { return elements_.end(); }
// Element access
Span<const ElementType> elements() const { return elements_; }
// Capacity
bool empty() const { return sorted_ids_.empty(); }
size_t size() const { return sorted_ids_.size(); }
// Modifiers
std::pair<iterator, bool> insert(int id, const ElementType& element);
std::pair<iterator, bool> insert(int id, ElementType&& element);
size_t erase(int id);
void clear();
// Lookup
iterator find(int id);
const_iterator find(int id) const;
bool contains(int id) const;
private:
std::vector<ElementType> elements_;
std::vector<int> sorted_ids_;
};
template<typename ElementType>
std::pair<typename IdMap<ElementType>::iterator, bool> IdMap<ElementType>::insert(
const int id,
const ElementType& element)
{
auto lower_bound = std::lower_bound(sorted_ids_.begin(), sorted_ids_.end(), id);
if (lower_bound != sorted_ids_.end() && id == *lower_bound)
{
return std::make_pair(
std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound)), false);
}
auto insert_element_at =
std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound));
sorted_ids_.insert(lower_bound, id);
return std::make_pair(elements_.insert(insert_element_at, element), true);
}
template<typename ElementType>
std::pair<typename IdMap<ElementType>::iterator, bool> IdMap<ElementType>::insert(
const int id,
ElementType&& element)
{
auto lower_bound = std::lower_bound(sorted_ids_.begin(), sorted_ids_.end(), id);
if (lower_bound != sorted_ids_.end() && id == *lower_bound)
{
return std::make_pair(
std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound)), false);
}
auto insert_element_at =
std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound));
sorted_ids_.insert(lower_bound, id);
return std::make_pair(elements_.insert(insert_element_at, std::move(element)), true);
}
template<typename ElementType>
size_t IdMap<ElementType>::erase(const int id)
{
auto lower_bound = std::lower_bound(sorted_ids_.begin(), sorted_ids_.end(), id);
if (lower_bound == sorted_ids_.end() || id != *lower_bound)
{
return 0ull;
}
auto erase_element_at =
std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound));
sorted_ids_.erase(lower_bound);
elements_.erase(erase_element_at);
return 1ull;
}
template<typename ElementType>
void IdMap<ElementType>::clear()
{
elements_.clear();
sorted_ids_.clear();
}
template<typename ElementType>
typename IdMap<ElementType>::iterator IdMap<ElementType>::find(const int id)
{
const auto lower_bound = std::lower_bound(sorted_ids_.cbegin(), sorted_ids_.cend(), id);
return (lower_bound == sorted_ids_.cend() || *lower_bound != id)
? elements_.end()
: std::next(elements_.begin(), std::distance(sorted_ids_.cbegin(), lower_bound));
}
template<typename ElementType>
typename IdMap<ElementType>::const_iterator IdMap<ElementType>::find(const int id) const
{
const auto lower_bound = std::lower_bound(sorted_ids_.cbegin(), sorted_ids_.cend(), id);
return (lower_bound == sorted_ids_.cend() || *lower_bound != id)
? elements_.cend()
: std::next(elements_.cbegin(), std::distance(sorted_ids_.cbegin(), lower_bound));
}
template<typename ElementType>
bool IdMap<ElementType>::contains(const int id) const
{
const auto lower_bound = std::lower_bound(sorted_ids_.cbegin(), sorted_ids_.cend(), id);
if (lower_bound == sorted_ids_.cend())
{
return false;
}
return *lower_bound == id;
}
// a very simple directional graph
template<typename NodeType>
class Graph
{
public:
Graph() : current_id_(0), nodes_(), edges_from_node_(), node_neighbors_(), edges_() {}
struct Edge
{
int id;
int from, to;
Edge() = default;
Edge(const int id, const int f, const int t) : id(id), from(f), to(t) {}
inline int opposite(const int n) const { return n == from ? to : from; }
inline bool contains(const int n) const { return n == from || n == to; }
};
// Element access
NodeType& node(int node_id);
const NodeType& node(int node_id) const;
Span<const int> neighbors(int node_id) const;
Span<const Edge> edges() const;
// Capacity
size_t num_edges_from_node(int node_id) const;
// Modifiers
int insert_node(const NodeType& node);
void erase_node(int node_id);
int insert_edge(int from, int to);
void erase_edge(int edge_id);
private:
int current_id_;
// These contains map to the node id
IdMap<NodeType> nodes_;
IdMap<int> edges_from_node_;
IdMap<std::vector<int>> node_neighbors_;
// This container maps to the edge id
IdMap<Edge> edges_;
};
template<typename NodeType>
NodeType& Graph<NodeType>::node(const int id)
{
return const_cast<NodeType&>(static_cast<const Graph*>(this)->node(id));
}
template<typename NodeType>
const NodeType& Graph<NodeType>::node(const int id) const
{
const auto iter = nodes_.find(id);
assert(iter != nodes_.end());
return *iter;
}
template<typename NodeType>
Span<const int> Graph<NodeType>::neighbors(int node_id) const
{
const auto iter = node_neighbors_.find(node_id);
assert(iter != node_neighbors_.end());
return *iter;
}
template<typename NodeType>
Span<const typename Graph<NodeType>::Edge> Graph<NodeType>::edges() const
{
return edges_.elements();
}
template<typename NodeType>
size_t Graph<NodeType>::num_edges_from_node(const int id) const
{
auto iter = edges_from_node_.find(id);
assert(iter != edges_from_node_.end());
return *iter;
}
template<typename NodeType>
int Graph<NodeType>::insert_node(const NodeType& node)
{
const int id = current_id_++;
assert(!nodes_.contains(id));
nodes_.insert(id, node);
edges_from_node_.insert(id, 0);
node_neighbors_.insert(id, std::vector<int>());
return id;
}
template<typename NodeType>
void Graph<NodeType>::erase_node(const int id)
{
// first, remove any potential dangling edges
{
static std::vector<int> edges_to_erase;
for (const Edge& edge : edges_.elements())
{
if (edge.contains(id))
{
edges_to_erase.push_back(edge.id);
}
}
for (const int edge_id : edges_to_erase)
{
erase_edge(edge_id);
}
edges_to_erase.clear();
}
nodes_.erase(id);
edges_from_node_.erase(id);
node_neighbors_.erase(id);
}
template<typename NodeType>
int Graph<NodeType>::insert_edge(const int from, const int to)
{
const int id = current_id_++;
assert(!edges_.contains(id));
assert(nodes_.contains(from));
assert(nodes_.contains(to));
edges_.insert(id, Edge(id, from, to));
// update neighbor count
assert(edges_from_node_.contains(from));
*edges_from_node_.find(from) += 1;
// update neighbor list
assert(node_neighbors_.contains(from));
node_neighbors_.find(from)->push_back(to);
return id;
}
template<typename NodeType>
void Graph<NodeType>::erase_edge(const int edge_id)
{
// This is a bit lazy, we find the pointer here, but we refind it when we erase the edge based
// on id key.
assert(edges_.contains(edge_id));
const Edge& edge = *edges_.find(edge_id);
// update neighbor count
assert(edges_from_node_.contains(edge.from));
int& edge_count = *edges_from_node_.find(edge.from);
assert(edge_count > 0);
edge_count -= 1;
// update neighbor list
{
assert(node_neighbors_.contains(edge.from));
auto neighbors = node_neighbors_.find(edge.from);
auto iter = std::find(neighbors->begin(), neighbors->end(), edge.to);
assert(iter != neighbors->end());
neighbors->erase(iter);
}
edges_.erase(edge_id);
}
template<typename NodeType, typename Visitor>
void dfs_traverse(const Graph<NodeType>& graph, const int start_node, Visitor visitor)
{
std::stack<int> stack;
stack.push(start_node);
while (!stack.empty())
{
const int current_node = stack.top();
stack.pop();
visitor(current_node);
for (const int neighbor : graph.neighbors(current_node))
{
stack.push(neighbor);
}
}
}
} // namespace example
+48
View File
@@ -0,0 +1,48 @@
#include "node_editor.h"
#include <imnodes.h>
#include <imgui.h>
namespace example
{
namespace
{
class HelloWorldNodeEditor
{
public:
void show()
{
ImGui::Begin("simple node editor");
ImNodes::BeginNodeEditor();
ImNodes::BeginNode(1);
ImNodes::BeginNodeTitleBar();
ImGui::TextUnformatted("simple node :)");
ImNodes::EndNodeTitleBar();
ImNodes::BeginInputAttribute(2);
ImGui::Text("input");
ImNodes::EndInputAttribute();
ImNodes::BeginOutputAttribute(3);
ImGui::Indent(40);
ImGui::Text("output");
ImNodes::EndOutputAttribute();
ImNodes::EndNode();
ImNodes::EndNodeEditor();
ImGui::End();
}
};
static HelloWorldNodeEditor editor;
} // namespace
void NodeEditorInitialize() { ImNodes::SetNodeGridSpacePos(1, ImVec2(200.0f, 200.0f)); }
void NodeEditorShow() { editor.show(); }
void NodeEditorShutdown() {}
} // namespace example
+128
View File
@@ -0,0 +1,128 @@
#include "node_editor.h"
#include <imgui.h>
#include <imgui_impl_sdl.h>
#include <imgui_impl_opengl3.h>
#include <imnodes.h>
#include <stdio.h>
#include <SDL.h>
#include <GL/gl3w.h>
int main(int, char**)
{
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0)
{
printf("Error: %s\n", SDL_GetError());
return -1;
}
#if __APPLE__
// GL 3.2 Core + GLSL 150
const char* glsl_version = "#version 150";
SDL_GL_SetAttribute(
SDL_GL_CONTEXT_FLAGS,
SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
#else
// GL 3.0 + GLSL 130
const char* glsl_version = "#version 130";
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#endif
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_DisplayMode current;
SDL_GetCurrentDisplayMode(0, &current);
SDL_Window* window = SDL_CreateWindow(
"imgui-node-editor example",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
1280,
720,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
SDL_GLContext gl_context = SDL_GL_CreateContext(window);
SDL_GL_MakeCurrent(window, gl_context);
SDL_GL_SetSwapInterval(1); // Enable vsync
if (gl3wInit())
{
fprintf(stderr, "Failed to initialize OpenGL loader!\n");
return 1;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
ImGui_ImplOpenGL3_Init(glsl_version);
ImNodes::CreateContext();
// Setup style
ImGui::StyleColorsDark();
ImNodes::StyleColorsDark();
bool done = false;
bool initialized = false;
{
const ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w);
}
while (!done)
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT)
done = true;
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE &&
event.window.windowID == SDL_GetWindowID(window))
done = true;
}
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
if (!initialized)
{
initialized = true;
example::NodeEditorInitialize();
}
example::NodeEditorShow();
// Rendering
ImGui::Render();
int fb_width, fb_height;
SDL_GL_GetDrawableSize(window, &fb_width, &fb_height);
glViewport(0, 0, fb_width, fb_height);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window);
}
example::NodeEditorShutdown();
ImNodes::DestroyContext();
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
SDL_GL_DeleteContext(gl_context);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
+137
View File
@@ -0,0 +1,137 @@
#include "node_editor.h"
#include <imnodes.h>
#include <imgui.h>
#include <SDL_scancode.h>
#include <algorithm>
#include <vector>
namespace example
{
namespace
{
struct Node
{
int id;
float value;
Node(const int i, const float v) : id(i), value(v) {}
};
struct Link
{
int id;
int start_attr, end_attr;
};
struct Editor
{
ImNodesEditorContext* context = nullptr;
std::vector<Node> nodes;
std::vector<Link> links;
int current_id = 0;
};
void show_editor(const char* editor_name, Editor& editor)
{
ImNodes::EditorContextSet(editor.context);
ImGui::Begin(editor_name);
ImGui::TextUnformatted("A -- add node");
ImNodes::BeginNodeEditor();
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
ImNodes::IsEditorHovered() && ImGui::IsKeyReleased(SDL_SCANCODE_A))
{
const int node_id = ++editor.current_id;
ImNodes::SetNodeScreenSpacePos(node_id, ImGui::GetMousePos());
editor.nodes.push_back(Node(node_id, 0.f));
}
for (Node& node : editor.nodes)
{
ImNodes::BeginNode(node.id);
ImNodes::BeginNodeTitleBar();
ImGui::TextUnformatted("node");
ImNodes::EndNodeTitleBar();
ImNodes::BeginInputAttribute(node.id << 8);
ImGui::TextUnformatted("input");
ImNodes::EndInputAttribute();
ImNodes::BeginStaticAttribute(node.id << 16);
ImGui::PushItemWidth(120.0f);
ImGui::DragFloat("value", &node.value, 0.01f);
ImGui::PopItemWidth();
ImNodes::EndStaticAttribute();
ImNodes::BeginOutputAttribute(node.id << 24);
const float text_width = ImGui::CalcTextSize("output").x;
ImGui::Indent(120.f + ImGui::CalcTextSize("value").x - text_width);
ImGui::TextUnformatted("output");
ImNodes::EndOutputAttribute();
ImNodes::EndNode();
}
for (const Link& link : editor.links)
{
ImNodes::Link(link.id, link.start_attr, link.end_attr);
}
ImNodes::EndNodeEditor();
{
Link link;
if (ImNodes::IsLinkCreated(&link.start_attr, &link.end_attr))
{
link.id = ++editor.current_id;
editor.links.push_back(link);
}
}
{
int link_id;
if (ImNodes::IsLinkDestroyed(&link_id))
{
auto iter = std::find_if(
editor.links.begin(), editor.links.end(), [link_id](const Link& link) -> bool {
return link.id == link_id;
});
assert(iter != editor.links.end());
editor.links.erase(iter);
}
}
ImGui::End();
}
Editor editor1;
Editor editor2;
} // namespace
void NodeEditorInitialize()
{
editor1.context = ImNodes::EditorContextCreate();
editor2.context = ImNodes::EditorContextCreate();
ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkDetachWithDragClick);
ImNodesIO& io = ImNodes::GetIO();
io.LinkDetachWithModifierClick.Modifier = &ImGui::GetIO().KeyCtrl;
}
void NodeEditorShow()
{
show_editor("editor1", editor1);
show_editor("editor2", editor2);
}
void NodeEditorShutdown()
{
ImNodes::PopAttributeFlag();
ImNodes::EditorContextFree(editor1.context);
ImNodes::EditorContextFree(editor2.context);
}
} // namespace example
+8
View File
@@ -0,0 +1,8 @@
#pragma once
namespace example
{
void NodeEditorInitialize();
void NodeEditorShow();
void NodeEditorShutdown();
} // namespace example
+206
View File
@@ -0,0 +1,206 @@
#include "node_editor.h"
#include <imnodes.h>
#include <imgui.h>
#include <SDL_keycode.h>
#include <algorithm>
#include <cassert>
#include <fstream>
#include <ios> // for std::streamsize
#include <stddef.h>
#include <vector>
namespace example
{
namespace
{
struct Node
{
int id;
float value;
Node() = default;
Node(const int i, const float v) : id(i), value(v) {}
};
struct Link
{
int id;
int start_attr, end_attr;
};
class SaveLoadEditor
{
public:
SaveLoadEditor() : nodes_(), links_(), current_id_(0) {}
void show()
{
ImGui::Begin("Save & load example");
ImGui::TextUnformatted("A -- add node");
ImGui::TextUnformatted(
"Close the executable and rerun it -- your nodes should be exactly "
"where you left them!");
ImNodes::BeginNodeEditor();
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
ImNodes::IsEditorHovered() && ImGui::IsKeyReleased(SDL_SCANCODE_A))
{
const int node_id = ++current_id_;
ImNodes::SetNodeScreenSpacePos(node_id, ImGui::GetMousePos());
nodes_.push_back(Node(node_id, 0.f));
}
for (Node& node : nodes_)
{
ImNodes::BeginNode(node.id);
ImNodes::BeginNodeTitleBar();
ImGui::TextUnformatted("node");
ImNodes::EndNodeTitleBar();
ImNodes::BeginInputAttribute(node.id << 8);
ImGui::TextUnformatted("input");
ImNodes::EndInputAttribute();
ImNodes::BeginStaticAttribute(node.id << 16);
ImGui::PushItemWidth(120.f);
ImGui::DragFloat("value", &node.value, 0.01f);
ImGui::PopItemWidth();
ImNodes::EndStaticAttribute();
ImNodes::BeginOutputAttribute(node.id << 24);
const float text_width = ImGui::CalcTextSize("output").x;
ImGui::Indent(120.f + ImGui::CalcTextSize("value").x - text_width);
ImGui::TextUnformatted("output");
ImNodes::EndOutputAttribute();
ImNodes::EndNode();
}
for (const Link& link : links_)
{
ImNodes::Link(link.id, link.start_attr, link.end_attr);
}
ImNodes::EndNodeEditor();
{
Link link;
if (ImNodes::IsLinkCreated(&link.start_attr, &link.end_attr))
{
link.id = ++current_id_;
links_.push_back(link);
}
}
{
int link_id;
if (ImNodes::IsLinkDestroyed(&link_id))
{
auto iter =
std::find_if(links_.begin(), links_.end(), [link_id](const Link& link) -> bool {
return link.id == link_id;
});
assert(iter != links_.end());
links_.erase(iter);
}
}
ImGui::End();
}
void save()
{
// Save the internal imnodes state
ImNodes::SaveCurrentEditorStateToIniFile("save_load.ini");
// Dump our editor state as bytes into a file
std::fstream fout(
"save_load.bytes", std::ios_base::out | std::ios_base::binary | std::ios_base::trunc);
// copy the node vector to file
const size_t num_nodes = nodes_.size();
fout.write(
reinterpret_cast<const char*>(&num_nodes),
static_cast<std::streamsize>(sizeof(size_t)));
fout.write(
reinterpret_cast<const char*>(nodes_.data()),
static_cast<std::streamsize>(sizeof(Node) * num_nodes));
// copy the link vector to file
const size_t num_links = links_.size();
fout.write(
reinterpret_cast<const char*>(&num_links),
static_cast<std::streamsize>(sizeof(size_t)));
fout.write(
reinterpret_cast<const char*>(links_.data()),
static_cast<std::streamsize>(sizeof(Link) * num_links));
// copy the current_id to file
fout.write(
reinterpret_cast<const char*>(&current_id_), static_cast<std::streamsize>(sizeof(int)));
}
void load()
{
// Load the internal imnodes state
ImNodes::LoadCurrentEditorStateFromIniFile("save_load.ini");
// Load our editor state into memory
std::fstream fin("save_load.bytes", std::ios_base::in | std::ios_base::binary);
if (!fin.is_open())
{
return;
}
// copy nodes into memory
size_t num_nodes;
fin.read(reinterpret_cast<char*>(&num_nodes), static_cast<std::streamsize>(sizeof(size_t)));
nodes_.resize(num_nodes);
fin.read(
reinterpret_cast<char*>(nodes_.data()),
static_cast<std::streamsize>(sizeof(Node) * num_nodes));
// copy links into memory
size_t num_links;
fin.read(reinterpret_cast<char*>(&num_links), static_cast<std::streamsize>(sizeof(size_t)));
links_.resize(num_links);
fin.read(
reinterpret_cast<char*>(links_.data()),
static_cast<std::streamsize>(sizeof(Link) * num_links));
// copy current_id into memory
fin.read(reinterpret_cast<char*>(&current_id_), static_cast<std::streamsize>(sizeof(int)));
}
private:
std::vector<Node> nodes_;
std::vector<Link> links_;
int current_id_;
};
static SaveLoadEditor editor;
} // namespace
void NodeEditorInitialize()
{
ImNodes::GetIO().LinkDetachWithModifierClick.Modifier = &ImGui::GetIO().KeyCtrl;
ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkDetachWithDragClick);
editor.load();
}
void NodeEditorShow() { editor.show(); }
void NodeEditorShutdown()
{
ImNodes::PopAttributeFlag();
editor.save();
}
} // namespace example