aboutsummaryrefslogtreecommitdiff
path: root/3rdparty
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2021-04-17 20:01:40 -0700
committerrtk0c <[email protected]>2021-04-17 20:01:40 -0700
commit7b6b229ad9d85d1145322b2edd5992a4629c2106 (patch)
treeedc5861d7e526ff6aae0fa8038a9ad07c743e0ab /3rdparty
parentdca1286661f61e51943863de8ce68849a9578363 (diff)
Change imnodes to imgui-node-editor (more mature, more features)
Diffstat (limited to '3rdparty')
-rw-r--r--3rdparty/imgui-node-editor/CMakeLists.txt7
-rw-r--r--3rdparty/imgui-node-editor/crude_json.cpp814
-rw-r--r--3rdparty/imgui-node-editor/crude_json.h223
-rw-r--r--3rdparty/imgui-node-editor/imgui_bezier_math.h142
-rw-r--r--3rdparty/imgui-node-editor/imgui_bezier_math.inl670
-rw-r--r--3rdparty/imgui-node-editor/imgui_canvas.cpp513
-rw-r--r--3rdparty/imgui-node-editor/imgui_canvas.h258
-rw-r--r--3rdparty/imgui-node-editor/imgui_extra_math.h71
-rw-r--r--3rdparty/imgui-node-editor/imgui_extra_math.inl187
-rw-r--r--3rdparty/imgui-node-editor/imgui_node_editor.cpp5316
-rw-r--r--3rdparty/imgui-node-editor/imgui_node_editor.h444
-rw-r--r--3rdparty/imgui-node-editor/imgui_node_editor_api.cpp637
-rw-r--r--3rdparty/imgui-node-editor/imgui_node_editor_internal.h1474
-rw-r--r--3rdparty/imgui-node-editor/imgui_node_editor_internal.inl55
-rw-r--r--3rdparty/imgui-node-editor/thedmd-imgui-node-editor.LICENSE.txt (renamed from 3rdparty/imnodes/Nelarius-imnodes.LICENSE.txt)4
-rw-r--r--3rdparty/imgui-node-editor/thedmd-imgui-node-editor.stamp2
-rw-r--r--3rdparty/imgui/CMakeLists.txt15
-rw-r--r--3rdparty/imnodes/CMakeLists.txt10
-rw-r--r--3rdparty/imnodes/Nelarius-imnodes.stamp3
-rw-r--r--3rdparty/imnodes/imnodes.cpp3089
-rw-r--r--3rdparty/imnodes/imnodes.h335
21 files changed, 10816 insertions, 3453 deletions
diff --git a/3rdparty/imgui-node-editor/CMakeLists.txt b/3rdparty/imgui-node-editor/CMakeLists.txt
new file mode 100644
index 0000000..2730a8e
--- /dev/null
+++ b/3rdparty/imgui-node-editor/CMakeLists.txt
@@ -0,0 +1,7 @@
+file(GLOB IMGUI_NODE_EDITOR_SOURCES *.cpp)
+
+add_library(imgui-node-editor ${IMGUI_NODE_EDITOR_SOURCES})
+target_include_directories(imgui-node-editor PRIVATE
+ ${CMAKE_SOURCE_DIR}/3rdparty/imgui-node-editor
+ ${CMAKE_SOURCE_DIR}/3rdparty/imgui
+)
diff --git a/3rdparty/imgui-node-editor/crude_json.cpp b/3rdparty/imgui-node-editor/crude_json.cpp
new file mode 100644
index 0000000..f2b3ba5
--- /dev/null
+++ b/3rdparty/imgui-node-editor/crude_json.cpp
@@ -0,0 +1,814 @@
+// Crude implementation of JSON value object and parser.
+//
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+# include "crude_json.h"
+# include <iomanip>
+# include <limits>
+# include <cstdlib>
+# include <clocale>
+# include <cmath>
+# include <cstring>
+
+
+namespace crude_json {
+
+value::value(value&& other)
+ : m_Type(other.m_Type)
+{
+ switch (m_Type)
+ {
+ case type_t::object: construct(m_Storage, std::move( *object_ptr(other.m_Storage))); break;
+ case type_t::array: construct(m_Storage, std::move( *array_ptr(other.m_Storage))); break;
+ case type_t::string: construct(m_Storage, std::move( *string_ptr(other.m_Storage))); break;
+ case type_t::boolean: construct(m_Storage, std::move(*boolean_ptr(other.m_Storage))); break;
+ case type_t::number: construct(m_Storage, std::move( *number_ptr(other.m_Storage))); break;
+ default: break;
+ }
+ destruct(other.m_Storage, other.m_Type);
+ other.m_Type = type_t::null;
+}
+
+value::value(const value& other)
+ : m_Type(other.m_Type)
+{
+ switch (m_Type)
+ {
+ case type_t::object: construct(m_Storage, *object_ptr(other.m_Storage)); break;
+ case type_t::array: construct(m_Storage, *array_ptr(other.m_Storage)); break;
+ case type_t::string: construct(m_Storage, *string_ptr(other.m_Storage)); break;
+ case type_t::boolean: construct(m_Storage, *boolean_ptr(other.m_Storage)); break;
+ case type_t::number: construct(m_Storage, *number_ptr(other.m_Storage)); break;
+ default: break;
+ }
+}
+
+value& value::operator[](size_t index)
+{
+ if (is_null())
+ m_Type = construct(m_Storage, type_t::array);
+
+ if (is_array())
+ {
+ auto& v = *array_ptr(m_Storage);
+ if (index >= v.size())
+ v.insert(v.end(), index - v.size() + 1, value());
+
+ return v[index];
+ }
+
+ CRUDE_ASSERT(false && "operator[] on unsupported type");
+ std::terminate();
+}
+
+const value& value::operator[](size_t index) const
+{
+ if (is_array())
+ return (*array_ptr(m_Storage))[index];
+
+ CRUDE_ASSERT(false && "operator[] on unsupported type");
+ std::terminate();
+}
+
+value& value::operator[](const string& key)
+{
+ if (is_null())
+ m_Type = construct(m_Storage, type_t::object);
+
+ if (is_object())
+ return (*object_ptr(m_Storage))[key];
+
+ CRUDE_ASSERT(false && "operator[] on unsupported type");
+ std::terminate();
+}
+
+const value& value::operator[](const string& key) const
+{
+ if (is_object())
+ {
+ auto& o = *object_ptr(m_Storage);
+ auto it = o.find(key);
+ CRUDE_ASSERT(it != o.end());
+ return it->second;
+ }
+
+ CRUDE_ASSERT(false && "operator[] on unsupported type");
+ std::terminate();
+}
+
+bool value::contains(const string& key) const
+{
+ if (is_object())
+ {
+ auto& o = *object_ptr(m_Storage);
+ auto it = o.find(key);
+ return it != o.end();
+ }
+
+ return false;
+}
+
+void value::push_back(const value& value)
+{
+ if (is_null())
+ m_Type = construct(m_Storage, type_t::array);
+
+ if (is_array())
+ {
+ auto& v = *array_ptr(m_Storage);
+ v.push_back(value);
+ }
+ else
+ {
+ CRUDE_ASSERT(false && "operator[] on unsupported type");
+ std::terminate();
+ }
+}
+
+void value::push_back(value&& value)
+{
+ if (is_null())
+ m_Type = construct(m_Storage, type_t::array);
+
+ if (is_array())
+ {
+ auto& v = *array_ptr(m_Storage);
+ v.push_back(std::move(value));
+ }
+ else
+ {
+ CRUDE_ASSERT(false && "operator[] on unsupported type");
+ std::terminate();
+ }
+}
+
+void value::swap(value& other)
+{
+ using std::swap;
+
+ if (m_Type == other.m_Type)
+ {
+ switch (m_Type)
+ {
+ case type_t::object: swap(*object_ptr(m_Storage), *object_ptr(other.m_Storage)); break;
+ case type_t::array: swap(*array_ptr(m_Storage), *array_ptr(other.m_Storage)); break;
+ case type_t::string: swap(*string_ptr(m_Storage), *string_ptr(other.m_Storage)); break;
+ case type_t::boolean: swap(*boolean_ptr(m_Storage), *boolean_ptr(other.m_Storage)); break;
+ case type_t::number: swap(*number_ptr(m_Storage), *number_ptr(other.m_Storage)); break;
+ default: break;
+ }
+ }
+ else
+ {
+ value tmp(std::move(other));
+ other.~value();
+ new (&other) value(std::move(*this));
+ this->~value();
+ new (this) value(std::move(tmp));
+ }
+}
+
+string value::dump(const int indent, const char indent_char) const
+{
+ dump_context_t context(indent, indent_char);
+
+ context.out.precision(std::numeric_limits<double>::max_digits10 + 1);
+ context.out << std::defaultfloat;
+
+ dump(context, 0);
+ return context.out.str();
+}
+
+void value::dump_context_t::write_indent(int level)
+{
+ if (indent <= 0 || level == 0)
+ return;
+
+ out.fill(indent_char);
+ out.width(indent * level);
+ out << indent_char;
+ out.width(0);
+}
+
+void value::dump_context_t::write_separator()
+{
+ if (indent < 0)
+ return;
+
+ out.put(' ');
+}
+
+void value::dump_context_t::write_newline()
+{
+ if (indent < 0)
+ return;
+
+ out.put('\n');
+}
+
+void value::dump(dump_context_t& context, int level) const
+{
+ context.write_indent(level);
+
+ switch (m_Type)
+ {
+ case type_t::null:
+ context.out << "null";
+ break;
+
+ case type_t::object:
+ context.out << '{';
+ {
+ context.write_newline();
+ bool first = true;
+ for (auto& entry : *object_ptr(m_Storage))
+ {
+ if (!first) { context.out << ','; context.write_newline(); } else first = false;
+ context.write_indent(level + 1);
+ context.out << '\"' << entry.first << "\":";
+ if (!entry.second.is_structured())
+ {
+ context.write_separator();
+ entry.second.dump(context, 0);
+ }
+ else
+ {
+ context.write_newline();
+ entry.second.dump(context, level + 1);
+ }
+ }
+ if (!first)
+ context.write_newline();
+ }
+ context.write_indent(level);
+ context.out << '}';
+ break;
+
+ case type_t::array:
+ context.out << '[';
+ {
+ context.write_newline();
+ bool first = true;
+ for (auto& entry : *array_ptr(m_Storage))
+ {
+ if (!first) { context.out << ','; context.write_newline(); } else first = false;
+ if (!entry.is_structured())
+ {
+ context.write_indent(level + 1);
+ entry.dump(context, 0);
+ }
+ else
+ {
+ entry.dump(context, level + 1);
+ }
+ }
+ if (!first)
+ context.write_newline();
+ }
+ context.write_indent(level);
+ context.out << ']';
+ break;
+
+ case type_t::string:
+ context.out << '\"';
+
+ if (string_ptr(m_Storage)->find_first_of("\"\\/\b\f\n\r") != string::npos || string_ptr(m_Storage)->find('\0') != string::npos)
+ {
+ for (auto c : *string_ptr(m_Storage))
+ {
+ if (c == '\"') context.out << "\\\"";
+ else if (c == '\\') context.out << "\\\\";
+ else if (c == '/') context.out << "\\/";
+ else if (c == '\b') context.out << "\\b";
+ else if (c == '\f') context.out << "\\f";
+ else if (c == '\n') context.out << "\\n";
+ else if (c == '\r') context.out << "\\r";
+ else if (c == '\t') context.out << "\\t";
+ else if (c == 0) context.out << "\\u0000";
+ else context.out << c;
+ }
+ }
+ else
+ context.out << *string_ptr(m_Storage);
+ context.out << '\"';
+ break;
+
+
+ case type_t::boolean:
+ if (*boolean_ptr(m_Storage))
+ context.out << "true";
+ else
+ context.out << "false";
+ break;
+
+ case type_t::number:
+ context.out << *number_ptr(m_Storage);
+ break;
+
+ default:
+ break;
+ }
+}
+
+struct value::parser
+{
+ parser(const char* begin, const char* end)
+ : m_Cursor(begin)
+ , m_End(end)
+ {
+ }
+
+ value parse()
+ {
+ value v;
+
+ // Switch to C locale to make strtod and strtol work as expected
+ auto previous_locale = std::setlocale(LC_NUMERIC, "C");
+
+ // Accept single value only when end of the stream is reached.
+ if (!accept_element(v) || !eof())
+ v = value(type_t::discarded);
+
+ if (previous_locale && strcmp(previous_locale, "C") != 0)
+ std::setlocale(LC_NUMERIC, previous_locale);
+
+ return v;
+ }
+
+private:
+ struct cursor_state
+ {
+ cursor_state(parser* p)
+ : m_Owner(p)
+ , m_LastCursor(p->m_Cursor)
+ {
+ }
+
+ void reset()
+ {
+ m_Owner->m_Cursor = m_LastCursor;
+ }
+
+ bool operator()(bool accept)
+ {
+ if (!accept)
+ reset();
+ else
+ m_LastCursor = m_Owner->m_Cursor;
+ return accept;
+ }
+
+ private:
+ parser* m_Owner;
+ const char* m_LastCursor;
+ };
+
+ cursor_state state()
+ {
+ return cursor_state(this);
+ }
+
+ bool accept_value(value& result)
+ {
+ return accept_object(result)
+ || accept_array(result)
+ || accept_string(result)
+ || accept_number(result)
+ || accept_boolean(result)
+ || accept_null(result);
+ }
+
+ bool accept_object(value& result)
+ {
+ auto s = state();
+
+ object o;
+ if (s(accept('{') && accept_ws() && accept('}')))
+ {
+ result = o;
+ return true;
+ }
+ else if (s(accept('{') && accept_members(o) && accept('}')))
+ {
+ result = std::move(o);
+ return true;
+ }
+
+ return false;
+ }
+
+ bool accept_members(object& o)
+ {
+ if (!accept_member(o))
+ return false;
+
+ while (true)
+ {
+ auto s = state();
+ if (!s(accept(',') && accept_member(o)))
+ break;
+ }
+
+ return true;
+ }
+
+ bool accept_member(object& o)
+ {
+ auto s = state();
+
+ value key;
+ value v;
+ if (s(accept_ws() && accept_string(key) && accept_ws() && accept(':') && accept_element(v)))
+ {
+ o.emplace(std::move(key.get<string>()), std::move(v));
+ return true;
+ }
+
+ return false;
+ }
+
+ bool accept_array(value& result)
+ {
+ auto s = state();
+
+ if (s(accept('[') && accept_ws() && accept(']')))
+ {
+ result = array();
+ return true;
+ }
+
+ array a;
+ if (s(accept('[') && accept_elements(a) && accept(']')))
+ {
+ result = std::move(a);
+ return true;
+ }
+
+ return false;
+ }
+
+ bool accept_elements(array& a)
+ {
+ value v;
+ if (!accept_element(v))
+ return false;
+
+ a.emplace_back(std::move(v));
+ while (true)
+ {
+ auto s = state();
+ v = nullptr;
+ if (!s(accept(',') && accept_element(v)))
+ break;
+ a.emplace_back(std::move(v));
+ }
+
+ return true;
+ }
+
+ bool accept_element(value& result)
+ {
+ auto s = state();
+ return s(accept_ws() && accept_value(result) && accept_ws());
+ }
+
+ bool accept_string(value& result)
+ {
+ auto s = state();
+
+ string v;
+ if (s(accept('\"') && accept_characters(v) && accept('\"')))
+ {
+ result = std::move(v);
+ return true;
+ }
+ else
+ return false;
+ }
+
+ bool accept_characters(string& result)
+ {
+ int c;
+ while (accept_character(c))
+ {
+ CRUDE_ASSERT(c < 128); // #todo: convert characters > 127 to UTF-8
+ result.push_back(static_cast<char>(c));
+ }
+
+ return true;
+ }
+
+ bool accept_character(int& c)
+ {
+ auto s = state();
+
+ if (accept('\\'))
+ {
+ return accept_escape(c);
+ }
+ else if (expect('\"'))
+ return false;
+
+ // #todo: Handle UTF-8 sequences.
+ return s((c = peek()) >= 0) && advance();
+ }
+
+ bool accept_escape(int& c)
+ {
+ if (accept('\"')) { c = '\"'; return true; }
+ if (accept('\\')) { c = '\\'; return true; }
+ if (accept('/')) { c = '/'; return true; }
+ if (accept('b')) { c = '\b'; return true; }
+ if (accept('f')) { c = '\f'; return true; }
+ if (accept('n')) { c = '\n'; return true; }
+ if (accept('r')) { c = '\r'; return true; }
+ if (accept('t')) { c = '\t'; return true; }
+
+ auto s = state();
+
+ string hex;
+ hex.reserve(4);
+ if (s(accept('u') && accept_hex(hex) && accept_hex(hex) && accept_hex(hex) && accept_hex(hex)))
+ {
+ char* end = nullptr;
+ auto v = std::strtol(hex.c_str(), &end, 16);
+ if (end != hex.c_str() + hex.size())
+ return false;
+
+ c = v;
+ return true;
+ }
+
+ return false;
+ }
+
+ bool accept_hex(string& result)
+ {
+ if (accept_digit(result))
+ return true;
+
+ auto c = peek();
+ if ((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))
+ {
+ advance();
+ result.push_back(static_cast<char>(c));
+ return true;
+ }
+
+ return false;
+ }
+
+ bool accept_number(value& result)
+ {
+ auto s = state();
+
+ string n;
+ if (s(accept_int(n) && accept_frac(n) && accept_exp(n)))
+ {
+ char* end = nullptr;
+ auto v = std::strtod(n.c_str(), &end);
+ if (end != n.c_str() + n.size())
+ return false;
+
+ if (v != 0 && !std::isnormal(v))
+ return false;
+
+ result = v;
+ return true;
+ }
+
+ return false;
+ }
+
+ bool accept_int(string& result)
+ {
+ auto s = state();
+
+ string part;
+ if (s(accept_onenine(part) && accept_digits(part)))
+ {
+ result += std::move(part);
+ return true;
+ }
+
+ part.resize(0);
+ if (accept_digit(part))
+ {
+ result += std::move(part);
+ return true;
+ }
+
+ part.resize(0);
+ if (s(accept('-') && accept_onenine(part) && accept_digits(part)))
+ {
+ result += '-';
+ result += std::move(part);
+ return true;
+ }
+
+ part.resize(0);
+ if (s(accept('-') && accept_digit(part)))
+ {
+ result += '-';
+ result += std::move(part);
+ return true;
+ }
+
+ return false;
+ }
+
+ bool accept_digits(string& result)
+ {
+ string part;
+ if (!accept_digit(part))
+ return false;
+
+ while (accept_digit(part))
+ ;
+
+ result += std::move(part);
+
+ return true;
+ }
+
+ bool accept_digit(string& result)
+ {
+ if (accept('0'))
+ {
+ result.push_back('0');
+ return true;
+ }
+ else if (accept_onenine(result))
+ return true;
+
+ return false;
+ }
+
+ bool accept_onenine(string& result)
+ {
+ auto c = peek();
+ if (c >= '1' && c <= '9')
+ {
+ result.push_back(static_cast<char>(c));
+ return advance();
+ }
+
+ return false;
+ }
+
+ bool accept_frac(string& result)
+ {
+ auto s = state();
+
+ string part;
+ if (s(accept('.') && accept_digits(part)))
+ {
+ result += '.';
+ result += std::move(part);
+ }
+
+ return true;
+ }
+
+ bool accept_exp(string& result)
+ {
+ auto s = state();
+
+ string part;
+ if (s(accept('e') && accept_sign(part) && accept_digits(part)))
+ {
+ result += 'e';
+ result += std::move(part);
+ return true;
+ }
+ part.resize(0);
+ if (s(accept('E') && accept_sign(part) && accept_digits(part)))
+ {
+ result += 'E';
+ result += std::move(part);
+ }
+
+ return true;
+ }
+
+ bool accept_sign(string& result)
+ {
+ if (accept('+'))
+ result.push_back('+');
+ else if (accept('-'))
+ result.push_back('-');
+
+ return true;
+ }
+
+ bool accept_ws()
+ {
+ while (expect('\x09') || expect('\x0A') || expect('\x0D') || expect('\x20'))
+ advance();
+ return true;
+ }
+
+ bool accept_boolean(value& result)
+ {
+ if (accept("true"))
+ {
+ result = true;
+ return true;
+ }
+ else if (accept("false"))
+ {
+ result = false;
+ return true;
+ }
+
+ return false;
+ }
+
+ bool accept_null(value& result)
+ {
+ if (accept("null"))
+ {
+ result = nullptr;
+ return true;
+ }
+
+ return false;
+ }
+
+ bool accept(char c)
+ {
+ if (expect(c))
+ return advance();
+ else
+ return false;
+ }
+
+ bool accept(const char* str)
+ {
+ auto last = m_Cursor;
+
+ while (*str)
+ {
+ if (eof() || *str != *m_Cursor)
+ {
+ m_Cursor = last;
+ return false;
+ }
+
+ advance();
+ ++str;
+ }
+
+ return true;
+ }
+
+ int peek() const
+ {
+ if (!eof())
+ return *m_Cursor;
+ else
+ return -1;
+ }
+
+ bool expect(char c)
+ {
+ return peek() == c;
+ }
+
+ bool advance(int count = 1)
+ {
+ if (m_Cursor + count > m_End)
+ {
+ m_Cursor = m_End;
+ return false;
+ }
+
+ m_Cursor += count;
+
+ return true;
+ }
+
+ bool eof() const
+ {
+ return m_Cursor == m_End;
+ }
+
+ const char* m_Cursor;
+ const char* m_End;
+};
+
+value value::parse(const string& data)
+{
+ auto p = parser(data.c_str(), data.c_str() + data.size());
+
+ auto v = p.parse();
+
+ return v;
+}
+
+} // namespace crude_json
diff --git a/3rdparty/imgui-node-editor/crude_json.h b/3rdparty/imgui-node-editor/crude_json.h
new file mode 100644
index 0000000..06eedd7
--- /dev/null
+++ b/3rdparty/imgui-node-editor/crude_json.h
@@ -0,0 +1,223 @@
+// Crude implementation of JSON value object and parser.
+//
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+# ifndef __CRUDE_JSON_H__
+# define __CRUDE_JSON_H__
+# pragma once
+
+# include <type_traits>
+# include <string>
+# include <vector>
+# include <map>
+# include <cstddef>
+# include <algorithm>
+# include <sstream>
+
+# ifndef CRUDE_ASSERT
+# include <cassert>
+# define CRUDE_ASSERT(expr) assert(expr)
+# endif
+
+namespace crude_json {
+
+struct value;
+
+using string = std::string;
+using object = std::map<string, value>;
+using array = std::vector<value>;
+using number = double;
+using boolean = bool;
+using null = std::nullptr_t;
+
+enum class type_t
+{
+ null,
+ object,
+ array,
+ string,
+ boolean,
+ number,
+ discarded
+};
+
+struct value
+{
+ value(type_t type = type_t::null): m_Type(construct(m_Storage, type)) {}
+ value(value&& other);
+ value(const value& other);
+
+ value( null) : m_Type(construct(m_Storage, null())) {}
+ value( object&& v): m_Type(construct(m_Storage, std::move(v))) {}
+ value(const object& v): m_Type(construct(m_Storage, v)) {}
+ value( array&& v): m_Type(construct(m_Storage, std::move(v))) {}
+ value(const array& v): m_Type(construct(m_Storage, v)) {}
+ value( string&& v): m_Type(construct(m_Storage, std::move(v))) {}
+ value(const string& v): m_Type(construct(m_Storage, v)) {}
+ value(const char* v): m_Type(construct(m_Storage, v)) {}
+ value( boolean v): m_Type(construct(m_Storage, v)) {}
+ value( number v): m_Type(construct(m_Storage, v)) {}
+ ~value() { destruct(m_Storage, m_Type); }
+
+ value& operator=(value&& other) { if (this != &other) { value(std::move(other)).swap(*this); } return *this; }
+ value& operator=(const value& other) { if (this != &other) { value( other).swap(*this); } return *this; }
+
+ value& operator=( null) { auto other = value( ); swap(other); return *this; }
+ value& operator=( object&& v) { auto other = value(std::move(v)); swap(other); return *this; }
+ value& operator=(const object& v) { auto other = value( v); swap(other); return *this; }
+ value& operator=( array&& v) { auto other = value(std::move(v)); swap(other); return *this; }
+ value& operator=(const array& v) { auto other = value( v); swap(other); return *this; }
+ value& operator=( string&& v) { auto other = value(std::move(v)); swap(other); return *this; }
+ value& operator=(const string& v) { auto other = value( v); swap(other); return *this; }
+ value& operator=(const char* v) { auto other = value( v); swap(other); return *this; }
+ value& operator=( boolean v) { auto other = value( v); swap(other); return *this; }
+ value& operator=( number v) { auto other = value( v); swap(other); return *this; }
+
+ type_t type() const { return m_Type; }
+
+ operator type_t() const { return m_Type; }
+
+ value& operator[](size_t index);
+ const value& operator[](size_t index) const;
+ value& operator[](const string& key);
+ const value& operator[](const string& key) const;
+
+ bool contains(const string& key) const;
+
+ void push_back(const value& value);
+ void push_back(value&& value);
+
+ bool is_primitive() const { return is_string() || is_number() || is_boolean() || is_null(); }
+ bool is_structured() const { return is_object() || is_array(); }
+ bool is_null() const { return m_Type == type_t::null; }
+ bool is_object() const { return m_Type == type_t::object; }
+ bool is_array() const { return m_Type == type_t::array; }
+ bool is_string() const { return m_Type == type_t::string; }
+ bool is_boolean() const { return m_Type == type_t::boolean; }
+ bool is_number() const { return m_Type == type_t::number; }
+ bool is_discarded() const { return m_Type == type_t::discarded; }
+
+ template <typename T> const T& get() const;
+ template <typename T> T& get();
+
+ string dump(const int indent = -1, const char indent_char = ' ') const;
+
+ void swap(value& other);
+
+ inline friend void swap(value& lhs, value& rhs) { lhs.swap(rhs); }
+
+ // Returns discarded value for invalid inputs.
+ static value parse(const string& data);
+
+private:
+ struct parser;
+
+ // VS2015: std::max() is not constexpr yet.
+# define CRUDE_MAX2(a, b) ((a) < (b) ? (b) : (a))
+# define CRUDE_MAX3(a, b, c) CRUDE_MAX2(CRUDE_MAX2(a, b), c)
+# define CRUDE_MAX4(a, b, c, d) CRUDE_MAX2(CRUDE_MAX3(a, b, c), d)
+# define CRUDE_MAX5(a, b, c, d, e) CRUDE_MAX2(CRUDE_MAX4(a, b, c, d), e)
+ enum
+ {
+ max_size = CRUDE_MAX5( sizeof(string), sizeof(object), sizeof(array), sizeof(number), sizeof(boolean)),
+ max_align = CRUDE_MAX5(alignof(string), alignof(object), alignof(array), alignof(number), alignof(boolean))
+ };
+# undef CRUDE_MAX5
+# undef CRUDE_MAX4
+# undef CRUDE_MAX3
+# undef CRUDE_MAX2
+ using storage_t = std::aligned_storage<max_size, max_align>::type;
+
+ static object* object_ptr( storage_t& storage) { return reinterpret_cast< object*>(&storage); }
+ static const object* object_ptr(const storage_t& storage) { return reinterpret_cast<const object*>(&storage); }
+ static array* array_ptr( storage_t& storage) { return reinterpret_cast< array*>(&storage); }
+ static const array* array_ptr(const storage_t& storage) { return reinterpret_cast<const array*>(&storage); }
+ static string* string_ptr( storage_t& storage) { return reinterpret_cast< string*>(&storage); }
+ static const string* string_ptr(const storage_t& storage) { return reinterpret_cast<const string*>(&storage); }
+ static boolean* boolean_ptr( storage_t& storage) { return reinterpret_cast< boolean*>(&storage); }
+ static const boolean* boolean_ptr(const storage_t& storage) { return reinterpret_cast<const boolean*>(&storage); }
+ static number* number_ptr( storage_t& storage) { return reinterpret_cast< number*>(&storage); }
+ static const number* number_ptr(const storage_t& storage) { return reinterpret_cast<const number*>(&storage); }
+
+ static type_t construct(storage_t& storage, type_t type)
+ {
+ switch (type)
+ {
+ case type_t::object: new (&storage) object(); break;
+ case type_t::array: new (&storage) array(); break;
+ case type_t::string: new (&storage) string(); break;
+ case type_t::boolean: new (&storage) boolean(); break;
+ case type_t::number: new (&storage) number(); break;
+ default: break;
+ }
+
+ return type;
+ }
+
+ static type_t construct(storage_t& storage, null) { (void)storage; return type_t::null; }
+ static type_t construct(storage_t& storage, object&& value) { new (&storage) object(std::forward<object>(value)); return type_t::object; }
+ static type_t construct(storage_t& storage, const object& value) { new (&storage) object(value); return type_t::object; }
+ static type_t construct(storage_t& storage, array&& value) { new (&storage) array(std::forward<array>(value)); return type_t::array; }
+ static type_t construct(storage_t& storage, const array& value) { new (&storage) array(value); return type_t::array; }
+ static type_t construct(storage_t& storage, string&& value) { new (&storage) string(std::forward<string>(value)); return type_t::string; }
+ static type_t construct(storage_t& storage, const string& value) { new (&storage) string(value); return type_t::string; }
+ static type_t construct(storage_t& storage, const char* value) { new (&storage) string(value); return type_t::string; }
+ static type_t construct(storage_t& storage, boolean value) { new (&storage) boolean(value); return type_t::boolean; }
+ static type_t construct(storage_t& storage, number value) { new (&storage) number(value); return type_t::number; }
+
+ static void destruct(storage_t& storage, type_t type)
+ {
+ switch (type)
+ {
+ case type_t::object: object_ptr(storage)->~object(); break;
+ case type_t::array: array_ptr(storage)->~array(); break;
+ case type_t::string: string_ptr(storage)->~string(); break;
+ default: break;
+ }
+ }
+
+ struct dump_context_t
+ {
+ std::ostringstream out;
+ const int indent = -1;
+ const char indent_char = ' ';
+
+ // VS2015: Aggregate initialization isn't a thing yet.
+ dump_context_t(const int indent, const char indent_char)
+ : indent(indent)
+ , indent_char(indent_char)
+ {
+ }
+
+ void write_indent(int level);
+ void write_separator();
+ void write_newline();
+ };
+
+ void dump(dump_context_t& context, int level) const;
+
+ storage_t m_Storage;
+ type_t m_Type;
+};
+
+template <> inline const object& value::get<object>() const { CRUDE_ASSERT(m_Type == type_t::object); return *object_ptr(m_Storage); }
+template <> inline const array& value::get<array>() const { CRUDE_ASSERT(m_Type == type_t::array); return *array_ptr(m_Storage); }
+template <> inline const string& value::get<string>() const { CRUDE_ASSERT(m_Type == type_t::string); return *string_ptr(m_Storage); }
+template <> inline const boolean& value::get<boolean>() const { CRUDE_ASSERT(m_Type == type_t::boolean); return *boolean_ptr(m_Storage); }
+template <> inline const number& value::get<number>() const { CRUDE_ASSERT(m_Type == type_t::number); return *number_ptr(m_Storage); }
+
+template <> inline object& value::get<object>() { CRUDE_ASSERT(m_Type == type_t::object); return *object_ptr(m_Storage); }
+template <> inline array& value::get<array>() { CRUDE_ASSERT(m_Type == type_t::array); return *array_ptr(m_Storage); }
+template <> inline string& value::get<string>() { CRUDE_ASSERT(m_Type == type_t::string); return *string_ptr(m_Storage); }
+template <> inline boolean& value::get<boolean>() { CRUDE_ASSERT(m_Type == type_t::boolean); return *boolean_ptr(m_Storage); }
+template <> inline number& value::get<number>() { CRUDE_ASSERT(m_Type == type_t::number); return *number_ptr(m_Storage); }
+
+
+} // namespace crude_json
+
+# endif // __CRUDE_JSON_H__ \ No newline at end of file
diff --git a/3rdparty/imgui-node-editor/imgui_bezier_math.h b/3rdparty/imgui-node-editor/imgui_bezier_math.h
new file mode 100644
index 0000000..09b76e7
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_bezier_math.h
@@ -0,0 +1,142 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# ifndef __IMGUI_BEZIER_MATH_H__
+# define __IMGUI_BEZIER_MATH_H__
+# pragma once
+
+
+//------------------------------------------------------------------------------
+# include "imgui_extra_math.h"
+
+
+//------------------------------------------------------------------------------
+template <typename T>
+struct ImCubicBezierPointsT
+{
+ T P0;
+ T P1;
+ T P2;
+ T P3;
+};
+using ImCubicBezierPoints = ImCubicBezierPointsT<ImVec2>;
+
+
+//------------------------------------------------------------------------------
+// Low-level Bezier curve sampling.
+template <typename T> inline T ImLinearBezier(const T& p0, const T& p1, float t);
+template <typename T> inline T ImLinearBezierDt(const T& p0, const T& p1, float t);
+template <typename T> inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t);
+template <typename T> inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t);
+template <typename T> inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t);
+template <typename T> inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t);
+
+
+// High-level Bezier sampling, automatically collapse to lower level Bezier curves if control points overlap.
+template <typename T> inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t);
+template <typename T> inline T ImCubicBezierSample(const ImCubicBezierPointsT<T>& curve, float t);
+template <typename T> inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t);
+template <typename T> inline T ImCubicBezierTangent(const ImCubicBezierPointsT<T>& curve, float t);
+
+
+// Calculate approximate length of Cubic Bezier curve.
+template <typename T> inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3);
+template <typename T> inline float ImCubicBezierLength(const ImCubicBezierPointsT<T>& curve);
+
+
+// Splits Cubic Bezier curve into two curves.
+template <typename T>
+struct ImCubicBezierSplitResultT
+{
+ ImCubicBezierPointsT<T> Left;
+ ImCubicBezierPointsT<T> Right;
+};
+using ImCubicBezierSplitResult = ImCubicBezierSplitResultT<ImVec2>;
+
+template <typename T> inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t);
+template <typename T> inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const ImCubicBezierPointsT<T>& curve, float t);
+
+
+// Returns bounding rectangle of Cubic Bezier curve.
+inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3);
+inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve);
+
+
+// Project point on Cubic Bezier curve.
+struct ImProjectResult
+{
+ ImVec2 Point; // Point on curve
+ float Time; // [0 - 1]
+ float Distance; // Distance to curve
+};
+
+inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions = 100);
+inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions = 100);
+
+
+// Calculate intersection between line and a Cubic Bezier curve.
+struct ImCubicBezierIntersectResult
+{
+ int Count;
+ ImVec2 Points[3];
+};
+
+inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1);
+inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line);
+
+
+// Adaptive Cubic Bezier subdivision.
+enum ImCubicBezierSubdivideFlags
+{
+ ImCubicBezierSubdivide_None = 0,
+ ImCubicBezierSubdivide_SkipFirst = 1
+};
+
+struct ImCubicBezierSubdivideSample
+{
+ ImVec2 Point;
+ ImVec2 Tangent;
+};
+
+using ImCubicBezierSubdivideCallback = void (*)(const ImCubicBezierSubdivideSample& p, void* user_pointer);
+
+inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
+inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
+
+
+// F has signature void(const ImCubicBezierSubdivideSample& p)
+template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
+template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
+
+// Fixed step Cubic Bezier subdivision.
+struct ImCubicBezierFixedStepSample
+{
+ float T;
+ float Length;
+ ImVec2 Point;
+ bool BreakSearch;
+};
+
+using ImCubicBezierFixedStepCallback = void (*)(ImCubicBezierFixedStepSample& sample, void* user_pointer);
+
+inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
+inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
+
+
+// F has signature void(const ImCubicBezierFixedStepSample& p)
+template <typename F> inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
+template <typename F> inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
+
+
+//------------------------------------------------------------------------------
+# include "imgui_bezier_math.inl"
+
+
+//------------------------------------------------------------------------------
+# endif // __IMGUI_BEZIER_MATH_H__
diff --git a/3rdparty/imgui-node-editor/imgui_bezier_math.inl b/3rdparty/imgui-node-editor/imgui_bezier_math.inl
new file mode 100644
index 0000000..c2c7c43
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_bezier_math.inl
@@ -0,0 +1,670 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# ifndef __IMGUI_BEZIER_MATH_INL__
+# define __IMGUI_BEZIER_MATH_INL__
+# pragma once
+
+
+//------------------------------------------------------------------------------
+# include "imgui_bezier_math.h"
+# include <map> // used in ImCubicBezierFixedStep
+
+
+//------------------------------------------------------------------------------
+template <typename T>
+inline T ImLinearBezier(const T& p0, const T& p1, float t)
+{
+ return p0 + t * (p1 - p0);
+}
+
+template <typename T>
+inline T ImLinearBezierDt(const T& p0, const T& p1, float t)
+{
+ IM_UNUSED(t);
+
+ return p1 - p0;
+}
+
+template <typename T>
+inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t)
+{
+ const auto a = 1 - t;
+
+ return a * a * p0 + 2 * t * a * p1 + t * t * p2;
+}
+
+template <typename T>
+inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t)
+{
+ return 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1);
+}
+
+template <typename T>
+inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t)
+{
+ const auto a = 1 - t;
+ const auto b = a * a * a;
+ const auto c = t * t * t;
+
+ return b * p0 + 3 * t * a * a * p1 + 3 * t * t * a * p2 + c * p3;
+}
+
+template <typename T>
+inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t)
+{
+ const auto a = 1 - t;
+ const auto b = a * a;
+ const auto c = t * t;
+ const auto d = 2 * t * a;
+
+ return -3 * p0 * b + 3 * p1 * (b - d) + 3 * p2 * (d - c) + 3 * p3 * c;
+}
+
+template <typename T>
+inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t)
+{
+ const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f;
+ const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f;
+
+ if (cp0_zero && cp1_zero)
+ return ImLinearBezier(p0, p3, t);
+ else if (cp0_zero)
+ return ImQuadraticBezier(p0, p2, p3, t);
+ else if (cp1_zero)
+ return ImQuadraticBezier(p0, p1, p3, t);
+ else
+ return ImCubicBezier(p0, p1, p2, p3, t);
+}
+
+template <typename T>
+inline T ImCubicBezierSample(const ImCubicBezierPointsT<T>& curve, float t)
+{
+ return ImCubicBezierSample(curve.P0, curve.P1, curve.P2, curve.P3, t);
+}
+
+template <typename T>
+inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t)
+{
+ const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f;
+ const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f;
+
+ if (cp0_zero && cp1_zero)
+ return ImLinearBezierDt(p0, p3, t);
+ else if (cp0_zero)
+ return ImQuadraticBezierDt(p0, p2, p3, t);
+ else if (cp1_zero)
+ return ImQuadraticBezierDt(p0, p1, p3, t);
+ else
+ return ImCubicBezierDt(p0, p1, p2, p3, t);
+}
+
+template <typename T>
+inline T ImCubicBezierTangent(const ImCubicBezierPointsT<T>& curve, float t)
+{
+ return ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, t);
+}
+
+template <typename T>
+inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3)
+{
+ // Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x))
+ static const float t_values[] =
+ {
+ -0.0640568928626056260850430826247450385909f,
+ 0.0640568928626056260850430826247450385909f,
+ -0.1911188674736163091586398207570696318404f,
+ 0.1911188674736163091586398207570696318404f,
+ -0.3150426796961633743867932913198102407864f,
+ 0.3150426796961633743867932913198102407864f,
+ -0.4337935076260451384870842319133497124524f,
+ 0.4337935076260451384870842319133497124524f,
+ -0.5454214713888395356583756172183723700107f,
+ 0.5454214713888395356583756172183723700107f,
+ -0.6480936519369755692524957869107476266696f,
+ 0.6480936519369755692524957869107476266696f,
+ -0.7401241915785543642438281030999784255232f,
+ 0.7401241915785543642438281030999784255232f,
+ -0.8200019859739029219539498726697452080761f,
+ 0.8200019859739029219539498726697452080761f,
+ -0.8864155270044010342131543419821967550873f,
+ 0.8864155270044010342131543419821967550873f,
+ -0.9382745520027327585236490017087214496548f,
+ 0.9382745520027327585236490017087214496548f,
+ -0.9747285559713094981983919930081690617411f,
+ 0.9747285559713094981983919930081690617411f,
+ -0.9951872199970213601799974097007368118745f,
+ 0.9951872199970213601799974097007368118745f
+ };
+
+ // Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article)
+ static const float c_values[] =
+ {
+ 0.1279381953467521569740561652246953718517f,
+ 0.1279381953467521569740561652246953718517f,
+ 0.1258374563468282961213753825111836887264f,
+ 0.1258374563468282961213753825111836887264f,
+ 0.1216704729278033912044631534762624256070f,
+ 0.1216704729278033912044631534762624256070f,
+ 0.1155056680537256013533444839067835598622f,
+ 0.1155056680537256013533444839067835598622f,
+ 0.1074442701159656347825773424466062227946f,
+ 0.1074442701159656347825773424466062227946f,
+ 0.0976186521041138882698806644642471544279f,
+ 0.0976186521041138882698806644642471544279f,
+ 0.0861901615319532759171852029837426671850f,
+ 0.0861901615319532759171852029837426671850f,
+ 0.0733464814110803057340336152531165181193f,
+ 0.0733464814110803057340336152531165181193f,
+ 0.0592985849154367807463677585001085845412f,
+ 0.0592985849154367807463677585001085845412f,
+ 0.0442774388174198061686027482113382288593f,
+ 0.0442774388174198061686027482113382288593f,
+ 0.0285313886289336631813078159518782864491f,
+ 0.0285313886289336631813078159518782864491f,
+ 0.0123412297999871995468056670700372915759f,
+ 0.0123412297999871995468056670700372915759f
+ };
+
+ static_assert(sizeof(t_values) / sizeof(*t_values) == sizeof(c_values) / sizeof(*c_values), "");
+
+ auto arc = [p0, p1, p2, p3](float t)
+ {
+ const auto p = ImCubicBezierDt(p0, p1, p2, p3, t);
+ const auto l = ImLength(p);
+ return l;
+ };
+
+ const auto z = 0.5f;
+ const auto n = sizeof(t_values) / sizeof(*t_values);
+
+ auto accumulator = 0.0f;
+ for (size_t i = 0; i < n; ++i)
+ {
+ const auto t = z * t_values[i] + z;
+ accumulator += c_values[i] * arc(t);
+ }
+
+ return z * accumulator;
+}
+
+template <typename T>
+inline float ImCubicBezierLength(const ImCubicBezierPointsT<T>& curve)
+{
+ return ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3);
+}
+
+template <typename T>
+inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t)
+{
+ const auto z1 = t;
+ const auto z2 = z1 * z1;
+ const auto z3 = z1 * z1 * z1;
+ const auto s1 = z1 - 1;
+ const auto s2 = s1 * s1;
+ const auto s3 = s1 * s1 * s1;
+
+ return ImCubicBezierSplitResultT<T>
+ {
+ ImCubicBezierPointsT<T>
+ {
+ p0,
+ z1 * p1 - s1 * p0,
+ z2 * p2 - 2 * z1 * s1 * p1 + s2 * p0,
+ z3 * p3 - 3 * z2 * s1 * p2 + 3 * z1 * s2 * p1 - s3 * p0
+ },
+ ImCubicBezierPointsT<T>
+ {
+ z3 * p0 - 3 * z2 * s1 * p1 + 3 * z1 * s2 * p2 - s3 * p3,
+ z2 * p1 - 2 * z1 * s1 * p2 + s2 * p3,
+ z1 * p2 - s1 * p3,
+ p3,
+ }
+ };
+}
+
+template <typename T>
+inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const ImCubicBezierPointsT<T>& curve, float t)
+{
+ return ImCubicBezierSplit(curve.P0, curve.P1, curve.P2, curve.P3, t);
+}
+
+inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3)
+{
+ auto a = 3 * p3 - 9 * p2 + 9 * p1 - 3 * p0;
+ auto b = 6 * p0 - 12 * p1 + 6 * p2;
+ auto c = 3 * p1 - 3 * p0;
+ auto delta_squared = ImMul(b, b) - 4 * ImMul(a, c);
+
+ auto tl = ImMin(p0, p3);
+ auto rb = ImMax(p0, p3);
+
+# define IM_VEC2_INDEX(v, i) *(&v.x + i)
+
+ for (int i = 0; i < 2; ++i)
+ {
+ if (IM_VEC2_INDEX(delta_squared, i) >= 0)
+ {
+ auto delta = ImSqrt(IM_VEC2_INDEX(delta_squared, i));
+
+ auto t0 = (-IM_VEC2_INDEX(b, i) + delta) / (2 * IM_VEC2_INDEX(a, i));
+ if (t0 > 0 && t0 < 1)
+ {
+ auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t0);
+ IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p);
+ IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p);
+ }
+
+ auto t1 = (-IM_VEC2_INDEX(b, i) - delta) / (2 * IM_VEC2_INDEX(a, i));
+ if (t1 > 0 && t1 < 1)
+ {
+ auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t1);
+ IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p);
+ IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p);
+ }
+ }
+ }
+
+# undef IM_VEC2_INDEX
+
+ return ImRect(tl, rb);
+}
+
+inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve)
+{
+ return ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3);
+}
+
+inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& point, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions)
+{
+ // http://pomax.github.io/bezierinfo/#projections
+
+ const float epsilon = 1e-5f;
+ const float fixed_step = 1.0f / static_cast<float>(subdivisions - 1);
+
+ ImProjectResult result;
+ result.Point = point;
+ result.Time = 0.0f;
+ result.Distance = FLT_MAX;
+
+ // Step 1: Coarse check
+ for (int i = 0; i < subdivisions; ++i)
+ {
+ auto t = i * fixed_step;
+ auto p = ImCubicBezier(p0, p1, p2, p3, t);
+ auto s = point - p;
+ auto d = ImDot(s, s);
+
+ if (d < result.Distance)
+ {
+ result.Point = p;
+ result.Time = t;
+ result.Distance = d;
+ }
+ }
+
+ if (result.Time == 0.0f || ImFabs(result.Time - 1.0f) <= epsilon)
+ {
+ result.Distance = ImSqrt(result.Distance);
+ return result;
+ }
+
+ // Step 2: Fine check
+ auto left = result.Time - fixed_step;
+ auto right = result.Time + fixed_step;
+ auto step = fixed_step * 0.1f;
+
+ for (auto t = left; t < right + step; t += step)
+ {
+ auto p = ImCubicBezier(p0, p1, p2, p3, t);
+ auto s = point - p;
+ auto d = ImDot(s, s);
+
+ if (d < result.Distance)
+ {
+ result.Point = p;
+ result.Time = t;
+ result.Distance = d;
+ }
+ }
+
+ result.Distance = ImSqrt(result.Distance);
+
+ return result;
+}
+
+inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions)
+{
+ return ImProjectOnCubicBezier(p, curve.P0, curve.P1, curve.P2, curve.P3, subdivisions);
+}
+
+inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1)
+{
+ auto cubic_roots = [](float a, float b, float c, float d, float* roots) -> int
+ {
+ int count = 0;
+
+ auto sign = [](float x) -> float { return x < 0 ? -1.0f : 1.0f; };
+
+ auto A = b / a;
+ auto B = c / a;
+ auto C = d / a;
+
+ auto Q = (3 * B - ImPow(A, 2)) / 9;
+ auto R = (9 * A * B - 27 * C - 2 * ImPow(A, 3)) / 54;
+ auto D = ImPow(Q, 3) + ImPow(R, 2); // polynomial discriminant
+
+ if (D >= 0) // complex or duplicate roots
+ {
+ auto S = sign(R + ImSqrt(D)) * ImPow(ImFabs(R + ImSqrt(D)), (1.0f / 3.0f));
+ auto T = sign(R - ImSqrt(D)) * ImPow(ImFabs(R - ImSqrt(D)), (1.0f / 3.0f));
+
+ roots[0] = -A / 3 + (S + T); // real root
+ roots[1] = -A / 3 - (S + T) / 2; // real part of complex root
+ roots[2] = -A / 3 - (S + T) / 2; // real part of complex root
+ auto Im = ImFabs(ImSqrt(3) * (S - T) / 2); // complex part of root pair
+
+ // discard complex roots
+ if (Im != 0)
+ count = 1;
+ else
+ count = 3;
+ }
+ else // distinct real roots
+ {
+ auto th = ImAcos(R / ImSqrt(-ImPow(Q, 3)));
+
+ roots[0] = 2 * ImSqrt(-Q) * ImCos(th / 3) - A / 3;
+ roots[1] = 2 * ImSqrt(-Q) * ImCos((th + 2 * IM_PI) / 3) - A / 3;
+ roots[2] = 2 * ImSqrt(-Q) * ImCos((th + 4 * IM_PI) / 3) - A / 3;
+
+ count = 3;
+ }
+
+ return count;
+ };
+
+ // https://github.com/kaishiqi/Geometric-Bezier/blob/master/GeometricBezier/src/kaishiqi/geometric/intersection/Intersection.as
+ //
+ // Start with Bezier using Bernstein polynomials for weighting functions:
+ // (1-t^3)P0 + 3t(1-t)^2P1 + 3t^2(1-t)P2 + t^3P3
+ //
+ // Expand and collect terms to form linear combinations of original Bezier
+ // controls. This ends up with a vector cubic in t:
+ // (-P0+3P1-3P2+P3)t^3 + (3P0-6P1+3P2)t^2 + (-3P0+3P1)t + P0
+ // /\ /\ /\ /\
+ // || || || ||
+ // c3 c2 c1 c0
+
+ // Calculate the coefficients
+ auto c3 = -p0 + 3 * p1 - 3 * p2 + p3;
+ auto c2 = 3 * p0 - 6 * p1 + 3 * p2;
+ auto c1 = -3 * p0 + 3 * p1;
+ auto c0 = p0;
+
+ // Convert line to normal form: ax + by + c = 0
+ auto a = a1.y - a0.y;
+ auto b = a0.x - a1.x;
+ auto c = a0.x * (a0.y - a1.y) + a0.y * (a1.x - a0.x);
+
+ // Rotate each cubic coefficient using line for new coordinate system?
+ // Find roots of rotated cubic
+ float roots[3];
+ auto rootCount = cubic_roots(
+ a * c3.x + b * c3.y,
+ a * c2.x + b * c2.y,
+ a * c1.x + b * c1.y,
+ a * c0.x + b * c0.y + c,
+ roots);
+
+ // Any roots in closed interval [0,1] are intersections on Bezier, but
+ // might not be on the line segment.
+ // Find intersections and calculate point coordinates
+
+ auto min = ImMin(a0, a1);
+ auto max = ImMax(a0, a1);
+
+ ImCubicBezierIntersectResult result;
+ auto points = result.Points;
+
+ for (int i = 0; i < rootCount; ++i)
+ {
+ auto root = roots[i];
+
+ if (0 <= root && root <= 1)
+ {
+ // We're within the Bezier curve
+ // Find point on Bezier
+ auto p = ImCubicBezier(p0, p1, p2, p3, root);
+
+ // See if point is on line segment
+ // Had to make special cases for vertical and horizontal lines due
+ // to slight errors in calculation of p00
+ if (a0.x == a1.x)
+ {
+ if (min.y <= p.y && p.y <= max.y)
+ *points++ = p;
+ }
+ else if (a0.y == a1.y)
+ {
+ if (min.x <= p.x && p.x <= max.x)
+ *points++ = p;
+ }
+ else if (p.x >= min.x && p.y >= min.y && p.x <= max.x && p.y <= max.y)
+ {
+ *points++ = p;
+ }
+ }
+ }
+
+ result.Count = static_cast<int>(points - result.Points);
+
+ return result;
+}
+
+inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line)
+{
+ return ImCubicBezierLineIntersect(curve.P0, curve.P1, curve.P2, curve.P3, line.A, line.B);
+}
+
+inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags)
+{
+ return ImCubicBezierSubdivide(callback, user_pointer, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags);
+}
+
+inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags)
+{
+ struct Tesselator
+ {
+ ImCubicBezierSubdivideCallback Callback;
+ void* UserPointer;
+ float TesselationTollerance;
+ ImCubicBezierSubdivideFlags Flags;
+
+ void Commit(const ImVec2& p, const ImVec2& t)
+ {
+ ImCubicBezierSubdivideSample sample;
+ sample.Point = p;
+ sample.Tangent = t;
+ Callback(sample, UserPointer);
+ }
+
+ void Subdivide(const ImCubicBezierPoints& curve, int level = 0)
+ {
+ float dx = curve.P3.x - curve.P0.x;
+ float dy = curve.P3.y - curve.P0.y;
+ float d2 = ((curve.P1.x - curve.P3.x) * dy - (curve.P1.y - curve.P3.y) * dx);
+ float d3 = ((curve.P2.x - curve.P3.x) * dy - (curve.P2.y - curve.P3.y) * dx);
+ d2 = (d2 >= 0) ? d2 : -d2;
+ d3 = (d3 >= 0) ? d3 : -d3;
+ if ((d2 + d3) * (d2 + d3) < TesselationTollerance * (dx * dx + dy * dy))
+ {
+ Commit(curve.P3, ImCubicBezierTangent(curve, 1.0f));
+ }
+ else if (level < 10)
+ {
+ const auto p12 = (curve.P0 + curve.P1) * 0.5f;
+ const auto p23 = (curve.P1 + curve.P2) * 0.5f;
+ const auto p34 = (curve.P2 + curve.P3) * 0.5f;
+ const auto p123 = (p12 + p23) * 0.5f;
+ const auto p234 = (p23 + p34) * 0.5f;
+ const auto p1234 = (p123 + p234) * 0.5f;
+
+ Subdivide(ImCubicBezierPoints { curve.P0, p12, p123, p1234 }, level + 1);
+ Subdivide(ImCubicBezierPoints { p1234, p234, p34, curve.P3 }, level + 1);
+ }
+ }
+ };
+
+ if (tess_tol < 0)
+ tess_tol = 1.118f; // sqrtf(1.25f)
+
+ Tesselator tesselator;
+ tesselator.Callback = callback;
+ tesselator.UserPointer = user_pointer;
+ tesselator.TesselationTollerance = tess_tol * tess_tol;
+ tesselator.Flags = flags;
+
+ if (!(tesselator.Flags & ImCubicBezierSubdivide_SkipFirst))
+ tesselator.Commit(curve.P0, ImCubicBezierTangent(curve, 0.0f));
+
+ tesselator.Subdivide(curve, 0);
+}
+
+template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags)
+{
+ auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer)
+ {
+ auto& callback = *reinterpret_cast<F*>(user_pointer);
+ callback(p);
+ };
+
+ ImCubicBezierSubdivide(handler, &callback, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags);
+}
+
+template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags)
+{
+ auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer)
+ {
+ auto& callback = *reinterpret_cast<F*>(user_pointer);
+ callback(p);
+ };
+
+ ImCubicBezierSubdivide(handler, &callback, curve, tess_tol, flags);
+}
+
+inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error)
+{
+ if (step <= 0.0f || !callback || max_value_error <= 0 || max_t_error <= 0)
+ return;
+
+ ImCubicBezierFixedStepSample sample;
+ sample.T = 0.0f;
+ sample.Length = 0.0f;
+ sample.Point = p0;
+ sample.BreakSearch = false;
+
+ callback(sample, user_pointer);
+ if (sample.BreakSearch)
+ return;
+
+ const auto total_length = ImCubicBezierLength(p0, p1, p2, p3);
+ const auto point_count = static_cast<int>(total_length / step) + (overshoot ? 2 : 1);
+ const auto t_min = 0.0f;
+ const auto t_max = step * point_count / total_length;
+ const auto t_0 = (t_min + t_max) * 0.5f;
+
+ // #todo: replace map with ImVector + binary search
+ std::map<float, float> cache;
+ for (int point_index = 1; point_index < point_count; ++point_index)
+ {
+ const auto targetLength = point_index * step;
+
+ float t_start = t_min;
+ float t_end = t_max;
+ float t = t_0;
+
+ float t_best = t;
+ float error_best = total_length;
+
+ while (true)
+ {
+ auto cacheIt = cache.find(t);
+ if (cacheIt == cache.end())
+ {
+ const auto front = ImCubicBezierSplit(p0, p1, p2, p3, t).Left;
+ const auto split_length = ImCubicBezierLength(front);
+
+ cacheIt = cache.emplace(t, split_length).first;
+ }
+
+ const auto length = cacheIt->second;
+ const auto error = targetLength - length;
+
+ if (error < error_best)
+ {
+ error_best = error;
+ t_best = t;
+ }
+
+ if (ImFabs(error) <= max_value_error || ImFabs(t_start - t_end) <= max_t_error)
+ {
+ sample.T = t;
+ sample.Length = length;
+ sample.Point = ImCubicBezier(p0, p1, p2, p3, t);
+
+ callback(sample, user_pointer);
+ if (sample.BreakSearch)
+ return;
+
+ break;
+ }
+ else if (error < 0.0f)
+ t_end = t;
+ else // if (error > 0.0f)
+ t_start = t;
+
+ t = (t_start + t_end) * 0.5f;
+ }
+ }
+}
+
+inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error)
+{
+ ImCubicBezierFixedStep(callback, user_pointer, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error);
+}
+
+// F has signature void(const ImCubicBezierFixedStepSample& p)
+template <typename F>
+inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error)
+{
+ auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer)
+ {
+ auto& callback = *reinterpret_cast<F*>(user_pointer);
+ callback(sample);
+ };
+
+ ImCubicBezierFixedStep(handler, &callback, p0, p1, p2, p3, step, overshoot, max_value_error, max_t_error);
+}
+
+template <typename F>
+inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error)
+{
+ auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer)
+ {
+ auto& callback = *reinterpret_cast<F*>(user_pointer);
+ callback(sample);
+ };
+
+ ImCubicBezierFixedStep(handler, &callback, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error);
+}
+
+
+//------------------------------------------------------------------------------
+# endif // __IMGUI_BEZIER_MATH_INL__
diff --git a/3rdparty/imgui-node-editor/imgui_canvas.cpp b/3rdparty/imgui-node-editor/imgui_canvas.cpp
new file mode 100644
index 0000000..df63a40
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_canvas.cpp
@@ -0,0 +1,513 @@
+# define IMGUI_DEFINE_MATH_OPERATORS
+# include "imgui_canvas.h"
+# include <type_traits>
+
+// https://stackoverflow.com/a/36079786
+# define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \
+ \
+ template <typename __boost_has_member_T__> \
+ class __trait_name__ \
+ { \
+ using check_type = ::std::remove_const_t<__boost_has_member_T__>; \
+ struct no_type {char x[2];}; \
+ using yes_type = char; \
+ \
+ struct base { void __member_name__() {}}; \
+ struct mixin : public base, public check_type {}; \
+ \
+ template <void (base::*)()> struct aux {}; \
+ \
+ template <typename U> static no_type test(aux<&U::__member_name__>*); \
+ template <typename U> static yes_type test(...); \
+ \
+ public: \
+ \
+ static constexpr bool value = (sizeof(yes_type) == sizeof(test<mixin>(0))); \
+ }
+
+namespace ImCanvasDetails {
+
+DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale);
+
+struct FringeScaleRef
+{
+ // Overload is present when ImDrawList does have _FringeScale member variable.
+ template <typename T>
+ static float& Get(typename std::enable_if<HasFringeScale<T>::value, T>::type* drawList)
+ {
+ return drawList->_FringeScale;
+ }
+
+ // Overload is present when ImDrawList does not have _FringeScale member variable.
+ template <typename T>
+ static float& Get(typename std::enable_if<!HasFringeScale<T>::value, T>::type*)
+ {
+ static float placeholder = 1.0f;
+ return placeholder;
+ }
+};
+
+DECLARE_HAS_MEMBER(HasVtxCurrentOffset, _VtxCurrentOffset);
+
+struct VtxCurrentOffsetRef
+{
+ // Overload is present when ImDrawList does have _FringeScale member variable.
+ template <typename T>
+ static unsigned int& Get(typename std::enable_if<HasVtxCurrentOffset<T>::value, T>::type* drawList)
+ {
+ return drawList->_VtxCurrentOffset;
+ }
+
+ // Overload is present when ImDrawList does not have _FringeScale member variable.
+ template <typename T>
+ static unsigned int& Get(typename std::enable_if<!HasVtxCurrentOffset<T>::value, T>::type* drawList)
+ {
+ return drawList->_CmdHeader.VtxOffset;
+ }
+};
+
+} // namespace ImCanvasDetails
+
+// Returns a reference to _FringeScale extension to ImDrawList
+//
+// If ImDrawList does not have _FringeScale a placeholder is returned.
+static inline float& ImFringeScaleRef(ImDrawList* drawList)
+{
+ using namespace ImCanvasDetails;
+ return FringeScaleRef::Get<ImDrawList>(drawList);
+}
+
+static inline unsigned int& ImVtxOffsetRef(ImDrawList* drawList)
+{
+ using namespace ImCanvasDetails;
+ return VtxCurrentOffsetRef::Get<ImDrawList>(drawList);
+}
+
+static inline ImVec2 ImSelectPositive(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x > 0.0f ? lhs.x : rhs.x, lhs.y > 0.0f ? lhs.y : rhs.y); }
+
+bool ImGuiEx::Canvas::Begin(const char* id, const ImVec2& size)
+{
+ return Begin(ImGui::GetID(id), size);
+}
+
+bool ImGuiEx::Canvas::Begin(ImGuiID id, const ImVec2& size)
+{
+ IM_ASSERT(m_InBeginEnd == false);
+
+ m_WidgetPosition = ImGui::GetCursorScreenPos();
+ m_WidgetSize = ImSelectPositive(size, ImGui::GetContentRegionAvail());
+ m_WidgetRect = ImRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize);
+ m_DrawList = ImGui::GetWindowDrawList();
+
+ UpdateViewTransformPosition();
+
+ if (ImGui::IsClippedEx(m_WidgetRect, id, false))
+ return false;
+
+ // Save current channel, so we can assert when user
+ // call canvas API with different one.
+ m_ExpectedChannel = m_DrawList->_Splitter._Current;
+
+ // #debug: Canvas content.
+ //m_DrawList->AddRectFilled(m_StartPos, m_StartPos + m_CurrentSize, IM_COL32(0, 0, 0, 64));
+ m_DrawList->AddRect(m_WidgetRect.Min, m_WidgetRect.Max, IM_COL32(255, 0, 255, 64));
+
+ ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f));
+
+# if IMGUI_EX_CANVAS_DEFERED()
+ m_Ranges.resize(0);
+# endif
+
+ SaveInputState();
+ SaveViewportState();
+
+ EnterLocalSpace();
+
+ // Emit dummy widget matching bounds of the canvas.
+ ImGui::SetCursorScreenPos(m_ViewRect.Min);
+ ImGui::Dummy(m_ViewRect.GetSize());
+
+ ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f));
+
+ m_InBeginEnd = true;
+
+ return true;
+}
+
+void ImGuiEx::Canvas::End()
+{
+ // If you're here your call to Begin() returned false,
+ // or Begin() wasn't called at all.
+ IM_ASSERT(m_InBeginEnd == true);
+
+ // If you're here, please make sure you do not interleave
+ // channel splitter with canvas.
+ // Always call canvas function with using same channel.
+ IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
+
+ //auto& io = ImGui::GetIO();
+
+ // Check: Unmatched calls to Suspend() / Resume(). Please check your code.
+ IM_ASSERT(m_SuspendCounter == 0);
+
+ LeaveLocalSpace();
+
+ // Emit dummy widget matching bounds of the canvas.
+ ImGui::SetCursorScreenPos(m_WidgetPosition);
+ ImGui::Dummy(m_WidgetSize);
+
+ // #debug: Rect around canvas. Content should be inside these bounds.
+ //m_DrawList->AddRect(m_WidgetPosition - ImVec2(1.0f, 1.0f), m_WidgetPosition + m_WidgetSize + ImVec2(1.0f, 1.0f), IM_COL32(196, 0, 0, 255));
+
+ m_InBeginEnd = false;
+}
+
+void ImGuiEx::Canvas::SetView(const ImVec2& origin, float scale)
+{
+ SetView(CanvasView(origin, scale));
+}
+
+void ImGuiEx::Canvas::SetView(const CanvasView& view)
+{
+ if (m_InBeginEnd)
+ LeaveLocalSpace();
+
+ if (m_View.Origin.x != view.Origin.x || m_View.Origin.y != view.Origin.y)
+ {
+ m_View.Origin = view.Origin;
+
+ UpdateViewTransformPosition();
+ }
+
+ if (m_View.Scale != view.Scale)
+ {
+ m_View.Scale = view.Scale;
+ m_View.InvScale = view.InvScale;
+ }
+
+ if (m_InBeginEnd)
+ EnterLocalSpace();
+}
+
+void ImGuiEx::Canvas::CenterView(const ImVec2& canvasPoint)
+{
+ auto view = CalcCenterView(canvasPoint);
+ SetView(view);
+}
+
+ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView(const ImVec2& canvasPoint) const
+{
+ auto localCenter = ToLocal(m_WidgetPosition + m_WidgetSize * 0.5f);
+ auto localOffset = canvasPoint - localCenter;
+ auto offset = FromLocalV(localOffset);
+
+ return CanvasView{ m_View.Origin - offset, m_View.Scale };
+}
+
+void ImGuiEx::Canvas::CenterView(const ImRect& canvasRect)
+{
+ auto view = CalcCenterView(canvasRect);
+
+ SetView(view);
+}
+
+ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView(const ImRect& canvasRect) const
+{
+ auto canvasRectSize = canvasRect.GetSize();
+
+ if (canvasRectSize.x <= 0.0f || canvasRectSize.y <= 0.0f)
+ return View();
+
+ auto widgetAspectRatio = m_WidgetSize.y > 0.0f ? m_WidgetSize.x / m_WidgetSize.y : 0.0f;
+ auto canvasRectAspectRatio = canvasRectSize.y > 0.0f ? canvasRectSize.x / canvasRectSize.y : 0.0f;
+
+ if (widgetAspectRatio <= 0.0f || canvasRectAspectRatio <= 0.0f)
+ return View();
+
+ auto newOrigin = m_View.Origin;
+ auto newScale = m_View.Scale;
+ if (canvasRectAspectRatio > widgetAspectRatio)
+ {
+ // width span across view
+ newScale = m_WidgetSize.x / canvasRectSize.x;
+ newOrigin = canvasRect.Min * -newScale;
+ newOrigin.y += (m_WidgetSize.y - canvasRectSize.y * newScale) * 0.5f;
+ }
+ else
+ {
+ // height span across view
+ newScale = m_WidgetSize.y / canvasRectSize.y;
+ newOrigin = canvasRect.Min * -newScale;
+ newOrigin.x += (m_WidgetSize.x - canvasRectSize.x * newScale) * 0.5f;
+ }
+
+ return CanvasView{ newOrigin, newScale };
+}
+
+void ImGuiEx::Canvas::Suspend()
+{
+ // If you're here, please make sure you do not interleave
+ // channel splitter with canvas.
+ // Always call canvas function with using same channel.
+ IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
+
+ if (m_SuspendCounter == 0)
+ LeaveLocalSpace();
+
+ ++m_SuspendCounter;
+}
+
+void ImGuiEx::Canvas::Resume()
+{
+ // If you're here, please make sure you do not interleave
+ // channel splitter with canvas.
+ // Always call canvas function with using same channel.
+ IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
+
+ // Check: Number of calls to Resume() do not match calls to Suspend(). Please check your code.
+ IM_ASSERT(m_SuspendCounter > 0);
+ if (--m_SuspendCounter == 0)
+ EnterLocalSpace();
+}
+
+ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point) const
+{
+ return point * m_View.Scale + m_ViewTransformPosition;
+}
+
+ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point, const CanvasView& view) const
+{
+ return point * view.Scale + view.Origin + m_WidgetPosition;
+}
+
+ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector) const
+{
+ return vector * m_View.Scale;
+}
+
+ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector, const CanvasView& view) const
+{
+ return vector * view.Scale;
+}
+
+ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point) const
+{
+ return (point - m_ViewTransformPosition) * m_View.InvScale;
+}
+
+ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point, const CanvasView& view) const
+{
+ return (point - view.Origin - m_WidgetPosition) * view.InvScale;
+}
+
+ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector) const
+{
+ return vector * m_View.InvScale;
+}
+
+ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector, const CanvasView& view) const
+{
+ return vector * view.InvScale;
+}
+
+ImRect ImGuiEx::Canvas::CalcViewRect(const CanvasView& view) const
+{
+ ImRect result;
+ result.Min = ImVec2(-view.Origin.x, -view.Origin.y) * view.InvScale;
+ result.Max = (m_WidgetSize - view.Origin) * view.InvScale;
+ return result;
+}
+
+void ImGuiEx::Canvas::UpdateViewTransformPosition()
+{
+ m_ViewTransformPosition = m_View.Origin + m_WidgetPosition;
+}
+
+void ImGuiEx::Canvas::SaveInputState()
+{
+ auto& io = ImGui::GetIO();
+ m_MousePosBackup = io.MousePos;
+ m_MousePosPrevBackup = io.MousePosPrev;
+ for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
+ m_MouseClickedPosBackup[i] = io.MouseClickedPos[i];
+
+ // Record cursor max to prevent scrollbars from appearing.
+ m_WindowCursorMaxBackup = ImGui::GetCurrentWindow()->DC.CursorMaxPos;
+}
+
+void ImGuiEx::Canvas::RestoreInputState()
+{
+ auto& io = ImGui::GetIO();
+ io.MousePos = m_MousePosBackup;
+ io.MousePosPrev = m_MousePosPrevBackup;
+ for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
+ io.MouseClickedPos[i] = m_MouseClickedPosBackup[i];
+ ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup;
+}
+
+void ImGuiEx::Canvas::SaveViewportState()
+{
+# if defined(IMGUI_HAS_VIEWPORT)
+ auto viewport = ImGui::GetWindowViewport();
+
+ m_ViewportPosBackup = viewport->Pos;
+ m_ViewportSizeBackup = viewport->Size;
+# endif
+}
+
+void ImGuiEx::Canvas::RestoreViewportState()
+{
+# if defined(IMGUI_HAS_VIEWPORT)
+ auto viewport = ImGui::GetWindowViewport();
+
+ viewport->Pos = m_ViewportPosBackup;
+ viewport->Size = m_ViewportSizeBackup;
+# endif
+}
+
+void ImGuiEx::Canvas::EnterLocalSpace()
+{
+ // Prepare ImDrawList for drawing in local coordinate system:
+ // - determine visible part of the canvas
+ // - start unique draw command
+ // - add clip rect matching canvas size
+ // - record current command index
+ // - record current vertex write index
+
+ // Determine visible part of the canvas. Make it before
+ // adding new command, to avoid round rip where command
+ // is removed in PopClipRect() and added again next PushClipRect().
+ ImGui::PushClipRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize, true);
+ auto clipped_clip_rect = m_DrawList->_ClipRectStack.back();
+ ImGui::PopClipRect();
+
+ // Make sure we do not share draw command with anyone. We don't want to mess
+ // with someones clip rectangle.
+
+ // #FIXME:
+ // This condition is not enough to avoid when user choose
+ // to use channel splitter.
+ //
+ // To deal with Suspend()/Resume() calls empty draw command
+ // is always added then splitter is active. Otherwise
+ // channel merger will collapse our draw command one with
+ // different clip rectangle.
+ //
+ // More investigation is needed. To get to the bottom of this.
+ if ((!m_DrawList->CmdBuffer.empty() && m_DrawList->CmdBuffer.back().ElemCount > 0) || m_DrawList->_Splitter._Count > 1)
+ m_DrawList->AddDrawCmd();
+
+# if IMGUI_EX_CANVAS_DEFERED()
+ m_Ranges.resize(m_Ranges.Size + 1);
+ m_CurrentRange = &m_Ranges.back();
+ m_CurrentRange->BeginComandIndex = ImMax(m_DrawList->CmdBuffer.Size - 1, 0);
+ m_CurrentRange->BeginVertexIndex = m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList);
+# endif
+ m_DrawListCommadBufferSize = ImMax(m_DrawList->CmdBuffer.Size - 1, 0);
+ m_DrawListStartVertexIndex = m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList);
+
+# if defined(IMGUI_HAS_VIEWPORT)
+ auto viewport_min = m_ViewportPosBackup;
+ auto viewport_max = m_ViewportPosBackup + m_ViewportSizeBackup;
+
+ viewport_min.x = (viewport_min.x - m_ViewTransformPosition.x) * m_View.InvScale;
+ viewport_min.y = (viewport_min.y - m_ViewTransformPosition.y) * m_View.InvScale;
+ viewport_max.x = (viewport_max.x - m_ViewTransformPosition.x) * m_View.InvScale;
+ viewport_max.y = (viewport_max.y - m_ViewTransformPosition.y) * m_View.InvScale;
+
+ auto viewport = ImGui::GetWindowViewport();
+ viewport->Pos = viewport_min;
+ viewport->Size = viewport_max - viewport_min;
+# endif
+
+ // Clip rectangle in parent canvas space and move it to local space.
+ clipped_clip_rect.x = (clipped_clip_rect.x - m_ViewTransformPosition.x) * m_View.InvScale;
+ clipped_clip_rect.y = (clipped_clip_rect.y - m_ViewTransformPosition.y) * m_View.InvScale;
+ clipped_clip_rect.z = (clipped_clip_rect.z - m_ViewTransformPosition.x) * m_View.InvScale;
+ clipped_clip_rect.w = (clipped_clip_rect.w - m_ViewTransformPosition.y) * m_View.InvScale;
+ ImGui::PushClipRect(ImVec2(clipped_clip_rect.x, clipped_clip_rect.y), ImVec2(clipped_clip_rect.z, clipped_clip_rect.w), false);
+
+ // Transform mouse position to local space.
+ auto& io = ImGui::GetIO();
+ io.MousePos = (m_MousePosBackup - m_ViewTransformPosition) * m_View.InvScale;
+ io.MousePosPrev = (m_MousePosPrevBackup - m_ViewTransformPosition) * m_View.InvScale;
+ for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
+ io.MouseClickedPos[i] = (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * m_View.InvScale;
+
+ m_ViewRect = CalcViewRect(m_View);;
+
+ auto& fringeScale = ImFringeScaleRef(m_DrawList);
+ m_LastFringeScale = fringeScale;
+ fringeScale *= m_View.InvScale;
+}
+
+void ImGuiEx::Canvas::LeaveLocalSpace()
+{
+ IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
+
+# if IMGUI_EX_CANVAS_DEFERED()
+ IM_ASSERT(m_CurrentRange != nullptr);
+
+ m_CurrentRange->EndVertexIndex = m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList);
+ m_CurrentRange->EndCommandIndex = m_DrawList->CmdBuffer.size();
+ if (m_CurrentRange->BeginVertexIndex == m_CurrentRange->EndVertexIndex)
+ {
+ // Drop empty range
+ m_Ranges.resize(m_Ranges.Size - 1);
+ }
+ m_CurrentRange = nullptr;
+# endif
+
+ // Move vertices to screen space.
+ auto vertex = m_DrawList->VtxBuffer.Data + m_DrawListStartVertexIndex;
+ auto vertexEnd = m_DrawList->VtxBuffer.Data + m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList);
+
+ // If canvas view is not scaled take a faster path.
+ if (m_View.Scale != 1.0f)
+ {
+ while (vertex < vertexEnd)
+ {
+ vertex->pos.x = vertex->pos.x * m_View.Scale + m_ViewTransformPosition.x;
+ vertex->pos.y = vertex->pos.y * m_View.Scale + m_ViewTransformPosition.y;
+ ++vertex;
+ }
+
+ // Move clip rectangles to screen space.
+ for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); ++i)
+ {
+ auto& command = m_DrawList->CmdBuffer[i];
+ command.ClipRect.x = command.ClipRect.x * m_View.Scale + m_ViewTransformPosition.x;
+ command.ClipRect.y = command.ClipRect.y * m_View.Scale + m_ViewTransformPosition.y;
+ command.ClipRect.z = command.ClipRect.z * m_View.Scale + m_ViewTransformPosition.x;
+ command.ClipRect.w = command.ClipRect.w * m_View.Scale + m_ViewTransformPosition.y;
+ }
+ }
+ else
+ {
+ while (vertex < vertexEnd)
+ {
+ vertex->pos.x = vertex->pos.x + m_ViewTransformPosition.x;
+ vertex->pos.y = vertex->pos.y + m_ViewTransformPosition.y;
+ ++vertex;
+ }
+
+ // Move clip rectangles to screen space.
+ for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); ++i)
+ {
+ auto& command = m_DrawList->CmdBuffer[i];
+ command.ClipRect.x = command.ClipRect.x + m_ViewTransformPosition.x;
+ command.ClipRect.y = command.ClipRect.y + m_ViewTransformPosition.y;
+ command.ClipRect.z = command.ClipRect.z + m_ViewTransformPosition.x;
+ command.ClipRect.w = command.ClipRect.w + m_ViewTransformPosition.y;
+ }
+ }
+
+ auto& fringeScale = ImFringeScaleRef(m_DrawList);
+ fringeScale = m_LastFringeScale;
+
+ // And pop \o/
+ ImGui::PopClipRect();
+
+ RestoreInputState();
+ RestoreViewportState();
+}
diff --git a/3rdparty/imgui-node-editor/imgui_canvas.h b/3rdparty/imgui-node-editor/imgui_canvas.h
new file mode 100644
index 0000000..44f4347
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_canvas.h
@@ -0,0 +1,258 @@
+// Canvas widget - view over infinite virtual space.
+//
+// Canvas allows you to draw your widgets anywhere over infinite space and provide
+// view over it with support for panning and scaling.
+//
+// When you enter a canvas ImGui is moved to virtual space which mean:
+// - ImGui::GetCursorScreenPos() return (0, 0) and which correspond to top left corner
+// of the canvas on the screen (this can be changed usign CanvasView()).
+// - Mouse input is brought to canvas space, so widgets works as usual.
+// - Everything you draw with ImDrawList will be in virtual space.
+//
+// By default origin point is on top left corner of canvas widget. It can be
+// changed with call to CanvasView() where you can specify what part of space
+// should be viewed by setting viewport origin point and scale. Current state
+// can be queried with CanvasViewOrigin() and CanvasViewScale().
+//
+// Viewport size is controlled by 'size' parameter in BeginCanvas(). You can query
+// it using CanvasContentMin/Max/Size functions. They are useful if you to not specify
+// canvas size in which case all free space is used.
+//
+// Bounds of visible region of infinite space can be queried using CanvasViewMin/Max/Size
+// functions. Everything that is drawn outside of this region will be clipped
+// as usual in ImGui.
+//
+// While drawing inside canvas you can translate position from world (usual ImGui space)
+// to virtual space and back usign CanvasFromWorld()/CanvasToWorld().
+//
+// Canvas can be nested in each other (they are regular widgets after all). There
+// is a way to transform position between current and parent canvas with
+// CanvasFromParent()/CanvasToParent().
+//
+// Sometimes in more elaborate scenarios you want to move out canvas virtual space,
+// do something and came back. You can do that with SuspendCanvas() and ResumeCanvas().
+//
+// Note:
+// It is not valid to call canvas API outside of BeginCanvas() / EndCanvas() scope.
+//
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+# ifndef __IMGUI_EX_CANVAS_H__
+# define __IMGUI_EX_CANVAS_H__
+# pragma once
+
+# include <imgui.h>
+# include <imgui_internal.h> // ImRect, ImFloor
+
+namespace ImGuiEx {
+
+struct CanvasView
+{
+ ImVec2 Origin;
+ float Scale = 1.0f;
+ float InvScale = 1.0f;
+
+ CanvasView() = default;
+ CanvasView(const ImVec2& origin, float scale)
+ : Origin(origin)
+ , Scale(scale)
+ , InvScale(scale ? 1.0f / scale : 0.0f)
+ {
+ }
+
+ void Set(const ImVec2& origin, float scale)
+ {
+ *this = CanvasView(origin, scale);
+ }
+};
+
+// Canvas widget represent view over infinite plane.
+//
+// It acts like a child window without scroll bars with
+// ability to zoom to specific part of canvas plane.
+//
+// Widgets are clipped according to current view exactly
+// same way ImGui do. To avoid `missing widgets` artifacts first
+// setup visible region with SetView() then draw content.
+//
+// Everything drawn with ImDrawList betwen calls to Begin()/End()
+// will be drawn on canvas plane. This behavior can be suspended
+// by calling Suspend() and resumed by calling Resume().
+//
+// Warning:
+// Please do not interleave canvas with use of channel splitter.
+// Keep channel splitter contained inside canvas or always
+// call canvas functions from same channel.
+struct Canvas
+{
+ // Begins drawing content of canvas plane.
+ //
+ // When false is returned that mean canvas is not visible to the
+ // user can drawing should be skipped and End() not called.
+ // When true is returned drawing must be ended with call to End().
+ //
+ // If any size component is equal to zero or less canvas will
+ // automatically expand to all available area on that axis.
+ // So (0, 300) will take horizontal space and have height
+ // of 300 points. (0, 0) will take all remaining space of
+ // the window.
+ //
+ // You can query size of the canvas while it is being drawn
+ // by calling Rect().
+ bool Begin(const char* id, const ImVec2& size);
+ bool Begin(ImGuiID id, const ImVec2& size);
+
+ // Ends interaction with canvas plane.
+ //
+ // Must be called only when Begin() retuned true.
+ void End();
+
+ // Sets visible region of canvas plane.
+ //
+ // Origin is an offset of infinite plane origin from top left
+ // corner of the canvas.
+ //
+ // Scale greater than 1 make canvas content be bigger, less than 1 smaller.
+ void SetView(const ImVec2& origin, float scale);
+ void SetView(const CanvasView& view);
+
+ // Centers view over specific point on canvas plane.
+ //
+ // View will be centered on specific point by changing origin
+ // but not scale.
+ void CenterView(const ImVec2& canvasPoint);
+
+ // Calculates view over specific point on canvas plane.
+ CanvasView CalcCenterView(const ImVec2& canvasPoint) const;
+
+ // Centers view over specific rectangle on canvas plane.
+ //
+ // Whole rectangle will fit in canvas view. This will affect both
+ // origin and scale.
+ void CenterView(const ImRect& canvasRect);
+
+ // Calculates view over specific rectangle on canvas plane.
+ CanvasView CalcCenterView(const ImRect& canvasRect) const;
+
+ // Suspends canvas by returning to normal ImGui transformation space.
+ // While suspended UI will not be drawn on canvas plane.
+ //
+ // Calls to Suspend()/Resume() are symetrical. Each call to Suspend()
+ // must be matched with call to Resume().
+ void Suspend();
+ void Resume();
+
+ // Transforms point from canvas plane to ImGui.
+ ImVec2 FromLocal(const ImVec2& point) const;
+ ImVec2 FromLocal(const ImVec2& point, const CanvasView& view) const;
+
+ // Transforms vector from canvas plant to ImGui.
+ ImVec2 FromLocalV(const ImVec2& vector) const;
+ ImVec2 FromLocalV(const ImVec2& vector, const CanvasView& view) const;
+
+ // Transforms point from ImGui to canvas plane.
+ ImVec2 ToLocal(const ImVec2& point) const;
+ ImVec2 ToLocal(const ImVec2& point, const CanvasView& view) const;
+
+ // Transforms vector from ImGui to canvas plane.
+ ImVec2 ToLocalV(const ImVec2& vector) const;
+ ImVec2 ToLocalV(const ImVec2& vector, const CanvasView& view) const;
+
+ // Returns widget bounds.
+ //
+ // Note:
+ // Rect is valid after call to Begin().
+ const ImRect& Rect() const { return m_WidgetRect; }
+
+ // Returns visible region on canvas plane (in canvas plane coordinates).
+ const ImRect& ViewRect() const { return m_ViewRect; }
+
+ // Calculates visible region for view.
+ ImRect CalcViewRect(const CanvasView& view) const;
+
+ // Returns current view.
+ const CanvasView& View() const { return m_View; }
+
+ // Returns origin of the view.
+ //
+ // Origin is an offset of infinite plane origin from top left
+ // corner of the canvas.
+ const ImVec2& ViewOrigin() const { return m_View.Origin; }
+
+ // Returns scale of the view.
+ float ViewScale() const { return m_View.Scale; }
+
+ // Returns true if canvas is suspended.
+ //
+ // See: Suspend()/Resume()
+ bool IsSuspended() const { return m_SuspendCounter > 0; }
+
+private:
+# define IMGUI_EX_CANVAS_DEFERED() 0
+
+# if IMGUI_EX_CANVAS_DEFERED()
+ struct Range
+ {
+ int BeginVertexIndex = 0;
+ int EndVertexIndex = 0;
+ int BeginComandIndex = 0;
+ int EndCommandIndex = 0;
+ };
+# endif
+
+ void UpdateViewTransformPosition();
+
+ void SaveInputState();
+ void RestoreInputState();
+
+ void SaveViewportState();
+ void RestoreViewportState();
+
+ void EnterLocalSpace();
+ void LeaveLocalSpace();
+
+ bool m_InBeginEnd = false;
+
+ ImVec2 m_WidgetPosition;
+ ImVec2 m_WidgetSize;
+ ImRect m_WidgetRect;
+
+ ImDrawList* m_DrawList = nullptr;
+ int m_ExpectedChannel = 0;
+
+# if IMGUI_EX_CANVAS_DEFERED()
+ ImVector<Range> m_Ranges;
+ Range* m_CurrentRange = nullptr;
+# endif
+
+ int m_DrawListCommadBufferSize = 0;
+ int m_DrawListStartVertexIndex = 0;
+
+ CanvasView m_View;
+ ImRect m_ViewRect;
+
+ ImVec2 m_ViewTransformPosition;
+
+ int m_SuspendCounter = 0;
+
+ float m_LastFringeScale = 1.0f;
+
+ ImVec2 m_MousePosBackup;
+ ImVec2 m_MousePosPrevBackup;
+ ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)];
+ ImVec2 m_WindowCursorMaxBackup;
+
+# if defined(IMGUI_HAS_VIEWPORT)
+ ImVec2 m_ViewportPosBackup;
+ ImVec2 m_ViewportSizeBackup;
+# endif
+};
+
+} // namespace ImGuiEx
+
+# endif // __IMGUI_EX_CANVAS_H__ \ No newline at end of file
diff --git a/3rdparty/imgui-node-editor/imgui_extra_math.h b/3rdparty/imgui-node-editor/imgui_extra_math.h
new file mode 100644
index 0000000..2a3a2fe
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_extra_math.h
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# ifndef __IMGUI_EXTRA_MATH_H__
+# define __IMGUI_EXTRA_MATH_H__
+# pragma once
+
+
+//------------------------------------------------------------------------------
+# include <imgui.h>
+# ifndef IMGUI_DEFINE_MATH_OPERATORS
+# define IMGUI_DEFINE_MATH_OPERATORS
+# endif
+# include <imgui_internal.h>
+
+
+//------------------------------------------------------------------------------
+struct ImLine
+{
+ ImVec2 A, B;
+};
+
+
+//------------------------------------------------------------------------------
+inline bool operator==(const ImVec2& lhs, const ImVec2& rhs);
+inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs);
+inline ImVec2 operator*(const float lhs, const ImVec2& rhs);
+inline ImVec2 operator-(const ImVec2& lhs);
+
+
+//------------------------------------------------------------------------------
+inline float ImLength(float v);
+inline float ImLength(const ImVec2& v);
+inline float ImLengthSqr(float v);
+inline ImVec2 ImNormalized(const ImVec2& v);
+
+
+//------------------------------------------------------------------------------
+inline bool ImRect_IsEmpty(const ImRect& rect);
+inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge);
+inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius);
+inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& b);
+inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b);
+inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b);
+
+
+
+//------------------------------------------------------------------------------
+namespace ImEasing {
+
+template <typename V, typename T>
+inline V EaseOutQuad(V b, V c, T t)
+{
+ return b - c * (t * (t - 2));
+}
+
+} // namespace ImEasing
+
+
+//------------------------------------------------------------------------------
+# include "imgui_extra_math.inl"
+
+
+//------------------------------------------------------------------------------
+# endif // __IMGUI_EXTRA_MATH_H__
diff --git a/3rdparty/imgui-node-editor/imgui_extra_math.inl b/3rdparty/imgui-node-editor/imgui_extra_math.inl
new file mode 100644
index 0000000..18fb25e
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_extra_math.inl
@@ -0,0 +1,187 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# ifndef __IMGUI_EXTRA_MATH_INL__
+# define __IMGUI_EXTRA_MATH_INL__
+# pragma once
+
+
+//------------------------------------------------------------------------------
+# include "imgui_extra_math.h"
+
+
+//------------------------------------------------------------------------------
+inline bool operator==(const ImVec2& lhs, const ImVec2& rhs)
+{
+ return lhs.x == rhs.x && lhs.y == rhs.y;
+}
+
+inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs)
+{
+ return lhs.x != rhs.x || lhs.y != rhs.y;
+}
+
+inline ImVec2 operator*(const float lhs, const ImVec2& rhs)
+{
+ return ImVec2(lhs * rhs.x, lhs * rhs.y);
+}
+
+inline ImVec2 operator-(const ImVec2& lhs)
+{
+ return ImVec2(-lhs.x, -lhs.y);
+}
+
+
+//------------------------------------------------------------------------------
+inline float ImLength(float v)
+{
+ return v;
+}
+
+inline float ImLength(const ImVec2& v)
+{
+ return ImSqrt(ImLengthSqr(v));
+}
+
+inline float ImLengthSqr(float v)
+{
+ return v * v;
+}
+
+inline ImVec2 ImNormalized(const ImVec2& v)
+{
+ return v * ImInvLength(v, 0.0f);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+inline bool ImRect_IsEmpty(const ImRect& rect)
+{
+ return rect.Min.x >= rect.Max.x
+ || rect.Min.y >= rect.Max.y;
+}
+
+inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge)
+{
+ if (!snap_to_edge && rect.Contains(p))
+ return p;
+
+ return ImVec2(
+ (p.x > rect.Max.x) ? rect.Max.x : (p.x < rect.Min.x ? rect.Min.x : p.x),
+ (p.y > rect.Max.y) ? rect.Max.y : (p.y < rect.Min.y ? rect.Min.y : p.y)
+ );
+}
+
+inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius)
+{
+ auto point = ImRect_ClosestPoint(rect, p, snap_to_edge);
+
+ const auto offset = p - point;
+ const auto distance_sq = offset.x * offset.x + offset.y * offset.y;
+ if (distance_sq <= 0)
+ return point;
+
+ const auto distance = ImSqrt(distance_sq);
+
+ return point + offset * (ImMin(distance, radius) * (1.0f / distance));
+}
+
+inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& other)
+{
+ ImVec2 result;
+ if (other.Min.x >= rect.Max.x)
+ result.x = rect.Max.x;
+ else if (other.Max.x <= rect.Min.x)
+ result.x = rect.Min.x;
+ else
+ result.x = (ImMax(rect.Min.x, other.Min.x) + ImMin(rect.Max.x, other.Max.x)) / 2;
+
+ if (other.Min.y >= rect.Max.y)
+ result.y = rect.Max.y;
+ else if (other.Max.y <= rect.Min.y)
+ result.y = rect.Min.y;
+ else
+ result.y = (ImMax(rect.Min.y, other.Min.y) + ImMin(rect.Max.y, other.Max.y)) / 2;
+
+ return result;
+}
+
+inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b)
+{
+ ImLine result;
+ result.A = ImRect_ClosestPoint(rect_a, rect_b);
+ result.B = ImRect_ClosestPoint(rect_b, rect_a);
+
+ auto distribute = [](float& a, float& b, float a0, float a1, float b0, float b1)
+ {
+ if (a0 >= b1 || a1 <= b0)
+ return;
+
+ const auto aw = a1 - a0;
+ const auto bw = b1 - b0;
+
+ if (aw > bw)
+ {
+ b = b0 + bw - bw * (a - a0) / aw;
+ a = b;
+ }
+ else if (aw < bw)
+ {
+ a = a0 + aw - aw * (b - b0) / bw;
+ b = a;
+ }
+ };
+
+ distribute(result.A.x, result.B.x, rect_a.Min.x, rect_a.Max.x, rect_b.Min.x, rect_b.Max.x);
+ distribute(result.A.y, result.B.y, rect_a.Min.y, rect_a.Max.y, rect_b.Min.y, rect_b.Max.y);
+
+ return result;
+}
+
+inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b)
+{
+ auto line = ImRect_ClosestLine(rect_a, rect_b);
+ if (radius_a < 0)
+ radius_a = 0;
+ if (radius_b < 0)
+ radius_b = 0;
+
+ if (radius_a == 0 && radius_b == 0)
+ return line;
+
+ const auto offset = line.B - line.A;
+ const auto length_sq = offset.x * offset.x + offset.y * offset.y;
+ const auto radius_a_sq = radius_a * radius_a;
+ const auto radius_b_sq = radius_b * radius_b;
+
+ if (length_sq <= 0)
+ return line;
+
+ const auto length = ImSqrt(length_sq);
+ const auto direction = ImVec2(offset.x / length, offset.y / length);
+
+ const auto total_radius_sq = radius_a_sq + radius_b_sq;
+ if (total_radius_sq > length_sq)
+ {
+ const auto scale = length / (radius_a + radius_b);
+ radius_a *= scale;
+ radius_b *= scale;
+ }
+
+ line.A = line.A + (direction * radius_a);
+ line.B = line.B - (direction * radius_b);
+
+ return line;
+}
+
+
+//------------------------------------------------------------------------------
+# endif // __IMGUI_EXTRA_MATH_INL__
diff --git a/3rdparty/imgui-node-editor/imgui_node_editor.cpp b/3rdparty/imgui-node-editor/imgui_node_editor.cpp
new file mode 100644
index 0000000..c1911c3
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_node_editor.cpp
@@ -0,0 +1,5316 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# include "imgui_node_editor_internal.h"
+# include <cstdio> // snprintf
+# include <string>
+# include <fstream>
+# include <bitset>
+# include <climits>
+# include <algorithm>
+# include <sstream>
+# include <streambuf>
+# include <type_traits>
+
+// https://stackoverflow.com/a/8597498
+# define DECLARE_HAS_NESTED(Name, Member) \
+ \
+ template<class T> \
+ struct has_nested_ ## Name \
+ { \
+ typedef char yes; \
+ typedef yes(&no)[2]; \
+ \
+ template<class U> static yes test(decltype(U::Member)*); \
+ template<class U> static no test(...); \
+ \
+ static bool const value = sizeof(test<T>(0)) == sizeof(yes); \
+ };
+
+
+namespace ax {
+namespace NodeEditor {
+namespace Detail {
+
+# define DECLARE_KEY_TESTER(Key) \
+ DECLARE_HAS_NESTED(Key, Key) \
+ struct KeyTester_ ## Key \
+ { \
+ template <typename T> \
+ static int Get(typename std::enable_if<has_nested_ ## Key<ImGuiKey_>::value, T>::type*) \
+ { \
+ return ImGui::GetKeyIndex(T::Key); \
+ } \
+ \
+ template <typename T> \
+ static int Get(typename std::enable_if<!has_nested_ ## Key<ImGuiKey_>::value, T>::type*) \
+ { \
+ return -1; \
+ } \
+ }
+
+DECLARE_KEY_TESTER(ImGuiKey_F);
+DECLARE_KEY_TESTER(ImGuiKey_D);
+
+static inline int GetKeyIndexForF()
+{
+ return KeyTester_ImGuiKey_F::Get<ImGuiKey_>(nullptr);
+}
+
+static inline int GetKeyIndexForD()
+{
+ return KeyTester_ImGuiKey_D::Get<ImGuiKey_>(nullptr);
+}
+
+} // namespace Detail
+} // namespace NodeEditor
+} // namespace ax
+
+
+//------------------------------------------------------------------------------
+namespace ed = ax::NodeEditor::Detail;
+
+
+//------------------------------------------------------------------------------
+static const int c_BackgroundChannelCount = 1;
+static const int c_LinkChannelCount = 4;
+static const int c_UserLayersCount = 5;
+
+static const int c_UserLayerChannelStart = 0;
+static const int c_BackgroundChannelStart = c_UserLayerChannelStart + c_UserLayersCount;
+static const int c_LinkStartChannel = c_BackgroundChannelStart + c_BackgroundChannelCount;
+static const int c_NodeStartChannel = c_LinkStartChannel + c_LinkChannelCount;
+
+static const int c_BackgroundChannel_SelectionRect = c_BackgroundChannelStart + 0;
+
+static const int c_UserChannel_Content = c_UserLayerChannelStart + 1;
+static const int c_UserChannel_Grid = c_UserLayerChannelStart + 2;
+static const int c_UserChannel_HintsBackground = c_UserLayerChannelStart + 3;
+static const int c_UserChannel_Hints = c_UserLayerChannelStart + 4;
+
+static const int c_LinkChannel_Selection = c_LinkStartChannel + 0;
+static const int c_LinkChannel_Links = c_LinkStartChannel + 1;
+static const int c_LinkChannel_Flow = c_LinkStartChannel + 2;
+static const int c_LinkChannel_NewLink = c_LinkStartChannel + 3;
+
+static const int c_ChannelsPerNode = 5;
+static const int c_NodeBaseChannel = 0;
+static const int c_NodeBackgroundChannel = 1;
+static const int c_NodeUserBackgroundChannel = 2;
+static const int c_NodePinChannel = 3;
+static const int c_NodeContentChannel = 4;
+
+static const float c_GroupSelectThickness = 6.0f; // canvas pixels
+static const float c_LinkSelectThickness = 5.0f; // canvas pixels
+static const float c_NavigationZoomMargin = 0.1f; // percentage of visible bounds
+static const float c_MouseZoomDuration = 0.15f; // seconds
+static const float c_SelectionFadeOutDuration = 0.15f; // seconds
+static const auto c_ScrollButtonIndex = 1;
+
+
+//------------------------------------------------------------------------------
+# if defined(_DEBUG) && defined(_WIN32)
+extern "C" __declspec(dllimport) void __stdcall OutputDebugStringA(const char* string);
+
+static void LogV(const char* fmt, va_list args)
+{
+ const int buffer_size = 1024;
+ static char buffer[1024];
+
+ vsnprintf(buffer, buffer_size - 1, fmt, args);
+ buffer[buffer_size - 1] = 0;
+
+ ImGui::LogText("\nNode Editor: %s", buffer);
+
+ OutputDebugStringA("NodeEditor: ");
+ OutputDebugStringA(buffer);
+ OutputDebugStringA("\n");
+}
+# endif
+
+void ed::Log(const char* fmt, ...)
+{
+# if defined(_DEBUG) && defined(_WIN32)
+ va_list args;
+ va_start(args, fmt);
+ LogV(fmt, args);
+ va_end(args);
+# endif
+}
+
+
+//------------------------------------------------------------------------------
+static bool IsGroup(const ed::Node* node)
+{
+ if (node && node->m_Type == ed::NodeType::Group)
+ return true;
+ else
+ return false;
+}
+
+
+//------------------------------------------------------------------------------
+static void ImDrawListSplitter_Grow(ImDrawList* draw_list, ImDrawListSplitter* splitter, int channels_count)
+{
+ IM_ASSERT(splitter != nullptr);
+ IM_ASSERT(splitter->_Count <= channels_count);
+
+ if (splitter->_Count == 1)
+ {
+ splitter->Split(draw_list, channels_count);
+ return;
+ }
+
+ int old_channels_count = splitter->_Channels.Size;
+ if (old_channels_count < channels_count)
+ splitter->_Channels.resize(channels_count);
+ int old_used_channels_count = splitter->_Count;
+ splitter->_Count = channels_count;
+
+ for (int i = old_used_channels_count; i < channels_count; i++)
+ {
+ if (i >= old_channels_count)
+ {
+ IM_PLACEMENT_NEW(&splitter->_Channels[i]) ImDrawChannel();
+ }
+ else
+ {
+ splitter->_Channels[i]._CmdBuffer.resize(0);
+ splitter->_Channels[i]._IdxBuffer.resize(0);
+ }
+ if (splitter->_Channels[i]._CmdBuffer.Size == 0)
+ {
+ ImDrawCmd draw_cmd;
+ draw_cmd.ClipRect = draw_list->_ClipRectStack.back();
+ draw_cmd.TextureId = draw_list->_TextureIdStack.back();
+ splitter->_Channels[i]._CmdBuffer.push_back(draw_cmd);
+ }
+ }
+}
+
+static void ImDrawList_ChannelsGrow(ImDrawList* draw_list, int channels_count)
+{
+ ImDrawListSplitter_Grow(draw_list, &draw_list->_Splitter, channels_count);
+}
+
+static void ImDrawListSplitter_SwapChannels(ImDrawListSplitter* splitter, int left, int right)
+{
+ IM_ASSERT(left < splitter->_Count && right < splitter->_Count);
+ if (left == right)
+ return;
+
+ auto currentChannel = splitter->_Current;
+
+ auto* leftCmdBuffer = &splitter->_Channels[left]._CmdBuffer;
+ auto* leftIdxBuffer = &splitter->_Channels[left]._IdxBuffer;
+ auto* rightCmdBuffer = &splitter->_Channels[right]._CmdBuffer;
+ auto* rightIdxBuffer = &splitter->_Channels[right]._IdxBuffer;
+
+ leftCmdBuffer->swap(*rightCmdBuffer);
+ leftIdxBuffer->swap(*rightIdxBuffer);
+
+ if (currentChannel == left)
+ splitter->_Current = right;
+ else if (currentChannel == right)
+ splitter->_Current = left;
+}
+
+static void ImDrawList_SwapChannels(ImDrawList* drawList, int left, int right)
+{
+ ImDrawListSplitter_SwapChannels(&drawList->_Splitter, left, right);
+}
+
+static void ImDrawList_SwapSplitter(ImDrawList* drawList, ImDrawListSplitter& splitter)
+{
+ auto& currentSplitter = drawList->_Splitter;
+
+ std::swap(currentSplitter._Current, splitter._Current);
+ std::swap(currentSplitter._Count, splitter._Count);
+ currentSplitter._Channels.swap(splitter._Channels);
+}
+
+//static void ImDrawList_TransformChannel_Inner(ImVector<ImDrawVert>& vtxBuffer, const ImVector<ImDrawIdx>& idxBuffer, const ImVector<ImDrawCmd>& cmdBuffer, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& postOffset)
+//{
+// auto idxRead = idxBuffer.Data;
+//
+// int indexOffset = 0;
+// for (auto& cmd : cmdBuffer)
+// {
+// auto idxCount = cmd.ElemCount;
+//
+// if (idxCount == 0) continue;
+//
+// auto minIndex = idxRead[indexOffset];
+// auto maxIndex = idxRead[indexOffset];
+//
+// for (auto i = 1u; i < idxCount; ++i)
+// {
+// auto idx = idxRead[indexOffset + i];
+// minIndex = std::min(minIndex, idx);
+// maxIndex = ImMax(maxIndex, idx);
+// }
+//
+// for (auto vtx = vtxBuffer.Data + minIndex, vtxEnd = vtxBuffer.Data + maxIndex + 1; vtx < vtxEnd; ++vtx)
+// {
+// vtx->pos.x = (vtx->pos.x + preOffset.x) * scale.x + postOffset.x;
+// vtx->pos.y = (vtx->pos.y + preOffset.y) * scale.y + postOffset.y;
+// }
+//
+// indexOffset += idxCount;
+// }
+//}
+
+//static void ImDrawList_TransformChannels(ImDrawList* drawList, int begin, int end, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& postOffset)
+//{
+// int lastCurrentChannel = drawList->_ChannelsCurrent;
+// if (lastCurrentChannel != 0)
+// drawList->ChannelsSetCurrent(0);
+//
+// auto& vtxBuffer = drawList->VtxBuffer;
+//
+// if (begin == 0 && begin != end)
+// {
+// ImDrawList_TransformChannel_Inner(vtxBuffer, drawList->IdxBuffer, drawList->CmdBuffer, preOffset, scale, postOffset);
+// ++begin;
+// }
+//
+// for (int channelIndex = begin; channelIndex < end; ++channelIndex)
+// {
+// auto& channel = drawList->_Channels[channelIndex];
+// ImDrawList_TransformChannel_Inner(vtxBuffer, channel.IdxBuffer, channel.CmdBuffer, preOffset, scale, postOffset);
+// }
+//
+// if (lastCurrentChannel != 0)
+// drawList->ChannelsSetCurrent(lastCurrentChannel);
+//}
+
+//static void ImDrawList_ClampClipRects_Inner(ImVector<ImDrawCmd>& cmdBuffer, const ImVec4& clipRect, const ImVec2& offset)
+//{
+// for (auto& cmd : cmdBuffer)
+// {
+// cmd.ClipRect.x = ImMax(cmd.ClipRect.x + offset.x, clipRect.x);
+// cmd.ClipRect.y = ImMax(cmd.ClipRect.y + offset.y, clipRect.y);
+// cmd.ClipRect.z = std::min(cmd.ClipRect.z + offset.x, clipRect.z);
+// cmd.ClipRect.w = std::min(cmd.ClipRect.w + offset.y, clipRect.w);
+// }
+//}
+
+//static void ImDrawList_TranslateAndClampClipRects(ImDrawList* drawList, int begin, int end, const ImVec2& offset)
+//{
+// int lastCurrentChannel = drawList->_ChannelsCurrent;
+// if (lastCurrentChannel != 0)
+// drawList->ChannelsSetCurrent(0);
+//
+// auto clipRect = drawList->_ClipRectStack.back();
+//
+// if (begin == 0 && begin != end)
+// {
+// ImDrawList_ClampClipRects_Inner(drawList->CmdBuffer, clipRect, offset);
+// ++begin;
+// }
+//
+// for (int channelIndex = begin; channelIndex < end; ++channelIndex)
+// {
+// auto& channel = drawList->_Channels[channelIndex];
+// ImDrawList_ClampClipRects_Inner(channel.CmdBuffer, clipRect, offset);
+// }
+//
+// if (lastCurrentChannel != 0)
+// drawList->ChannelsSetCurrent(lastCurrentChannel);
+//}
+
+static void ImDrawList_PathBezierOffset(ImDrawList* drawList, float offset, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3)
+{
+ using namespace ed;
+
+ auto acceptPoint = [drawList, offset](const ImCubicBezierSubdivideSample& r)
+ {
+ drawList->PathLineTo(r.Point + ImNormalized(ImVec2(-r.Tangent.y, r.Tangent.x)) * offset);
+ };
+
+ ImCubicBezierSubdivide(acceptPoint, p0, p1, p2, p3);
+}
+
+/*
+static void ImDrawList_PolyFillScanFlood(ImDrawList *draw, std::vector<ImVec2>* poly, ImColor color, int gap = 1, float strokeWidth = 1.0f)
+{
+ std::vector<ImVec2> scanHits;
+ ImVec2 min, max; // polygon min/max points
+ auto io = ImGui::GetIO();
+ float y;
+ bool isMinMaxDone = false;
+ unsigned int polysize = poly->size();
+
+ // find the orthagonal bounding box
+ // probably can put this as a predefined
+ if (!isMinMaxDone)
+ {
+ min.x = min.y = FLT_MAX;
+ max.x = max.y = FLT_MIN;
+ for (auto p : *poly)
+ {
+ if (p.x < min.x) min.x = p.x;
+ if (p.y < min.y) min.y = p.y;
+ if (p.x > max.x) max.x = p.x;
+ if (p.y > max.y) max.y = p.y;
+ }
+ isMinMaxDone = true;
+ }
+
+ // Bounds check
+ if ((max.x < 0) || (min.x > io.DisplaySize.x) || (max.y < 0) || (min.y > io.DisplaySize.y)) return;
+
+ // Vertically clip
+ if (min.y < 0) min.y = 0;
+ if (max.y > io.DisplaySize.y) max.y = io.DisplaySize.y;
+
+ // so we know we start on the outside of the object we step out by 1.
+ min.x -= 1;
+ max.x += 1;
+
+ // Initialise our starting conditions
+ y = min.y;
+
+ // Go through each scan line iteratively, jumping by 'gap' pixels each time
+ while (y < max.y)
+ {
+ scanHits.clear();
+
+ {
+ int jump = 1;
+ ImVec2 fp = poly->at(0);
+
+ for (size_t i = 0; i < polysize - 1; i++)
+ {
+ ImVec2 pa = poly->at(i);
+ ImVec2 pb = poly->at(i + 1);
+
+ // jump double/dud points
+ if (pa.x == pb.x && pa.y == pb.y) continue;
+
+ // if we encounter our hull/poly start point, then we've now created the
+ // closed
+ // hull, jump the next segment and reset the first-point
+ if ((!jump) && (fp.x == pb.x) && (fp.y == pb.y))
+ {
+ if (i < polysize - 2)
+ {
+ fp = poly->at(i + 2);
+ jump = 1;
+ i++;
+ }
+ }
+ else
+ {
+ jump = 0;
+ }
+
+ // test to see if this segment makes the scan-cut.
+ if ((pa.y > pb.y && y < pa.y && y > pb.y) || (pa.y < pb.y && y > pa.y && y < pb.y))
+ {
+ ImVec2 intersect;
+
+ intersect.y = y;
+ if (pa.x == pb.x)
+ {
+ intersect.x = pa.x;
+ }
+ else
+ {
+ intersect.x = (pb.x - pa.x) / (pb.y - pa.y) * (y - pa.y) + pa.x;
+ }
+ scanHits.push_back(intersect);
+ }
+ }
+
+ // Sort the scan hits by X, so we have a proper left->right ordering
+ sort(scanHits.begin(), scanHits.end(), [](ImVec2 const &a, ImVec2 const &b) { return a.x < b.x; });
+
+ // generate the line segments.
+ {
+ int i = 0;
+ int l = scanHits.size() - 1; // we need pairs of points, this prevents segfault.
+ for (i = 0; i < l; i += 2)
+ {
+ draw->AddLine(scanHits[i], scanHits[i + 1], color, strokeWidth);
+ }
+ }
+ }
+ y += gap;
+ } // for each scan line
+ scanHits.clear();
+}
+*/
+
+static void ImDrawList_AddBezierWithArrows(ImDrawList* drawList, const ImCubicBezierPoints& curve, float thickness,
+ float startArrowSize, float startArrowWidth, float endArrowSize, float endArrowWidth,
+ bool fill, ImU32 color, float strokeThickness)
+{
+ using namespace ax;
+
+ if ((color >> 24) == 0)
+ return;
+
+ const auto half_thickness = thickness * 0.5f;
+
+ if (fill)
+ {
+ drawList->AddBezierCurve(curve.P0, curve.P1, curve.P2, curve.P3, color, thickness);
+
+ if (startArrowSize > 0.0f)
+ {
+ const auto start_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f));
+ const auto start_n = ImVec2(-start_dir.y, start_dir.x);
+ const auto half_width = startArrowWidth * 0.5f;
+ const auto tip = curve.P0 - start_dir * startArrowSize;
+
+ drawList->PathLineTo(curve.P0 - start_n * ImMax(half_width, half_thickness));
+ drawList->PathLineTo(curve.P0 + start_n * ImMax(half_width, half_thickness));
+ drawList->PathLineTo(tip);
+ drawList->PathFillConvex(color);
+ }
+
+ if (endArrowSize > 0.0f)
+ {
+ const auto end_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f));
+ const auto end_n = ImVec2( -end_dir.y, end_dir.x);
+ const auto half_width = endArrowWidth * 0.5f;
+ const auto tip = curve.P3 + end_dir * endArrowSize;
+
+ drawList->PathLineTo(curve.P3 + end_n * ImMax(half_width, half_thickness));
+ drawList->PathLineTo(curve.P3 - end_n * ImMax(half_width, half_thickness));
+ drawList->PathLineTo(tip);
+ drawList->PathFillConvex(color);
+ }
+ }
+ else
+ {
+ if (startArrowSize > 0.0f)
+ {
+ const auto start_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f));
+ const auto start_n = ImVec2(-start_dir.y, start_dir.x);
+ const auto half_width = startArrowWidth * 0.5f;
+ const auto tip = curve.P0 - start_dir * startArrowSize;
+
+ if (half_width > half_thickness)
+ drawList->PathLineTo(curve.P0 - start_n * half_width);
+ drawList->PathLineTo(tip);
+ if (half_width > half_thickness)
+ drawList->PathLineTo(curve.P0 + start_n * half_width);
+ }
+
+ ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P0, curve.P1, curve.P2, curve.P3);
+
+ if (endArrowSize > 0.0f)
+ {
+ const auto end_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f));
+ const auto end_n = ImVec2( -end_dir.y, end_dir.x);
+ const auto half_width = endArrowWidth * 0.5f;
+ const auto tip = curve.P3 + end_dir * endArrowSize;
+
+ if (half_width > half_thickness)
+ drawList->PathLineTo(curve.P3 + end_n * half_width);
+ drawList->PathLineTo(tip);
+ if (half_width > half_thickness)
+ drawList->PathLineTo(curve.P3 - end_n * half_width);
+ }
+
+ ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P3, curve.P2, curve.P1, curve.P0);
+
+ drawList->PathStroke(color, true, strokeThickness);
+ }
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Pin
+//
+//------------------------------------------------------------------------------
+void ed::Pin::Draw(ImDrawList* drawList, DrawFlags flags)
+{
+ if (flags & Hovered)
+ {
+ drawList->ChannelsSetCurrent(m_Node->m_Channel + c_NodePinChannel);
+
+ drawList->AddRectFilled(m_Bounds.Min, m_Bounds.Max,
+ m_Color, m_Rounding, m_Corners);
+
+ if (m_BorderWidth > 0.0f)
+ {
+ FringeScaleScope fringe(1.0f);
+ drawList->AddRect(m_Bounds.Min, m_Bounds.Max,
+ m_BorderColor, m_Rounding, m_Corners, m_BorderWidth);
+ }
+
+ if (!Editor->IsSelected(m_Node))
+ m_Node->Draw(drawList, flags);
+ }
+}
+
+ImVec2 ed::Pin::GetClosestPoint(const ImVec2& p) const
+{
+ return ImRect_ClosestPoint(m_Pivot, p, true, m_Radius + m_ArrowSize);
+}
+
+ImLine ed::Pin::GetClosestLine(const Pin* pin) const
+{
+ return ImRect_ClosestLine(m_Pivot, pin->m_Pivot, m_Radius + m_ArrowSize, pin->m_Radius + pin->m_ArrowSize);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Node
+//
+//------------------------------------------------------------------------------
+bool ed::Node::AcceptDrag()
+{
+ m_DragStart = m_Bounds.Min;
+ return true;
+}
+
+void ed::Node::UpdateDrag(const ImVec2& offset)
+{
+ auto size = m_Bounds.GetSize();
+ m_Bounds.Min = ImFloor(m_DragStart + offset);
+ m_Bounds.Max = m_Bounds.Min + size;
+}
+
+bool ed::Node::EndDrag()
+{
+ return m_Bounds.Min != m_DragStart;
+}
+
+void ed::Node::Draw(ImDrawList* drawList, DrawFlags flags)
+{
+ if (flags == Detail::Object::None)
+ {
+ drawList->ChannelsSetCurrent(m_Channel + c_NodeBackgroundChannel);
+
+ drawList->AddRectFilled(
+ m_Bounds.Min,
+ m_Bounds.Max,
+ m_Color, m_Rounding);
+
+ if (IsGroup(this))
+ {
+ drawList->AddRectFilled(
+ m_GroupBounds.Min,
+ m_GroupBounds.Max,
+ m_GroupColor, m_GroupRounding);
+
+ if (m_GroupBorderWidth > 0.0f)
+ {
+ FringeScaleScope fringe(1.0f);
+
+ drawList->AddRect(
+ m_GroupBounds.Min,
+ m_GroupBounds.Max,
+ m_GroupBorderColor, m_GroupRounding, 15, m_GroupBorderWidth);
+ }
+ }
+
+# if 0
+ // #debug: highlight group regions
+ auto drawRect = [drawList](const ImRect& rect, ImU32 color)
+ {
+ if (ImRect_IsEmpty(rect)) return;
+ drawList->AddRectFilled(rect.Min, rect.Max, color);
+ };
+
+ drawRect(GetRegionBounds(NodeRegion::Top), IM_COL32(255, 0, 0, 64));
+ drawRect(GetRegionBounds(NodeRegion::Bottom), IM_COL32(255, 0, 0, 64));
+ drawRect(GetRegionBounds(NodeRegion::Left), IM_COL32(0, 255, 0, 64));
+ drawRect(GetRegionBounds(NodeRegion::Right), IM_COL32(0, 255, 0, 64));
+ drawRect(GetRegionBounds(NodeRegion::TopLeft), IM_COL32(255, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::TopRight), IM_COL32(255, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::BottomLeft), IM_COL32(255, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::BottomRight), IM_COL32(255, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::Center), IM_COL32(0, 0, 255, 64));
+ drawRect(GetRegionBounds(NodeRegion::Header), IM_COL32(0, 255, 255, 64));
+# endif
+
+ DrawBorder(drawList, m_BorderColor, m_BorderWidth);
+ }
+ else if (flags & Selected)
+ {
+ const auto borderColor = Editor->GetColor(StyleColor_SelNodeBorder);
+ const auto& editorStyle = Editor->GetStyle();
+
+ drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel);
+
+ DrawBorder(drawList, borderColor, editorStyle.SelectedNodeBorderWidth);
+ }
+ else if (!IsGroup(this) && (flags & Hovered))
+ {
+ const auto borderColor = Editor->GetColor(StyleColor_HovNodeBorder);
+ const auto& editorStyle = Editor->GetStyle();
+
+ drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel);
+
+ DrawBorder(drawList, borderColor, editorStyle.HoveredNodeBorderWidth);
+ }
+}
+
+void ed::Node::DrawBorder(ImDrawList* drawList, ImU32 color, float thickness)
+{
+ if (thickness > 0.0f)
+ {
+ drawList->AddRect(m_Bounds.Min, m_Bounds.Max,
+ color, m_Rounding, 15, thickness);
+ }
+}
+
+void ed::Node::GetGroupedNodes(std::vector<Node*>& result, bool append)
+{
+ if (!append)
+ result.resize(0);
+
+ if (!IsGroup(this))
+ return;
+
+ const auto firstNodeIndex = result.size();
+ Editor->FindNodesInRect(m_GroupBounds, result, true, false);
+
+ for (auto index = firstNodeIndex; index < result.size(); ++index)
+ result[index]->GetGroupedNodes(result, true);
+}
+
+ImRect ed::Node::GetRegionBounds(NodeRegion region) const
+{
+ if (m_Type == NodeType::Node)
+ {
+ if (region == NodeRegion::Header)
+ return m_Bounds;
+ }
+ else if (m_Type == NodeType::Group)
+ {
+ const float activeAreaMinimumSize = ImMax(ImMax(
+ Editor->GetView().InvScale * c_GroupSelectThickness,
+ m_GroupBorderWidth), c_GroupSelectThickness);
+ const float minimumSize = activeAreaMinimumSize * 5;
+
+ auto bounds = m_Bounds;
+ if (bounds.GetWidth() < minimumSize)
+ bounds.Expand(ImVec2(minimumSize - bounds.GetWidth(), 0.0f));
+ if (bounds.GetHeight() < minimumSize)
+ bounds.Expand(ImVec2(0.0f, minimumSize - bounds.GetHeight()));
+
+ if (region == NodeRegion::Top)
+ {
+ bounds.Max.y = bounds.Min.y + activeAreaMinimumSize;
+ bounds.Min.x += activeAreaMinimumSize;
+ bounds.Max.x -= activeAreaMinimumSize;
+ return bounds;
+ }
+ else if (region == NodeRegion::Bottom)
+ {
+ bounds.Min.y = bounds.Max.y - activeAreaMinimumSize;
+ bounds.Min.x += activeAreaMinimumSize;
+ bounds.Max.x -= activeAreaMinimumSize;
+ return bounds;
+ }
+ else if (region == NodeRegion::Left)
+ {
+ bounds.Max.x = bounds.Min.x + activeAreaMinimumSize;
+ bounds.Min.y += activeAreaMinimumSize;
+ bounds.Max.y -= activeAreaMinimumSize;
+ return bounds;
+ }
+ else if (region == NodeRegion::Right)
+ {
+ bounds.Min.x = bounds.Max.x - activeAreaMinimumSize;
+ bounds.Min.y += activeAreaMinimumSize;
+ bounds.Max.y -= activeAreaMinimumSize;
+ return bounds;
+ }
+ else if (region == NodeRegion::TopLeft)
+ {
+ bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2;
+ bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2;
+ return bounds;
+ }
+ else if (region == NodeRegion::TopRight)
+ {
+ bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2;
+ bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2;
+ return bounds;
+ }
+ else if (region == NodeRegion::BottomRight)
+ {
+ bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2;
+ bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2;
+ return bounds;
+ }
+ else if (region == NodeRegion::BottomLeft)
+ {
+ bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2;
+ bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2;
+ return bounds;
+ }
+ else if (region == NodeRegion::Header)
+ {
+ bounds.Min.x += activeAreaMinimumSize;
+ bounds.Max.x -= activeAreaMinimumSize;
+ bounds.Min.y += activeAreaMinimumSize;
+ bounds.Max.y = ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y);
+ return bounds;
+ }
+ else if (region == NodeRegion::Center)
+ {
+ bounds.Max.x -= activeAreaMinimumSize;
+ bounds.Min.y = ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y);
+ bounds.Min.x += activeAreaMinimumSize;
+ bounds.Max.y -= activeAreaMinimumSize;
+ return bounds;
+ }
+ }
+
+ return ImRect();
+}
+
+ed::NodeRegion ed::Node::GetRegion(const ImVec2& point) const
+{
+ if (m_Type == NodeType::Node)
+ {
+ if (m_Bounds.Contains(point))
+ return NodeRegion::Header;
+ else
+ return NodeRegion::None;
+ }
+ else if (m_Type == NodeType::Group)
+ {
+ static const NodeRegion c_Regions[] =
+ {
+ // Corners first, they may overlap other regions.
+ NodeRegion::TopLeft,
+ NodeRegion::TopRight,
+ NodeRegion::BottomLeft,
+ NodeRegion::BottomRight,
+ NodeRegion::Header,
+ NodeRegion::Top,
+ NodeRegion::Bottom,
+ NodeRegion::Left,
+ NodeRegion::Right,
+ NodeRegion::Center
+ };
+
+ for (auto region : c_Regions)
+ {
+ auto bounds = GetRegionBounds(region);
+ if (bounds.Contains(point))
+ return region;
+ }
+ }
+
+ return NodeRegion::None;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Link
+//
+//------------------------------------------------------------------------------
+void ed::Link::Draw(ImDrawList* drawList, DrawFlags flags)
+{
+ if (flags == None)
+ {
+ drawList->ChannelsSetCurrent(c_LinkChannel_Links);
+
+ Draw(drawList, m_Color, 0.0f);
+ }
+ else if (flags & Selected)
+ {
+ const auto borderColor = Editor->GetColor(StyleColor_SelLinkBorder);
+
+ drawList->ChannelsSetCurrent(c_LinkChannel_Selection);
+
+ Draw(drawList, borderColor, 4.5f);
+ }
+ else if (flags & Hovered)
+ {
+ const auto borderColor = Editor->GetColor(StyleColor_HovLinkBorder);
+
+ drawList->ChannelsSetCurrent(c_LinkChannel_Selection);
+
+ Draw(drawList, borderColor, 2.0f);
+ }
+}
+
+void ed::Link::Draw(ImDrawList* drawList, ImU32 color, float extraThickness) const
+{
+ if (!m_IsLive)
+ return;
+
+ const auto curve = GetCurve();
+
+ ImDrawList_AddBezierWithArrows(drawList, curve, m_Thickness + extraThickness,
+ m_StartPin && m_StartPin->m_ArrowSize > 0.0f ? m_StartPin->m_ArrowSize + extraThickness : 0.0f,
+ m_StartPin && m_StartPin->m_ArrowWidth > 0.0f ? m_StartPin->m_ArrowWidth + extraThickness : 0.0f,
+ m_EndPin && m_EndPin->m_ArrowSize > 0.0f ? m_EndPin->m_ArrowSize + extraThickness : 0.0f,
+ m_EndPin && m_EndPin->m_ArrowWidth > 0.0f ? m_EndPin->m_ArrowWidth + extraThickness : 0.0f,
+ true, color, 1.0f);
+}
+
+void ed::Link::UpdateEndpoints()
+{
+ const auto line = m_StartPin->GetClosestLine(m_EndPin);
+ m_Start = line.A;
+ m_End = line.B;
+}
+
+ImCubicBezierPoints ed::Link::GetCurve() const
+{
+ auto easeLinkStrength = [](const ImVec2& a, const ImVec2& b, float strength)
+ {
+ const auto distanceX = b.x - a.x;
+ const auto distanceY = b.y - a.y;
+ const auto distance = ImSqrt(distanceX * distanceX + distanceY * distanceY);
+ const auto halfDistance = distance * 0.5f;
+
+ if (halfDistance < strength)
+ strength = strength * ImSin(IM_PI * 0.5f * halfDistance / strength);
+
+ return strength;
+ };
+
+ const auto startStrength = easeLinkStrength(m_Start, m_End, m_StartPin->m_Strength);
+ const auto endStrength = easeLinkStrength(m_Start, m_End, m_EndPin->m_Strength);
+ const auto cp0 = m_Start + m_StartPin->m_Dir * startStrength;
+ const auto cp1 = m_End + m_EndPin->m_Dir * endStrength;
+
+ ImCubicBezierPoints result;
+ result.P0 = m_Start;
+ result.P1 = cp0;
+ result.P2 = cp1;
+ result.P3 = m_End;
+
+ return result;
+}
+
+bool ed::Link::TestHit(const ImVec2& point, float extraThickness) const
+{
+ if (!m_IsLive)
+ return false;
+
+ auto bounds = GetBounds();
+ if (extraThickness > 0.0f)
+ bounds.Expand(extraThickness);
+
+ if (!bounds.Contains(point))
+ return false;
+
+ const auto bezier = GetCurve();
+ const auto result = ImProjectOnCubicBezier(point, bezier.P0, bezier.P1, bezier.P2, bezier.P3, 50);
+
+ return result.Distance <= m_Thickness + extraThickness;
+}
+
+bool ed::Link::TestHit(const ImRect& rect, bool allowIntersect) const
+{
+ if (!m_IsLive)
+ return false;
+
+ const auto bounds = GetBounds();
+
+ if (rect.Contains(bounds))
+ return true;
+
+ if (!allowIntersect || !rect.Overlaps(bounds))
+ return false;
+
+ const auto bezier = GetCurve();
+
+ const auto p0 = rect.GetTL();
+ const auto p1 = rect.GetTR();
+ const auto p2 = rect.GetBR();
+ const auto p3 = rect.GetBL();
+
+ if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p0, p1).Count > 0)
+ return true;
+ if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p1, p2).Count > 0)
+ return true;
+ if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p2, p3).Count > 0)
+ return true;
+ if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p3, p0).Count > 0)
+ return true;
+
+ return false;
+}
+
+ImRect ed::Link::GetBounds() const
+{
+ if (m_IsLive)
+ {
+ const auto curve = GetCurve();
+ auto bounds = ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3);
+
+ if (bounds.GetWidth() == 0.0f)
+ {
+ bounds.Min.x -= 0.5f;
+ bounds.Max.x += 0.5f;
+ }
+
+ if (bounds.GetHeight() == 0.0f)
+ {
+ bounds.Min.y -= 0.5f;
+ bounds.Max.y += 0.5f;
+ }
+
+ if (m_StartPin->m_ArrowSize)
+ {
+ const auto start_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f));
+ const auto p0 = curve.P0;
+ const auto p1 = curve.P0 - start_dir * m_StartPin->m_ArrowSize;
+ const auto min = ImMin(p0, p1);
+ const auto max = ImMax(p0, p1);
+ auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1)));
+ bounds.Add(arrowBounds);
+ }
+
+ if (m_EndPin->m_ArrowSize)
+ {
+ const auto end_dir = ImNormalized(ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f));
+ const auto p0 = curve.P3;
+ const auto p1 = curve.P3 + end_dir * m_EndPin->m_ArrowSize;
+ const auto min = ImMin(p0, p1);
+ const auto max = ImMax(p0, p1);
+ auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1)));
+ bounds.Add(arrowBounds);
+ }
+
+ return bounds;
+ }
+ else
+ return ImRect();
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Editor Context
+//
+//------------------------------------------------------------------------------
+ed::EditorContext::EditorContext(const ax::NodeEditor::Config* config)
+ : m_IsFirstFrame(true)
+ , m_IsWindowActive(false)
+ , m_ShortcutsEnabled(true)
+ , m_Style()
+ , m_Nodes()
+ , m_Pins()
+ , m_Links()
+ , m_SelectionId(1)
+ , m_LastActiveLink(nullptr)
+ , m_Canvas()
+ , m_IsCanvasVisible(false)
+ , m_NodeBuilder(this)
+ , m_HintBuilder(this)
+ , m_CurrentAction(nullptr)
+ , m_NavigateAction(this, m_Canvas)
+ , m_SizeAction(this)
+ , m_DragAction(this)
+ , m_SelectAction(this)
+ , m_ContextMenuAction(this)
+ , m_ShortcutAction(this)
+ , m_CreateItemAction(this)
+ , m_DeleteItemsAction(this)
+ , m_AnimationControllers{ &m_FlowAnimationController }
+ , m_FlowAnimationController(this)
+ , m_DoubleClickedNode(0)
+ , m_DoubleClickedPin(0)
+ , m_DoubleClickedLink(0)
+ , m_BackgroundClicked(false)
+ , m_BackgroundDoubleClicked(false)
+ , m_IsInitialized(false)
+ , m_Settings()
+ , m_Config(config)
+ , m_ExternalChannel(0)
+{
+}
+
+ed::EditorContext::~EditorContext()
+{
+ if (m_IsInitialized)
+ SaveSettings();
+
+ for (auto link : m_Links) delete link.m_Object;
+ for (auto pin : m_Pins) delete pin.m_Object;
+ for (auto node : m_Nodes) delete node.m_Object;
+
+ m_Splitter.ClearFreeMemory();
+}
+
+void ed::EditorContext::Begin(const char* id, const ImVec2& size)
+{
+ if (!m_IsInitialized)
+ {
+ LoadSettings();
+ m_IsInitialized = true;
+ }
+
+ //ImGui::LogToClipboard();
+ //Log("---- begin ----");
+
+ for (auto node : m_Nodes) node->Reset();
+ for (auto pin : m_Pins) pin->Reset();
+ for (auto link : m_Links) link->Reset();
+
+ auto drawList = ImGui::GetWindowDrawList();
+
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+ m_ExternalChannel = drawList->_Splitter._Current;
+
+ ImGui::PushID(id);
+
+ auto availableContentSize = ImGui::GetContentRegionAvail();
+ ImVec2 canvasSize = ImFloor(size);
+ if (canvasSize.x <= 0.0f)
+ canvasSize.x = ImMax(4.0f, availableContentSize.x);
+ if (canvasSize.y <= 0.0f)
+ canvasSize.y = ImMax(4.0f, availableContentSize.y);
+
+ m_IsCanvasVisible = m_Canvas.Begin(id, canvasSize);
+
+ //ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
+ //ImGui::BeginChild(id, size, false,
+ // ImGuiWindowFlags_NoMove |
+ // ImGuiWindowFlags_NoScrollbar |
+ // ImGuiWindowFlags_NoScrollWithMouse);
+
+ ImGui::CaptureKeyboardFromApp();
+
+ m_IsWindowActive = ImGui::IsWindowFocused();
+
+ //
+ m_NavigateAction.SetWindow(m_Canvas.ViewRect().Min, m_Canvas.ViewRect().GetSize());
+
+ if (m_CurrentAction && m_CurrentAction->IsDragging() && m_NavigateAction.MoveOverEdge())
+ {
+ auto& io = ImGui::GetIO();
+ auto offset = m_NavigateAction.GetMoveOffset();
+ for (int i = 0; i < 5; ++i)
+ io.MouseClickedPos[i] = io.MouseClickedPos[i] - offset;
+ }
+ else
+ m_NavigateAction.StopMoveOverEdge();
+
+ m_Canvas.SetView(m_NavigateAction.GetView());
+
+ // #debug #clip
+ //ImGui::Text("CLIP = { x=%g y=%g w=%g h=%g r=%g b=%g }",
+ // clipMin.x, clipMin.y, clipMax.x - clipMin.x, clipMax.y - clipMin.y, clipMax.x, clipMax.y);
+
+ // Reserve channels for background and links
+ ImDrawList_ChannelsGrow(drawList, c_NodeStartChannel);
+
+ if (HasSelectionChanged())
+ ++m_SelectionId;
+
+ m_LastSelectedObjects = m_SelectedObjects;
+}
+
+void ed::EditorContext::End()
+{
+ //auto& io = ImGui::GetIO();
+ auto control = BuildControl(m_CurrentAction && m_CurrentAction->IsDragging()); // NavigateAction.IsMovingOverEdge()
+ auto drawList = ImGui::GetWindowDrawList();
+ //auto& editorStyle = GetStyle();
+
+ m_DoubleClickedNode = control.DoubleClickedNode ? control.DoubleClickedNode->m_ID : 0;
+ m_DoubleClickedPin = control.DoubleClickedPin ? control.DoubleClickedPin->m_ID : 0;
+ m_DoubleClickedLink = control.DoubleClickedLink ? control.DoubleClickedLink->m_ID : 0;
+ m_BackgroundClicked = control.BackgroundClicked;
+ m_BackgroundDoubleClicked = control.BackgroundDoubleClicked;
+
+ //if (DoubleClickedNode) LOG_TRACE(0, "DOUBLE CLICK NODE: %d", DoubleClickedNode);
+ //if (DoubleClickedPin) LOG_TRACE(0, "DOUBLE CLICK PIN: %d", DoubleClickedPin);
+ //if (DoubleClickedLink) LOG_TRACE(0, "DOUBLE CLICK LINK: %d", DoubleClickedLink);
+ //if (BackgroundDoubleClicked) LOG_TRACE(0, "DOUBLE CLICK BACKGROUND", DoubleClickedLink);
+
+ const bool isSelecting = m_CurrentAction && m_CurrentAction->AsSelect() != nullptr;
+ const bool isDragging = m_CurrentAction && m_CurrentAction->AsDrag() != nullptr;
+ //const bool isSizing = CurrentAction && CurrentAction->AsSize() != nullptr;
+
+ // Draw nodes
+ for (auto node : m_Nodes)
+ if (node->m_IsLive && node->IsVisible())
+ node->Draw(drawList);
+
+ // Draw links
+ for (auto link : m_Links)
+ if (link->m_IsLive && link->IsVisible())
+ link->Draw(drawList);
+
+ // Highlight selected objects
+ {
+ auto selectedObjects = &m_SelectedObjects;
+ if (auto selectAction = m_CurrentAction ? m_CurrentAction->AsSelect() : nullptr)
+ selectedObjects = &selectAction->m_CandidateObjects;
+
+ for (auto selectedObject : *selectedObjects)
+ if (selectedObject->IsVisible())
+ selectedObject->Draw(drawList, Object::Selected);
+ }
+
+ if (!isSelecting)
+ {
+ auto hoveredObject = control.HotObject;
+ if (auto dragAction = m_CurrentAction ? m_CurrentAction->AsDrag() : nullptr)
+ hoveredObject = dragAction->m_DraggedObject;
+ if (auto sizeAction = m_CurrentAction ? m_CurrentAction->AsSize() : nullptr)
+ hoveredObject = sizeAction->m_SizedNode;
+
+ if (hoveredObject && !IsSelected(hoveredObject) && hoveredObject->IsVisible())
+ hoveredObject->Draw(drawList, Object::Hovered);
+ }
+
+ // Draw animations
+ for (auto controller : m_AnimationControllers)
+ controller->Draw(drawList);
+
+ if (m_CurrentAction && !m_CurrentAction->Process(control))
+ m_CurrentAction = nullptr;
+
+ if (m_NavigateAction.m_IsActive)
+ m_NavigateAction.Process(control);
+ else
+ m_NavigateAction.Accept(control);
+
+ if (nullptr == m_CurrentAction)
+ {
+ EditorAction* possibleAction = nullptr;
+
+ auto accept = [&possibleAction, &control](EditorAction& action)
+ {
+ auto result = action.Accept(control);
+
+ if (result == EditorAction::True)
+ return true;
+ else if (/*!possibleAction &&*/ result == EditorAction::Possible)
+ possibleAction = &action;
+ else if (result == EditorAction::Possible)
+ action.Reject();
+
+ return false;
+ };
+
+ if (accept(m_ContextMenuAction))
+ m_CurrentAction = &m_ContextMenuAction;
+ else if (accept(m_ShortcutAction))
+ m_CurrentAction = &m_ShortcutAction;
+ else if (accept(m_SizeAction))
+ m_CurrentAction = &m_SizeAction;
+ else if (accept(m_DragAction))
+ m_CurrentAction = &m_DragAction;
+ else if (accept(m_SelectAction))
+ m_CurrentAction = &m_SelectAction;
+ else if (accept(m_CreateItemAction))
+ m_CurrentAction = &m_CreateItemAction;
+ else if (accept(m_DeleteItemsAction))
+ m_CurrentAction = &m_DeleteItemsAction;
+
+ if (possibleAction)
+ ImGui::SetMouseCursor(possibleAction->GetCursor());
+
+ if (m_CurrentAction && possibleAction)
+ possibleAction->Reject();
+ }
+
+ if (m_CurrentAction)
+ ImGui::SetMouseCursor(m_CurrentAction->GetCursor());
+
+ // Draw selection rectangle
+ m_SelectAction.Draw(drawList);
+
+ bool sortGroups = false;
+ if (control.ActiveNode)
+ {
+ if (!IsGroup(control.ActiveNode))
+ {
+ // Bring active node to front
+ auto activeNodeIt = std::find(m_Nodes.begin(), m_Nodes.end(), control.ActiveNode);
+ std::rotate(activeNodeIt, activeNodeIt + 1, m_Nodes.end());
+ }
+ else if (!isDragging && m_CurrentAction && m_CurrentAction->AsDrag())
+ {
+ // Bring content of dragged group to front
+ std::vector<Node*> nodes;
+ control.ActiveNode->GetGroupedNodes(nodes);
+
+ std::stable_partition(m_Nodes.begin(), m_Nodes.end(), [&nodes](Node* node)
+ {
+ return std::find(nodes.begin(), nodes.end(), node) == nodes.end();
+ });
+
+ sortGroups = true;
+ }
+ }
+
+ // Sort nodes if bounds of node changed
+ if (sortGroups || ((m_Settings.m_DirtyReason & (SaveReasonFlags::Position | SaveReasonFlags::Size)) != SaveReasonFlags::None))
+ {
+ // Bring all groups before regular nodes
+ auto groupsItEnd = std::stable_partition(m_Nodes.begin(), m_Nodes.end(), IsGroup);
+
+ // Sort groups by area
+ std::sort(m_Nodes.begin(), groupsItEnd, [this](Node* lhs, Node* rhs)
+ {
+ const auto& lhsSize = lhs == m_SizeAction.m_SizedNode ? m_SizeAction.GetStartGroupBounds().GetSize() : lhs->m_GroupBounds.GetSize();
+ const auto& rhsSize = rhs == m_SizeAction.m_SizedNode ? m_SizeAction.GetStartGroupBounds().GetSize() : rhs->m_GroupBounds.GetSize();
+
+ const auto lhsArea = lhsSize.x * lhsSize.y;
+ const auto rhsArea = rhsSize.x * rhsSize.y;
+
+ return lhsArea > rhsArea;
+ });
+ }
+
+# if 1
+ // Every node has few channels assigned. Grow channel list
+ // to hold twice as much of channels and place them in
+ // node drawing order.
+ {
+ // Copy group nodes
+ auto liveNodeCount = static_cast<int>(std::count_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return node->m_IsLive; }));
+
+ // Reserve two additional channels for sorted list of channels
+ auto nodeChannelCount = drawList->_Splitter._Count;
+ ImDrawList_ChannelsGrow(drawList, drawList->_Splitter._Count + c_ChannelsPerNode * liveNodeCount + c_LinkChannelCount);
+
+ int targetChannel = nodeChannelCount;
+
+ auto copyNode = [&targetChannel, drawList](Node* node)
+ {
+ if (!node->m_IsLive)
+ return;
+
+ for (int i = 0; i < c_ChannelsPerNode; ++i)
+ ImDrawList_SwapChannels(drawList, node->m_Channel + i, targetChannel + i);
+
+ node->m_Channel = targetChannel;
+ targetChannel += c_ChannelsPerNode;
+ };
+
+ auto groupsItEnd = std::find_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return !IsGroup(node); });
+
+ // Copy group nodes
+ std::for_each(m_Nodes.begin(), groupsItEnd, copyNode);
+
+ // Copy links
+ for (int i = 0; i < c_LinkChannelCount; ++i, ++targetChannel)
+ ImDrawList_SwapChannels(drawList, c_LinkStartChannel + i, targetChannel);
+
+ // Copy normal nodes
+ std::for_each(groupsItEnd, m_Nodes.end(), copyNode);
+ }
+# endif
+
+ // ImGui::PopClipRect();
+
+ // Draw grid
+# if 1 // #FIXME
+ {
+ //auto& style = ImGui::GetStyle();
+
+ drawList->ChannelsSetCurrent(c_UserChannel_Grid);
+
+ ImVec2 offset = m_Canvas.ViewOrigin() * (1.0f / m_Canvas.ViewScale());
+ ImU32 GRID_COLOR = GetColor(StyleColor_Grid, ImClamp(m_Canvas.ViewScale() * m_Canvas.ViewScale(), 0.0f, 1.0f));
+ float GRID_SX = 32.0f;// * m_Canvas.ViewScale();
+ float GRID_SY = 32.0f;// * m_Canvas.ViewScale();
+ ImVec2 VIEW_POS = m_Canvas.ViewRect().Min;
+ ImVec2 VIEW_SIZE = m_Canvas.ViewRect().GetSize();
+
+ drawList->AddRectFilled(VIEW_POS, VIEW_POS + VIEW_SIZE, GetColor(StyleColor_Bg));
+
+ for (float x = fmodf(offset.x, GRID_SX); x < VIEW_SIZE.x; x += GRID_SX)
+ drawList->AddLine(ImVec2(x, 0.0f) + VIEW_POS, ImVec2(x, VIEW_SIZE.y) + VIEW_POS, GRID_COLOR);
+ for (float y = fmodf(offset.y, GRID_SY); y < VIEW_SIZE.y; y += GRID_SY)
+ drawList->AddLine(ImVec2(0.0f, y) + VIEW_POS, ImVec2(VIEW_SIZE.x, y) + VIEW_POS, GRID_COLOR);
+ }
+# endif
+
+# if 0
+ {
+ auto userChannel = drawList->_Splitter._Count;
+ auto channelsToCopy = c_UserLayersCount;
+ ImDrawList_ChannelsGrow(drawList, userChannel + channelsToCopy);
+ for (int i = 0; i < channelsToCopy; ++i)
+ ImDrawList_SwapChannels(drawList, userChannel + i, c_UserLayerChannelStart + i);
+ }
+# endif
+
+# if 0
+ {
+ auto preOffset = ImVec2(0, 0);
+ auto postOffset = m_OldCanvas.WindowScreenPos + m_OldCanvas.ClientOrigin;
+ auto scale = m_OldCanvas.Zoom;
+
+ ImDrawList_TransformChannels(drawList, 0, 1, preOffset, scale, postOffset);
+ ImDrawList_TransformChannels(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, preOffset, scale, postOffset);
+
+ auto clipTranslation = m_OldCanvas.WindowScreenPos - m_OldCanvas.FromScreen(m_OldCanvas.WindowScreenPos);
+ ImGui::PushClipRect(m_OldCanvas.WindowScreenPos + ImVec2(1, 1), m_OldCanvas.WindowScreenPos + m_OldCanvas.WindowScreenSize - ImVec2(1, 1), false);
+ ImDrawList_TranslateAndClampClipRects(drawList, 0, 1, clipTranslation);
+ ImDrawList_TranslateAndClampClipRects(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, clipTranslation);
+ ImGui::PopClipRect();
+
+ // #debug: Static grid in local space
+ //for (float x = 0; x < Canvas.WindowScreenSize.x; x += 100)
+ // drawList->AddLine(ImVec2(x, 0.0f) + Canvas.WindowScreenPos, ImVec2(x, Canvas.WindowScreenSize.y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128));
+ //for (float y = 0; y < Canvas.WindowScreenSize.y; y += 100)
+ // drawList->AddLine(ImVec2(0.0f, y) + Canvas.WindowScreenPos, ImVec2(Canvas.WindowScreenSize.x, y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128));
+ }
+# endif
+
+# if 1
+ // Move user and hint channels to top
+ {
+ // Clip plane is transformed to global space.
+ // These channels already have clip planes in global space, so
+ // we move them to clip plane. Batch transformation in canvas
+ // will bring them back to global space.
+ auto preTransformClipRect = [this, drawList](int channelIndex)
+ {
+ ImDrawChannel& channel = drawList->_Splitter._Channels[channelIndex];
+ for (ImDrawCmd& cmd : channel._CmdBuffer)
+ {
+ auto a = ToCanvas(ImVec2(cmd.ClipRect.x, cmd.ClipRect.y));
+ auto b = ToCanvas(ImVec2(cmd.ClipRect.z, cmd.ClipRect.w));
+ cmd.ClipRect = ImVec4(a.x, a.y, b.x, b.y);
+ }
+ };
+
+ drawList->ChannelsSetCurrent(0);
+
+ auto channelCount = drawList->_Splitter._Count;
+ ImDrawList_ChannelsGrow(drawList, channelCount + 3);
+ ImDrawList_SwapChannels(drawList, c_UserChannel_HintsBackground, channelCount + 0);
+ ImDrawList_SwapChannels(drawList, c_UserChannel_Hints, channelCount + 1);
+ ImDrawList_SwapChannels(drawList, c_UserChannel_Content, channelCount + 2);
+
+ preTransformClipRect(channelCount + 0);
+ preTransformClipRect(channelCount + 1);
+ preTransformClipRect(channelCount + 2);
+ }
+# endif
+
+ UpdateAnimations();
+
+ drawList->ChannelsMerge();
+
+ // #debug
+ // drawList->AddRectFilled(ImVec2(-10.0f, -10.0f), ImVec2(10.0f, 10.0f), IM_COL32(255, 0, 255, 255));
+
+ // ImGui::EndChild();
+ // ImGui::PopStyleColor();
+ if (m_IsCanvasVisible)
+ m_Canvas.End();
+
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+
+ // Draw border
+ {
+ auto& style = ImGui::GetStyle();
+ auto borderShadoColor = style.Colors[ImGuiCol_BorderShadow];
+ auto borderColor = style.Colors[ImGuiCol_Border];
+ drawList->AddRect(m_Canvas.Rect().Min + ImVec2(1, 1), m_Canvas.Rect().Max - ImVec2(1, 1), ImColor(borderShadoColor));
+ drawList->AddRect(m_Canvas.Rect().Min, m_Canvas.Rect().Max, ImColor(borderColor));
+ }
+
+ // ShowMetrics(control);
+
+ ImGui::PopID();
+
+ if (!m_CurrentAction && m_IsFirstFrame && !m_Settings.m_Selection.empty())
+ {
+ ClearSelection();
+ for (auto id : m_Settings.m_Selection)
+ if (auto object = FindObject(id))
+ SelectObject(object);
+ }
+
+ if (HasSelectionChanged())
+ MakeDirty(SaveReasonFlags::Selection);
+
+ if (m_Settings.m_IsDirty && !m_CurrentAction)
+ SaveSettings();
+
+ m_IsFirstFrame = false;
+}
+
+bool ed::EditorContext::DoLink(LinkId id, PinId startPinId, PinId endPinId, ImU32 color, float thickness)
+{
+ //auto& editorStyle = GetStyle();
+
+ auto startPin = FindPin(startPinId);
+ auto endPin = FindPin(endPinId);
+
+ if (!startPin || !startPin->m_IsLive || !endPin || !endPin->m_IsLive)
+ return false;
+
+ startPin->m_HasConnection = true;
+ endPin->m_HasConnection = true;
+
+ auto link = GetLink(id);
+ link->m_StartPin = startPin;
+ link->m_EndPin = endPin;
+ link->m_Color = color;
+ link->m_Thickness = thickness;
+ link->m_IsLive = true;
+
+ link->UpdateEndpoints();
+
+ return true;
+}
+
+void ed::EditorContext::SetNodePosition(NodeId nodeId, const ImVec2& position)
+{
+ auto node = FindNode(nodeId);
+ if (!node)
+ {
+ node = CreateNode(nodeId);
+ node->m_IsLive = false;
+ }
+
+ if (node->m_Bounds.Min != position)
+ {
+ node->m_Bounds.Translate(position - node->m_Bounds.Min);
+ node->m_Bounds.Floor();
+ MakeDirty(NodeEditor::SaveReasonFlags::Position, node);
+ }
+}
+
+ImVec2 ed::EditorContext::GetNodePosition(NodeId nodeId)
+{
+ auto node = FindNode(nodeId);
+ if (!node)
+ return ImVec2(FLT_MAX, FLT_MAX);
+
+ return node->m_Bounds.Min;
+}
+
+ImVec2 ed::EditorContext::GetNodeSize(NodeId nodeId)
+{
+ auto node = FindNode(nodeId);
+ if (!node)
+ return ImVec2(0, 0);
+
+ return node->m_Bounds.GetSize();
+}
+
+void ed::EditorContext::MarkNodeToRestoreState(Node* node)
+{
+ node->m_RestoreState = true;
+}
+
+void ed::EditorContext::RestoreNodeState(Node* node)
+{
+ auto settings = m_Settings.FindNode(node->m_ID);
+ if (!settings)
+ return;
+
+ // Load state from config (if possible)
+ if (!NodeSettings::Parse(m_Config.LoadNode(node->m_ID), *settings))
+ return;
+
+ node->m_Bounds.Min = settings->m_Location;
+ node->m_Bounds.Max = node->m_Bounds.Min + settings->m_Size;
+ node->m_Bounds.Floor();
+ node->m_GroupBounds.Min = settings->m_Location;
+ node->m_GroupBounds.Max = node->m_GroupBounds.Min + settings->m_GroupSize;
+ node->m_GroupBounds.Floor();
+}
+
+void ed::EditorContext::ClearSelection()
+{
+ m_SelectedObjects.clear();
+}
+
+void ed::EditorContext::SelectObject(Object* object)
+{
+ m_SelectedObjects.push_back(object);
+}
+
+void ed::EditorContext::DeselectObject(Object* object)
+{
+ auto objectIt = std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), object);
+ if (objectIt != m_SelectedObjects.end())
+ m_SelectedObjects.erase(objectIt);
+}
+
+void ed::EditorContext::SetSelectedObject(Object* object)
+{
+ ClearSelection();
+ SelectObject(object);
+}
+
+void ed::EditorContext::ToggleObjectSelection(Object* object)
+{
+ if (IsSelected(object))
+ DeselectObject(object);
+ else
+ SelectObject(object);
+}
+
+bool ed::EditorContext::IsSelected(Object* object)
+{
+ return std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), object) != m_SelectedObjects.end();
+}
+
+const ed::vector<ed::Object*>& ed::EditorContext::GetSelectedObjects()
+{
+ return m_SelectedObjects;
+}
+
+bool ed::EditorContext::IsAnyNodeSelected()
+{
+ for (auto object : m_SelectedObjects)
+ if (object->AsNode())
+ return true;
+
+ return false;
+}
+
+bool ed::EditorContext::IsAnyLinkSelected()
+{
+ for (auto object : m_SelectedObjects)
+ if (object->AsLink())
+ return true;
+
+ return false;
+}
+
+bool ed::EditorContext::HasSelectionChanged()
+{
+ return m_LastSelectedObjects != m_SelectedObjects;
+}
+
+ed::Node* ed::EditorContext::FindNodeAt(const ImVec2& p)
+{
+ for (auto node : m_Nodes)
+ if (node->TestHit(p))
+ return node;
+
+ return nullptr;
+}
+
+void ed::EditorContext::FindNodesInRect(const ImRect& r, vector<Node*>& result, bool append, bool includeIntersecting)
+{
+ if (!append)
+ result.resize(0);
+
+ if (ImRect_IsEmpty(r))
+ return;
+
+ for (auto node : m_Nodes)
+ if (node->TestHit(r, includeIntersecting))
+ result.push_back(node);
+}
+
+void ed::EditorContext::FindLinksInRect(const ImRect& r, vector<Link*>& result, bool append)
+{
+ if (!append)
+ result.resize(0);
+
+ if (ImRect_IsEmpty(r))
+ return;
+
+ for (auto link : m_Links)
+ if (link->TestHit(r))
+ result.push_back(link);
+}
+
+void ed::EditorContext::FindLinksForNode(NodeId nodeId, vector<Link*>& result, bool add)
+{
+ if (!add)
+ result.clear();
+
+ for (auto link : m_Links)
+ {
+ if (!link->m_IsLive)
+ continue;
+
+ if (link->m_StartPin->m_Node->m_ID == nodeId || link->m_EndPin->m_Node->m_ID == nodeId)
+ result.push_back(link);
+ }
+}
+
+bool ed::EditorContext::PinHadAnyLinks(PinId pinId)
+{
+ auto pin = FindPin(pinId);
+ if (!pin || !pin->m_IsLive)
+ return false;
+
+ return pin->m_HasConnection || pin->m_HadConnection;
+}
+
+void ed::EditorContext::NotifyLinkDeleted(Link* link)
+{
+ if (m_LastActiveLink == link)
+ m_LastActiveLink = nullptr;
+}
+
+void ed::EditorContext::Suspend(SuspendFlags flags)
+{
+ auto drawList = ImGui::GetWindowDrawList();
+ auto lastChannel = drawList->_Splitter._Current;
+ drawList->ChannelsSetCurrent(m_ExternalChannel);
+ m_Canvas.Suspend();
+ drawList->ChannelsSetCurrent(lastChannel);
+ if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter)
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+}
+
+void ed::EditorContext::Resume(SuspendFlags flags)
+{
+ auto drawList = ImGui::GetWindowDrawList();
+ if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter)
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+ auto lastChannel = drawList->_Splitter._Current;
+ drawList->ChannelsSetCurrent(m_ExternalChannel);
+ m_Canvas.Resume();
+ drawList->ChannelsSetCurrent(lastChannel);
+}
+
+bool ed::EditorContext::IsSuspended()
+{
+ return m_Canvas.IsSuspended();
+}
+
+bool ed::EditorContext::IsActive()
+{
+ return m_IsWindowActive;
+}
+
+ed::Pin* ed::EditorContext::CreatePin(PinId id, PinKind kind)
+{
+ IM_ASSERT(nullptr == FindObject(id));
+ auto pin = new Pin(this, id, kind);
+ m_Pins.push_back({id, pin});
+ std::sort(m_Pins.begin(), m_Pins.end());
+ return pin;
+}
+
+ed::Node* ed::EditorContext::CreateNode(NodeId id)
+{
+ IM_ASSERT(nullptr == FindObject(id));
+ auto node = new Node(this, id);
+ m_Nodes.push_back({id, node});
+ //std::sort(Nodes.begin(), Nodes.end());
+
+ auto settings = m_Settings.FindNode(id);
+ if (!settings)
+ settings = m_Settings.AddNode(id);
+
+ if (!settings->m_WasUsed)
+ {
+ settings->m_WasUsed = true;
+ RestoreNodeState(node);
+ }
+
+ node->m_Bounds.Min = settings->m_Location;
+ node->m_Bounds.Max = node->m_Bounds.Min;
+ node->m_Bounds.Floor();
+
+ if (settings->m_GroupSize.x > 0 || settings->m_GroupSize.y > 0)
+ {
+ node->m_Type = NodeType::Group;
+ node->m_GroupBounds.Min = settings->m_Location;
+ node->m_GroupBounds.Max = node->m_GroupBounds.Min + settings->m_GroupSize;
+ node->m_GroupBounds.Floor();
+ }
+
+ node->m_IsLive = false;
+
+ return node;
+}
+
+ed::Link* ed::EditorContext::CreateLink(LinkId id)
+{
+ IM_ASSERT(nullptr == FindObject(id));
+ auto link = new Link(this, id);
+ m_Links.push_back({id, link});
+ std::sort(m_Links.begin(), m_Links.end());
+
+ return link;
+}
+
+template <typename C, typename Id>
+static inline auto FindItemInLinear(C& container, Id id)
+{
+# if defined(_DEBUG)
+ auto start = container.data();
+ auto end = container.data() + container.size();
+ for (auto it = start; it < end; ++it)
+ if ((*it).m_ID == id)
+ return it->m_Object;
+# else
+ for (auto item : container)
+ if (item.m_ID == id)
+ return item.m_Object;
+# endif
+
+ return static_cast<decltype(container[0].m_Object)>(nullptr);
+}
+
+template <typename C, typename Id>
+static inline auto FindItemIn(C& container, Id id)
+{
+//# if defined(_DEBUG)
+// auto start = container.data();
+// auto end = container.data() + container.size();
+// for (auto it = start; it < end; ++it)
+// if ((*it)->ID == id)
+// return *it;
+//# else
+// for (auto item : container)
+// if (item->ID == id)
+// return item;
+//# endif
+ auto key = typename C::value_type{ id, nullptr };
+ auto first = container.cbegin();
+ auto last = container.cend();
+ auto it = std::lower_bound(first, last, key);
+ if (it != last && (key.m_ID == it->m_ID))
+ return it->m_Object;
+ else
+ return static_cast<decltype(it->m_Object)>(nullptr);
+}
+
+ed::Node* ed::EditorContext::FindNode(NodeId id)
+{
+ return FindItemInLinear(m_Nodes, id);
+}
+
+ed::Pin* ed::EditorContext::FindPin(PinId id)
+{
+ return FindItemIn(m_Pins, id);
+}
+
+ed::Link* ed::EditorContext::FindLink(LinkId id)
+{
+ return FindItemIn(m_Links, id);
+}
+
+ed::Object* ed::EditorContext::FindObject(ObjectId id)
+{
+ if (id.IsNodeId())
+ return FindNode(id.AsNodeId());
+ else if (id.IsLinkId())
+ return FindLink(id.AsLinkId());
+ else if (id.IsPinId())
+ return FindPin(id.AsPinId());
+ else
+ return nullptr;
+}
+
+ed::Node* ed::EditorContext::GetNode(NodeId id)
+{
+ auto node = FindNode(id);
+ if (!node)
+ node = CreateNode(id);
+ return node;
+}
+
+ed::Pin* ed::EditorContext::GetPin(PinId id, PinKind kind)
+{
+ if (auto pin = FindPin(id))
+ {
+ pin->m_Kind = kind;
+ return pin;
+ }
+ else
+ return CreatePin(id, kind);
+}
+
+ed::Link* ed::EditorContext::GetLink(LinkId id)
+{
+ if (auto link = FindLink(id))
+ return link;
+ else
+ return CreateLink(id);
+}
+
+void ed::EditorContext::LoadSettings()
+{
+ ed::Settings::Parse(m_Config.Load(), m_Settings);
+
+ m_NavigateAction.m_Scroll = m_Settings.m_ViewScroll;
+ m_NavigateAction.m_Zoom = m_Settings.m_ViewZoom;
+}
+
+void ed::EditorContext::SaveSettings()
+{
+ m_Config.BeginSave();
+
+ for (auto& node : m_Nodes)
+ {
+ auto settings = m_Settings.FindNode(node->m_ID);
+ settings->m_Location = node->m_Bounds.Min;
+ settings->m_Size = node->m_Bounds.GetSize();
+ if (IsGroup(node))
+ settings->m_GroupSize = node->m_GroupBounds.GetSize();
+
+ if (!node->m_RestoreState && settings->m_IsDirty && m_Config.SaveNodeSettings)
+ {
+ if (m_Config.SaveNode(node->m_ID, settings->Serialize().dump(), settings->m_DirtyReason))
+ settings->ClearDirty();
+ }
+ }
+
+ m_Settings.m_Selection.resize(0);
+ for (auto& object : m_SelectedObjects)
+ m_Settings.m_Selection.push_back(object->ID());
+
+ m_Settings.m_ViewScroll = m_NavigateAction.m_Scroll;
+ m_Settings.m_ViewZoom = m_NavigateAction.m_Zoom;
+
+ if (m_Config.Save(m_Settings.Serialize(), m_Settings.m_DirtyReason))
+ m_Settings.ClearDirty();
+
+ m_Config.EndSave();
+}
+
+void ed::EditorContext::MakeDirty(SaveReasonFlags reason)
+{
+ m_Settings.MakeDirty(reason);
+}
+
+void ed::EditorContext::MakeDirty(SaveReasonFlags reason, Node* node)
+{
+ m_Settings.MakeDirty(reason, node);
+}
+
+ed::Link* ed::EditorContext::FindLinkAt(const ImVec2& p)
+{
+ for (auto& link : m_Links)
+ if (link->TestHit(p, c_LinkSelectThickness))
+ return link;
+
+ return nullptr;
+}
+
+ImU32 ed::EditorContext::GetColor(StyleColor colorIndex) const
+{
+ return ImColor(m_Style.Colors[colorIndex]);
+}
+
+ImU32 ed::EditorContext::GetColor(StyleColor colorIndex, float alpha) const
+{
+ auto color = m_Style.Colors[colorIndex];
+ return ImColor(color.x, color.y, color.z, color.w * alpha);
+}
+
+void ed::EditorContext::RegisterAnimation(Animation* animation)
+{
+ m_LiveAnimations.push_back(animation);
+}
+
+void ed::EditorContext::UnregisterAnimation(Animation* animation)
+{
+ auto it = std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), animation);
+ if (it != m_LiveAnimations.end())
+ m_LiveAnimations.erase(it);
+}
+
+void ed::EditorContext::UpdateAnimations()
+{
+ m_LastLiveAnimations = m_LiveAnimations;
+
+ for (auto animation : m_LastLiveAnimations)
+ {
+ const bool isLive = (std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), animation) != m_LiveAnimations.end());
+
+ if (isLive)
+ animation->Update();
+ }
+}
+
+void ed::EditorContext::Flow(Link* link)
+{
+ m_FlowAnimationController.Flow(link);
+}
+
+void ed::EditorContext::SetUserContext(bool globalSpace)
+{
+ const auto mousePos = ImGui::GetMousePos();
+
+ // Move drawing cursor to mouse location and prepare layer for
+ // content added by user.
+ if (globalSpace)
+ ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos));
+ else
+ ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos));
+ //ImGui::SetCursorScreenPos(ImFloor(mousePos));
+ //ImGui::SetCursorScreenPos(ImVec2(floorf(mousePos.x), floorf(mousePos.y)));
+
+ if (!IsSuspended())
+ {
+ auto drawList = ImGui::GetWindowDrawList();
+ drawList->ChannelsSetCurrent(c_UserChannel_Content);
+ }
+
+ // #debug
+ // drawList->AddCircleFilled(ImGui::GetMousePos(), 4, IM_COL32(0, 255, 0, 255));
+}
+
+void ed::EditorContext::EnableShortcuts(bool enable)
+{
+ m_ShortcutsEnabled = enable;
+}
+
+bool ed::EditorContext::AreShortcutsEnabled()
+{
+ return m_ShortcutsEnabled;
+}
+
+ed::Control ed::EditorContext::BuildControl(bool allowOffscreen)
+{
+ if (!allowOffscreen && !ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
+ return Control(nullptr, nullptr, nullptr, nullptr, false, false, false, false);
+
+ const auto mousePos = ImGui::GetMousePos();
+
+ // Expand clip rectangle to always contain cursor
+ auto editorRect = m_Canvas.ViewRect();
+ auto isMouseOffscreen = allowOffscreen && !editorRect.Contains(mousePos);
+ if (isMouseOffscreen)
+ {
+ // Extend clip rect to capture off-screen mouse cursor
+ editorRect.Add(ImFloor(mousePos));
+ editorRect.Add(ImVec2(ImCeil(mousePos.x), ImCeil(mousePos.y)));
+
+ ImGui::PushClipRect(editorRect.Min, editorRect.Max, false);
+ }
+
+ Object* hotObject = nullptr;
+ Object* activeObject = nullptr;
+ Object* clickedObject = nullptr;
+ Object* doubleClickedObject = nullptr;
+
+ // Emits invisible button and returns true if it is clicked.
+ auto emitInteractiveArea = [](ObjectId id, const ImRect& rect)
+ {
+ char idString[33] = { 0 }; // itoa can output 33 bytes maximum
+ snprintf(idString, 32, "%p", id.AsPointer());
+ ImGui::SetCursorScreenPos(rect.Min);
+
+ // debug
+ //if (id < 0) return ImGui::Button(idString, to_imvec(rect.size));
+
+ auto result = ImGui::InvisibleButton(idString, rect.GetSize());
+
+ // #debug
+ //ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(0, 255, 0, 64));
+
+ return result;
+ };
+
+ // Check input interactions over area.
+ auto checkInteractionsInArea = [&emitInteractiveArea, &hotObject, &activeObject, &clickedObject, &doubleClickedObject](ObjectId id, const ImRect& rect, Object* object)
+ {
+ if (emitInteractiveArea(id, rect))
+ clickedObject = object;
+ if (!doubleClickedObject && ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered())
+ doubleClickedObject = object;
+
+ if (!hotObject && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
+ hotObject = object;
+
+ if (ImGui::IsItemActive())
+ activeObject = object;
+ };
+
+ // Process live nodes and pins.
+ for (auto nodeIt = m_Nodes.rbegin(), nodeItEnd = m_Nodes.rend(); nodeIt != nodeItEnd; ++nodeIt)
+ {
+ auto node = *nodeIt;
+
+ if (!node->m_IsLive) continue;
+
+ // Check for interactions with live pins in node before
+ // processing node itself. Pins does not overlap each other
+ // and all are within node bounds.
+ for (auto pin = node->m_LastPin; pin; pin = pin->m_PreviousPin)
+ {
+ if (!pin->m_IsLive) continue;
+
+ checkInteractionsInArea(pin->m_ID, pin->m_Bounds, pin);
+ }
+
+ // Check for interactions with node.
+ if (node->m_Type == NodeType::Group)
+ {
+ // Node with a hole
+ ImGui::PushID(node->m_ID.AsPointer());
+
+ static const NodeRegion c_Regions[] =
+ {
+ NodeRegion::TopLeft,
+ NodeRegion::TopRight,
+ NodeRegion::BottomLeft,
+ NodeRegion::BottomRight,
+ NodeRegion::Top,
+ NodeRegion::Bottom,
+ NodeRegion::Left,
+ NodeRegion::Right,
+ NodeRegion::Header,
+ };
+
+ for (auto region : c_Regions)
+ {
+ auto bounds = node->GetRegionBounds(region);
+ if (ImRect_IsEmpty(bounds))
+ continue;
+ checkInteractionsInArea(NodeId(static_cast<int>(region)), bounds, node);
+ }
+
+ ImGui::PopID();
+ }
+ else
+ checkInteractionsInArea(node->m_ID, node->m_Bounds, node);
+ }
+
+ // Links are not regular widgets and must be done manually since
+ // ImGui does not support interactive elements with custom hit maps.
+ //
+ // Links can steal input from background.
+
+ // Links are just over background. So if anything else
+ // is hovered we can skip them.
+ if (nullptr == hotObject)
+ hotObject = FindLinkAt(mousePos);
+
+ // Check for interaction with background.
+ auto backgroundClicked = emitInteractiveArea(NodeId(0), editorRect);
+ auto backgroundDoubleClicked = !doubleClickedObject && ImGui::IsItemHovered() ? ImGui::IsMouseDoubleClicked(0) : false;
+ auto isBackgroundActive = ImGui::IsItemActive();
+ auto isBackgroundHot = !hotObject;
+ auto isDragging = ImGui::IsMouseDragging(0, 1) || ImGui::IsMouseDragging(1, 1) || ImGui::IsMouseDragging(2, 1);
+
+ if (backgroundDoubleClicked)
+ backgroundClicked = false;
+
+ if (isMouseOffscreen)
+ ImGui::PopClipRect();
+
+ // Process link input using background interactions.
+ auto hotLink = hotObject ? hotObject->AsLink() : nullptr;
+
+ // ImGui take care of tracking active items. With link
+ // we must do this ourself.
+ if (!isDragging && isBackgroundActive && hotLink && !m_LastActiveLink)
+ m_LastActiveLink = hotLink;
+ if (isBackgroundActive && m_LastActiveLink)
+ {
+ activeObject = m_LastActiveLink;
+ isBackgroundActive = false;
+ }
+ else if (!isBackgroundActive && m_LastActiveLink)
+ m_LastActiveLink = nullptr;
+
+ // Steal click from backgrounds if link is hovered.
+ if (!isDragging && backgroundClicked && hotLink)
+ {
+ clickedObject = hotLink;
+ backgroundClicked = false;
+ }
+
+ // Steal double-click from backgrounds if link is hovered.
+ if (!isDragging && backgroundDoubleClicked && hotLink)
+ {
+ doubleClickedObject = hotLink;
+ backgroundDoubleClicked = false;
+ }
+
+ return Control(hotObject, activeObject, clickedObject, doubleClickedObject,
+ isBackgroundHot, isBackgroundActive, backgroundClicked, backgroundDoubleClicked);
+}
+
+void ed::EditorContext::ShowMetrics(const Control& control)
+{
+ auto& io = ImGui::GetIO();
+
+ auto getObjectName = [](Object* object)
+ {
+ if (!object) return "";
+ else if (object->AsNode()) return "Node";
+ else if (object->AsPin()) return "Pin";
+ else if (object->AsLink()) return "Link";
+ else return "";
+ };
+
+ auto getHotObjectName = [&control, &getObjectName]()
+ {
+ if (control.HotObject)
+ return getObjectName(control.HotObject);
+ else if (control.BackgroundHot)
+ return "Background";
+ else
+ return "<unknown>";
+ };
+
+ auto getActiveObjectName = [&control, &getObjectName]()
+ {
+ if (control.ActiveObject)
+ return getObjectName(control.ActiveObject);
+ else if (control.BackgroundActive)
+ return "Background";
+ else
+ return "<unknown>";
+ };
+
+ auto liveNodeCount = (int)std::count_if(m_Nodes.begin(), m_Nodes.end(), [](Node* node) { return node->m_IsLive; });
+ auto livePinCount = (int)std::count_if(m_Pins.begin(), m_Pins.end(), [](Pin* pin) { return pin->m_IsLive; });
+ auto liveLinkCount = (int)std::count_if(m_Links.begin(), m_Links.end(), [](Link* link) { return link->m_IsLive; });
+
+ auto canvasRect = m_Canvas.Rect();
+ auto viewRect = m_Canvas.ViewRect();
+ auto localMousePos = m_Canvas.ToLocal(io.MousePos);
+ auto globalMousePos = io.MousePos;
+
+ ImGui::SetCursorScreenPos(canvasRect.Min + ImVec2(5, 5));
+ ImGui::BeginGroup();
+ ImGui::Text("Is Editor Active: %s", ImGui::IsWindowHovered() ? "true" : "false");
+ ImGui::Text("View Position: { x=%g y=%g }", viewRect.Min.x, viewRect.Min.y);
+ ImGui::Text("View Size: { w=%g h=%g }", viewRect.GetWidth(), viewRect.GetHeight());
+ ImGui::Text("Canvas Size: { w=%g h=%g }", canvasRect.GetWidth(), canvasRect.GetHeight());
+ ImGui::Text("Mouse: { x=%.0f y=%.0f } global: { x=%g y=%g }", localMousePos.x, localMousePos.y, globalMousePos.x, globalMousePos.y);
+ ImGui::Text("Live Nodes: %d", liveNodeCount);
+ ImGui::Text("Live Pins: %d", livePinCount);
+ ImGui::Text("Live Links: %d", liveLinkCount);
+ ImGui::Text("Hot Object: %s (%p)", getHotObjectName(), control.HotObject ? control.HotObject->ID().AsPointer() : nullptr);
+ if (auto node = control.HotObject ? control.HotObject->AsNode() : nullptr)
+ {
+ ImGui::SameLine();
+ ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), node->m_Bounds.GetHeight());
+ }
+ ImGui::Text("Active Object: %s (%p)", getActiveObjectName(), control.ActiveObject ? control.ActiveObject->ID().AsPointer() : nullptr);
+ if (auto node = control.ActiveObject ? control.ActiveObject->AsNode() : nullptr)
+ {
+ ImGui::SameLine();
+ ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), node->m_Bounds.GetHeight());
+ }
+ ImGui::Text("Action: %s", m_CurrentAction ? m_CurrentAction->GetName() : "<none>");
+ ImGui::Text("Action Is Dragging: %s", m_CurrentAction && m_CurrentAction->IsDragging() ? "Yes" : "No");
+ m_NavigateAction.ShowMetrics();
+ m_SizeAction.ShowMetrics();
+ m_DragAction.ShowMetrics();
+ m_SelectAction.ShowMetrics();
+ m_ContextMenuAction.ShowMetrics();
+ m_CreateItemAction.ShowMetrics();
+ m_DeleteItemsAction.ShowMetrics();
+ ImGui::EndGroup();
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Node Settings
+//
+//------------------------------------------------------------------------------
+void ed::NodeSettings::ClearDirty()
+{
+ m_IsDirty = false;
+ m_DirtyReason = SaveReasonFlags::None;
+}
+
+void ed::NodeSettings::MakeDirty(SaveReasonFlags reason)
+{
+ m_IsDirty = true;
+ m_DirtyReason = m_DirtyReason | reason;
+}
+
+ed::json::value ed::NodeSettings::Serialize()
+{
+ json::value result;
+ result["location"]["x"] = m_Location.x;
+ result["location"]["y"] = m_Location.y;
+
+ if (m_GroupSize.x > 0 || m_GroupSize.y > 0)
+ {
+ result["group_size"]["x"] = m_GroupSize.x;
+ result["group_size"]["y"] = m_GroupSize.y;
+ }
+
+ return result;
+}
+
+bool ed::NodeSettings::Parse(const std::string& string, NodeSettings& settings)
+{
+ auto settingsValue = json::value::parse(string);
+ if (settingsValue.is_discarded())
+ return false;
+
+ return Parse(settingsValue, settings);
+}
+
+bool ed::NodeSettings::Parse(const json::value& data, NodeSettings& result)
+{
+ if (!data.is_object())
+ return false;
+
+ auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool
+ {
+ if (v.is_object())
+ {
+ auto xValue = v["x"];
+ auto yValue = v["y"];
+
+ if (xValue.is_number() && yValue.is_number())
+ {
+ result.x = static_cast<float>(xValue.get<double>());
+ result.y = static_cast<float>(yValue.get<double>());
+
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ if (!tryParseVector(data["location"], result.m_Location))
+ return false;
+
+ if (data.contains("group_size") && !tryParseVector(data["group_size"], result.m_GroupSize))
+ return false;
+
+ return true;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Settings
+//
+//------------------------------------------------------------------------------
+ed::NodeSettings* ed::Settings::AddNode(NodeId id)
+{
+ m_Nodes.push_back(NodeSettings(id));
+ return &m_Nodes.back();
+}
+
+ed::NodeSettings* ed::Settings::FindNode(NodeId id)
+{
+ for (auto& settings : m_Nodes)
+ if (settings.m_ID == id)
+ return &settings;
+
+ return nullptr;
+}
+
+void ed::Settings::ClearDirty(Node* node)
+{
+ if (node)
+ {
+ auto settings = FindNode(node->m_ID);
+ IM_ASSERT(settings);
+ settings->ClearDirty();
+ }
+ else
+ {
+ m_IsDirty = false;
+ m_DirtyReason = SaveReasonFlags::None;
+
+ for (auto& knownNode : m_Nodes)
+ knownNode.ClearDirty();
+ }
+}
+
+void ed::Settings::MakeDirty(SaveReasonFlags reason, Node* node)
+{
+ m_IsDirty = true;
+ m_DirtyReason = m_DirtyReason | reason;
+
+ if (node)
+ {
+ auto settings = FindNode(node->m_ID);
+ IM_ASSERT(settings);
+
+ settings->MakeDirty(reason);
+ }
+}
+
+std::string ed::Settings::Serialize()
+{
+ json::value result;
+
+ auto serializeObjectId = [](ObjectId id)
+ {
+ auto value = std::to_string(reinterpret_cast<uintptr_t>(id.AsPointer()));
+ switch (id.Type())
+ {
+ default:
+ case NodeEditor::Detail::ObjectType::None: return value;
+ case NodeEditor::Detail::ObjectType::Node: return "node:" + value;
+ case NodeEditor::Detail::ObjectType::Link: return "link:" + value;
+ case NodeEditor::Detail::ObjectType::Pin: return "pin:" + value;
+ }
+ };
+
+ auto& nodes = result["nodes"];
+ for (auto& node : m_Nodes)
+ {
+ if (node.m_WasUsed)
+ nodes[serializeObjectId(node.m_ID)] = node.Serialize();
+ }
+
+ auto& selection = result["selection"];
+ for (auto& id : m_Selection)
+ selection.push_back(serializeObjectId(id));
+
+ auto& view = result["view"];
+ view["scroll"]["x"] = m_ViewScroll.x;
+ view["scroll"]["y"] = m_ViewScroll.y;
+ view["zoom"] = m_ViewZoom;
+
+ return result.dump();
+}
+
+bool ed::Settings::Parse(const std::string& string, Settings& settings)
+{
+ Settings result = settings;
+
+ auto settingsValue = json::value::parse(string);
+ if (settingsValue.is_discarded())
+ return false;
+
+ if (!settingsValue.is_object())
+ return false;
+
+ auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool
+ {
+ if (v.is_object() && v.contains("x") && v.contains("y"))
+ {
+ auto xValue = v["x"];
+ auto yValue = v["y"];
+
+ if (xValue.is_number() && yValue.is_number())
+ {
+ result.x = static_cast<float>(xValue.get<double>());
+ result.y = static_cast<float>(yValue.get<double>());
+
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ auto deserializeObjectId = [](const std::string& str)
+ {
+ auto separator = str.find_first_of(':');
+ auto idStart = str.c_str() + ((separator != std::string::npos) ? separator + 1 : 0);
+ auto id = reinterpret_cast<void*>(strtoull(idStart, nullptr, 10));
+ if (str.compare(0, separator, "node") == 0)
+ return ObjectId(NodeId(id));
+ else if (str.compare(0, separator, "link") == 0)
+ return ObjectId(LinkId(id));
+ else if (str.compare(0, separator, "pin") == 0)
+ return ObjectId(PinId(id));
+ else
+ // fallback to old format
+ return ObjectId(NodeId(id)); //return ObjectId();
+ };
+
+ //auto& settingsObject = settingsValue.get<json::object>();
+
+ auto& nodesValue = settingsValue["nodes"];
+ if (nodesValue.is_object())
+ {
+ for (auto& node : nodesValue.get<json::object>())
+ {
+ auto id = deserializeObjectId(node.first.c_str()).AsNodeId();
+
+ auto nodeSettings = result.FindNode(id);
+ if (!nodeSettings)
+ nodeSettings = result.AddNode(id);
+
+ NodeSettings::Parse(node.second, *nodeSettings);
+ }
+ }
+
+ auto& selectionValue = settingsValue["selection"];
+ if (selectionValue.is_array())
+ {
+ const auto selectionArray = selectionValue.get<json::array>();
+
+ result.m_Selection.reserve(selectionArray.size());
+ result.m_Selection.resize(0);
+ for (auto& selection : selectionArray)
+ {
+ if (selection.is_string())
+ result.m_Selection.push_back(deserializeObjectId(selection.get<json::string>()));
+ }
+ }
+
+ auto& viewValue = settingsValue["view"];
+ if (viewValue.is_object())
+ {
+ auto& viewScrollValue = viewValue["scroll"];
+ auto& viewZoomValue = viewValue["zoom"];
+
+ if (!tryParseVector(viewScrollValue, result.m_ViewScroll))
+ result.m_ViewScroll = ImVec2(0, 0);
+
+ result.m_ViewZoom = viewZoomValue.is_number() ? static_cast<float>(viewZoomValue.get<double>()) : 1.0f;
+ }
+
+ settings = std::move(result);
+
+ return true;
+}
+
+
+
+//------------------------------------------------------------------------------
+//
+// Animation
+//
+//------------------------------------------------------------------------------
+ed::Animation::Animation(EditorContext* editor):
+ Editor(editor),
+ m_State(Stopped),
+ m_Time(0.0f),
+ m_Duration(0.0f)
+{
+}
+
+ed::Animation::~Animation()
+{
+ Stop();
+}
+
+void ed::Animation::Play(float duration)
+{
+ if (IsPlaying())
+ Stop();
+
+ m_State = Playing;
+ if (duration < 0)
+ duration = 0.0f;
+
+ m_Time = 0.0f;
+ m_Duration = duration;
+
+ OnPlay();
+
+ Editor->RegisterAnimation(this);
+
+ if (duration == 0.0f)
+ Stop();
+}
+
+void ed::Animation::Stop()
+{
+ if (!IsPlaying())
+ return;
+
+ m_State = Stopped;
+
+ Editor->UnregisterAnimation(this);
+
+ OnStop();
+}
+
+void ed::Animation::Finish()
+{
+ if (!IsPlaying())
+ return;
+
+ OnFinish();
+
+ Stop();
+}
+
+void ed::Animation::Update()
+{
+ if (!IsPlaying())
+ return;
+
+ m_Time += ImMax(0.0f, ImGui::GetIO().DeltaTime);
+ if (m_Time < m_Duration)
+ {
+ const float progress = GetProgress();
+ OnUpdate(progress);
+ }
+ else
+ {
+ OnFinish();
+ Stop();
+ }
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Navigate Animation
+//
+//------------------------------------------------------------------------------
+ed::NavigateAnimation::NavigateAnimation(EditorContext* editor, NavigateAction& scrollAction):
+ Animation(editor),
+ Action(scrollAction)
+{
+}
+
+void ed::NavigateAnimation::NavigateTo(const ImRect& target, float duration)
+{
+ Stop();
+
+ m_Start = Action.GetViewRect();
+ m_Target = target;
+
+ // Skip tiny animations
+ auto minoffset = m_Target.Min - m_Start.Min;
+ auto maxOffset = m_Target.Max - m_Start.Max;
+ auto epsilon = 1e-4f;
+ if (ImFabs(minoffset.x) < epsilon && ImFabs(minoffset.y) < epsilon &&
+ ImFabs(maxOffset.x) < epsilon && ImFabs(maxOffset.y) < epsilon)
+ {
+ duration = 0;
+ }
+
+ Play(duration);
+}
+
+void ed::NavigateAnimation::OnUpdate(float progress)
+{
+ ImRect current;
+ current.Min = ImEasing::EaseOutQuad(m_Start.Min, m_Target.Min - m_Start.Min, progress);
+ current.Max = ImEasing::EaseOutQuad(m_Start.Max, m_Target.Max - m_Start.Max, progress);
+ Action.SetViewRect(current);
+}
+
+void ed::NavigateAnimation::OnStop()
+{
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+}
+
+void ed::NavigateAnimation::OnFinish()
+{
+ Action.SetViewRect(m_Target);
+
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Flow Animation
+//
+//------------------------------------------------------------------------------
+ed::FlowAnimation::FlowAnimation(FlowAnimationController* controller):
+ Animation(controller->Editor),
+ Controller(controller),
+ m_Link(nullptr),
+ m_Offset(0.0f),
+ m_PathLength(0.0f)
+{
+}
+
+void ed::FlowAnimation::Flow(ed::Link* link, float markerDistance, float speed, float duration)
+{
+ Stop();
+
+ if (m_Link != link)
+ {
+ m_Offset = 0.0f;
+ ClearPath();
+ }
+
+ if (m_MarkerDistance != markerDistance)
+ ClearPath();
+
+ m_MarkerDistance = markerDistance;
+ m_Speed = speed;
+ m_Link = link;
+
+ Play(duration);
+}
+
+void ed::FlowAnimation::Draw(ImDrawList* drawList)
+{
+ if (!IsPlaying() || !IsLinkValid() || !m_Link->IsVisible())
+ return;
+
+ if (!IsPathValid())
+ UpdatePath();
+
+ m_Offset = fmodf(m_Offset, m_MarkerDistance);
+
+ const auto progress = GetProgress();
+
+ const auto flowAlpha = 1.0f - progress * progress;
+ const auto flowColor = Editor->GetColor(StyleColor_Flow, flowAlpha);
+ //const auto flowPath = Link->GetCurve();
+
+ m_Link->Draw(drawList, flowColor, 2.0f);
+
+ if (IsPathValid())
+ {
+ //Offset = 0;
+
+ const auto markerAlpha = powf(1.0f - progress, 0.35f);
+ const auto markerRadius = 4.0f * (1.0f - progress) + 2.0f;
+ const auto markerColor = Editor->GetColor(StyleColor_FlowMarker, markerAlpha);
+
+ for (float d = m_Offset; d < m_PathLength; d += m_MarkerDistance)
+ drawList->AddCircleFilled(SamplePath(d), markerRadius, markerColor);
+ }
+}
+
+bool ed::FlowAnimation::IsLinkValid() const
+{
+ return m_Link && m_Link->m_IsLive;
+}
+
+bool ed::FlowAnimation::IsPathValid() const
+{
+ return m_Path.size() > 1 && m_PathLength > 0.0f && m_Link->m_Start == m_LastStart && m_Link->m_End == m_LastEnd;
+}
+
+void ed::FlowAnimation::UpdatePath()
+{
+ if (!IsLinkValid())
+ {
+ ClearPath();
+ return;
+ }
+
+ const auto curve = m_Link->GetCurve();
+
+ m_LastStart = m_Link->m_Start;
+ m_LastEnd = m_Link->m_End;
+ m_PathLength = ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3);
+
+ auto collectPointsCallback = [this](ImCubicBezierFixedStepSample& result)
+ {
+ m_Path.push_back(CurvePoint{ result.Length, result.Point });
+ };
+
+ const auto step = ImMax(m_MarkerDistance * 0.5f, 15.0f);
+
+ m_Path.resize(0);
+ ImCubicBezierFixedStep(collectPointsCallback, curve, step, false, 0.5f, 0.001f);
+}
+
+void ed::FlowAnimation::ClearPath()
+{
+ vector<CurvePoint>().swap(m_Path);
+ m_PathLength = 0.0f;
+}
+
+ImVec2 ed::FlowAnimation::SamplePath(float distance)
+{
+ //distance = ImMax(0.0f, std::min(distance, PathLength));
+
+ auto endPointIt = std::find_if(m_Path.begin(), m_Path.end(), [distance](const CurvePoint& p) { return distance < p.Distance; });
+ if (endPointIt == m_Path.end())
+ endPointIt = m_Path.end() - 1;
+ else if (endPointIt == m_Path.begin())
+ endPointIt = m_Path.begin() + 1;
+
+ const auto& start = endPointIt[-1];
+ const auto& end = *endPointIt;
+ const auto t = (distance - start.Distance) / (end.Distance - start.Distance);
+
+ return start.Point + (end.Point - start.Point) * t;
+}
+
+void ed::FlowAnimation::OnUpdate(float progress)
+{
+ IM_UNUSED(progress);
+
+ m_Offset += m_Speed * ImGui::GetIO().DeltaTime;
+}
+
+void ed::FlowAnimation::OnStop()
+{
+ Controller->Release(this);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Flow Animation Controller
+//
+//------------------------------------------------------------------------------
+ed::FlowAnimationController::FlowAnimationController(EditorContext* editor):
+ AnimationController(editor)
+{
+}
+
+ed::FlowAnimationController::~FlowAnimationController()
+{
+ for (auto animation : m_Animations)
+ delete animation;
+}
+
+void ed::FlowAnimationController::Flow(Link* link)
+{
+ if (!link || !link->m_IsLive)
+ return;
+
+ auto& editorStyle = GetStyle();
+
+ auto animation = GetOrCreate(link);
+
+ animation->Flow(link, editorStyle.FlowMarkerDistance, editorStyle.FlowSpeed, editorStyle.FlowDuration);
+}
+
+void ed::FlowAnimationController::Draw(ImDrawList* drawList)
+{
+ if (m_Animations.empty())
+ return;
+
+ drawList->ChannelsSetCurrent(c_LinkChannel_Flow);
+
+ for (auto animation : m_Animations)
+ animation->Draw(drawList);
+}
+
+ed::FlowAnimation* ed::FlowAnimationController::GetOrCreate(Link* link)
+{
+ // Return live animation which match target link
+ {
+ auto animationIt = std::find_if(m_Animations.begin(), m_Animations.end(), [link](FlowAnimation* animation) { return animation->m_Link == link; });
+ if (animationIt != m_Animations.end())
+ return *animationIt;
+ }
+
+ // There are no live animations for target link, try to reuse inactive old one
+ if (!m_FreePool.empty())
+ {
+ auto animation = m_FreePool.back();
+ m_FreePool.pop_back();
+ return animation;
+ }
+
+ // Cache miss, allocate new one
+ auto animation = new FlowAnimation(this);
+ m_Animations.push_back(animation);
+
+ return animation;
+}
+
+void ed::FlowAnimationController::Release(FlowAnimation* animation)
+{
+ IM_UNUSED(animation);
+}
+
+
+
+//------------------------------------------------------------------------------
+//
+// Navigate Action
+//
+//------------------------------------------------------------------------------
+const float ed::NavigateAction::s_ZoomLevels[] =
+{
+ 0.1f, 0.15f, 0.20f, 0.25f, 0.33f, 0.5f, 0.75f, 1.0f, 1.25f, 1.50f, 2.0f, 2.5f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f
+};
+
+const int ed::NavigateAction::s_ZoomLevelCount = sizeof(s_ZoomLevels) / sizeof(*s_ZoomLevels);
+
+ed::NavigateAction::NavigateAction(EditorContext* editor, ImGuiEx::Canvas& canvas):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_Zoom(1),
+ m_Scroll(0, 0),
+ m_ScrollStart(0, 0),
+ m_ScrollDelta(0, 0),
+ m_Canvas(canvas),
+ m_WindowScreenPos(0, 0),
+ m_WindowScreenSize(0, 0),
+ m_Animation(editor, *this),
+ m_Reason(NavigationReason::Unknown),
+ m_LastSelectionId(0),
+ m_LastObject(nullptr),
+ m_MovingOverEdge(false),
+ m_MoveOffset(0, 0)
+{
+}
+
+ed::EditorAction::AcceptResult ed::NavigateAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ if (ImGui::IsWindowHovered() /*&& !ImGui::IsAnyItemActive()*/ && ImGui::IsMouseDragging(c_ScrollButtonIndex, 0.0f))
+ {
+ m_IsActive = true;
+ m_ScrollStart = m_Scroll;
+ m_ScrollDelta = ImGui::GetMouseDragDelta(c_ScrollButtonIndex);
+ m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom;
+ }
+
+ auto& io = ImGui::GetIO();
+
+ if (ImGui::IsWindowFocused() && ImGui::IsKeyPressed(GetKeyIndexForF()) && Editor->AreShortcutsEnabled())
+ {
+ const auto allowZoomIn = io.KeyShift;
+
+ auto findHotObjectToZoom = [this, &control, &io]() -> Object*
+ {
+ if (control.HotObject)
+ {
+ if (auto pin = control.HotObject->AsPin())
+ return pin->m_Node;
+ else
+ return control.HotObject;
+ }
+ else if (control.BackgroundHot)
+ {
+ auto node = Editor->FindNodeAt(io.MousePos);
+ if (IsGroup(node))
+ return node;
+ }
+
+ return nullptr;
+ };
+
+ bool navigateToContent = false;
+ if (!Editor->GetSelectedObjects().empty())
+ {
+ if (m_Reason != NavigationReason::Selection || m_LastSelectionId != Editor->GetSelectionId() || allowZoomIn)
+ {
+ m_LastSelectionId = Editor->GetSelectionId();
+ NavigateTo(Editor->GetSelectionBounds(), allowZoomIn, -1.0f, NavigationReason::Selection);
+ }
+ else
+ navigateToContent = true;
+ }
+ else if(auto hotObject = findHotObjectToZoom())
+ {
+ if (m_Reason != NavigationReason::Object || m_LastObject != hotObject || allowZoomIn)
+ {
+ m_LastObject = hotObject;
+ auto bounds = hotObject->GetBounds();
+ NavigateTo(bounds, allowZoomIn, -1.0f, NavigationReason::Object);
+ }
+ else
+ navigateToContent = true;
+ }
+ else
+ navigateToContent = true;
+
+ if (navigateToContent)
+ NavigateTo(Editor->GetContentBounds(), true, -1.0f, NavigationReason::Content);
+ }
+
+ // // #debug
+ // if (auto drawList = ImGui::GetWindowDrawList())
+ // drawList->AddCircleFilled(io.MousePos, 4.0f, IM_COL32(255, 0, 255, 255));
+
+ if (HandleZoom(control))
+ return True;
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::NavigateAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ if (!m_IsActive)
+ return false;
+
+ if (ImGui::IsMouseDragging(c_ScrollButtonIndex, 0.0f))
+ {
+ m_ScrollDelta = ImGui::GetMouseDragDelta(c_ScrollButtonIndex);
+ m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom;
+
+// if (IsActive && Animation.IsPlaying())
+// Animation.Target = Animation.Target - ScrollDelta * Animation.TargetZoom;
+ }
+ else
+ {
+ if (m_Scroll != m_ScrollStart)
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+
+ m_IsActive = false;
+ }
+
+ // #TODO: Handle zoom while scrolling
+ // HandleZoom(control);
+
+ return m_IsActive;
+}
+
+bool ed::NavigateAction::HandleZoom(const Control& control)
+{
+ IM_UNUSED(control);
+
+ const auto currentAction = Editor->GetCurrentAction();
+ const auto allowOffscreen = currentAction && currentAction->IsDragging();
+
+ auto& io = ImGui::GetIO();
+
+ if (!io.MouseWheel || (!allowOffscreen && !ImGui::IsWindowHovered()))// && !ImGui::IsAnyItemActive())
+ return false;
+
+ auto savedScroll = m_Scroll;
+ auto savedZoom = m_Zoom;
+
+ m_Animation.Finish();
+
+ auto mousePos = io.MousePos;
+ auto steps = (int)io.MouseWheel;
+ auto newZoom = MatchZoom(steps, s_ZoomLevels[steps < 0 ? 0 : s_ZoomLevelCount - 1]);
+
+ auto oldView = GetView();
+ m_Zoom = newZoom;
+ auto newView = GetView();
+
+ auto screenPos = m_Canvas.FromLocal(mousePos, oldView);
+ auto canvasPos = m_Canvas.ToLocal(screenPos, newView);
+
+ auto offset = (canvasPos - mousePos) * m_Zoom;
+ auto targetScroll = m_Scroll - offset;
+
+ if (m_Scroll != savedScroll || m_Zoom != savedZoom)
+ {
+ m_Scroll = savedScroll;
+ m_Zoom = savedZoom;
+
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+ }
+
+ auto targetRect = m_Canvas.CalcViewRect(ImGuiEx::CanvasView(-targetScroll, newZoom));
+
+ NavigateTo(targetRect, c_MouseZoomDuration, NavigationReason::MouseZoom);
+
+ return true;
+}
+
+void ed::NavigateAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+ ImGui::Text(" Scroll: { x=%g y=%g }", m_Scroll.x, m_Scroll.y);
+ ImGui::Text(" Zoom: %g", m_Zoom);
+}
+
+void ed::NavigateAction::NavigateTo(const ImRect& bounds, bool zoomIn, float duration, NavigationReason reason)
+{
+ if (ImRect_IsEmpty(bounds))
+ return;
+
+ if (duration < 0.0f)
+ duration = GetStyle().ScrollDuration;
+
+ if (!zoomIn)
+ {
+ auto viewRect = m_Canvas.ViewRect();
+ auto viewRectCenter = viewRect.GetCenter();
+ auto targetCenter = bounds.GetCenter();
+
+ viewRect.Translate(targetCenter - viewRectCenter);
+
+ NavigateTo(viewRect, duration, reason);
+ }
+ else
+ {
+ // Grow rect by 5% to leave some reasonable margin
+ // from the edges of the canvas.
+ auto rect = bounds;
+ auto extend = ImMax(rect.GetWidth(), rect.GetHeight());
+ rect.Expand(extend * c_NavigationZoomMargin * 0.5f);
+
+ NavigateTo(rect, duration, reason);
+ }
+}
+
+void ed::NavigateAction::NavigateTo(const ImRect& target, float duration, NavigationReason reason)
+{
+ m_Reason = reason;
+
+ m_Animation.NavigateTo(target, duration);
+}
+
+void ed::NavigateAction::StopNavigation()
+{
+ m_Animation.Stop();
+}
+
+void ed::NavigateAction::FinishNavigation()
+{
+ m_Animation.Finish();
+}
+
+bool ed::NavigateAction::MoveOverEdge()
+{
+ // Don't interrupt non-edge animations
+ if (m_Animation.IsPlaying())
+ return false;
+
+ auto& io = ImGui::GetIO();
+ const auto screenRect = m_Canvas.ViewRect();
+ const auto screenMousePos = io.MousePos;
+
+ // Mouse is over screen, do nothing
+ if (screenRect.Contains(screenMousePos))
+ return false;
+
+ const auto screenPointOnEdge = ImRect_ClosestPoint(screenRect, screenMousePos, true);
+ const auto direction = screenPointOnEdge - screenMousePos;
+ const auto offset = -direction * io.DeltaTime * 10.0f;
+
+ m_Scroll = m_Scroll + offset;
+
+ m_MoveOffset = offset;
+ m_MovingOverEdge = true;
+
+ return true;
+}
+
+void ed::NavigateAction::StopMoveOverEdge()
+{
+ if (m_MovingOverEdge)
+ {
+ Editor->MakeDirty(SaveReasonFlags::Navigation);
+
+ m_MoveOffset = ImVec2(0, 0);
+ m_MovingOverEdge = false;
+ }
+}
+
+void ed::NavigateAction::SetWindow(ImVec2 position, ImVec2 size)
+{
+ m_WindowScreenPos = position;
+ m_WindowScreenSize = size;
+}
+
+ImGuiEx::CanvasView ed::NavigateAction::GetView() const
+{
+ return ImGuiEx::CanvasView(-m_Scroll, m_Zoom);
+}
+
+ImVec2 ed::NavigateAction::GetViewOrigin() const
+{
+ return -m_Scroll;
+}
+
+float ed::NavigateAction::GetViewScale() const
+{
+ return m_Zoom;
+}
+
+void ed::NavigateAction::SetViewRect(const ImRect& rect)
+{
+ auto view = m_Canvas.CalcCenterView(rect);
+ m_Scroll = -view.Origin;
+ m_Zoom = view.Scale;
+}
+
+ImRect ed::NavigateAction::GetViewRect() const
+{
+ return m_Canvas.CalcViewRect(GetView());
+}
+
+float ed::NavigateAction::MatchZoom(int steps, float fallbackZoom)
+{
+ auto currentZoomIndex = MatchZoomIndex(steps);
+ if (currentZoomIndex < 0)
+ return fallbackZoom;
+
+ auto currentZoom = s_ZoomLevels[currentZoomIndex];
+ if (fabsf(currentZoom - m_Zoom) > 0.001f)
+ return currentZoom;
+
+ auto newIndex = currentZoomIndex + steps;
+ if (newIndex >= 0 && newIndex < s_ZoomLevelCount)
+ return s_ZoomLevels[newIndex];
+ else
+ return fallbackZoom;
+}
+
+int ed::NavigateAction::MatchZoomIndex(int direction)
+{
+ int bestIndex = -1;
+ float bestDistance = 0.0f;
+
+ for (int i = 0; i < s_ZoomLevelCount; ++i)
+ {
+ auto distance = fabsf(s_ZoomLevels[i] - m_Zoom);
+ if (distance < bestDistance || bestIndex < 0)
+ {
+ bestDistance = distance;
+ bestIndex = i;
+ }
+ }
+
+ if (bestDistance > 0.001f)
+ {
+ if (direction > 0)
+ {
+ ++bestIndex;
+
+ if (bestIndex >= s_ZoomLevelCount)
+ bestIndex = s_ZoomLevelCount - 1;
+ }
+ else if (direction < 0)
+ {
+ --bestIndex;
+
+ if (bestIndex < 0)
+ bestIndex = 0;
+ }
+ }
+
+ return bestIndex;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Size Action
+//
+//------------------------------------------------------------------------------
+ed::SizeAction::SizeAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_Clean(false),
+ m_SizedNode(nullptr),
+ m_Pivot(NodeRegion::None),
+ m_Cursor(ImGuiMouseCursor_Arrow)
+{
+}
+
+ed::EditorAction::AcceptResult ed::SizeAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ if (control.ActiveNode && IsGroup(control.ActiveNode) && ImGui::IsMouseDragging(0, 0))
+ {
+ //const auto mousePos = to_point(ImGui::GetMousePos());
+ //const auto closestPoint = control.ActiveNode->Bounds.get_closest_point_hollow(mousePos, static_cast<int>(control.ActiveNode->Rounding));
+
+ auto pivot = GetRegion(control.ActiveNode);
+ if (pivot != NodeRegion::Header && pivot != NodeRegion::Center)
+ {
+ m_StartBounds = control.ActiveNode->m_Bounds;
+ m_StartGroupBounds = control.ActiveNode->m_GroupBounds;
+ m_LastSize = control.ActiveNode->m_Bounds.GetSize();
+ m_MinimumSize = ImVec2(0, 0);
+ m_LastDragOffset = ImVec2(0, 0);
+ m_Pivot = pivot;
+ m_Cursor = ChooseCursor(m_Pivot);
+ m_SizedNode = control.ActiveNode;
+ m_IsActive = true;
+ }
+ }
+ else if (control.HotNode && IsGroup(control.HotNode))
+ {
+ m_Cursor = ChooseCursor(GetRegion(control.HotNode));
+ return Possible;
+ }
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::SizeAction::Process(const Control& control)
+{
+ if (m_Clean)
+ {
+ m_Clean = false;
+
+ if (m_SizedNode->m_Bounds.Min != m_StartBounds.Min || m_SizedNode->m_GroupBounds.Min != m_StartGroupBounds.Min)
+ Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, m_SizedNode);
+
+ if (m_SizedNode->m_Bounds.GetSize() != m_StartBounds.GetSize() || m_SizedNode->m_GroupBounds.GetSize() != m_StartGroupBounds.GetSize())
+ Editor->MakeDirty(SaveReasonFlags::Size | SaveReasonFlags::User, m_SizedNode);
+
+ m_SizedNode = nullptr;
+ }
+
+ if (!m_IsActive)
+ return false;
+
+ if (control.ActiveNode == m_SizedNode)
+ {
+ const auto dragOffset = (control.ActiveNode == m_SizedNode) ? ImGui::GetMouseDragDelta(0, 0.0f) : m_LastDragOffset;
+ m_LastDragOffset = dragOffset;
+
+ if (m_MinimumSize.x == 0.0f && m_LastSize.x != m_SizedNode->m_Bounds.GetWidth())
+ m_MinimumSize.x = m_SizedNode->m_Bounds.GetWidth();
+ if (m_MinimumSize.y == 0.0f && m_LastSize.y != m_SizedNode->m_Bounds.GetHeight())
+ m_MinimumSize.y = m_SizedNode->m_Bounds.GetHeight();
+
+ auto minimumSize = ImMax(m_MinimumSize, m_StartBounds.GetSize() - m_StartGroupBounds.GetSize());
+
+
+ auto newBounds = m_StartBounds;
+
+ if ((m_Pivot & NodeRegion::Top) == NodeRegion::Top)
+ newBounds.Min.y = ImMin(newBounds.Max.y - minimumSize.y, Editor->AlignPointToGrid(newBounds.Min.y + dragOffset.y));
+ if ((m_Pivot & NodeRegion::Bottom) == NodeRegion::Bottom)
+ newBounds.Max.y = ImMax(newBounds.Min.y + minimumSize.y, Editor->AlignPointToGrid(newBounds.Max.y + dragOffset.y));
+ if ((m_Pivot & NodeRegion::Left) == NodeRegion::Left)
+ newBounds.Min.x = ImMin(newBounds.Max.x - minimumSize.x, Editor->AlignPointToGrid(newBounds.Min.x + dragOffset.x));
+ if ((m_Pivot & NodeRegion::Right) == NodeRegion::Right)
+ newBounds.Max.x = ImMax(newBounds.Min.x + minimumSize.x, Editor->AlignPointToGrid(newBounds.Max.x + dragOffset.x));
+
+ newBounds.Floor();
+
+ m_LastSize = newBounds.GetSize();
+
+ m_SizedNode->m_Bounds = newBounds;
+ m_SizedNode->m_GroupBounds = newBounds;
+ m_SizedNode->m_GroupBounds.Min.x -= m_StartBounds.Min.x - m_StartGroupBounds.Min.x;
+ m_SizedNode->m_GroupBounds.Min.y -= m_StartBounds.Min.y - m_StartGroupBounds.Min.y;
+ m_SizedNode->m_GroupBounds.Max.x -= m_StartBounds.Max.x - m_StartGroupBounds.Max.x;
+ m_SizedNode->m_GroupBounds.Max.y -= m_StartBounds.Max.y - m_StartGroupBounds.Max.y;
+ }
+ else if (!control.ActiveNode)
+ {
+ m_Clean = true;
+ m_IsActive = false;
+ return true;
+ }
+
+ return m_IsActive;
+}
+
+void ed::SizeAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ auto getObjectName = [](Object* object)
+ {
+ if (!object) return "";
+ else if (object->AsNode()) return "Node";
+ else if (object->AsPin()) return "Pin";
+ else if (object->AsLink()) return "Link";
+ else return "";
+ };
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+ ImGui::Text(" Node: %s (%p)", getObjectName(m_SizedNode), m_SizedNode ? m_SizedNode->m_ID.AsPointer() : nullptr);
+ if (m_SizedNode && m_IsActive)
+ {
+ ImGui::Text(" Bounds: { x=%g y=%g w=%g h=%g }", m_SizedNode->m_Bounds.Min.x, m_SizedNode->m_Bounds.Min.y, m_SizedNode->m_Bounds.GetWidth(), m_SizedNode->m_Bounds.GetHeight());
+ ImGui::Text(" Group Bounds: { x=%g y=%g w=%g h=%g }", m_SizedNode->m_GroupBounds.Min.x, m_SizedNode->m_GroupBounds.Min.y, m_SizedNode->m_GroupBounds.GetWidth(), m_SizedNode->m_GroupBounds.GetHeight());
+ ImGui::Text(" Start Bounds: { x=%g y=%g w=%g h=%g }", m_StartBounds.Min.x, m_StartBounds.Min.y, m_StartBounds.GetWidth(), m_StartBounds.GetHeight());
+ ImGui::Text(" Start Group Bounds: { x=%g y=%g w=%g h=%g }", m_StartGroupBounds.Min.x, m_StartGroupBounds.Min.y, m_StartGroupBounds.GetWidth(), m_StartGroupBounds.GetHeight());
+ ImGui::Text(" Minimum Size: { w=%g h=%g }", m_MinimumSize.x, m_MinimumSize.y);
+ ImGui::Text(" Last Size: { w=%g h=%g }", m_LastSize.x, m_LastSize.y);
+ }
+}
+
+ed::NodeRegion ed::SizeAction::GetRegion(Node* node)
+{
+ return node->GetRegion(ImGui::GetMousePos());
+}
+
+ImGuiMouseCursor ed::SizeAction::ChooseCursor(NodeRegion region)
+{
+ switch (region)
+ {
+ default:
+ case NodeRegion::Center:
+ return ImGuiMouseCursor_Arrow;
+
+ case NodeRegion::Top:
+ case NodeRegion::Bottom:
+ return ImGuiMouseCursor_ResizeNS;
+
+ case NodeRegion::Left:
+ case NodeRegion::Right:
+ return ImGuiMouseCursor_ResizeEW;
+
+ case NodeRegion::TopLeft:
+ case NodeRegion::BottomRight:
+ return ImGuiMouseCursor_ResizeNWSE;
+
+ case NodeRegion::TopRight:
+ case NodeRegion::BottomLeft:
+ return ImGuiMouseCursor_ResizeNESW;
+ }
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Drag Action
+//
+//------------------------------------------------------------------------------
+ed::DragAction::DragAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_Clear(false),
+ m_DraggedObject(nullptr)
+{
+}
+
+ed::EditorAction::AcceptResult ed::DragAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ if (control.ActiveObject && ImGui::IsMouseDragging(0))
+ {
+ if (!control.ActiveObject->AcceptDrag())
+ return False;
+
+ m_DraggedObject = control.ActiveObject;
+
+ m_Objects.resize(0);
+ m_Objects.push_back(m_DraggedObject);
+
+ if (Editor->IsSelected(m_DraggedObject))
+ {
+ for (auto selectedObject : Editor->GetSelectedObjects())
+ if (auto selectedNode = selectedObject->AsNode())
+ if (selectedNode != m_DraggedObject && selectedNode->AcceptDrag())
+ m_Objects.push_back(selectedNode);
+ }
+
+ auto& io = ImGui::GetIO();
+ if (!io.KeyShift)
+ {
+ std::vector<Node*> groupedNodes;
+ for (auto object : m_Objects)
+ if (auto node = object->AsNode())
+ node->GetGroupedNodes(groupedNodes, true);
+
+ auto isAlreadyPicked = [this](Node* node)
+ {
+ return std::find(m_Objects.begin(), m_Objects.end(), node) != m_Objects.end();
+ };
+
+ for (auto candidate : groupedNodes)
+ if (!isAlreadyPicked(candidate) && candidate->AcceptDrag())
+ m_Objects.push_back(candidate);
+ }
+
+ m_IsActive = true;
+ }
+ else if (control.HotNode && IsGroup(control.HotNode) && control.HotNode->GetRegion(ImGui::GetMousePos()) == NodeRegion::Header)
+ {
+ return Possible;
+ }
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::DragAction::Process(const Control& control)
+{
+ if (m_Clear)
+ {
+ m_Clear = false;
+
+ for (auto object : m_Objects)
+ {
+ if (object->EndDrag())
+ Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, object->AsNode());
+ }
+
+ m_Objects.resize(0);
+
+ m_DraggedObject = nullptr;
+ }
+
+ if (!m_IsActive)
+ return false;
+
+ if (control.ActiveObject == m_DraggedObject)
+ {
+ auto dragOffset = ImGui::GetMouseDragDelta(0, 0.0f);
+
+ auto draggedOrigin = m_DraggedObject->DragStartLocation();
+ auto alignPivot = ImVec2(0, 0);
+
+ // TODO: Move this experimental alignment to closes pivot out of internals to node API
+ if (auto draggedNode = m_DraggedObject->AsNode())
+ {
+ float x = FLT_MAX;
+ float y = FLT_MAX;
+
+ auto testPivot = [this, &x, &y, &draggedOrigin, &dragOffset, &alignPivot](const ImVec2& pivot)
+ {
+ auto initial = draggedOrigin + dragOffset + pivot;
+ auto candidate = Editor->AlignPointToGrid(initial) - draggedOrigin - pivot;
+
+ if (ImFabs(candidate.x) < ImFabs(ImMin(x, FLT_MAX)))
+ {
+ x = candidate.x;
+ alignPivot.x = pivot.x;
+ }
+
+ if (ImFabs(candidate.y) < ImFabs(ImMin(y, FLT_MAX)))
+ {
+ y = candidate.y;
+ alignPivot.y = pivot.y;
+ }
+ };
+
+ for (auto pin = draggedNode->m_LastPin; pin; pin = pin->m_PreviousPin)
+ {
+ auto pivot = pin->m_Pivot.GetCenter() - draggedNode->m_Bounds.Min;
+ testPivot(pivot);
+ }
+
+ //testPivot(point(0, 0));
+ }
+
+ auto alignedOffset = Editor->AlignPointToGrid(draggedOrigin + dragOffset + alignPivot) - draggedOrigin - alignPivot;
+
+ if (!ImGui::GetIO().KeyAlt)
+ dragOffset = alignedOffset;
+
+ for (auto object : m_Objects)
+ object->UpdateDrag(dragOffset);
+ }
+ else if (!control.ActiveObject)
+ {
+ m_Clear = true;
+
+ m_IsActive = false;
+ return true;
+ }
+
+ return m_IsActive;
+}
+
+void ed::DragAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ auto getObjectName = [](Object* object)
+ {
+ if (!object) return "";
+ else if (object->AsNode()) return "Node";
+ else if (object->AsPin()) return "Pin";
+ else if (object->AsLink()) return "Link";
+ else return "";
+ };
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+ ImGui::Text(" Node: %s (%p)", getObjectName(m_DraggedObject), m_DraggedObject ? m_DraggedObject->ID().AsPointer() : nullptr);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Select Action
+//
+//------------------------------------------------------------------------------
+ed::SelectAction::SelectAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_SelectGroups(false),
+ m_SelectLinkMode(false),
+ m_CommitSelection(false),
+ m_StartPoint(),
+ m_Animation(editor)
+{
+}
+
+ed::EditorAction::AcceptResult ed::SelectAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ auto& io = ImGui::GetIO();
+ m_SelectGroups = io.KeyShift;
+ m_SelectLinkMode = io.KeyAlt;
+
+ m_SelectedObjectsAtStart.clear();
+
+ if (control.BackgroundActive && ImGui::IsMouseDragging(0, 1))
+ {
+ m_IsActive = true;
+ m_StartPoint = ImGui::GetMousePos();
+ m_EndPoint = m_StartPoint;
+
+ // Links and nodes cannot be selected together
+ if ((m_SelectLinkMode && Editor->IsAnyNodeSelected()) ||
+ (!m_SelectLinkMode && Editor->IsAnyLinkSelected()))
+ {
+ Editor->ClearSelection();
+ }
+
+ if (io.KeyCtrl)
+ m_SelectedObjectsAtStart = Editor->GetSelectedObjects();
+ }
+ else if (control.BackgroundClicked)
+ {
+ Editor->ClearSelection();
+ }
+ else
+ {
+ Object* clickedObject = control.ClickedNode ? static_cast<Object*>(control.ClickedNode) : static_cast<Object*>(control.ClickedLink);
+
+ if (clickedObject)
+ {
+ // Links and nodes cannot be selected together
+ if ((clickedObject->AsLink() && Editor->IsAnyNodeSelected()) ||
+ (clickedObject->AsNode() && Editor->IsAnyLinkSelected()))
+ {
+ Editor->ClearSelection();
+ }
+
+ if (io.KeyCtrl)
+ Editor->ToggleObjectSelection(clickedObject);
+ else
+ Editor->SetSelectedObject(clickedObject);
+ }
+ }
+
+ if (m_IsActive)
+ m_Animation.Stop();
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::SelectAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ if (m_CommitSelection)
+ {
+ Editor->ClearSelection();
+ for (auto object : m_CandidateObjects)
+ Editor->SelectObject(object);
+
+ m_CandidateObjects.clear();
+
+ m_CommitSelection = false;
+ }
+
+ if (!m_IsActive)
+ return false;
+
+ if (ImGui::IsMouseDragging(0, 0))
+ {
+ m_EndPoint = ImGui::GetMousePos();
+
+ auto topLeft = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), std::min(m_StartPoint.y, m_EndPoint.y));
+ auto bottomRight = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), ImMax(m_StartPoint.y, m_EndPoint.y));
+ auto rect = ImRect(topLeft, bottomRight);
+ if (rect.GetWidth() <= 0)
+ rect.Max.x = rect.Min.x + 1;
+ if (rect.GetHeight() <= 0)
+ rect.Max.y = rect.Min.y + 1;
+
+ vector<Node*> nodes;
+ vector<Link*> links;
+
+ if (m_SelectLinkMode)
+ {
+ Editor->FindLinksInRect(rect, links);
+ m_CandidateObjects.assign(links.begin(), links.end());
+ }
+ else
+ {
+ Editor->FindNodesInRect(rect, nodes);
+ m_CandidateObjects.assign(nodes.begin(), nodes.end());
+
+ if (m_SelectGroups)
+ {
+ auto endIt = std::remove_if(m_CandidateObjects.begin(), m_CandidateObjects.end(), [](Object* object) { return !IsGroup(object->AsNode()); });
+ m_CandidateObjects.erase(endIt, m_CandidateObjects.end());
+ }
+ else
+ {
+ auto endIt = std::remove_if(m_CandidateObjects.begin(), m_CandidateObjects.end(), [](Object* object) { return IsGroup(object->AsNode()); });
+ m_CandidateObjects.erase(endIt, m_CandidateObjects.end());
+ }
+ }
+
+ m_CandidateObjects.insert(m_CandidateObjects.end(), m_SelectedObjectsAtStart.begin(), m_SelectedObjectsAtStart.end());
+ std::sort(m_CandidateObjects.begin(), m_CandidateObjects.end());
+ m_CandidateObjects.erase(std::unique(m_CandidateObjects.begin(), m_CandidateObjects.end()), m_CandidateObjects.end());
+ }
+ else
+ {
+ m_IsActive = false;
+
+ m_Animation.Play(c_SelectionFadeOutDuration);
+
+ m_CommitSelection = true;
+
+ return true;
+ }
+
+ return m_IsActive;
+}
+
+void ed::SelectAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+}
+
+void ed::SelectAction::Draw(ImDrawList* drawList)
+{
+ if (!m_IsActive && !m_Animation.IsPlaying())
+ return;
+
+ const auto alpha = m_Animation.IsPlaying() ? ImEasing::EaseOutQuad(1.0f, -1.0f, m_Animation.GetProgress()) : 1.0f;
+
+ const auto fillColor = Editor->GetColor(m_SelectLinkMode ? StyleColor_LinkSelRect : StyleColor_NodeSelRect, alpha);
+ const auto outlineColor = Editor->GetColor(m_SelectLinkMode ? StyleColor_LinkSelRectBorder : StyleColor_NodeSelRectBorder, alpha);
+
+ drawList->ChannelsSetCurrent(c_BackgroundChannel_SelectionRect);
+
+ auto min = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), std::min(m_StartPoint.y, m_EndPoint.y));
+ auto max = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), ImMax(m_StartPoint.y, m_EndPoint.y));
+
+ drawList->AddRectFilled(min, max, fillColor);
+ FringeScaleScope fringe(1.0f);
+ drawList->AddRect(min, max, outlineColor);
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Context Menu Action
+//
+//------------------------------------------------------------------------------
+ed::ContextMenuAction::ContextMenuAction(EditorContext* editor):
+ EditorAction(editor),
+ m_CandidateMenu(Menu::None),
+ m_CurrentMenu(Menu::None),
+ m_ContextId()
+{
+}
+
+ed::EditorAction::AcceptResult ed::ContextMenuAction::Accept(const Control& control)
+{
+ const auto isPressed = ImGui::IsMouseClicked(1);
+ const auto isReleased = ImGui::IsMouseReleased(1);
+ const auto isDragging = ImGui::IsMouseDragging(1);
+
+ if (isPressed || isReleased || isDragging)
+ {
+ Menu candidateMenu = ContextMenuAction::None;
+ ObjectId contextId;
+
+ if (auto hotObejct = control.HotObject)
+ {
+ if (hotObejct->AsNode())
+ candidateMenu = Node;
+ else if (hotObejct->AsPin())
+ candidateMenu = Pin;
+ else if (hotObejct->AsLink())
+ candidateMenu = Link;
+
+ if (candidateMenu != None)
+ contextId = hotObejct->ID();
+ }
+ else if (control.BackgroundHot)
+ candidateMenu = Background;
+
+ if (isPressed)
+ {
+ m_CandidateMenu = candidateMenu;
+ m_ContextId = contextId;
+ return Possible;
+ }
+ else if (isReleased && m_CandidateMenu == candidateMenu && m_ContextId == contextId)
+ {
+ m_CurrentMenu = m_CandidateMenu;
+ m_CandidateMenu = ContextMenuAction::None;
+ return True;
+ }
+ else
+ {
+ m_CandidateMenu = None;
+ m_CurrentMenu = None;
+ m_ContextId = ObjectId();
+ return False;
+ }
+ }
+
+ return False;
+}
+
+bool ed::ContextMenuAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ m_CandidateMenu = None;
+ m_CurrentMenu = None;
+ m_ContextId = ObjectId();
+ return false;
+}
+
+void ed::ContextMenuAction::Reject()
+{
+ m_CandidateMenu = None;
+ m_CurrentMenu = None;
+ m_ContextId = ObjectId();
+}
+
+void ed::ContextMenuAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ auto getMenuName = [](Menu menu)
+ {
+ switch (menu)
+ {
+ default:
+ case None: return "None";
+ case Node: return "Node";
+ case Pin: return "Pin";
+ case Link: return "Link";
+ case Background: return "Background";
+ }
+ };
+
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Menu: %s", getMenuName(m_CurrentMenu));
+}
+
+bool ed::ContextMenuAction::ShowNodeContextMenu(NodeId* nodeId)
+{
+ if (m_CurrentMenu != Node)
+ return false;
+
+ *nodeId = m_ContextId.AsNodeId();
+ Editor->SetUserContext();
+ return true;
+}
+
+bool ed::ContextMenuAction::ShowPinContextMenu(PinId* pinId)
+{
+ if (m_CurrentMenu != Pin)
+ return false;
+
+ *pinId = m_ContextId.AsPinId();
+ Editor->SetUserContext();
+ return true;
+}
+
+bool ed::ContextMenuAction::ShowLinkContextMenu(LinkId* linkId)
+{
+ if (m_CurrentMenu != Link)
+ return false;
+
+ *linkId = m_ContextId.AsLinkId();
+ Editor->SetUserContext();
+ return true;
+}
+
+bool ed::ContextMenuAction::ShowBackgroundContextMenu()
+{
+ if (m_CurrentMenu != Background)
+ return false;
+
+ Editor->SetUserContext();
+ return true;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Cut/Copy/Paste Action
+//
+//------------------------------------------------------------------------------
+ed::ShortcutAction::ShortcutAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_InAction(false),
+ m_CurrentAction(Action::None),
+ m_Context()
+{
+}
+
+ed::EditorAction::AcceptResult ed::ShortcutAction::Accept(const Control& control)
+{
+ if (!Editor->IsActive() || !Editor->AreShortcutsEnabled())
+ return False;
+
+ Action candidateAction = None;
+
+ auto& io = ImGui::GetIO();
+ if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_X)))
+ candidateAction = Cut;
+ if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C)))
+ candidateAction = Copy;
+ if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_V)))
+ candidateAction = Paste;
+ if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(GetKeyIndexForD()))
+ candidateAction = Duplicate;
+ if (!io.KeyCtrl && !io.KeyShift && !io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Space)))
+ candidateAction = CreateNode;
+
+ if (candidateAction != None)
+ {
+ if (candidateAction != Paste && candidateAction != CreateNode)
+ {
+ auto& selection = Editor->GetSelectedObjects();
+ if (!selection.empty())
+ {
+ // #TODO: Find a way to simplify logic.
+
+ m_Context.assign(selection.begin(), selection.end());
+
+ // Expand groups
+ vector<Node*> extra;
+ for (auto object : m_Context)
+ {
+ auto node = object->AsNode();
+ if (IsGroup(node))
+ node->GetGroupedNodes(extra, true);
+ }
+
+ // Apply groups and remove duplicates
+ if (!extra.empty())
+ {
+ m_Context.insert(m_Context.end(), extra.begin(), extra.end());
+ std::sort(m_Context.begin(), m_Context.end());
+ m_Context.erase(std::unique(m_Context.begin(), m_Context.end()), m_Context.end());
+ }
+ }
+ else if (control.HotObject && control.HotObject->IsSelectable() && !IsGroup(control.HotObject->AsNode()))
+ {
+ m_Context.push_back(control.HotObject);
+ }
+
+ if (m_Context.empty())
+ return False;
+
+ // Does copying only links make sense?
+ //const auto hasOnlyLinks = std::all_of(Context.begin(), Context.end(), [](Object* object) { return object->AsLink() != nullptr; });
+ //if (hasOnlyLinks)
+ // return False;
+
+ // If no links are selected, pick all links between nodes within context
+ const auto hasAnyLinks = std::any_of(m_Context.begin(), m_Context.end(), [](Object* object) { return object->AsLink() != nullptr; });
+ if (!hasAnyLinks && m_Context.size() > 1) // one node cannot make connection to anything
+ {
+ // Collect nodes in sorted vector viable for binary search
+ std::vector<ObjectWrapper<Node>> nodes;
+
+ nodes.reserve(m_Context.size());
+ std::for_each(m_Context.begin(), m_Context.end(), [&nodes](Object* object) { if (auto node = object->AsNode()) nodes.push_back({node->m_ID, node}); });
+
+ std::sort(nodes.begin(), nodes.end());
+
+ auto isNodeInContext = [&nodes](NodeId nodeId)
+ {
+ return std::binary_search(nodes.begin(), nodes.end(), ObjectWrapper<Node>{nodeId, nullptr});
+ };
+
+ // Collect links connected to nodes and drop those reaching out of context
+ std::vector<Link*> links;
+
+ for (auto node : nodes)
+ Editor->FindLinksForNode(node.m_ID, links, true);
+
+ // Remove duplicates
+ std::sort(links.begin(), links.end());
+ links.erase(std::unique(links.begin(), links.end()), links.end());
+
+ // Drop out of context links
+ links.erase(std::remove_if(links.begin(), links.end(), [&isNodeInContext](Link* link)
+ {
+ return !isNodeInContext(link->m_StartPin->m_Node->m_ID) || !isNodeInContext(link->m_EndPin->m_Node->m_ID);
+ }), links.end());
+
+ // Append links and remove duplicates
+ m_Context.insert(m_Context.end(), links.begin(), links.end());
+ }
+ }
+ else
+ m_Context.resize(0);
+
+ m_IsActive = true;
+ m_CurrentAction = candidateAction;
+
+ return True;
+ }
+
+ return False;
+}
+
+bool ed::ShortcutAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ m_IsActive = false;
+ m_CurrentAction = None;
+ m_Context.resize(0);
+ return false;
+}
+
+void ed::ShortcutAction::Reject()
+{
+ m_IsActive = false;
+ m_CurrentAction = None;
+ m_Context.resize(0);
+}
+
+void ed::ShortcutAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ auto getActionName = [](Action action)
+ {
+ switch (action)
+ {
+ default:
+ case None: return "None";
+ case Cut: return "Cut";
+ case Copy: return "Copy";
+ case Paste: return "Paste";
+ case Duplicate: return "Duplicate";
+ case CreateNode: return "CreateNode";
+ }
+ };
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Action: %s", getActionName(m_CurrentAction));
+}
+
+bool ed::ShortcutAction::Begin()
+{
+ if (m_IsActive)
+ m_InAction = true;
+ return m_IsActive;
+}
+
+void ed::ShortcutAction::End()
+{
+ if (m_IsActive)
+ m_InAction = false;
+}
+
+bool ed::ShortcutAction::AcceptCut()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == Cut;
+}
+
+bool ed::ShortcutAction::AcceptCopy()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == Copy;
+}
+
+bool ed::ShortcutAction::AcceptPaste()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == Paste;
+}
+
+bool ed::ShortcutAction::AcceptDuplicate()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == Duplicate;
+}
+
+bool ed::ShortcutAction::AcceptCreateNode()
+{
+ IM_ASSERT(m_InAction);
+ return m_CurrentAction == CreateNode;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Create Item Action
+//
+//------------------------------------------------------------------------------
+ed::CreateItemAction::CreateItemAction(EditorContext* editor):
+ EditorAction(editor),
+ m_InActive(false),
+ m_NextStage(None),
+ m_CurrentStage(None),
+ m_ItemType(NoItem),
+ m_UserAction(Unknown),
+ m_LinkColor(IM_COL32_WHITE),
+ m_LinkThickness(1.0f),
+ m_LinkStart(nullptr),
+ m_LinkEnd(nullptr),
+
+ m_IsActive(false),
+ m_DraggedPin(nullptr),
+
+ m_IsInGlobalSpace(false)
+{
+}
+
+ed::EditorAction::AcceptResult ed::CreateItemAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return EditorAction::False;
+
+ if (control.ActivePin && ImGui::IsMouseDragging(0))
+ {
+ m_DraggedPin = control.ActivePin;
+ DragStart(m_DraggedPin);
+
+ Editor->ClearSelection();
+ }
+ else if (control.HotPin)
+ {
+ return EditorAction::Possible;
+ }
+ else
+ return EditorAction::False;
+
+ m_IsActive = true;
+
+ return EditorAction::True;
+}
+
+bool ed::CreateItemAction::Process(const Control& control)
+{
+ IM_ASSERT(m_IsActive);
+
+ if (!m_IsActive)
+ return false;
+
+ if (m_DraggedPin && control.ActivePin == m_DraggedPin && (m_CurrentStage == Possible))
+ {
+ const auto draggingFromSource = (m_DraggedPin->m_Kind == PinKind::Output);
+
+ ed::Pin cursorPin(Editor, 0, draggingFromSource ? PinKind::Input : PinKind::Output);
+ cursorPin.m_Pivot = ImRect(ImGui::GetMousePos(), ImGui::GetMousePos());
+ cursorPin.m_Dir = -m_DraggedPin->m_Dir;
+ cursorPin.m_Strength = m_DraggedPin->m_Strength;
+
+ ed::Link candidate(Editor, 0);
+ candidate.m_Color = m_LinkColor;
+ candidate.m_StartPin = draggingFromSource ? m_DraggedPin : &cursorPin;
+ candidate.m_EndPin = draggingFromSource ? &cursorPin : m_DraggedPin;
+
+ ed::Pin*& freePin = draggingFromSource ? candidate.m_EndPin : candidate.m_StartPin;
+
+ if (control.HotPin)
+ {
+ DropPin(control.HotPin);
+
+ if (m_UserAction == UserAccept)
+ freePin = control.HotPin;
+ }
+ else if (control.BackgroundHot)
+ DropNode();
+ else
+ DropNothing();
+
+ auto drawList = ImGui::GetWindowDrawList();
+ drawList->ChannelsSetCurrent(c_LinkChannel_NewLink);
+
+ candidate.UpdateEndpoints();
+ candidate.Draw(drawList, m_LinkColor, m_LinkThickness);
+ }
+ else if (m_CurrentStage == Possible || !control.ActivePin)
+ {
+ if (!ImGui::IsWindowHovered())
+ {
+ m_DraggedPin = nullptr;
+ DropNothing();
+ }
+
+ DragEnd();
+ m_IsActive = false;
+ }
+
+ return m_IsActive;
+}
+
+void ed::CreateItemAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ auto getStageName = [](Stage stage)
+ {
+ switch (stage)
+ {
+ case None: return "None";
+ case Possible: return "Possible";
+ case Create: return "Create";
+ default: return "<unknown>";
+ }
+ };
+
+ auto getActionName = [](Action action)
+ {
+ switch (action)
+ {
+ default:
+ case Unknown: return "Unknown";
+ case UserReject: return "Reject";
+ case UserAccept: return "Accept";
+ }
+ };
+
+ auto getItemName = [](Type item)
+ {
+ switch (item)
+ {
+ default:
+ case NoItem: return "None";
+ case Node: return "Node";
+ case Link: return "Link";
+ }
+ };
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Stage: %s", getStageName(m_CurrentStage));
+ ImGui::Text(" User Action: %s", getActionName(m_UserAction));
+ ImGui::Text(" Item Type: %s", getItemName(m_ItemType));
+}
+
+void ed::CreateItemAction::SetStyle(ImU32 color, float thickness)
+{
+ m_LinkColor = color;
+ m_LinkThickness = thickness;
+}
+
+bool ed::CreateItemAction::Begin()
+{
+ IM_ASSERT(false == m_InActive);
+
+ m_InActive = true;
+ m_CurrentStage = m_NextStage;
+ m_UserAction = Unknown;
+ m_LinkColor = IM_COL32_WHITE;
+ m_LinkThickness = 1.0f;
+
+ if (m_CurrentStage == None)
+ return false;
+
+ m_LastChannel = ImGui::GetWindowDrawList()->_Splitter._Current;
+
+ return true;
+}
+
+void ed::CreateItemAction::End()
+{
+ IM_ASSERT(m_InActive);
+
+ if (m_IsInGlobalSpace)
+ {
+ ImGui::PopClipRect();
+ Editor->Resume(SuspendFlags::KeepSplitter);
+
+ auto currentChannel = ImGui::GetWindowDrawList()->_Splitter._Current;
+ if (currentChannel != m_LastChannel)
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(m_LastChannel);
+
+ m_IsInGlobalSpace = false;
+ }
+
+ m_InActive = false;
+}
+
+void ed::CreateItemAction::DragStart(Pin* startPin)
+{
+ IM_ASSERT(!m_InActive);
+
+ m_NextStage = Possible;
+ m_LinkStart = startPin;
+ m_LinkEnd = nullptr;
+}
+
+void ed::CreateItemAction::DragEnd()
+{
+ IM_ASSERT(!m_InActive);
+
+ if (m_CurrentStage == Possible && m_UserAction == UserAccept)
+ {
+ m_NextStage = Create;
+ }
+ else
+ {
+ m_NextStage = None;
+ m_ItemType = NoItem;
+ m_LinkStart = nullptr;
+ m_LinkEnd = nullptr;
+ }
+}
+
+void ed::CreateItemAction::DropPin(Pin* endPin)
+{
+ IM_ASSERT(!m_InActive);
+
+ m_ItemType = Link;
+ m_LinkEnd = endPin;
+}
+
+void ed::CreateItemAction::DropNode()
+{
+ IM_ASSERT(!m_InActive);
+
+ m_ItemType = Node;
+ m_LinkEnd = nullptr;
+}
+
+void ed::CreateItemAction::DropNothing()
+{
+ IM_ASSERT(!m_InActive);
+
+ m_ItemType = NoItem;
+ m_LinkEnd = nullptr;
+}
+
+ed::CreateItemAction::Result ed::CreateItemAction::RejectItem()
+{
+ IM_ASSERT(m_InActive);
+
+ if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem)
+ return Indeterminate;
+
+ m_UserAction = UserReject;
+
+ return True;
+}
+
+ed::CreateItemAction::Result ed::CreateItemAction::AcceptItem()
+{
+ IM_ASSERT(m_InActive);
+
+ if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem)
+ return Indeterminate;
+
+ m_UserAction = UserAccept;
+
+ if (m_CurrentStage == Create)
+ {
+ m_NextStage = None;
+ m_ItemType = NoItem;
+ m_LinkStart = nullptr;
+ m_LinkEnd = nullptr;
+ return True;
+ }
+ else
+ return False;
+}
+
+ed::CreateItemAction::Result ed::CreateItemAction::QueryLink(PinId* startId, PinId* endId)
+{
+ IM_ASSERT(m_InActive);
+
+ if (!m_InActive || m_CurrentStage == None || m_ItemType != Link)
+ return Indeterminate;
+
+ auto linkStartId = m_LinkStart->m_ID;
+ auto linkEndId = m_LinkEnd->m_ID;
+
+ *startId = linkStartId;
+ *endId = linkEndId;
+
+ Editor->SetUserContext(true);
+
+ if (!m_IsInGlobalSpace)
+ {
+ Editor->Suspend(SuspendFlags::KeepSplitter);
+
+ auto rect = Editor->GetRect();
+ ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false);
+ m_IsInGlobalSpace = true;
+ }
+
+ return True;
+}
+
+ed::CreateItemAction::Result ed::CreateItemAction::QueryNode(PinId* pinId)
+{
+ IM_ASSERT(m_InActive);
+
+ if (!m_InActive || m_CurrentStage == None || m_ItemType != Node)
+ return Indeterminate;
+
+ *pinId = m_LinkStart ? m_LinkStart->m_ID : 0;
+
+ Editor->SetUserContext(true);
+
+ if (!m_IsInGlobalSpace)
+ {
+ Editor->Suspend(SuspendFlags::KeepSplitter);
+
+ auto rect = Editor->GetRect();
+ ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false);
+ m_IsInGlobalSpace = true;
+ }
+
+ return True;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Delete Items Action
+//
+//------------------------------------------------------------------------------
+ed::DeleteItemsAction::DeleteItemsAction(EditorContext* editor):
+ EditorAction(editor),
+ m_IsActive(false),
+ m_InInteraction(false),
+ m_CurrentItemType(Unknown),
+ m_UserAction(Undetermined)
+{
+}
+
+ed::EditorAction::AcceptResult ed::DeleteItemsAction::Accept(const Control& control)
+{
+ IM_ASSERT(!m_IsActive);
+
+ if (m_IsActive)
+ return False;
+
+ auto addDeadLinks = [this]()
+ {
+ vector<ed::Link*> links;
+ for (auto object : m_CandidateObjects)
+ {
+ auto node = object->AsNode();
+ if (!node)
+ continue;
+
+ Editor->FindLinksForNode(node->m_ID, links, true);
+ }
+ if (!links.empty())
+ {
+ std::sort(links.begin(), links.end());
+ links.erase(std::unique(links.begin(), links.end()), links.end());
+ m_CandidateObjects.insert(m_CandidateObjects.end(), links.begin(), links.end());
+ }
+ };
+
+ auto& io = ImGui::GetIO();
+ if (ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete)) && Editor->AreShortcutsEnabled())
+ {
+ auto& selection = Editor->GetSelectedObjects();
+ if (!selection.empty())
+ {
+ m_CandidateObjects = selection;
+ addDeadLinks();
+ m_IsActive = true;
+ return True;
+ }
+ }
+ else if (control.ClickedLink && io.KeyAlt)
+ {
+ m_CandidateObjects.clear();
+ m_CandidateObjects.push_back(control.ClickedLink);
+ m_IsActive = true;
+ return True;
+ }
+
+ else if (!m_ManuallyDeletedObjects.empty())
+ {
+ m_CandidateObjects = m_ManuallyDeletedObjects;
+ m_ManuallyDeletedObjects.clear();
+ addDeadLinks();
+ m_IsActive = true;
+ return True;
+ }
+
+ return m_IsActive ? True : False;
+}
+
+bool ed::DeleteItemsAction::Process(const Control& control)
+{
+ IM_UNUSED(control);
+
+ if (!m_IsActive)
+ return false;
+
+ m_IsActive = false;
+ return true;
+}
+
+void ed::DeleteItemsAction::ShowMetrics()
+{
+ EditorAction::ShowMetrics();
+
+ //auto getObjectName = [](Object* object)
+ //{
+ // if (!object) return "";
+ // else if (object->AsNode()) return "Node";
+ // else if (object->AsPin()) return "Pin";
+ // else if (object->AsLink()) return "Link";
+ // else return "";
+ //};
+
+ ImGui::Text("%s:", GetName());
+ ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no");
+ //ImGui::Text(" Node: %s (%d)", getObjectName(DeleteItemsgedNode), DeleteItemsgedNode ? DeleteItemsgedNode->ID : 0);
+}
+
+bool ed::DeleteItemsAction::Add(Object* object)
+{
+ if (Editor->GetCurrentAction() != nullptr)
+ return false;
+
+ m_ManuallyDeletedObjects.push_back(object);
+
+ return true;
+}
+
+bool ed::DeleteItemsAction::Begin()
+{
+ if (!m_IsActive)
+ return false;
+
+ IM_ASSERT(!m_InInteraction);
+ m_InInteraction = true;
+
+ m_CurrentItemType = Unknown;
+ m_UserAction = Undetermined;
+
+ return m_IsActive;
+}
+
+void ed::DeleteItemsAction::End()
+{
+ if (!m_IsActive)
+ return;
+
+ IM_ASSERT(m_InInteraction);
+ m_InInteraction = false;
+}
+
+bool ed::DeleteItemsAction::QueryLink(LinkId* linkId, PinId* startId, PinId* endId)
+{
+ ObjectId objectId;
+ if (!QueryItem(&objectId, Link))
+ return false;
+
+ if (auto id = objectId.AsLinkId())
+ *linkId = id;
+ else
+ return false;
+
+ if (startId || endId)
+ {
+ auto link = Editor->FindLink(*linkId);
+ if (startId)
+ *startId = link->m_StartPin->m_ID;
+ if (endId)
+ *endId = link->m_EndPin->m_ID;
+ }
+
+ return true;
+}
+
+bool ed::DeleteItemsAction::QueryNode(NodeId* nodeId)
+{
+ ObjectId objectId;
+ if (!QueryItem(&objectId, Node))
+ return false;
+
+ if (auto id = objectId.AsNodeId())
+ *nodeId = id;
+ else
+ return false;
+
+ return true;
+}
+
+bool ed::DeleteItemsAction::QueryItem(ObjectId* itemId, IteratorType itemType)
+{
+ if (!m_InInteraction)
+ return false;
+
+ if (m_CurrentItemType != itemType)
+ {
+ m_CurrentItemType = itemType;
+ m_CandidateItemIndex = 0;
+ }
+ else if (m_UserAction == Undetermined)
+ {
+ RejectItem();
+ }
+
+ m_UserAction = Undetermined;
+
+ auto itemCount = (int)m_CandidateObjects.size();
+ while (m_CandidateItemIndex < itemCount)
+ {
+ auto item = m_CandidateObjects[m_CandidateItemIndex];
+ if (itemType == Node)
+ {
+ if (auto node = item->AsNode())
+ {
+ *itemId = node->m_ID;
+ return true;
+ }
+ }
+ else if (itemType == Link)
+ {
+ if (auto link = item->AsLink())
+ {
+ *itemId = link->m_ID;
+ return true;
+ }
+ }
+
+ ++m_CandidateItemIndex;
+ }
+
+ if (m_CandidateItemIndex == itemCount)
+ m_CurrentItemType = Unknown;
+
+ return false;
+}
+
+bool ed::DeleteItemsAction::AcceptItem()
+{
+ if (!m_InInteraction)
+ return false;
+
+ m_UserAction = Accepted;
+
+ RemoveItem();
+
+ return true;
+}
+
+void ed::DeleteItemsAction::RejectItem()
+{
+ if (!m_InInteraction)
+ return;
+
+ m_UserAction = Rejected;
+
+ RemoveItem();
+}
+
+void ed::DeleteItemsAction::RemoveItem()
+{
+ auto item = m_CandidateObjects[m_CandidateItemIndex];
+ m_CandidateObjects.erase(m_CandidateObjects.begin() + m_CandidateItemIndex);
+
+ Editor->DeselectObject(item);
+
+ if (m_CurrentItemType == Link)
+ Editor->NotifyLinkDeleted(item->AsLink());
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Node Builder
+//
+//------------------------------------------------------------------------------
+ed::NodeBuilder::NodeBuilder(EditorContext* editor):
+ Editor(editor),
+ m_CurrentNode(nullptr),
+ m_CurrentPin(nullptr)
+{
+}
+
+ed::NodeBuilder::~NodeBuilder()
+{
+ m_Splitter.ClearFreeMemory();
+ m_PinSplitter.ClearFreeMemory();
+}
+
+void ed::NodeBuilder::Begin(NodeId nodeId)
+{
+ IM_ASSERT(nullptr == m_CurrentNode);
+
+ m_CurrentNode = Editor->GetNode(nodeId);
+
+ if (m_CurrentNode->m_RestoreState)
+ {
+ Editor->RestoreNodeState(m_CurrentNode);
+ m_CurrentNode->m_RestoreState = false;
+ }
+
+ if (m_CurrentNode->m_CenterOnScreen)
+ {
+ auto bounds = Editor->GetViewRect();
+ auto offset = bounds.GetCenter() - m_CurrentNode->m_Bounds.GetCenter();
+
+ if (ImLengthSqr(offset) > 0)
+ {
+ if (::IsGroup(m_CurrentNode))
+ {
+ std::vector<Node*> groupedNodes;
+ m_CurrentNode->GetGroupedNodes(groupedNodes);
+ groupedNodes.push_back(m_CurrentNode);
+
+ for (auto node : groupedNodes)
+ {
+ node->m_Bounds.Translate(ImFloor(offset));
+ node->m_GroupBounds.Translate(ImFloor(offset));
+ Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, node);
+ }
+ }
+ else
+ {
+ m_CurrentNode->m_Bounds.Translate(ImFloor(offset));
+ m_CurrentNode->m_GroupBounds.Translate(ImFloor(offset));
+ Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, m_CurrentNode);
+ }
+ }
+
+ m_CurrentNode->m_CenterOnScreen = false;
+ }
+
+ // Position node on screen
+ ImGui::SetCursorScreenPos(m_CurrentNode->m_Bounds.Min);
+
+ auto& editorStyle = Editor->GetStyle();
+
+ const auto alpha = ImGui::GetStyle().Alpha;
+
+ m_CurrentNode->m_IsLive = true;
+ m_CurrentNode->m_LastPin = nullptr;
+ m_CurrentNode->m_Color = Editor->GetColor(StyleColor_NodeBg, alpha);
+ m_CurrentNode->m_BorderColor = Editor->GetColor(StyleColor_NodeBorder, alpha);
+ m_CurrentNode->m_BorderWidth = editorStyle.NodeBorderWidth;
+ m_CurrentNode->m_Rounding = editorStyle.NodeRounding;
+ m_CurrentNode->m_GroupColor = Editor->GetColor(StyleColor_GroupBg, alpha);
+ m_CurrentNode->m_GroupBorderColor = Editor->GetColor(StyleColor_GroupBorder, alpha);
+ m_CurrentNode->m_GroupBorderWidth = editorStyle.GroupBorderWidth;
+ m_CurrentNode->m_GroupRounding = editorStyle.GroupRounding;
+
+ m_IsGroup = false;
+
+ // Grow channel list and select user channel
+ if (auto drawList = ImGui::GetWindowDrawList())
+ {
+ m_CurrentNode->m_Channel = drawList->_Splitter._Count;
+ ImDrawList_ChannelsGrow(drawList, drawList->_Splitter._Count + c_ChannelsPerNode);
+ drawList->ChannelsSetCurrent(m_CurrentNode->m_Channel + c_NodeContentChannel);
+
+ m_Splitter.Clear();
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+ }
+
+ // Begin outer group
+ ImGui::BeginGroup();
+
+ // Apply frame padding. Begin inner group if necessary.
+ if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0)
+ {
+ ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2(editorStyle.NodePadding.x, editorStyle.NodePadding.y));
+ ImGui::BeginGroup();
+ }
+}
+
+void ed::NodeBuilder::End()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ if (auto drawList = ImGui::GetWindowDrawList())
+ {
+ IM_ASSERT(drawList->_Splitter._Count == 1); // Did you forgot to call drawList->ChannelsMerge()?
+ ImDrawList_SwapSplitter(drawList, m_Splitter);
+ }
+
+ // Apply frame padding. This must be done in this convoluted way if outer group
+ // size must contain inner group padding.
+ auto& editorStyle = Editor->GetStyle();
+ if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0)
+ {
+ ImGui::EndGroup();
+ ImGui::SameLine(0, editorStyle.NodePadding.z);
+ ImGui::Dummy(ImVec2(0, 0));
+ ImGui::SetCursorPosY(ImGui::GetCursorPosY() + editorStyle.NodePadding.w);
+ }
+
+ // End outer group.
+ ImGui::EndGroup();
+
+ m_NodeRect = ImGui_GetItemRect();
+ m_NodeRect.Floor();
+
+ if (m_CurrentNode->m_Bounds.GetSize() != m_NodeRect.GetSize())
+ {
+ m_CurrentNode->m_Bounds.Max = m_CurrentNode->m_Bounds.Min + m_NodeRect.GetSize();
+ Editor->MakeDirty(SaveReasonFlags::Size, m_CurrentNode);
+ }
+
+ if (m_IsGroup)
+ {
+ // Groups cannot have pins. Discard them.
+ for (auto pin = m_CurrentNode->m_LastPin; pin; pin = pin->m_PreviousPin)
+ pin->Reset();
+
+ m_CurrentNode->m_Type = NodeType::Group;
+ m_CurrentNode->m_GroupBounds = m_GroupBounds;
+ m_CurrentNode->m_LastPin = nullptr;
+ }
+ else
+ m_CurrentNode->m_Type = NodeType::Node;
+
+ m_CurrentNode = nullptr;
+}
+
+void ed::NodeBuilder::BeginPin(PinId pinId, PinKind kind)
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+ IM_ASSERT(nullptr == m_CurrentPin);
+ IM_ASSERT(false == m_IsGroup);
+
+ auto& editorStyle = Editor->GetStyle();
+
+ m_CurrentPin = Editor->GetPin(pinId, kind);
+ m_CurrentPin->m_Node = m_CurrentNode;
+
+ m_CurrentPin->m_IsLive = true;
+ m_CurrentPin->m_Color = Editor->GetColor(StyleColor_PinRect);
+ m_CurrentPin->m_BorderColor = Editor->GetColor(StyleColor_PinRectBorder);
+ m_CurrentPin->m_BorderWidth = editorStyle.PinBorderWidth;
+ m_CurrentPin->m_Rounding = editorStyle.PinRounding;
+ m_CurrentPin->m_Corners = static_cast<int>(editorStyle.PinCorners);
+ m_CurrentPin->m_Radius = editorStyle.PinRadius;
+ m_CurrentPin->m_ArrowSize = editorStyle.PinArrowSize;
+ m_CurrentPin->m_ArrowWidth = editorStyle.PinArrowWidth;
+ m_CurrentPin->m_Dir = kind == PinKind::Output ? editorStyle.SourceDirection : editorStyle.TargetDirection;
+ m_CurrentPin->m_Strength = editorStyle.LinkStrength;
+
+ m_CurrentPin->m_PreviousPin = m_CurrentNode->m_LastPin;
+ m_CurrentNode->m_LastPin = m_CurrentPin;
+
+ m_PivotAlignment = editorStyle.PivotAlignment;
+ m_PivotSize = editorStyle.PivotSize;
+ m_PivotScale = editorStyle.PivotScale;
+ m_ResolvePinRect = true;
+ m_ResolvePivot = true;
+
+ if (auto drawList = ImGui::GetWindowDrawList())
+ {
+ m_PinSplitter.Clear();
+ ImDrawList_SwapSplitter(drawList, m_PinSplitter);
+ }
+
+ ImGui::BeginGroup();
+}
+
+void ed::NodeBuilder::EndPin()
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ if (auto drawList = ImGui::GetWindowDrawList())
+ {
+ IM_ASSERT(drawList->_Splitter._Count == 1); // Did you forgot to call drawList->ChannelsMerge()?
+ ImDrawList_SwapSplitter(drawList, m_PinSplitter);
+ }
+
+ ImGui::EndGroup();
+
+ if (m_ResolvePinRect)
+ m_CurrentPin->m_Bounds = ImGui_GetItemRect();
+
+ if (m_ResolvePivot)
+ {
+ auto& pinRect = m_CurrentPin->m_Bounds;
+
+ if (m_PivotSize.x < 0)
+ m_PivotSize.x = pinRect.GetWidth();
+ if (m_PivotSize.y < 0)
+ m_PivotSize.y = pinRect.GetHeight();
+
+ m_CurrentPin->m_Pivot.Min = pinRect.Min + ImMul(pinRect.GetSize(), m_PivotAlignment);
+ m_CurrentPin->m_Pivot.Max = m_CurrentPin->m_Pivot.Min + ImMul(m_PivotSize, m_PivotScale);
+ }
+
+ // #debug: Draw pin bounds
+ //ImGui::GetWindowDrawList()->AddRect(m_CurrentPin->m_Bounds.Min, m_CurrentPin->m_Bounds.Max, IM_COL32(255, 255, 0, 255));
+
+ // #debug: Draw pin pivot rectangle
+ //ImGui::GetWindowDrawList()->AddRect(m_CurrentPin->m_Pivot.Min, m_CurrentPin->m_Pivot.Max, IM_COL32(255, 0, 255, 255));
+
+ m_CurrentPin = nullptr;
+}
+
+void ed::NodeBuilder::PinRect(const ImVec2& a, const ImVec2& b)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_CurrentPin->m_Bounds = ImRect(a, b);
+ m_CurrentPin->m_Bounds.Floor();
+ m_ResolvePinRect = false;
+}
+
+void ed::NodeBuilder::PinPivotRect(const ImVec2& a, const ImVec2& b)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_CurrentPin->m_Pivot = ImRect(a, b);
+ m_ResolvePivot = false;
+}
+
+void ed::NodeBuilder::PinPivotSize(const ImVec2& size)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_PivotSize = size;
+ m_ResolvePivot = true;
+}
+
+void ed::NodeBuilder::PinPivotScale(const ImVec2& scale)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_PivotScale = scale;
+ m_ResolvePivot = true;
+}
+
+void ed::NodeBuilder::PinPivotAlignment(const ImVec2& alignment)
+{
+ IM_ASSERT(nullptr != m_CurrentPin);
+
+ m_PivotAlignment = alignment;
+ m_ResolvePivot = true;
+}
+
+void ed::NodeBuilder::Group(const ImVec2& size)
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+ IM_ASSERT(nullptr == m_CurrentPin);
+ IM_ASSERT(false == m_IsGroup);
+
+ m_IsGroup = true;
+
+ if (IsGroup(m_CurrentNode))
+ ImGui::Dummy(m_CurrentNode->m_GroupBounds.GetSize());
+ else
+ ImGui::Dummy(size);
+
+ m_GroupBounds = ImGui_GetItemRect();
+ m_GroupBounds.Floor();
+}
+
+ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList() const
+{
+ return GetUserBackgroundDrawList(m_CurrentNode);
+}
+
+ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList(Node* node) const
+{
+ if (node && node->m_IsLive)
+ {
+ auto drawList = ImGui::GetWindowDrawList();
+ drawList->ChannelsSetCurrent(node->m_Channel + c_NodeUserBackgroundChannel);
+ return drawList;
+ }
+ else
+ return nullptr;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Node Builder
+//
+//------------------------------------------------------------------------------
+ed::HintBuilder::HintBuilder(EditorContext* editor):
+ Editor(editor),
+ m_IsActive(false),
+ m_CurrentNode(nullptr)
+{
+}
+
+bool ed::HintBuilder::Begin(NodeId nodeId)
+{
+ IM_ASSERT(nullptr == m_CurrentNode);
+
+ auto& view = Editor->GetView();
+ auto& rect = Editor->GetRect();
+
+ const float c_min_zoom = 0.75f;
+ const float c_max_zoom = 0.50f;
+
+ if (view.Scale > 0.75f)
+ return false;
+
+ auto node = Editor->FindNode(nodeId);
+ if (!IsGroup(node))
+ return false;
+
+ m_CurrentNode = node;
+
+ m_LastChannel = ImGui::GetWindowDrawList()->_Splitter._Current;
+
+ Editor->Suspend(SuspendFlags::KeepSplitter);
+
+ const auto alpha = ImMax(0.0f, std::min(1.0f, (view.Scale - c_min_zoom) / (c_max_zoom - c_min_zoom)));
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground);
+ ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false);
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_Hints);
+ ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false);
+
+ ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
+
+ m_IsActive = true;
+
+ return true;
+}
+
+void ed::HintBuilder::End()
+{
+ if (!m_IsActive)
+ return;
+
+ ImGui::PopStyleVar();
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_Hints);
+ ImGui::PopClipRect();
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground);
+ ImGui::PopClipRect();
+
+ ImGui::GetWindowDrawList()->ChannelsSetCurrent(m_LastChannel);
+
+ Editor->Resume(SuspendFlags::KeepSplitter);
+
+ m_IsActive = false;
+ m_CurrentNode = nullptr;
+}
+
+ImVec2 ed::HintBuilder::GetGroupMin()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ return Editor->ToScreen(m_CurrentNode->m_Bounds.Min);
+}
+
+ImVec2 ed::HintBuilder::GetGroupMax()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ return Editor->ToScreen(m_CurrentNode->m_Bounds.Max);
+}
+
+ImDrawList* ed::HintBuilder::GetForegroundDrawList()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ auto drawList = ImGui::GetWindowDrawList();
+
+ drawList->ChannelsSetCurrent(c_UserChannel_Hints);
+
+ return drawList;
+}
+
+ImDrawList* ed::HintBuilder::GetBackgroundDrawList()
+{
+ IM_ASSERT(nullptr != m_CurrentNode);
+
+ auto drawList = ImGui::GetWindowDrawList();
+
+ drawList->ChannelsSetCurrent(c_UserChannel_HintsBackground);
+
+ return drawList;
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Style
+//
+//------------------------------------------------------------------------------
+void ed::Style::PushColor(StyleColor colorIndex, const ImVec4& color)
+{
+ ColorModifier modifier;
+ modifier.Index = colorIndex;
+ modifier.Value = Colors[colorIndex];
+ m_ColorStack.push_back(modifier);
+ Colors[colorIndex] = color;
+}
+
+void ed::Style::PopColor(int count)
+{
+ while (count > 0)
+ {
+ auto& modifier = m_ColorStack.back();
+ Colors[modifier.Index] = modifier.Value;
+ m_ColorStack.pop_back();
+ --count;
+ }
+}
+
+void ed::Style::PushVar(StyleVar varIndex, float value)
+{
+ auto* var = GetVarFloatAddr(varIndex);
+ IM_ASSERT(var != nullptr);
+ VarModifier modifier;
+ modifier.Index = varIndex;
+ modifier.Value = ImVec4(*var, 0, 0, 0);
+ *var = value;
+ m_VarStack.push_back(modifier);
+}
+
+void ed::Style::PushVar(StyleVar varIndex, const ImVec2& value)
+{
+ auto* var = GetVarVec2Addr(varIndex);
+ IM_ASSERT(var != nullptr);
+ VarModifier modifier;
+ modifier.Index = varIndex;
+ modifier.Value = ImVec4(var->x, var->y, 0, 0);
+ *var = value;
+ m_VarStack.push_back(modifier);
+}
+
+void ed::Style::PushVar(StyleVar varIndex, const ImVec4& value)
+{
+ auto* var = GetVarVec4Addr(varIndex);
+ IM_ASSERT(var != nullptr);
+ VarModifier modifier;
+ modifier.Index = varIndex;
+ modifier.Value = *var;
+ *var = value;
+ m_VarStack.push_back(modifier);
+}
+
+void ed::Style::PopVar(int count)
+{
+ while (count > 0)
+ {
+ auto& modifier = m_VarStack.back();
+ if (auto floatValue = GetVarFloatAddr(modifier.Index))
+ *floatValue = modifier.Value.x;
+ else if (auto vec2Value = GetVarVec2Addr(modifier.Index))
+ *vec2Value = ImVec2(modifier.Value.x, modifier.Value.y);
+ else if (auto vec4Value = GetVarVec4Addr(modifier.Index))
+ *vec4Value = modifier.Value;
+ m_VarStack.pop_back();
+ --count;
+ }
+}
+
+const char* ed::Style::GetColorName(StyleColor colorIndex) const
+{
+ switch (colorIndex)
+ {
+ case StyleColor_Bg: return "Bg";
+ case StyleColor_Grid: return "Grid";
+ case StyleColor_NodeBg: return "NodeBg";
+ case StyleColor_NodeBorder: return "NodeBorder";
+ case StyleColor_HovNodeBorder: return "HoveredNodeBorder";
+ case StyleColor_SelNodeBorder: return "SelNodeBorder";
+ case StyleColor_NodeSelRect: return "NodeSelRect";
+ case StyleColor_NodeSelRectBorder: return "NodeSelRectBorder";
+ case StyleColor_HovLinkBorder: return "HoveredLinkBorder";
+ case StyleColor_SelLinkBorder: return "SelLinkBorder";
+ case StyleColor_LinkSelRect: return "LinkSelRect";
+ case StyleColor_LinkSelRectBorder: return "LinkSelRectBorder";
+ case StyleColor_PinRect: return "PinRect";
+ case StyleColor_PinRectBorder: return "PinRectBorder";
+ case StyleColor_Flow: return "Flow";
+ case StyleColor_FlowMarker: return "FlowMarker";
+ case StyleColor_GroupBg: return "GroupBg";
+ case StyleColor_GroupBorder: return "GroupBorder";
+ case StyleColor_Count: break;
+ }
+
+ IM_ASSERT(0);
+ return "Unknown";
+}
+
+float* ed::Style::GetVarFloatAddr(StyleVar idx)
+{
+ switch (idx)
+ {
+ case StyleVar_NodeRounding: return &NodeRounding;
+ case StyleVar_NodeBorderWidth: return &NodeBorderWidth;
+ case StyleVar_HoveredNodeBorderWidth: return &HoveredNodeBorderWidth;
+ case StyleVar_SelectedNodeBorderWidth: return &SelectedNodeBorderWidth;
+ case StyleVar_PinRounding: return &PinRounding;
+ case StyleVar_PinBorderWidth: return &PinBorderWidth;
+ case StyleVar_LinkStrength: return &LinkStrength;
+ case StyleVar_ScrollDuration: return &ScrollDuration;
+ case StyleVar_FlowMarkerDistance: return &FlowMarkerDistance;
+ case StyleVar_FlowSpeed: return &FlowSpeed;
+ case StyleVar_FlowDuration: return &FlowDuration;
+ case StyleVar_PinCorners: return &PinCorners;
+ case StyleVar_PinRadius: return &PinRadius;
+ case StyleVar_PinArrowSize: return &PinArrowSize;
+ case StyleVar_PinArrowWidth: return &PinArrowWidth;
+ case StyleVar_GroupRounding: return &GroupRounding;
+ case StyleVar_GroupBorderWidth: return &GroupBorderWidth;
+ default: return nullptr;
+ }
+}
+
+ImVec2* ed::Style::GetVarVec2Addr(StyleVar idx)
+{
+ switch (idx)
+ {
+ case StyleVar_SourceDirection: return &SourceDirection;
+ case StyleVar_TargetDirection: return &TargetDirection;
+ case StyleVar_PivotAlignment: return &PivotAlignment;
+ case StyleVar_PivotSize: return &PivotSize;
+ case StyleVar_PivotScale: return &PivotScale;
+ default: return nullptr;
+ }
+}
+
+ImVec4* ed::Style::GetVarVec4Addr(StyleVar idx)
+{
+ switch (idx)
+ {
+ case StyleVar_NodePadding: return &NodePadding;
+ default: return nullptr;
+ }
+}
+
+
+
+
+//------------------------------------------------------------------------------
+//
+// Config
+//
+//------------------------------------------------------------------------------
+ed::Config::Config(const ax::NodeEditor::Config* config)
+{
+ if (config)
+ *static_cast<ax::NodeEditor::Config*>(this) = *config;
+}
+
+std::string ed::Config::Load()
+{
+ std::string data;
+
+ if (LoadSettings)
+ {
+ const auto size = LoadSettings(nullptr, UserPointer);
+ if (size > 0)
+ {
+ data.resize(size);
+ LoadSettings(const_cast<char*>(data.data()), UserPointer);
+ }
+ }
+ else if (SettingsFile)
+ {
+ std::ifstream file(SettingsFile);
+ if (file)
+ {
+ file.seekg(0, std::ios_base::end);
+ auto size = static_cast<size_t>(file.tellg());
+ file.seekg(0, std::ios_base::beg);
+
+ data.reserve(size);
+ data.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
+ }
+ }
+
+ return data;
+}
+
+std::string ed::Config::LoadNode(NodeId nodeId)
+{
+ std::string data;
+
+ if (LoadNodeSettings)
+ {
+ const auto size = LoadNodeSettings(nodeId, nullptr, UserPointer);
+ if (size > 0)
+ {
+ data.resize(size);
+ LoadNodeSettings(nodeId, const_cast<char*>(data.data()), UserPointer);
+ }
+ }
+
+ return data;
+}
+
+void ed::Config::BeginSave()
+{
+ if (BeginSaveSession)
+ BeginSaveSession(UserPointer);
+}
+
+bool ed::Config::Save(const std::string& data, SaveReasonFlags flags)
+{
+ if (SaveSettings)
+ {
+ return SaveSettings(data.c_str(), data.size(), flags, UserPointer);
+ }
+ else if (SettingsFile)
+ {
+ std::ofstream settingsFile(SettingsFile);
+ if (settingsFile)
+ settingsFile << data;
+
+ return !!settingsFile;
+ }
+
+ return false;
+}
+
+bool ed::Config::SaveNode(NodeId nodeId, const std::string& data, SaveReasonFlags flags)
+{
+ if (SaveNodeSettings)
+ return SaveNodeSettings(nodeId, data.c_str(), data.size(), flags, UserPointer);
+
+ return false;
+}
+
+void ed::Config::EndSave()
+{
+ if (EndSaveSession)
+ EndSaveSession(UserPointer);
+}
diff --git a/3rdparty/imgui-node-editor/imgui_node_editor.h b/3rdparty/imgui-node-editor/imgui_node_editor.h
new file mode 100644
index 0000000..02282a1
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_node_editor.h
@@ -0,0 +1,444 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# ifndef __IMGUI_NODE_EDITOR_H__
+# define __IMGUI_NODE_EDITOR_H__
+# pragma once
+
+
+//------------------------------------------------------------------------------
+# include <imgui.h>
+# include <cstdint> // std::uintXX_t
+# include <utility> // std::move
+
+
+//------------------------------------------------------------------------------
+namespace ax {
+namespace NodeEditor {
+
+
+//------------------------------------------------------------------------------
+struct NodeId;
+struct LinkId;
+struct PinId;
+
+
+//------------------------------------------------------------------------------
+enum class SaveReasonFlags: uint32_t
+{
+ None = 0x00000000,
+ Navigation = 0x00000001,
+ Position = 0x00000002,
+ Size = 0x00000004,
+ Selection = 0x00000008,
+ User = 0x00000010
+};
+
+inline SaveReasonFlags operator |(SaveReasonFlags lhs, SaveReasonFlags rhs) { return static_cast<SaveReasonFlags>(static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)); }
+inline SaveReasonFlags operator &(SaveReasonFlags lhs, SaveReasonFlags rhs) { return static_cast<SaveReasonFlags>(static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs)); }
+
+using ConfigSaveSettings = bool (*)(const char* data, size_t size, SaveReasonFlags reason, void* userPointer);
+using ConfigLoadSettings = size_t (*)(char* data, void* userPointer);
+
+using ConfigSaveNodeSettings = bool (*)(NodeId nodeId, const char* data, size_t size, SaveReasonFlags reason, void* userPointer);
+using ConfigLoadNodeSettings = size_t (*)(NodeId nodeId, char* data, void* userPointer);
+
+using ConfigSession = void (*)(void* userPointer);
+
+struct Config
+{
+ const char* SettingsFile;
+ ConfigSession BeginSaveSession;
+ ConfigSession EndSaveSession;
+ ConfigSaveSettings SaveSettings;
+ ConfigLoadSettings LoadSettings;
+ ConfigSaveNodeSettings SaveNodeSettings;
+ ConfigLoadNodeSettings LoadNodeSettings;
+ void* UserPointer;
+
+ Config()
+ : SettingsFile("NodeEditor.json")
+ , BeginSaveSession(nullptr)
+ , EndSaveSession(nullptr)
+ , SaveSettings(nullptr)
+ , LoadSettings(nullptr)
+ , SaveNodeSettings(nullptr)
+ , LoadNodeSettings(nullptr)
+ , UserPointer(nullptr)
+ {
+ }
+};
+
+
+//------------------------------------------------------------------------------
+enum class PinKind
+{
+ Input,
+ Output
+};
+
+
+//------------------------------------------------------------------------------
+enum StyleColor
+{
+ StyleColor_Bg,
+ StyleColor_Grid,
+ StyleColor_NodeBg,
+ StyleColor_NodeBorder,
+ StyleColor_HovNodeBorder,
+ StyleColor_SelNodeBorder,
+ StyleColor_NodeSelRect,
+ StyleColor_NodeSelRectBorder,
+ StyleColor_HovLinkBorder,
+ StyleColor_SelLinkBorder,
+ StyleColor_LinkSelRect,
+ StyleColor_LinkSelRectBorder,
+ StyleColor_PinRect,
+ StyleColor_PinRectBorder,
+ StyleColor_Flow,
+ StyleColor_FlowMarker,
+ StyleColor_GroupBg,
+ StyleColor_GroupBorder,
+
+ StyleColor_Count
+};
+
+enum StyleVar
+{
+ StyleVar_NodePadding,
+ StyleVar_NodeRounding,
+ StyleVar_NodeBorderWidth,
+ StyleVar_HoveredNodeBorderWidth,
+ StyleVar_SelectedNodeBorderWidth,
+ StyleVar_PinRounding,
+ StyleVar_PinBorderWidth,
+ StyleVar_LinkStrength,
+ StyleVar_SourceDirection,
+ StyleVar_TargetDirection,
+ StyleVar_ScrollDuration,
+ StyleVar_FlowMarkerDistance,
+ StyleVar_FlowSpeed,
+ StyleVar_FlowDuration,
+ StyleVar_PivotAlignment,
+ StyleVar_PivotSize,
+ StyleVar_PivotScale,
+ StyleVar_PinCorners,
+ StyleVar_PinRadius,
+ StyleVar_PinArrowSize,
+ StyleVar_PinArrowWidth,
+ StyleVar_GroupRounding,
+ StyleVar_GroupBorderWidth,
+
+ StyleVar_Count
+};
+
+struct Style
+{
+ ImVec4 NodePadding;
+ float NodeRounding;
+ float NodeBorderWidth;
+ float HoveredNodeBorderWidth;
+ float SelectedNodeBorderWidth;
+ float PinRounding;
+ float PinBorderWidth;
+ float LinkStrength;
+ ImVec2 SourceDirection;
+ ImVec2 TargetDirection;
+ float ScrollDuration;
+ float FlowMarkerDistance;
+ float FlowSpeed;
+ float FlowDuration;
+ ImVec2 PivotAlignment;
+ ImVec2 PivotSize;
+ ImVec2 PivotScale;
+ float PinCorners;
+ float PinRadius;
+ float PinArrowSize;
+ float PinArrowWidth;
+ float GroupRounding;
+ float GroupBorderWidth;
+ ImVec4 Colors[StyleColor_Count];
+
+ Style()
+ {
+ NodePadding = ImVec4(8, 8, 8, 8);
+ NodeRounding = 12.0f;
+ NodeBorderWidth = 1.5f;
+ HoveredNodeBorderWidth = 3.5f;
+ SelectedNodeBorderWidth = 3.5f;
+ PinRounding = 4.0f;
+ PinBorderWidth = 0.0f;
+ LinkStrength = 100.0f;
+ SourceDirection = ImVec2(1.0f, 0.0f);
+ TargetDirection = ImVec2(-1.0f, 0.0f);
+ ScrollDuration = 0.35f;
+ FlowMarkerDistance = 30.0f;
+ FlowSpeed = 150.0f;
+ FlowDuration = 2.0f;
+ PivotAlignment = ImVec2(0.5f, 0.5f);
+ PivotSize = ImVec2(0.0f, 0.0f);
+ PivotScale = ImVec2(1, 1);
+ PinCorners = ImDrawCornerFlags_All;
+ PinRadius = 0.0f;
+ PinArrowSize = 0.0f;
+ PinArrowWidth = 0.0f;
+ GroupRounding = 6.0f;
+ GroupBorderWidth = 1.0f;
+
+ Colors[StyleColor_Bg] = ImColor( 60, 60, 70, 200);
+ Colors[StyleColor_Grid] = ImColor(120, 120, 120, 40);
+ Colors[StyleColor_NodeBg] = ImColor( 32, 32, 32, 200);
+ Colors[StyleColor_NodeBorder] = ImColor(255, 255, 255, 96);
+ Colors[StyleColor_HovNodeBorder] = ImColor( 50, 176, 255, 255);
+ Colors[StyleColor_SelNodeBorder] = ImColor(255, 176, 50, 255);
+ Colors[StyleColor_NodeSelRect] = ImColor( 5, 130, 255, 64);
+ Colors[StyleColor_NodeSelRectBorder] = ImColor( 5, 130, 255, 128);
+ Colors[StyleColor_HovLinkBorder] = ImColor( 50, 176, 255, 255);
+ Colors[StyleColor_SelLinkBorder] = ImColor(255, 176, 50, 255);
+ Colors[StyleColor_LinkSelRect] = ImColor( 5, 130, 255, 64);
+ Colors[StyleColor_LinkSelRectBorder] = ImColor( 5, 130, 255, 128);
+ Colors[StyleColor_PinRect] = ImColor( 60, 180, 255, 100);
+ Colors[StyleColor_PinRectBorder] = ImColor( 60, 180, 255, 128);
+ Colors[StyleColor_Flow] = ImColor(255, 128, 64, 255);
+ Colors[StyleColor_FlowMarker] = ImColor(255, 128, 64, 255);
+ Colors[StyleColor_GroupBg] = ImColor( 0, 0, 0, 160);
+ Colors[StyleColor_GroupBorder] = ImColor(255, 255, 255, 32);
+ }
+};
+
+
+//------------------------------------------------------------------------------
+struct EditorContext;
+
+
+//------------------------------------------------------------------------------
+void SetCurrentEditor(EditorContext* ctx);
+EditorContext* GetCurrentEditor();
+EditorContext* CreateEditor(const Config* config = nullptr);
+void DestroyEditor(EditorContext* ctx);
+
+Style& GetStyle();
+const char* GetStyleColorName(StyleColor colorIndex);
+
+void PushStyleColor(StyleColor colorIndex, const ImVec4& color);
+void PopStyleColor(int count = 1);
+
+void PushStyleVar(StyleVar varIndex, float value);
+void PushStyleVar(StyleVar varIndex, const ImVec2& value);
+void PushStyleVar(StyleVar varIndex, const ImVec4& value);
+void PopStyleVar(int count = 1);
+
+void Begin(const char* id, const ImVec2& size = ImVec2(0, 0));
+void End();
+
+void BeginNode(NodeId id);
+void BeginPin(PinId id, PinKind kind);
+void PinRect(const ImVec2& a, const ImVec2& b);
+void PinPivotRect(const ImVec2& a, const ImVec2& b);
+void PinPivotSize(const ImVec2& size);
+void PinPivotScale(const ImVec2& scale);
+void PinPivotAlignment(const ImVec2& alignment);
+void EndPin();
+void Group(const ImVec2& size);
+void EndNode();
+
+bool BeginGroupHint(NodeId nodeId);
+ImVec2 GetGroupMin();
+ImVec2 GetGroupMax();
+ImDrawList* GetHintForegroundDrawList();
+ImDrawList* GetHintBackgroundDrawList();
+void EndGroupHint();
+
+// TODO: Add a way to manage node background channels
+ImDrawList* GetNodeBackgroundDrawList(NodeId nodeId);
+
+bool Link(LinkId id, PinId startPinId, PinId endPinId, const ImVec4& color = ImVec4(1, 1, 1, 1), float thickness = 1.0f);
+
+void Flow(LinkId linkId);
+
+bool BeginCreate(const ImVec4& color = ImVec4(1, 1, 1, 1), float thickness = 1.0f);
+bool QueryNewLink(PinId* startId, PinId* endId);
+bool QueryNewLink(PinId* startId, PinId* endId, const ImVec4& color, float thickness = 1.0f);
+bool QueryNewNode(PinId* pinId);
+bool QueryNewNode(PinId* pinId, const ImVec4& color, float thickness = 1.0f);
+bool AcceptNewItem();
+bool AcceptNewItem(const ImVec4& color, float thickness = 1.0f);
+void RejectNewItem();
+void RejectNewItem(const ImVec4& color, float thickness = 1.0f);
+void EndCreate();
+
+bool BeginDelete();
+bool QueryDeletedLink(LinkId* linkId, PinId* startId = nullptr, PinId* endId = nullptr);
+bool QueryDeletedNode(NodeId* nodeId);
+bool AcceptDeletedItem();
+void RejectDeletedItem();
+void EndDelete();
+
+void SetNodePosition(NodeId nodeId, const ImVec2& editorPosition);
+ImVec2 GetNodePosition(NodeId nodeId);
+ImVec2 GetNodeSize(NodeId nodeId);
+void CenterNodeOnScreen(NodeId nodeId);
+
+void RestoreNodeState(NodeId nodeId);
+
+void Suspend();
+void Resume();
+bool IsSuspended();
+
+bool IsActive();
+
+bool HasSelectionChanged();
+int GetSelectedObjectCount();
+int GetSelectedNodes(NodeId* nodes, int size);
+int GetSelectedLinks(LinkId* links, int size);
+void ClearSelection();
+void SelectNode(NodeId nodeId, bool append = false);
+void SelectLink(LinkId linkId, bool append = false);
+void DeselectNode(NodeId nodeId);
+void DeselectLink(LinkId linkId);
+
+bool DeleteNode(NodeId nodeId);
+bool DeleteLink(LinkId linkId);
+
+void NavigateToContent(float duration = -1);
+void NavigateToSelection(bool zoomIn = false, float duration = -1);
+
+bool ShowNodeContextMenu(NodeId* nodeId);
+bool ShowPinContextMenu(PinId* pinId);
+bool ShowLinkContextMenu(LinkId* linkId);
+bool ShowBackgroundContextMenu();
+
+void EnableShortcuts(bool enable);
+bool AreShortcutsEnabled();
+
+bool BeginShortcut();
+bool AcceptCut();
+bool AcceptCopy();
+bool AcceptPaste();
+bool AcceptDuplicate();
+bool AcceptCreateNode();
+int GetActionContextSize();
+int GetActionContextNodes(NodeId* nodes, int size);
+int GetActionContextLinks(LinkId* links, int size);
+void EndShortcut();
+
+float GetCurrentZoom();
+
+NodeId GetDoubleClickedNode();
+PinId GetDoubleClickedPin();
+LinkId GetDoubleClickedLink();
+bool IsBackgroundClicked();
+bool IsBackgroundDoubleClicked();
+
+bool PinHadAnyLinks(PinId pinId);
+
+ImVec2 GetScreenSize();
+ImVec2 ScreenToCanvas(const ImVec2& pos);
+ImVec2 CanvasToScreen(const ImVec2& pos);
+
+
+
+
+
+
+
+
+
+
+//------------------------------------------------------------------------------
+namespace Details {
+
+template <typename T, typename Tag>
+struct SafeType
+{
+ SafeType(T t)
+ : m_Value(std::move(t))
+ {
+ }
+
+ SafeType(const SafeType&) = default;
+
+ template <typename T2, typename Tag2>
+ SafeType(
+ const SafeType
+ <
+ typename std::enable_if<!std::is_same<T, T2>::value, T2>::type,
+ typename std::enable_if<!std::is_same<Tag, Tag2>::value, Tag2>::type
+ >&) = delete;
+
+ SafeType& operator=(const SafeType&) = default;
+
+ explicit operator T() const { return Get(); }
+
+ T Get() const { return m_Value; }
+
+private:
+ T m_Value;
+};
+
+
+template <typename Tag>
+struct SafePointerType
+ : SafeType<uintptr_t, Tag>
+{
+ static const Tag Invalid;
+
+ using SafeType<uintptr_t, Tag>::SafeType;
+
+ SafePointerType()
+ : SafePointerType(Invalid)
+ {
+ }
+
+ template <typename T = void> explicit SafePointerType(T* ptr): SafePointerType(reinterpret_cast<uintptr_t>(ptr)) {}
+ template <typename T = void> T* AsPointer() const { return reinterpret_cast<T*>(this->Get()); }
+
+ explicit operator bool() const { return *this != Invalid; }
+};
+
+template <typename Tag>
+const Tag SafePointerType<Tag>::Invalid = { 0 };
+
+template <typename Tag>
+inline bool operator==(const SafePointerType<Tag>& lhs, const SafePointerType<Tag>& rhs)
+{
+ return lhs.Get() == rhs.Get();
+}
+
+template <typename Tag>
+inline bool operator!=(const SafePointerType<Tag>& lhs, const SafePointerType<Tag>& rhs)
+{
+ return lhs.Get() != rhs.Get();
+}
+
+} // namespace Details
+
+struct NodeId final: Details::SafePointerType<NodeId>
+{
+ using SafePointerType::SafePointerType;
+};
+
+struct LinkId final: Details::SafePointerType<LinkId>
+{
+ using SafePointerType::SafePointerType;
+};
+
+struct PinId final: Details::SafePointerType<PinId>
+{
+ using SafePointerType::SafePointerType;
+};
+
+
+//------------------------------------------------------------------------------
+} // namespace Editor
+} // namespace ax
+
+
+//------------------------------------------------------------------------------
+# endif // __IMGUI_NODE_EDITOR_H__ \ No newline at end of file
diff --git a/3rdparty/imgui-node-editor/imgui_node_editor_api.cpp b/3rdparty/imgui-node-editor/imgui_node_editor_api.cpp
new file mode 100644
index 0000000..d468b4e
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_node_editor_api.cpp
@@ -0,0 +1,637 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# include "imgui_node_editor_internal.h"
+# include <algorithm>
+
+
+//------------------------------------------------------------------------------
+static ax::NodeEditor::Detail::EditorContext* s_Editor = nullptr;
+
+
+//------------------------------------------------------------------------------
+template <typename C, typename I, typename F>
+static int BuildIdList(C& container, I* list, int listSize, F&& accept)
+{
+ if (list != nullptr)
+ {
+ int count = 0;
+ for (auto object : container)
+ {
+ if (listSize <= 0)
+ break;
+
+ if (accept(object))
+ {
+ list[count] = I(object->ID().AsPointer());
+ ++count;
+ --listSize;
+ }
+ }
+
+ return count;
+ }
+ else
+ return static_cast<int>(std::count_if(container.begin(), container.end(), accept));
+}
+
+
+//------------------------------------------------------------------------------
+ax::NodeEditor::EditorContext* ax::NodeEditor::CreateEditor(const Config* config)
+{
+ return reinterpret_cast<ax::NodeEditor::EditorContext*>(new ax::NodeEditor::Detail::EditorContext(config));
+}
+
+void ax::NodeEditor::DestroyEditor(EditorContext* ctx)
+{
+ if (GetCurrentEditor() == ctx)
+ SetCurrentEditor(nullptr);
+
+ auto editor = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(ctx);
+
+ delete editor;
+}
+
+void ax::NodeEditor::SetCurrentEditor(EditorContext* ctx)
+{
+ s_Editor = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(ctx);
+}
+
+ax::NodeEditor::EditorContext* ax::NodeEditor::GetCurrentEditor()
+{
+ return reinterpret_cast<ax::NodeEditor::EditorContext*>(s_Editor);
+}
+
+ax::NodeEditor::Style& ax::NodeEditor::GetStyle()
+{
+ return s_Editor->GetStyle();
+}
+
+const char* ax::NodeEditor::GetStyleColorName(StyleColor colorIndex)
+{
+ return s_Editor->GetStyle().GetColorName(colorIndex);
+}
+
+void ax::NodeEditor::PushStyleColor(StyleColor colorIndex, const ImVec4& color)
+{
+ s_Editor->GetStyle().PushColor(colorIndex, color);
+}
+
+void ax::NodeEditor::PopStyleColor(int count)
+{
+ s_Editor->GetStyle().PopColor(count);
+}
+
+void ax::NodeEditor::PushStyleVar(StyleVar varIndex, float value)
+{
+ s_Editor->GetStyle().PushVar(varIndex, value);
+}
+
+void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec2& value)
+{
+ s_Editor->GetStyle().PushVar(varIndex, value);
+}
+
+void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec4& value)
+{
+ s_Editor->GetStyle().PushVar(varIndex, value);
+}
+
+void ax::NodeEditor::PopStyleVar(int count)
+{
+ s_Editor->GetStyle().PopVar(count);
+}
+
+void ax::NodeEditor::Begin(const char* id, const ImVec2& size)
+{
+ s_Editor->Begin(id, size);
+}
+
+void ax::NodeEditor::End()
+{
+ s_Editor->End();
+}
+
+void ax::NodeEditor::BeginNode(NodeId id)
+{
+ s_Editor->GetNodeBuilder().Begin(id);
+}
+
+void ax::NodeEditor::BeginPin(PinId id, PinKind kind)
+{
+ s_Editor->GetNodeBuilder().BeginPin(id, kind);
+}
+
+void ax::NodeEditor::PinRect(const ImVec2& a, const ImVec2& b)
+{
+ s_Editor->GetNodeBuilder().PinRect(a, b);
+}
+
+void ax::NodeEditor::PinPivotRect(const ImVec2& a, const ImVec2& b)
+{
+ s_Editor->GetNodeBuilder().PinPivotRect(a, b);
+}
+
+void ax::NodeEditor::PinPivotSize(const ImVec2& size)
+{
+ s_Editor->GetNodeBuilder().PinPivotSize(size);
+}
+
+void ax::NodeEditor::PinPivotScale(const ImVec2& scale)
+{
+ s_Editor->GetNodeBuilder().PinPivotScale(scale);
+}
+
+void ax::NodeEditor::PinPivotAlignment(const ImVec2& alignment)
+{
+ s_Editor->GetNodeBuilder().PinPivotAlignment(alignment);
+}
+
+void ax::NodeEditor::EndPin()
+{
+ s_Editor->GetNodeBuilder().EndPin();
+}
+
+void ax::NodeEditor::Group(const ImVec2& size)
+{
+ s_Editor->GetNodeBuilder().Group(size);
+}
+
+void ax::NodeEditor::EndNode()
+{
+ s_Editor->GetNodeBuilder().End();
+}
+
+bool ax::NodeEditor::BeginGroupHint(NodeId nodeId)
+{
+ return s_Editor->GetHintBuilder().Begin(nodeId);
+}
+
+ImVec2 ax::NodeEditor::GetGroupMin()
+{
+ return s_Editor->GetHintBuilder().GetGroupMin();
+}
+
+ImVec2 ax::NodeEditor::GetGroupMax()
+{
+ return s_Editor->GetHintBuilder().GetGroupMax();
+}
+
+ImDrawList* ax::NodeEditor::GetHintForegroundDrawList()
+{
+ return s_Editor->GetHintBuilder().GetForegroundDrawList();
+}
+
+ImDrawList* ax::NodeEditor::GetHintBackgroundDrawList()
+{
+ return s_Editor->GetHintBuilder().GetBackgroundDrawList();
+}
+
+void ax::NodeEditor::EndGroupHint()
+{
+ s_Editor->GetHintBuilder().End();
+}
+
+ImDrawList* ax::NodeEditor::GetNodeBackgroundDrawList(NodeId nodeId)
+{
+ if (auto node = s_Editor->FindNode(nodeId))
+ return s_Editor->GetNodeBuilder().GetUserBackgroundDrawList(node);
+ else
+ return nullptr;
+}
+
+bool ax::NodeEditor::Link(LinkId id, PinId startPinId, PinId endPinId, const ImVec4& color/* = ImVec4(1, 1, 1, 1)*/, float thickness/* = 1.0f*/)
+{
+ return s_Editor->DoLink(id, startPinId, endPinId, ImColor(color), thickness);
+}
+
+void ax::NodeEditor::Flow(LinkId linkId)
+{
+ if (auto link = s_Editor->FindLink(linkId))
+ s_Editor->Flow(link);
+}
+
+bool ax::NodeEditor::BeginCreate(const ImVec4& color, float thickness)
+{
+ auto& context = s_Editor->GetItemCreator();
+
+ if (context.Begin())
+ {
+ context.SetStyle(ImColor(color), thickness);
+ return true;
+ }
+ else
+ return false;
+}
+
+bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId)
+{
+ using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
+
+ auto& context = s_Editor->GetItemCreator();
+
+ return context.QueryLink(startId, endId) == Result::True;
+}
+
+bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId, const ImVec4& color, float thickness)
+{
+ using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
+
+ auto& context = s_Editor->GetItemCreator();
+
+ auto result = context.QueryLink(startId, endId);
+ if (result != Result::Indeterminate)
+ context.SetStyle(ImColor(color), thickness);
+
+ return result == Result::True;
+}
+
+bool ax::NodeEditor::QueryNewNode(PinId* pinId)
+{
+ using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
+
+ auto& context = s_Editor->GetItemCreator();
+
+ return context.QueryNode(pinId) == Result::True;
+}
+
+bool ax::NodeEditor::QueryNewNode(PinId* pinId, const ImVec4& color, float thickness)
+{
+ using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
+
+ auto& context = s_Editor->GetItemCreator();
+
+ auto result = context.QueryNode(pinId);
+ if (result != Result::Indeterminate)
+ context.SetStyle(ImColor(color), thickness);
+
+ return result == Result::True;
+}
+
+bool ax::NodeEditor::AcceptNewItem()
+{
+ using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
+
+ auto& context = s_Editor->GetItemCreator();
+
+ return context.AcceptItem() == Result::True;
+}
+
+bool ax::NodeEditor::AcceptNewItem(const ImVec4& color, float thickness)
+{
+ using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
+
+ auto& context = s_Editor->GetItemCreator();
+
+ auto result = context.AcceptItem();
+ if (result != Result::Indeterminate)
+ context.SetStyle(ImColor(color), thickness);
+
+ return result == Result::True;
+}
+
+void ax::NodeEditor::RejectNewItem()
+{
+ auto& context = s_Editor->GetItemCreator();
+
+ context.RejectItem();
+}
+
+void ax::NodeEditor::RejectNewItem(const ImVec4& color, float thickness)
+{
+ using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
+
+ auto& context = s_Editor->GetItemCreator();
+
+ if (context.RejectItem() != Result::Indeterminate)
+ context.SetStyle(ImColor(color), thickness);
+}
+
+void ax::NodeEditor::EndCreate()
+{
+ auto& context = s_Editor->GetItemCreator();
+
+ context.End();
+}
+
+bool ax::NodeEditor::BeginDelete()
+{
+ auto& context = s_Editor->GetItemDeleter();
+
+ return context.Begin();
+}
+
+bool ax::NodeEditor::QueryDeletedLink(LinkId* linkId, PinId* startId, PinId* endId)
+{
+ auto& context = s_Editor->GetItemDeleter();
+
+ return context.QueryLink(linkId, startId, endId);
+}
+
+bool ax::NodeEditor::QueryDeletedNode(NodeId* nodeId)
+{
+ auto& context = s_Editor->GetItemDeleter();
+
+ return context.QueryNode(nodeId);
+}
+
+bool ax::NodeEditor::AcceptDeletedItem()
+{
+ auto& context = s_Editor->GetItemDeleter();
+
+ return context.AcceptItem();
+}
+
+void ax::NodeEditor::RejectDeletedItem()
+{
+ auto& context = s_Editor->GetItemDeleter();
+
+ context.RejectItem();
+}
+
+void ax::NodeEditor::EndDelete()
+{
+ auto& context = s_Editor->GetItemDeleter();
+
+ context.End();
+}
+
+void ax::NodeEditor::SetNodePosition(NodeId nodeId, const ImVec2& position)
+{
+ s_Editor->SetNodePosition(nodeId, position);
+}
+
+ImVec2 ax::NodeEditor::GetNodePosition(NodeId nodeId)
+{
+ return s_Editor->GetNodePosition(nodeId);
+}
+
+ImVec2 ax::NodeEditor::GetNodeSize(NodeId nodeId)
+{
+ return s_Editor->GetNodeSize(nodeId);
+}
+
+void ax::NodeEditor::CenterNodeOnScreen(NodeId nodeId)
+{
+ if (auto node = s_Editor->FindNode(nodeId))
+ node->CenterOnScreenInNextFrame();
+}
+
+void ax::NodeEditor::RestoreNodeState(NodeId nodeId)
+{
+ if (auto node = s_Editor->FindNode(nodeId))
+ s_Editor->MarkNodeToRestoreState(node);
+}
+
+void ax::NodeEditor::Suspend()
+{
+ s_Editor->Suspend();
+}
+
+void ax::NodeEditor::Resume()
+{
+ s_Editor->Resume();
+}
+
+bool ax::NodeEditor::IsSuspended()
+{
+ return s_Editor->IsSuspended();
+}
+
+bool ax::NodeEditor::IsActive()
+{
+ return s_Editor->IsActive();
+}
+
+bool ax::NodeEditor::HasSelectionChanged()
+{
+ return s_Editor->HasSelectionChanged();
+}
+
+int ax::NodeEditor::GetSelectedObjectCount()
+{
+ return (int)s_Editor->GetSelectedObjects().size();
+}
+
+int ax::NodeEditor::GetSelectedNodes(NodeId* nodes, int size)
+{
+ return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, [](auto object)
+ {
+ return object->AsNode() != nullptr;
+ });
+}
+
+int ax::NodeEditor::GetSelectedLinks(LinkId* links, int size)
+{
+ return BuildIdList(s_Editor->GetSelectedObjects(), links, size, [](auto object)
+ {
+ return object->AsLink() != nullptr;
+ });
+}
+
+void ax::NodeEditor::ClearSelection()
+{
+ s_Editor->ClearSelection();
+}
+
+void ax::NodeEditor::SelectNode(NodeId nodeId, bool append)
+{
+ if (auto node = s_Editor->FindNode(nodeId))
+ {
+ if (append)
+ s_Editor->SelectObject(node);
+ else
+ s_Editor->SetSelectedObject(node);
+ }
+}
+
+void ax::NodeEditor::SelectLink(LinkId linkId, bool append)
+{
+ if (auto link = s_Editor->FindLink(linkId))
+ {
+ if (append)
+ s_Editor->SelectObject(link);
+ else
+ s_Editor->SetSelectedObject(link);
+ }
+}
+
+void ax::NodeEditor::DeselectNode(NodeId nodeId)
+{
+ if (auto node = s_Editor->FindNode(nodeId))
+ s_Editor->DeselectObject(node);
+}
+
+void ax::NodeEditor::DeselectLink(LinkId linkId)
+{
+ if (auto link = s_Editor->FindLink(linkId))
+ s_Editor->DeselectObject(link);
+}
+
+bool ax::NodeEditor::DeleteNode(NodeId nodeId)
+{
+ if (auto node = s_Editor->FindNode(nodeId))
+ return s_Editor->GetItemDeleter().Add(node);
+ else
+ return false;
+}
+
+bool ax::NodeEditor::DeleteLink(LinkId linkId)
+{
+ if (auto link = s_Editor->FindLink(linkId))
+ return s_Editor->GetItemDeleter().Add(link);
+ else
+ return false;
+}
+
+void ax::NodeEditor::NavigateToContent(float duration)
+{
+ s_Editor->NavigateTo(s_Editor->GetContentBounds(), true, duration);
+}
+
+void ax::NodeEditor::NavigateToSelection(bool zoomIn, float duration)
+{
+ s_Editor->NavigateTo(s_Editor->GetSelectionBounds(), zoomIn, duration);
+}
+
+bool ax::NodeEditor::ShowNodeContextMenu(NodeId* nodeId)
+{
+ return s_Editor->GetContextMenu().ShowNodeContextMenu(nodeId);
+}
+
+bool ax::NodeEditor::ShowPinContextMenu(PinId* pinId)
+{
+ return s_Editor->GetContextMenu().ShowPinContextMenu(pinId);
+}
+
+bool ax::NodeEditor::ShowLinkContextMenu(LinkId* linkId)
+{
+ return s_Editor->GetContextMenu().ShowLinkContextMenu(linkId);
+}
+
+bool ax::NodeEditor::ShowBackgroundContextMenu()
+{
+ return s_Editor->GetContextMenu().ShowBackgroundContextMenu();
+}
+
+void ax::NodeEditor::EnableShortcuts(bool enable)
+{
+ s_Editor->EnableShortcuts(enable);
+}
+
+bool ax::NodeEditor::AreShortcutsEnabled()
+{
+ return s_Editor->AreShortcutsEnabled();
+}
+
+bool ax::NodeEditor::BeginShortcut()
+{
+ return s_Editor->GetShortcut().Begin();
+}
+
+bool ax::NodeEditor::AcceptCut()
+{
+ return s_Editor->GetShortcut().AcceptCut();
+}
+
+bool ax::NodeEditor::AcceptCopy()
+{
+ return s_Editor->GetShortcut().AcceptCopy();
+}
+
+bool ax::NodeEditor::AcceptPaste()
+{
+ return s_Editor->GetShortcut().AcceptPaste();
+}
+
+bool ax::NodeEditor::AcceptDuplicate()
+{
+ return s_Editor->GetShortcut().AcceptDuplicate();
+}
+
+bool ax::NodeEditor::AcceptCreateNode()
+{
+ return s_Editor->GetShortcut().AcceptCreateNode();
+}
+
+int ax::NodeEditor::GetActionContextSize()
+{
+ return static_cast<int>(s_Editor->GetShortcut().m_Context.size());
+}
+
+int ax::NodeEditor::GetActionContextNodes(NodeId* nodes, int size)
+{
+ return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, [](auto object)
+ {
+ return object->AsNode() != nullptr;
+ });
+}
+
+int ax::NodeEditor::GetActionContextLinks(LinkId* links, int size)
+{
+ return BuildIdList(s_Editor->GetSelectedObjects(), links, size, [](auto object)
+ {
+ return object->AsLink() != nullptr;
+ });
+}
+
+void ax::NodeEditor::EndShortcut()
+{
+ return s_Editor->GetShortcut().End();
+}
+
+float ax::NodeEditor::GetCurrentZoom()
+{
+ return s_Editor->GetView().InvScale;
+}
+
+ax::NodeEditor::NodeId ax::NodeEditor::GetDoubleClickedNode()
+{
+ return s_Editor->GetDoubleClickedNode();
+}
+
+ax::NodeEditor::PinId ax::NodeEditor::GetDoubleClickedPin()
+{
+ return s_Editor->GetDoubleClickedPin();
+}
+
+ax::NodeEditor::LinkId ax::NodeEditor::GetDoubleClickedLink()
+{
+ return s_Editor->GetDoubleClickedLink();
+}
+
+bool ax::NodeEditor::IsBackgroundClicked()
+{
+ return s_Editor->IsBackgroundClicked();
+}
+
+bool ax::NodeEditor::IsBackgroundDoubleClicked()
+{
+ return s_Editor->IsBackgroundDoubleClicked();
+}
+
+bool ax::NodeEditor::PinHadAnyLinks(PinId pinId)
+{
+ return s_Editor->PinHadAnyLinks(pinId);
+}
+
+ImVec2 ax::NodeEditor::GetScreenSize()
+{
+ return s_Editor->GetRect().GetSize();
+}
+
+ImVec2 ax::NodeEditor::ScreenToCanvas(const ImVec2& pos)
+{
+ return s_Editor->ToCanvas(pos);
+}
+
+ImVec2 ax::NodeEditor::CanvasToScreen(const ImVec2& pos)
+{
+ return s_Editor->ToScreen(pos);
+}
diff --git a/3rdparty/imgui-node-editor/imgui_node_editor_internal.h b/3rdparty/imgui-node-editor/imgui_node_editor_internal.h
new file mode 100644
index 0000000..cef1bd5
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_node_editor_internal.h
@@ -0,0 +1,1474 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# ifndef __IMGUI_NODE_EDITOR_INTERNAL_H__
+# define __IMGUI_NODE_EDITOR_INTERNAL_H__
+# pragma once
+
+
+//------------------------------------------------------------------------------
+# include "imgui_node_editor.h"
+
+
+//------------------------------------------------------------------------------
+# include <imgui.h>
+# define IMGUI_DEFINE_MATH_OPERATORS
+# include <imgui_internal.h>
+# include "imgui_extra_math.h"
+# include "imgui_bezier_math.h"
+# include "imgui_canvas.h"
+
+# include "crude_json.h"
+
+# include <vector>
+# include <string>
+
+
+//------------------------------------------------------------------------------
+namespace ax {
+namespace NodeEditor {
+namespace Detail {
+
+
+//------------------------------------------------------------------------------
+namespace ed = ax::NodeEditor::Detail;
+namespace json = crude_json;
+
+
+//------------------------------------------------------------------------------
+using std::vector;
+using std::string;
+
+
+//------------------------------------------------------------------------------
+void Log(const char* fmt, ...);
+
+
+//------------------------------------------------------------------------------
+//inline ImRect ToRect(const ax::rectf& rect);
+//inline ImRect ToRect(const ax::rect& rect);
+inline ImRect ImGui_GetItemRect();
+
+
+//------------------------------------------------------------------------------
+// https://stackoverflow.com/a/36079786
+# define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \
+ \
+ template <typename __boost_has_member_T__> \
+ class __trait_name__ \
+ { \
+ using check_type = ::std::remove_const_t<__boost_has_member_T__>; \
+ struct no_type {char x[2];}; \
+ using yes_type = char; \
+ \
+ struct base { void __member_name__() {}}; \
+ struct mixin : public base, public check_type {}; \
+ \
+ template <void (base::*)()> struct aux {}; \
+ \
+ template <typename U> static no_type test(aux<&U::__member_name__>*); \
+ template <typename U> static yes_type test(...); \
+ \
+ public: \
+ \
+ static constexpr bool value = (sizeof(yes_type) == sizeof(test<mixin>(0))); \
+ }
+
+DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale);
+
+# undef DECLARE_HAS_MEMBER
+
+struct FringeScaleRef
+{
+ // Overload is present when ImDrawList does have _FringeScale member variable.
+ template <typename T>
+ static float& Get(typename std::enable_if<HasFringeScale<T>::value, T>::type* drawList)
+ {
+ return drawList->_FringeScale;
+ }
+
+ // Overload is present when ImDrawList does not have _FringeScale member variable.
+ template <typename T>
+ static float& Get(typename std::enable_if<!HasFringeScale<T>::value, T>::type*)
+ {
+ static float placeholder = 1.0f;
+ return placeholder;
+ }
+};
+
+static inline float& ImFringeScaleRef(ImDrawList* drawList)
+{
+ return FringeScaleRef::Get<ImDrawList>(drawList);
+}
+
+struct FringeScaleScope
+{
+
+ FringeScaleScope(float scale)
+ : m_LastFringeScale(ImFringeScaleRef(ImGui::GetWindowDrawList()))
+ {
+ ImFringeScaleRef(ImGui::GetWindowDrawList()) = scale;
+ }
+
+ ~FringeScaleScope()
+ {
+ ImFringeScaleRef(ImGui::GetWindowDrawList()) = m_LastFringeScale;
+ }
+
+private:
+ float m_LastFringeScale;
+};
+
+
+//------------------------------------------------------------------------------
+enum class ObjectType
+{
+ None,
+ Node,
+ Link,
+ Pin
+};
+
+using ax::NodeEditor::PinKind;
+using ax::NodeEditor::StyleColor;
+using ax::NodeEditor::StyleVar;
+using ax::NodeEditor::SaveReasonFlags;
+
+using ax::NodeEditor::NodeId;
+using ax::NodeEditor::PinId;
+using ax::NodeEditor::LinkId;
+
+struct ObjectId final: Details::SafePointerType<ObjectId>
+{
+ using Super = Details::SafePointerType<ObjectId>;
+ using Super::Super;
+
+ ObjectId(): Super(Invalid), m_Type(ObjectType::None) {}
+ ObjectId(PinId pinId): Super(pinId.AsPointer()), m_Type(ObjectType::Pin) {}
+ ObjectId(NodeId nodeId): Super(nodeId.AsPointer()), m_Type(ObjectType::Node) {}
+ ObjectId(LinkId linkId): Super(linkId.AsPointer()), m_Type(ObjectType::Link) {}
+
+ explicit operator PinId() const { return AsPinId(); }
+ explicit operator NodeId() const { return AsNodeId(); }
+ explicit operator LinkId() const { return AsLinkId(); }
+
+ PinId AsPinId() const { IM_ASSERT(IsPinId()); return PinId(AsPointer()); }
+ NodeId AsNodeId() const { IM_ASSERT(IsNodeId()); return NodeId(AsPointer()); }
+ LinkId AsLinkId() const { IM_ASSERT(IsLinkId()); return LinkId(AsPointer()); }
+
+ bool IsPinId() const { return m_Type == ObjectType::Pin; }
+ bool IsNodeId() const { return m_Type == ObjectType::Node; }
+ bool IsLinkId() const { return m_Type == ObjectType::Link; }
+
+ ObjectType Type() const { return m_Type; }
+
+private:
+ ObjectType m_Type;
+};
+
+struct EditorContext;
+
+struct Node;
+struct Pin;
+struct Link;
+
+template <typename T, typename Id = typename T::IdType>
+struct ObjectWrapper
+{
+ Id m_ID;
+ T* m_Object;
+
+ T* operator->() { return m_Object; }
+ const T* operator->() const { return m_Object; }
+
+ operator T*() { return m_Object; }
+ operator const T*() const { return m_Object; }
+
+ bool operator<(const ObjectWrapper& rhs) const
+ {
+ return m_ID.AsPointer() < rhs.m_ID.AsPointer();
+ }
+};
+
+struct Object
+{
+ enum DrawFlags
+ {
+ None = 0,
+ Hovered = 1,
+ Selected = 2
+ };
+
+ inline friend DrawFlags operator|(DrawFlags lhs, DrawFlags rhs) { return static_cast<DrawFlags>(static_cast<int>(lhs) | static_cast<int>(rhs)); }
+ inline friend DrawFlags operator&(DrawFlags lhs, DrawFlags rhs) { return static_cast<DrawFlags>(static_cast<int>(lhs) & static_cast<int>(rhs)); }
+ inline friend DrawFlags& operator|=(DrawFlags& lhs, DrawFlags rhs) { lhs = lhs | rhs; return lhs; }
+ inline friend DrawFlags& operator&=(DrawFlags& lhs, DrawFlags rhs) { lhs = lhs & rhs; return lhs; }
+
+ EditorContext* const Editor;
+
+ bool m_IsLive;
+
+ Object(EditorContext* editor)
+ : Editor(editor)
+ , m_IsLive(true)
+ {
+ }
+
+ virtual ~Object() = default;
+
+ virtual ObjectId ID() = 0;
+
+ bool IsVisible() const
+ {
+ if (!m_IsLive)
+ return false;
+
+ const auto bounds = GetBounds();
+
+ return ImGui::IsRectVisible(bounds.Min, bounds.Max);
+ }
+
+ virtual void Reset() { m_IsLive = false; }
+
+ virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) = 0;
+
+ virtual bool AcceptDrag() { return false; }
+ virtual void UpdateDrag(const ImVec2& offset) { IM_UNUSED(offset); }
+ virtual bool EndDrag() { return false; }
+ virtual ImVec2 DragStartLocation() { return GetBounds().Min; }
+
+ virtual bool IsDraggable() { bool result = AcceptDrag(); EndDrag(); return result; }
+ virtual bool IsSelectable() { return false; }
+
+ virtual bool TestHit(const ImVec2& point, float extraThickness = 0.0f) const
+ {
+ if (!m_IsLive)
+ return false;
+
+ auto bounds = GetBounds();
+ if (extraThickness > 0)
+ bounds.Expand(extraThickness);
+
+ return bounds.Contains(point);
+ }
+
+ virtual bool TestHit(const ImRect& rect, bool allowIntersect = true) const
+ {
+ if (!m_IsLive)
+ return false;
+
+ const auto bounds = GetBounds();
+
+ return !ImRect_IsEmpty(bounds) && (allowIntersect ? bounds.Overlaps(rect) : rect.Contains(bounds));
+ }
+
+ virtual ImRect GetBounds() const = 0;
+
+ virtual Node* AsNode() { return nullptr; }
+ virtual Pin* AsPin() { return nullptr; }
+ virtual Link* AsLink() { return nullptr; }
+};
+
+struct Pin final: Object
+{
+ using IdType = PinId;
+
+ PinId m_ID;
+ PinKind m_Kind;
+ Node* m_Node;
+ ImRect m_Bounds;
+ ImRect m_Pivot;
+ Pin* m_PreviousPin;
+ ImU32 m_Color;
+ ImU32 m_BorderColor;
+ float m_BorderWidth;
+ float m_Rounding;
+ int m_Corners;
+ ImVec2 m_Dir;
+ float m_Strength;
+ float m_Radius;
+ float m_ArrowSize;
+ float m_ArrowWidth;
+ bool m_HasConnection;
+ bool m_HadConnection;
+
+ Pin(EditorContext* editor, PinId id, PinKind kind)
+ : Object(editor)
+ , m_ID(id)
+ , m_Kind(kind)
+ , m_Node(nullptr)
+ , m_Bounds()
+ , m_PreviousPin(nullptr)
+ , m_Color(IM_COL32_WHITE)
+ , m_BorderColor(IM_COL32_BLACK)
+ , m_BorderWidth(0)
+ , m_Rounding(0)
+ , m_Corners(0)
+ , m_Dir(0, 0)
+ , m_Strength(0)
+ , m_Radius(0)
+ , m_ArrowSize(0)
+ , m_ArrowWidth(0)
+ , m_HasConnection(false)
+ , m_HadConnection(false)
+ {
+ }
+
+ virtual ObjectId ID() override { return m_ID; }
+
+ virtual void Reset() override final
+ {
+ m_HadConnection = m_HasConnection && m_IsLive;
+ m_HasConnection = false;
+
+ Object::Reset();
+ }
+
+ virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) override final;
+
+ ImVec2 GetClosestPoint(const ImVec2& p) const;
+ ImLine GetClosestLine(const Pin* pin) const;
+
+ virtual ImRect GetBounds() const override final { return m_Bounds; }
+
+ virtual Pin* AsPin() override final { return this; }
+};
+
+enum class NodeType
+{
+ Node,
+ Group
+};
+
+enum class NodeRegion : uint8_t
+{
+ None = 0x00,
+ Top = 0x01,
+ Bottom = 0x02,
+ Left = 0x04,
+ Right = 0x08,
+ Center = 0x10,
+ Header = 0x20,
+ TopLeft = Top | Left,
+ TopRight = Top | Right,
+ BottomLeft = Bottom | Left,
+ BottomRight = Bottom | Right,
+};
+
+inline NodeRegion operator |(NodeRegion lhs, NodeRegion rhs) { return static_cast<NodeRegion>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs)); }
+inline NodeRegion operator &(NodeRegion lhs, NodeRegion rhs) { return static_cast<NodeRegion>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs)); }
+
+
+struct Node final: Object
+{
+ using IdType = NodeId;
+
+ NodeId m_ID;
+ NodeType m_Type;
+ ImRect m_Bounds;
+ int m_Channel;
+ Pin* m_LastPin;
+ ImVec2 m_DragStart;
+
+ ImU32 m_Color;
+ ImU32 m_BorderColor;
+ float m_BorderWidth;
+ float m_Rounding;
+
+ ImU32 m_GroupColor;
+ ImU32 m_GroupBorderColor;
+ float m_GroupBorderWidth;
+ float m_GroupRounding;
+ ImRect m_GroupBounds;
+
+ bool m_RestoreState;
+ bool m_CenterOnScreen;
+
+ Node(EditorContext* editor, NodeId id)
+ : Object(editor)
+ , m_ID(id)
+ , m_Type(NodeType::Node)
+ , m_Bounds()
+ , m_Channel(0)
+ , m_LastPin(nullptr)
+ , m_DragStart()
+ , m_Color(IM_COL32_WHITE)
+ , m_BorderColor(IM_COL32_BLACK)
+ , m_BorderWidth(0)
+ , m_Rounding(0)
+ , m_GroupBounds()
+ , m_RestoreState(false)
+ , m_CenterOnScreen(false)
+ {
+ }
+
+ virtual ObjectId ID() override { return m_ID; }
+
+ bool AcceptDrag() override;
+ void UpdateDrag(const ImVec2& offset) override;
+ bool EndDrag() override; // return true, when changed
+ ImVec2 DragStartLocation() override { return m_DragStart; }
+
+ virtual bool IsSelectable() override { return true; }
+
+ virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) override final;
+ void DrawBorder(ImDrawList* drawList, ImU32 color, float thickness = 1.0f);
+
+ void GetGroupedNodes(std::vector<Node*>& result, bool append = false);
+
+ void CenterOnScreenInNextFrame() { m_CenterOnScreen = true; }
+
+ ImRect GetRegionBounds(NodeRegion region) const;
+ NodeRegion GetRegion(const ImVec2& point) const;
+
+ virtual ImRect GetBounds() const override final { return m_Bounds; }
+
+ virtual Node* AsNode() override final { return this; }
+};
+
+struct Link final: Object
+{
+ using IdType = LinkId;
+
+ LinkId m_ID;
+ Pin* m_StartPin;
+ Pin* m_EndPin;
+ ImU32 m_Color;
+ float m_Thickness;
+ ImVec2 m_Start;
+ ImVec2 m_End;
+
+ Link(EditorContext* editor, LinkId id)
+ : Object(editor)
+ , m_ID(id)
+ , m_StartPin(nullptr)
+ , m_EndPin(nullptr)
+ , m_Color(IM_COL32_WHITE)
+ , m_Thickness(1.0f)
+ {
+ }
+
+ virtual ObjectId ID() override { return m_ID; }
+
+ virtual bool IsSelectable() override { return true; }
+
+ virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) override final;
+ void Draw(ImDrawList* drawList, ImU32 color, float extraThickness = 0.0f) const;
+
+ void UpdateEndpoints();
+
+ ImCubicBezierPoints GetCurve() const;
+
+ virtual bool TestHit(const ImVec2& point, float extraThickness = 0.0f) const override final;
+ virtual bool TestHit(const ImRect& rect, bool allowIntersect = true) const override final;
+
+ virtual ImRect GetBounds() const override final;
+
+ virtual Link* AsLink() override final { return this; }
+};
+
+struct NodeSettings
+{
+ NodeId m_ID;
+ ImVec2 m_Location;
+ ImVec2 m_Size;
+ ImVec2 m_GroupSize;
+ bool m_WasUsed;
+
+ bool m_Saved;
+ bool m_IsDirty;
+ SaveReasonFlags m_DirtyReason;
+
+ NodeSettings(NodeId id)
+ : m_ID(id)
+ , m_Location(0, 0)
+ , m_Size(0, 0)
+ , m_GroupSize(0, 0)
+ , m_WasUsed(false)
+ , m_Saved(false)
+ , m_IsDirty(false)
+ , m_DirtyReason(SaveReasonFlags::None)
+ {
+ }
+
+ void ClearDirty();
+ void MakeDirty(SaveReasonFlags reason);
+
+ json::value Serialize();
+
+ static bool Parse(const std::string& string, NodeSettings& settings);
+ static bool Parse(const json::value& data, NodeSettings& result);
+};
+
+struct Settings
+{
+ bool m_IsDirty;
+ SaveReasonFlags m_DirtyReason;
+
+ vector<NodeSettings> m_Nodes;
+ vector<ObjectId> m_Selection;
+ ImVec2 m_ViewScroll;
+ float m_ViewZoom;
+
+ Settings()
+ : m_IsDirty(false)
+ , m_DirtyReason(SaveReasonFlags::None)
+ , m_ViewScroll(0, 0)
+ , m_ViewZoom(1.0f)
+ {
+ }
+
+ NodeSettings* AddNode(NodeId id);
+ NodeSettings* FindNode(NodeId id);
+
+ void ClearDirty(Node* node = nullptr);
+ void MakeDirty(SaveReasonFlags reason, Node* node = nullptr);
+
+ std::string Serialize();
+
+ static bool Parse(const std::string& string, Settings& settings);
+};
+
+struct Control
+{
+ Object* HotObject;
+ Object* ActiveObject;
+ Object* ClickedObject;
+ Object* DoubleClickedObject;
+ Node* HotNode;
+ Node* ActiveNode;
+ Node* ClickedNode;
+ Node* DoubleClickedNode;
+ Pin* HotPin;
+ Pin* ActivePin;
+ Pin* ClickedPin;
+ Pin* DoubleClickedPin;
+ Link* HotLink;
+ Link* ActiveLink;
+ Link* ClickedLink;
+ Link* DoubleClickedLink;
+ bool BackgroundHot;
+ bool BackgroundActive;
+ bool BackgroundClicked;
+ bool BackgroundDoubleClicked;
+
+ Control(Object* hotObject, Object* activeObject, Object* clickedObject, Object* doubleClickedObject,
+ bool backgroundHot, bool backgroundActive, bool backgroundClicked, bool backgroundDoubleClicked)
+ : HotObject(hotObject)
+ , ActiveObject(activeObject)
+ , ClickedObject(clickedObject)
+ , DoubleClickedObject(doubleClickedObject)
+ , HotNode(nullptr)
+ , ActiveNode(nullptr)
+ , ClickedNode(nullptr)
+ , DoubleClickedNode(nullptr)
+ , HotPin(nullptr)
+ , ActivePin(nullptr)
+ , ClickedPin(nullptr)
+ , DoubleClickedPin(nullptr)
+ , HotLink(nullptr)
+ , ActiveLink(nullptr)
+ , ClickedLink(nullptr)
+ , DoubleClickedLink(nullptr)
+ , BackgroundHot(backgroundHot)
+ , BackgroundActive(backgroundActive)
+ , BackgroundClicked(backgroundClicked)
+ , BackgroundDoubleClicked(backgroundDoubleClicked)
+ {
+ if (hotObject)
+ {
+ HotNode = hotObject->AsNode();
+ HotPin = hotObject->AsPin();
+ HotLink = hotObject->AsLink();
+
+ if (HotPin)
+ HotNode = HotPin->m_Node;
+ }
+
+ if (activeObject)
+ {
+ ActiveNode = activeObject->AsNode();
+ ActivePin = activeObject->AsPin();
+ ActiveLink = activeObject->AsLink();
+ }
+
+ if (clickedObject)
+ {
+ ClickedNode = clickedObject->AsNode();
+ ClickedPin = clickedObject->AsPin();
+ ClickedLink = clickedObject->AsLink();
+ }
+
+ if (doubleClickedObject)
+ {
+ DoubleClickedNode = doubleClickedObject->AsNode();
+ DoubleClickedPin = doubleClickedObject->AsPin();
+ DoubleClickedLink = doubleClickedObject->AsLink();
+ }
+ }
+};
+
+struct NavigateAction;
+struct SizeAction;
+struct DragAction;
+struct SelectAction;
+struct CreateItemAction;
+struct DeleteItemsAction;
+struct ContextMenuAction;
+struct ShortcutAction;
+
+struct AnimationController;
+struct FlowAnimationController;
+
+struct Animation
+{
+ enum State
+ {
+ Playing,
+ Stopped
+ };
+
+ EditorContext* Editor;
+ State m_State;
+ float m_Time;
+ float m_Duration;
+
+ Animation(EditorContext* editor);
+ virtual ~Animation();
+
+ void Play(float duration);
+ void Stop();
+ void Finish();
+ void Update();
+
+ bool IsPlaying() const { return m_State == Playing; }
+
+ float GetProgress() const { return m_Time / m_Duration; }
+
+protected:
+ virtual void OnPlay() {}
+ virtual void OnFinish() {}
+ virtual void OnStop() {}
+
+ virtual void OnUpdate(float progress) { IM_UNUSED(progress); }
+};
+
+struct NavigateAnimation final: Animation
+{
+ NavigateAction& Action;
+ ImRect m_Start;
+ ImRect m_Target;
+
+ NavigateAnimation(EditorContext* editor, NavigateAction& scrollAction);
+
+ void NavigateTo(const ImRect& target, float duration);
+
+private:
+ void OnUpdate(float progress) override final;
+ void OnStop() override final;
+ void OnFinish() override final;
+};
+
+struct FlowAnimation final: Animation
+{
+ FlowAnimationController* Controller;
+ Link* m_Link;
+ float m_Speed;
+ float m_MarkerDistance;
+ float m_Offset;
+
+ FlowAnimation(FlowAnimationController* controller);
+
+ void Flow(Link* link, float markerDistance, float speed, float duration);
+
+ void Draw(ImDrawList* drawList);
+
+private:
+ struct CurvePoint
+ {
+ float Distance;
+ ImVec2 Point;
+ };
+
+ ImVec2 m_LastStart;
+ ImVec2 m_LastEnd;
+ float m_PathLength;
+ vector<CurvePoint> m_Path;
+
+ bool IsLinkValid() const;
+ bool IsPathValid() const;
+ void UpdatePath();
+ void ClearPath();
+
+ ImVec2 SamplePath(float distance);
+
+ void OnUpdate(float progress) override final;
+ void OnStop() override final;
+};
+
+struct AnimationController
+{
+ EditorContext* Editor;
+
+ AnimationController(EditorContext* editor)
+ : Editor(editor)
+ {
+ }
+
+ virtual ~AnimationController()
+ {
+ }
+
+ virtual void Draw(ImDrawList* drawList)
+ {
+ IM_UNUSED(drawList);
+ }
+};
+
+struct FlowAnimationController final : AnimationController
+{
+ FlowAnimationController(EditorContext* editor);
+ virtual ~FlowAnimationController();
+
+ void Flow(Link* link);
+
+ virtual void Draw(ImDrawList* drawList) override final;
+
+ void Release(FlowAnimation* animation);
+
+private:
+ FlowAnimation* GetOrCreate(Link* link);
+
+ vector<FlowAnimation*> m_Animations;
+ vector<FlowAnimation*> m_FreePool;
+};
+
+struct EditorAction
+{
+ enum AcceptResult { False, True, Possible };
+
+ EditorAction(EditorContext* editor)
+ : Editor(editor)
+ {
+ }
+
+ virtual ~EditorAction() {}
+
+ virtual const char* GetName() const = 0;
+
+ virtual AcceptResult Accept(const Control& control) = 0;
+ virtual bool Process(const Control& control) = 0;
+ virtual void Reject() {} // celled when Accept return 'Possible' and was rejected
+
+ virtual ImGuiMouseCursor GetCursor() { return ImGuiMouseCursor_Arrow; }
+
+ virtual bool IsDragging() { return false; }
+
+ virtual void ShowMetrics() {}
+
+ virtual NavigateAction* AsNavigate() { return nullptr; }
+ virtual SizeAction* AsSize() { return nullptr; }
+ virtual DragAction* AsDrag() { return nullptr; }
+ virtual SelectAction* AsSelect() { return nullptr; }
+ virtual CreateItemAction* AsCreateItem() { return nullptr; }
+ virtual DeleteItemsAction* AsDeleteItems() { return nullptr; }
+ virtual ContextMenuAction* AsContextMenu() { return nullptr; }
+ virtual ShortcutAction* AsCutCopyPaste() { return nullptr; }
+
+ EditorContext* Editor;
+};
+
+struct NavigateAction final: EditorAction
+{
+ enum class NavigationReason
+ {
+ Unknown,
+ MouseZoom,
+ Selection,
+ Object,
+ Content,
+ Edge
+ };
+
+ bool m_IsActive;
+ float m_Zoom;
+ ImVec2 m_Scroll;
+ ImVec2 m_ScrollStart;
+ ImVec2 m_ScrollDelta;
+
+ NavigateAction(EditorContext* editor, ImGuiEx::Canvas& canvas);
+
+ virtual const char* GetName() const override final { return "Navigate"; }
+
+ virtual AcceptResult Accept(const Control& control) override final;
+ virtual bool Process(const Control& control) override final;
+
+ virtual void ShowMetrics() override final;
+
+ virtual NavigateAction* AsNavigate() override final { return this; }
+
+ void NavigateTo(const ImRect& bounds, bool zoomIn, float duration = -1.0f, NavigationReason reason = NavigationReason::Unknown);
+ void StopNavigation();
+ void FinishNavigation();
+
+ bool MoveOverEdge();
+ void StopMoveOverEdge();
+ bool IsMovingOverEdge() const { return m_MovingOverEdge; }
+ ImVec2 GetMoveOffset() const { return m_MoveOffset; }
+
+ void SetWindow(ImVec2 position, ImVec2 size);
+
+ ImGuiEx::CanvasView GetView() const;
+ ImVec2 GetViewOrigin() const;
+ float GetViewScale() const;
+
+ void SetViewRect(const ImRect& rect);
+ ImRect GetViewRect() const;
+
+private:
+ ImGuiEx::Canvas& m_Canvas;
+ ImVec2 m_WindowScreenPos;
+ ImVec2 m_WindowScreenSize;
+
+ NavigateAnimation m_Animation;
+ NavigationReason m_Reason;
+ uint64_t m_LastSelectionId;
+ Object* m_LastObject;
+ bool m_MovingOverEdge;
+ ImVec2 m_MoveOffset;
+
+ bool HandleZoom(const Control& control);
+
+ void NavigateTo(const ImRect& target, float duration = -1.0f, NavigationReason reason = NavigationReason::Unknown);
+
+ float MatchZoom(int steps, float fallbackZoom);
+ int MatchZoomIndex(int direction);
+
+ static const float s_ZoomLevels[];
+ static const int s_ZoomLevelCount;
+};
+
+struct SizeAction final: EditorAction
+{
+ bool m_IsActive;
+ bool m_Clean;
+ Node* m_SizedNode;
+
+ SizeAction(EditorContext* editor);
+
+ virtual const char* GetName() const override final { return "Size"; }
+
+ virtual AcceptResult Accept(const Control& control) override final;
+ virtual bool Process(const Control& control) override final;
+
+ virtual ImGuiMouseCursor GetCursor() override final { return m_Cursor; }
+
+ virtual void ShowMetrics() override final;
+
+ virtual SizeAction* AsSize() override final { return this; }
+
+ virtual bool IsDragging() override final { return m_IsActive; }
+
+ const ImRect& GetStartGroupBounds() const { return m_StartGroupBounds; }
+
+private:
+ NodeRegion GetRegion(Node* node);
+ ImGuiMouseCursor ChooseCursor(NodeRegion region);
+
+ ImRect m_StartBounds;
+ ImRect m_StartGroupBounds;
+ ImVec2 m_LastSize;
+ ImVec2 m_MinimumSize;
+ ImVec2 m_LastDragOffset;
+ ed::NodeRegion m_Pivot;
+ ImGuiMouseCursor m_Cursor;
+};
+
+struct DragAction final: EditorAction
+{
+ bool m_IsActive;
+ bool m_Clear;
+ Object* m_DraggedObject;
+ vector<Object*> m_Objects;
+
+ DragAction(EditorContext* editor);
+
+ virtual const char* GetName() const override final { return "Drag"; }
+
+ virtual AcceptResult Accept(const Control& control) override final;
+ virtual bool Process(const Control& control) override final;
+
+ virtual ImGuiMouseCursor GetCursor() override final { return ImGuiMouseCursor_ResizeAll; }
+
+ virtual bool IsDragging() override final { return m_IsActive; }
+
+ virtual void ShowMetrics() override final;
+
+ virtual DragAction* AsDrag() override final { return this; }
+};
+
+struct SelectAction final: EditorAction
+{
+ bool m_IsActive;
+
+ bool m_SelectGroups;
+ bool m_SelectLinkMode;
+ bool m_CommitSelection;
+ ImVec2 m_StartPoint;
+ ImVec2 m_EndPoint;
+ vector<Object*> m_CandidateObjects;
+ vector<Object*> m_SelectedObjectsAtStart;
+
+ Animation m_Animation;
+
+ SelectAction(EditorContext* editor);
+
+ virtual const char* GetName() const override final { return "Select"; }
+
+ virtual AcceptResult Accept(const Control& control) override final;
+ virtual bool Process(const Control& control) override final;
+
+ virtual void ShowMetrics() override final;
+
+ virtual bool IsDragging() override final { return m_IsActive; }
+
+ virtual SelectAction* AsSelect() override final { return this; }
+
+ void Draw(ImDrawList* drawList);
+};
+
+struct ContextMenuAction final: EditorAction
+{
+ enum Menu { None, Node, Pin, Link, Background };
+
+ Menu m_CandidateMenu;
+ Menu m_CurrentMenu;
+ ObjectId m_ContextId;
+
+ ContextMenuAction(EditorContext* editor);
+
+ virtual const char* GetName() const override final { return "Context Menu"; }
+
+ virtual AcceptResult Accept(const Control& control) override final;
+ virtual bool Process(const Control& control) override final;
+ virtual void Reject() override final;
+
+ virtual void ShowMetrics() override final;
+
+ virtual ContextMenuAction* AsContextMenu() override final { return this; }
+
+ bool ShowNodeContextMenu(NodeId* nodeId);
+ bool ShowPinContextMenu(PinId* pinId);
+ bool ShowLinkContextMenu(LinkId* linkId);
+ bool ShowBackgroundContextMenu();
+};
+
+struct ShortcutAction final: EditorAction
+{
+ enum Action { None, Cut, Copy, Paste, Duplicate, CreateNode };
+
+ bool m_IsActive;
+ bool m_InAction;
+ Action m_CurrentAction;
+ vector<Object*> m_Context;
+
+ ShortcutAction(EditorContext* editor);
+
+ virtual const char* GetName() const override final { return "Shortcut"; }
+
+ virtual AcceptResult Accept(const Control& control) override final;
+ virtual bool Process(const Control& control) override final;
+ virtual void Reject() override final;
+
+ virtual void ShowMetrics() override final;
+
+ virtual ShortcutAction* AsCutCopyPaste() override final { return this; }
+
+ bool Begin();
+ void End();
+
+ bool AcceptCut();
+ bool AcceptCopy();
+ bool AcceptPaste();
+ bool AcceptDuplicate();
+ bool AcceptCreateNode();
+};
+
+struct CreateItemAction final : EditorAction
+{
+ enum Stage
+ {
+ None,
+ Possible,
+ Create
+ };
+
+ enum Action
+ {
+ Unknown,
+ UserReject,
+ UserAccept
+ };
+
+ enum Type
+ {
+ NoItem,
+ Node,
+ Link
+ };
+
+ enum Result
+ {
+ True,
+ False,
+ Indeterminate
+ };
+
+ bool m_InActive;
+ Stage m_NextStage;
+
+ Stage m_CurrentStage;
+ Type m_ItemType;
+ Action m_UserAction;
+ ImU32 m_LinkColor;
+ float m_LinkThickness;
+ Pin* m_LinkStart;
+ Pin* m_LinkEnd;
+
+ bool m_IsActive;
+ Pin* m_DraggedPin;
+
+ int m_LastChannel = -1;
+
+
+ CreateItemAction(EditorContext* editor);
+
+ virtual const char* GetName() const override final { return "Create Item"; }
+
+ virtual AcceptResult Accept(const Control& control) override final;
+ virtual bool Process(const Control& control) override final;
+
+ virtual ImGuiMouseCursor GetCursor() override final { return ImGuiMouseCursor_Arrow; }
+
+ virtual void ShowMetrics() override final;
+
+ virtual bool IsDragging() override final { return m_IsActive; }
+
+ virtual CreateItemAction* AsCreateItem() override final { return this; }
+
+ void SetStyle(ImU32 color, float thickness);
+
+ bool Begin();
+ void End();
+
+ Result RejectItem();
+ Result AcceptItem();
+
+ Result QueryLink(PinId* startId, PinId* endId);
+ Result QueryNode(PinId* pinId);
+
+private:
+ bool m_IsInGlobalSpace;
+
+ void DragStart(Pin* startPin);
+ void DragEnd();
+ void DropPin(Pin* endPin);
+ void DropNode();
+ void DropNothing();
+};
+
+struct DeleteItemsAction final: EditorAction
+{
+ bool m_IsActive;
+ bool m_InInteraction;
+
+ DeleteItemsAction(EditorContext* editor);
+
+ virtual const char* GetName() const override final { return "Delete Items"; }
+
+ virtual AcceptResult Accept(const Control& control) override final;
+ virtual bool Process(const Control& control) override final;
+
+ virtual void ShowMetrics() override final;
+
+ virtual DeleteItemsAction* AsDeleteItems() override final { return this; }
+
+ bool Add(Object* object);
+
+ bool Begin();
+ void End();
+
+ bool QueryLink(LinkId* linkId, PinId* startId = nullptr, PinId* endId = nullptr);
+ bool QueryNode(NodeId* nodeId);
+
+ bool AcceptItem();
+ void RejectItem();
+
+private:
+ enum IteratorType { Unknown, Link, Node };
+ enum UserAction { Undetermined, Accepted, Rejected };
+
+ bool QueryItem(ObjectId* itemId, IteratorType itemType);
+ void RemoveItem();
+
+ vector<Object*> m_ManuallyDeletedObjects;
+
+ IteratorType m_CurrentItemType;
+ UserAction m_UserAction;
+ vector<Object*> m_CandidateObjects;
+ int m_CandidateItemIndex;
+};
+
+struct NodeBuilder
+{
+ EditorContext* const Editor;
+
+ Node* m_CurrentNode;
+ Pin* m_CurrentPin;
+
+ ImRect m_NodeRect;
+
+ ImRect m_PivotRect;
+ ImVec2 m_PivotAlignment;
+ ImVec2 m_PivotSize;
+ ImVec2 m_PivotScale;
+ bool m_ResolvePinRect;
+ bool m_ResolvePivot;
+
+ ImRect m_GroupBounds;
+ bool m_IsGroup;
+
+ ImDrawListSplitter m_Splitter;
+ ImDrawListSplitter m_PinSplitter;
+
+ NodeBuilder(EditorContext* editor);
+ ~NodeBuilder();
+
+ void Begin(NodeId nodeId);
+ void End();
+
+ void BeginPin(PinId pinId, PinKind kind);
+ void EndPin();
+
+ void PinRect(const ImVec2& a, const ImVec2& b);
+ void PinPivotRect(const ImVec2& a, const ImVec2& b);
+ void PinPivotSize(const ImVec2& size);
+ void PinPivotScale(const ImVec2& scale);
+ void PinPivotAlignment(const ImVec2& alignment);
+
+ void Group(const ImVec2& size);
+
+ ImDrawList* GetUserBackgroundDrawList() const;
+ ImDrawList* GetUserBackgroundDrawList(Node* node) const;
+};
+
+struct HintBuilder
+{
+ EditorContext* const Editor;
+ bool m_IsActive;
+ Node* m_CurrentNode;
+ float m_LastFringe = 1.0f;
+ int m_LastChannel = 0;
+
+ HintBuilder(EditorContext* editor);
+
+ bool Begin(NodeId nodeId);
+ void End();
+
+ ImVec2 GetGroupMin();
+ ImVec2 GetGroupMax();
+
+ ImDrawList* GetForegroundDrawList();
+ ImDrawList* GetBackgroundDrawList();
+};
+
+struct Style: ax::NodeEditor::Style
+{
+ void PushColor(StyleColor colorIndex, const ImVec4& color);
+ void PopColor(int count = 1);
+
+ void PushVar(StyleVar varIndex, float value);
+ void PushVar(StyleVar varIndex, const ImVec2& value);
+ void PushVar(StyleVar varIndex, const ImVec4& value);
+ void PopVar(int count = 1);
+
+ const char* GetColorName(StyleColor colorIndex) const;
+
+private:
+ struct ColorModifier
+ {
+ StyleColor Index;
+ ImVec4 Value;
+ };
+
+ struct VarModifier
+ {
+ StyleVar Index;
+ ImVec4 Value;
+ };
+
+ float* GetVarFloatAddr(StyleVar idx);
+ ImVec2* GetVarVec2Addr(StyleVar idx);
+ ImVec4* GetVarVec4Addr(StyleVar idx);
+
+ vector<ColorModifier> m_ColorStack;
+ vector<VarModifier> m_VarStack;
+};
+
+struct Config: ax::NodeEditor::Config
+{
+ Config(const ax::NodeEditor::Config* config);
+
+ std::string Load();
+ std::string LoadNode(NodeId nodeId);
+
+ void BeginSave();
+ bool Save(const std::string& data, SaveReasonFlags flags);
+ bool SaveNode(NodeId nodeId, const std::string& data, SaveReasonFlags flags);
+ void EndSave();
+};
+
+enum class SuspendFlags : uint8_t
+{
+ None = 0,
+ KeepSplitter = 1
+};
+
+inline SuspendFlags operator |(SuspendFlags lhs, SuspendFlags rhs) { return static_cast<SuspendFlags>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs)); }
+inline SuspendFlags operator &(SuspendFlags lhs, SuspendFlags rhs) { return static_cast<SuspendFlags>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs)); }
+
+
+struct EditorContext
+{
+ EditorContext(const ax::NodeEditor::Config* config = nullptr);
+ ~EditorContext();
+
+ Style& GetStyle() { return m_Style; }
+
+ void Begin(const char* id, const ImVec2& size = ImVec2(0, 0));
+ void End();
+
+ bool DoLink(LinkId id, PinId startPinId, PinId endPinId, ImU32 color, float thickness);
+
+
+ NodeBuilder& GetNodeBuilder() { return m_NodeBuilder; }
+ HintBuilder& GetHintBuilder() { return m_HintBuilder; }
+
+ EditorAction* GetCurrentAction() { return m_CurrentAction; }
+
+ CreateItemAction& GetItemCreator() { return m_CreateItemAction; }
+ DeleteItemsAction& GetItemDeleter() { return m_DeleteItemsAction; }
+ ContextMenuAction& GetContextMenu() { return m_ContextMenuAction; }
+ ShortcutAction& GetShortcut() { return m_ShortcutAction; }
+
+ const ImGuiEx::CanvasView& GetView() const { return m_Canvas.View(); }
+ const ImRect& GetViewRect() const { return m_Canvas.ViewRect(); }
+ const ImRect& GetRect() const { return m_Canvas.Rect(); }
+
+ void SetNodePosition(NodeId nodeId, const ImVec2& screenPosition);
+ ImVec2 GetNodePosition(NodeId nodeId);
+ ImVec2 GetNodeSize(NodeId nodeId);
+
+ void MarkNodeToRestoreState(Node* node);
+ void RestoreNodeState(Node* node);
+
+ void ClearSelection();
+ void SelectObject(Object* object);
+ void DeselectObject(Object* object);
+ void SetSelectedObject(Object* object);
+ void ToggleObjectSelection(Object* object);
+ bool IsSelected(Object* object);
+ const vector<Object*>& GetSelectedObjects();
+ bool IsAnyNodeSelected();
+ bool IsAnyLinkSelected();
+ bool HasSelectionChanged();
+ uint64_t GetSelectionId() const { return m_SelectionId; }
+
+ Node* FindNodeAt(const ImVec2& p);
+ void FindNodesInRect(const ImRect& r, vector<Node*>& result, bool append = false, bool includeIntersecting = true);
+ void FindLinksInRect(const ImRect& r, vector<Link*>& result, bool append = false);
+
+ void FindLinksForNode(NodeId nodeId, vector<Link*>& result, bool add = false);
+
+ bool PinHadAnyLinks(PinId pinId);
+
+ ImVec2 ToCanvas(const ImVec2& point) const { return m_Canvas.ToLocal(point); }
+ ImVec2 ToScreen(const ImVec2& point) const { return m_Canvas.FromLocal(point); }
+
+ void NotifyLinkDeleted(Link* link);
+
+ void Suspend(SuspendFlags flags = SuspendFlags::None);
+ void Resume(SuspendFlags flags = SuspendFlags::None);
+ bool IsSuspended();
+
+ bool IsActive();
+
+ void MakeDirty(SaveReasonFlags reason);
+ void MakeDirty(SaveReasonFlags reason, Node* node);
+
+ Pin* CreatePin(PinId id, PinKind kind);
+ Node* CreateNode(NodeId id);
+ Link* CreateLink(LinkId id);
+
+ Node* FindNode(NodeId id);
+ Pin* FindPin(PinId id);
+ Link* FindLink(LinkId id);
+ Object* FindObject(ObjectId id);
+
+ Node* GetNode(NodeId id);
+ Pin* GetPin(PinId id, PinKind kind);
+ Link* GetLink(LinkId id);
+
+ Link* FindLinkAt(const ImVec2& p);
+
+ template <typename T>
+ ImRect GetBounds(const std::vector<T*>& objects)
+ {
+ ImRect bounds(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX);
+
+ for (auto object : objects)
+ if (object->m_IsLive)
+ bounds.Add(object->GetBounds());
+
+ if (ImRect_IsEmpty(bounds))
+ bounds = ImRect();
+
+ return bounds;
+ }
+
+ template <typename T>
+ ImRect GetBounds(const std::vector<ObjectWrapper<T>>& objects)
+ {
+ ImRect bounds(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX);
+
+ for (auto object : objects)
+ if (object.m_Object->m_IsLive)
+ bounds.Add(object.m_Object->GetBounds());
+
+ if (ImRect_IsEmpty(bounds))
+ bounds = ImRect();
+
+ return bounds;
+ }
+
+ ImRect GetSelectionBounds() { return GetBounds(m_SelectedObjects); }
+ ImRect GetContentBounds() { return GetBounds(m_Nodes); }
+
+ ImU32 GetColor(StyleColor colorIndex) const;
+ ImU32 GetColor(StyleColor colorIndex, float alpha) const;
+
+ void NavigateTo(const ImRect& bounds, bool zoomIn = false, float duration = -1) { m_NavigateAction.NavigateTo(bounds, zoomIn, duration); }
+
+ void RegisterAnimation(Animation* animation);
+ void UnregisterAnimation(Animation* animation);
+
+ void Flow(Link* link);
+
+ void SetUserContext(bool globalSpace = false);
+
+ void EnableShortcuts(bool enable);
+ bool AreShortcutsEnabled();
+
+ NodeId GetDoubleClickedNode() const { return m_DoubleClickedNode; }
+ PinId GetDoubleClickedPin() const { return m_DoubleClickedPin; }
+ LinkId GetDoubleClickedLink() const { return m_DoubleClickedLink; }
+ bool IsBackgroundClicked() const { return m_BackgroundClicked; }
+ bool IsBackgroundDoubleClicked() const { return m_BackgroundDoubleClicked; }
+
+ float AlignPointToGrid(float p) const
+ {
+ if (!ImGui::GetIO().KeyAlt)
+ return p - ImFmod(p, 16.0f);
+ else
+ return p;
+ }
+
+ ImVec2 AlignPointToGrid(const ImVec2& p) const
+ {
+ return ImVec2(AlignPointToGrid(p.x), AlignPointToGrid(p.y));
+ }
+
+private:
+ void LoadSettings();
+ void SaveSettings();
+
+ Control BuildControl(bool allowOffscreen);
+
+ void ShowMetrics(const Control& control);
+
+ void UpdateAnimations();
+
+ bool m_IsFirstFrame;
+ bool m_IsWindowActive;
+
+ bool m_ShortcutsEnabled;
+
+ Style m_Style;
+
+ vector<ObjectWrapper<Node>> m_Nodes;
+ vector<ObjectWrapper<Pin>> m_Pins;
+ vector<ObjectWrapper<Link>> m_Links;
+
+ vector<Object*> m_SelectedObjects;
+
+ vector<Object*> m_LastSelectedObjects;
+ uint64_t m_SelectionId;
+
+ Link* m_LastActiveLink;
+
+ vector<Animation*> m_LiveAnimations;
+ vector<Animation*> m_LastLiveAnimations;
+
+ ImGuiEx::Canvas m_Canvas;
+ bool m_IsCanvasVisible;
+
+ NodeBuilder m_NodeBuilder;
+ HintBuilder m_HintBuilder;
+
+ EditorAction* m_CurrentAction;
+ NavigateAction m_NavigateAction;
+ SizeAction m_SizeAction;
+ DragAction m_DragAction;
+ SelectAction m_SelectAction;
+ ContextMenuAction m_ContextMenuAction;
+ ShortcutAction m_ShortcutAction;
+ CreateItemAction m_CreateItemAction;
+ DeleteItemsAction m_DeleteItemsAction;
+
+ vector<AnimationController*> m_AnimationControllers;
+ FlowAnimationController m_FlowAnimationController;
+
+ NodeId m_DoubleClickedNode;
+ PinId m_DoubleClickedPin;
+ LinkId m_DoubleClickedLink;
+ bool m_BackgroundClicked;
+ bool m_BackgroundDoubleClicked;
+
+ bool m_IsInitialized;
+ Settings m_Settings;
+
+ Config m_Config;
+
+ int m_ExternalChannel;
+ ImDrawListSplitter m_Splitter;
+};
+
+
+//------------------------------------------------------------------------------
+} // namespace Detail
+} // namespace Editor
+} // namespace ax
+
+
+//------------------------------------------------------------------------------
+# include "imgui_node_editor_internal.inl"
+
+
+//------------------------------------------------------------------------------
+# endif // __IMGUI_NODE_EDITOR_INTERNAL_H__
diff --git a/3rdparty/imgui-node-editor/imgui_node_editor_internal.inl b/3rdparty/imgui-node-editor/imgui_node_editor_internal.inl
new file mode 100644
index 0000000..0d16231
--- /dev/null
+++ b/3rdparty/imgui-node-editor/imgui_node_editor_internal.inl
@@ -0,0 +1,55 @@
+//------------------------------------------------------------------------------
+// LICENSE
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// CREDITS
+// Written by Michal Cichon
+//------------------------------------------------------------------------------
+# ifndef __IMGUI_NODE_EDITOR_INTERNAL_INL__
+# define __IMGUI_NODE_EDITOR_INTERNAL_INL__
+# pragma once
+
+
+//------------------------------------------------------------------------------
+# include "imgui_node_editor_internal.h"
+
+
+//------------------------------------------------------------------------------
+namespace ax {
+namespace NodeEditor {
+namespace Detail {
+
+
+//------------------------------------------------------------------------------
+//inline ImRect ToRect(const ax::rectf& rect)
+//{
+// return ImRect(
+// to_imvec(rect.top_left()),
+// to_imvec(rect.bottom_right())
+// );
+//}
+//
+//inline ImRect ToRect(const ax::rect& rect)
+//{
+// return ImRect(
+// to_imvec(rect.top_left()),
+// to_imvec(rect.bottom_right())
+// );
+//}
+
+inline ImRect ImGui_GetItemRect()
+{
+ return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
+}
+
+
+//------------------------------------------------------------------------------
+} // namespace Detail
+} // namespace Editor
+} // namespace ax
+
+
+//------------------------------------------------------------------------------
+# endif // __IMGUI_NODE_EDITOR_INTERNAL_INL__
diff --git a/3rdparty/imnodes/Nelarius-imnodes.LICENSE.txt b/3rdparty/imgui-node-editor/thedmd-imgui-node-editor.LICENSE.txt
index 49425fd..0c8aa6a 100644
--- a/3rdparty/imnodes/Nelarius-imnodes.LICENSE.txt
+++ b/3rdparty/imgui-node-editor/thedmd-imgui-node-editor.LICENSE.txt
@@ -1,6 +1,6 @@
-The MIT License (MIT)
+MIT License
-Copyright (c) 2014-2021 Omar Cornut
+Copyright (c) 2019 Michał Cichoń
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/3rdparty/imgui-node-editor/thedmd-imgui-node-editor.stamp b/3rdparty/imgui-node-editor/thedmd-imgui-node-editor.stamp
new file mode 100644
index 0000000..8c5bdc7
--- /dev/null
+++ b/3rdparty/imgui-node-editor/thedmd-imgui-node-editor.stamp
@@ -0,0 +1,2 @@
+# This file labels the commit which our source is from
+687a72f940c76cf5064e13fe55fa0408c18fcbe4
diff --git a/3rdparty/imgui/CMakeLists.txt b/3rdparty/imgui/CMakeLists.txt
index d00e8a2..de80f9a 100644
--- a/3rdparty/imgui/CMakeLists.txt
+++ b/3rdparty/imgui/CMakeLists.txt
@@ -1,17 +1,4 @@
-set(IMGUI_SOURCES
- imgui.h
- imgui.cpp
- imconfig.h
- imgui_internal.h
- imgui_demo.cpp
- imgui_draw.cpp
- imgui_tables.cpp
- imgui_widgets.cpp
- imgui_stdlib.cpp
- imstb_rectpack.h
- imstb_textedit.h
- imstb_truetype.h
-)
+file(GLOB IMGUI_SOURCES *.cpp)
# We don't build the files in backend/ because they are included by various entrypoint implementations
# depending on build flags. Technically it is possible to write then here too, but doing so would require repeating
diff --git a/3rdparty/imnodes/CMakeLists.txt b/3rdparty/imnodes/CMakeLists.txt
deleted file mode 100644
index bb1290c..0000000
--- a/3rdparty/imnodes/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-set(IMNODES_SOURCES
- imnodes.h
- imnodes.cpp
-)
-
-add_library(imnodes ${IMNODES_SOURCES})
-target_include_directories(imnodes PRIVATE
- ${CMAKE_SOURCE_DIR}/3rdparty/imnodes
- ${CMAKE_SOURCE_DIR}/3rdparty/imgui
-)
diff --git a/3rdparty/imnodes/Nelarius-imnodes.stamp b/3rdparty/imnodes/Nelarius-imnodes.stamp
deleted file mode 100644
index 2b18546..0000000
--- a/3rdparty/imnodes/Nelarius-imnodes.stamp
+++ /dev/null
@@ -1,3 +0,0 @@
-# This file labels the commit which our source is from
-# Note: namespace `imnodes` is replaced with `ImNodes` for stylistic consistency
-857cc860af05ac0f6a4039c2af33d982377b6cf4
diff --git a/3rdparty/imnodes/imnodes.cpp b/3rdparty/imnodes/imnodes.cpp
deleted file mode 100644
index b12fb6c..0000000
--- a/3rdparty/imnodes/imnodes.cpp
+++ /dev/null
@@ -1,3089 +0,0 @@
-// the structure of this file:
-//
-// [SECTION] internal data structures
-// [SECTION] global struct
-// [SECTION] editor context definition
-// [SECTION] draw list helper
-// [SECTION] ObjectPool implementation
-// [SECTION] ui state logic
-// [SECTION] render helpers
-// [SECTION] API implementation
-
-#include "imnodes.h"
-
-#include <imgui.h>
-#define IMGUI_DEFINE_MATH_OPERATORS
-#include <imgui_internal.h>
-
-// Check minimum ImGui version
-#define MINIMUM_COMPATIBLE_IMGUI_VERSION 17400
-#if IMGUI_VERSION_NUM < MINIMUM_COMPATIBLE_IMGUI_VERSION
-#error "Minimum ImGui version requirement not met -- please use a newer version!"
-#endif
-
-#include <assert.h>
-#include <limits.h>
-#include <math.h>
-#include <new>
-#include <stdint.h>
-#include <stdio.h> // for fwrite, ssprintf, sscanf
-#include <stdlib.h>
-#include <string.h> // strlen, strncmp
-
-namespace ImNodes
-{
-namespace
-{
-enum ScopeFlags
-{
- Scope_None = 1,
- Scope_Editor = 1 << 1,
- Scope_Node = 1 << 2,
- Scope_Attribute = 1 << 3
-};
-
-enum AttributeType
-{
- AttributeType_None,
- AttributeType_Input,
- AttributeType_Output
-};
-
-enum ElementStateChange
-{
- ElementStateChange_None = 0,
- ElementStateChange_LinkStarted = 1 << 0,
- ElementStateChange_LinkDropped = 1 << 1,
- ElementStateChange_LinkCreated = 1 << 2
-};
-
-// [SECTION] internal data structures
-
-// The object T must have the following interface:
-//
-// struct T
-// {
-// T();
-//
-// int id;
-// };
-template<typename T>
-struct ObjectPool
-{
- ImVector<T> pool;
- ImVector<bool> in_use;
- ImVector<int> free_list;
- ImGuiStorage id_map;
-
- ObjectPool() : pool(), in_use(), free_list(), id_map() {}
-};
-
-// Emulates std::optional<int> using the sentinel value `invalid_index`.
-struct OptionalIndex
-{
- OptionalIndex() : m_index(invalid_index) {}
- OptionalIndex(const int value) : m_index(value) {}
-
- // Observers
-
- inline bool has_value() const { return m_index != invalid_index; }
-
- inline int value() const
- {
- assert(has_value());
- return m_index;
- }
-
- // Modifiers
-
- inline OptionalIndex& operator=(const int value)
- {
- m_index = value;
- return *this;
- }
-
- inline void reset() { m_index = invalid_index; }
-
- inline bool operator==(const OptionalIndex& rhs) const { return m_index == rhs.m_index; }
-
- inline bool operator==(const int rhs) const { return m_index == rhs; }
-
- inline bool operator!=(const OptionalIndex& rhs) const { return m_index != rhs.m_index; }
-
- inline bool operator!=(const int rhs) const { return m_index != rhs; }
-
- static const int invalid_index = -1;
-
-private:
- int m_index;
-};
-
-struct NodeData
-{
- int id;
- ImVec2 origin; // The node origin is in editor space
- ImRect title_bar_content_rect;
- ImRect rect;
-
- struct
- {
- ImU32 background, background_hovered, background_selected, outline, titlebar,
- titlebar_hovered, titlebar_selected;
- } color_style;
-
- struct
- {
- float corner_rounding;
- ImVec2 padding;
- float border_thickness;
- } layout_style;
-
- ImVector<int> pin_indices;
- bool draggable;
-
- NodeData(const int node_id)
- : id(node_id), origin(100.0f, 100.0f), title_bar_content_rect(),
- rect(ImVec2(0.0f, 0.0f), ImVec2(0.0f, 0.0f)), color_style(), layout_style(),
- pin_indices(), draggable(true)
- {
- }
-
- ~NodeData() { id = INT_MIN; }
-};
-
-struct PinData
-{
- int id;
- int parent_node_idx;
- ImRect attribute_rect;
- AttributeType type;
- PinShape shape;
- ImVec2 pos; // screen-space coordinates
- int flags;
-
- struct
- {
- ImU32 background, hovered;
- } color_style;
-
- PinData(const int pin_id)
- : id(pin_id), parent_node_idx(), attribute_rect(), type(AttributeType_None),
- shape(PinShape_CircleFilled), pos(), flags(AttributeFlags_None), color_style()
- {
- }
-};
-
-struct LinkData
-{
- int id;
- int start_pin_idx, end_pin_idx;
-
- struct
- {
- ImU32 base, hovered, selected;
- } color_style;
-
- LinkData(const int link_id) : id(link_id), start_pin_idx(), end_pin_idx(), color_style() {}
-};
-
-struct LinkPredicate
-{
- bool operator()(const LinkData& lhs, const LinkData& rhs) const
- {
- // Do a unique compare by sorting the pins' addresses.
- // This catches duplicate links, whether they are in the
- // same direction or not.
- // Sorting by pin index should have the uniqueness guarantees as sorting
- // by id -- each unique id will get one slot in the link pool array.
-
- int lhs_start = lhs.start_pin_idx;
- int lhs_end = lhs.end_pin_idx;
- int rhs_start = rhs.start_pin_idx;
- int rhs_end = rhs.end_pin_idx;
-
- if (lhs_start > lhs_end)
- {
- ImSwap(lhs_start, lhs_end);
- }
-
- if (rhs_start > rhs_end)
- {
- ImSwap(rhs_start, rhs_end);
- }
-
- return lhs_start == rhs_start && lhs_end == rhs_end;
- }
-};
-
-struct BezierCurve
-{
- // the curve control points
- ImVec2 p0, p1, p2, p3;
-};
-
-struct LinkBezierData
-{
- BezierCurve bezier;
- int num_segments;
-};
-
-enum ClickInteractionType
-{
- ClickInteractionType_Node,
- ClickInteractionType_Link,
- ClickInteractionType_LinkCreation,
- ClickInteractionType_Panning,
- ClickInteractionType_BoxSelection,
- ClickInteractionType_None
-};
-
-enum LinkCreationType
-{
- LinkCreationType_Standard,
- LinkCreationType_FromDetach
-};
-
-struct ClickInteractionState
-{
- struct
- {
- int start_pin_idx;
- OptionalIndex end_pin_idx;
- LinkCreationType link_creation_type;
- } link_creation;
-
- struct
- {
- ImRect rect;
- } box_selector;
-};
-
-struct ColorStyleElement
-{
- ImU32 color;
- ColorStyle item;
-
- ColorStyleElement(const ImU32 c, const ColorStyle s) : color(c), item(s) {}
-};
-
-struct StyleElement
-{
- StyleVar item;
- float value;
-
- StyleElement(const float value, const StyleVar variable) : item(variable), value(value) {}
-};
-} // namespace
-// [SECTION] global struct
-// this stores data which only lives for one frame
-struct Context
-{
- EditorContext* default_editor_ctx;
- EditorContext* editor_ctx;
-
- // Canvas draw list and helper state
- ImDrawList* canvas_draw_list;
- ImGuiStorage node_idx_to_submission_idx;
- ImVector<int> node_idx_submission_order;
- ImVector<int> node_indices_overlapping_with_mouse;
- ImVector<int> occluded_pin_indices;
-
- // Canvas extents
- ImVec2 canvas_origin_screen_space;
- ImRect canvas_rect_screen_space;
-
- // Debug helpers
- ScopeFlags current_scope;
-
- // Configuration state
- IO io;
- Style style;
- ImVector<ColorStyleElement> color_modifier_stack;
- ImVector<StyleElement> style_modifier_stack;
- ImGuiTextBuffer text_buffer;
-
- int current_attribute_flags;
- ImVector<int> attribute_flag_stack;
-
- // UI element state
- int current_node_idx;
- int current_pin_idx;
- int current_attribute_id;
-
- OptionalIndex hovered_node_idx;
- OptionalIndex interactive_node_idx;
- OptionalIndex hovered_link_idx;
- OptionalIndex hovered_pin_idx;
- int hovered_pin_flags;
-
- OptionalIndex deleted_link_idx;
- OptionalIndex snap_link_idx;
-
- // Event helper state
- int element_state_change;
-
- int active_attribute_id;
- bool active_attribute;
-
- // ImGui::IO cache
-
- ImVec2 mouse_pos;
-
- bool left_mouse_clicked;
- bool left_mouse_released;
- bool middle_mouse_clicked;
- bool left_mouse_dragging;
- bool middle_mouse_dragging;
-};
-
-Context* g = NULL;
-
-namespace
-{
-
-EditorContext& editor_context_get()
-{
- // No editor context was set! Did you forget to call ImNodes::Initialize?
- assert(g->editor_ctx != NULL);
- return *g->editor_ctx;
-}
-
-inline ImVec2 eval_bezier(float t, const BezierCurve& bezier)
-{
- // B(t) = (1-t)**3 p0 + 3(1 - t)**2 t P1 + 3(1-t)t**2 P2 + t**3 P3
- return ImVec2(
- (1 - t) * (1 - t) * (1 - t) * bezier.p0.x + 3 * (1 - t) * (1 - t) * t * bezier.p1.x +
- 3 * (1 - t) * t * t * bezier.p2.x + t * t * t * bezier.p3.x,
- (1 - t) * (1 - t) * (1 - t) * bezier.p0.y + 3 * (1 - t) * (1 - t) * t * bezier.p1.y +
- 3 * (1 - t) * t * t * bezier.p2.y + t * t * t * bezier.p3.y);
-}
-
-// Calculates the closest point along each bezier curve segment.
-ImVec2 get_closest_point_on_cubic_bezier(
- const int num_segments,
- const ImVec2& p,
- const BezierCurve& bezier)
-{
- IM_ASSERT(num_segments > 0);
- ImVec2 p_last = bezier.p0;
- ImVec2 p_closest;
- float p_closest_dist = FLT_MAX;
- float t_step = 1.0f / (float)num_segments;
- for (int i = 1; i <= num_segments; ++i)
- {
- ImVec2 p_current = eval_bezier(t_step * i, bezier);
- ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p);
- float dist = ImLengthSqr(p - p_line);
- if (dist < p_closest_dist)
- {
- p_closest = p_line;
- p_closest_dist = dist;
- }
- p_last = p_current;
- }
- return p_closest;
-}
-
-inline float get_distance_to_cubic_bezier(
- const ImVec2& pos,
- const BezierCurve& bezier,
- const int num_segments)
-{
- const ImVec2 point_on_curve = get_closest_point_on_cubic_bezier(num_segments, pos, bezier);
-
- const ImVec2 to_curve = point_on_curve - pos;
- return ImSqrt(ImLengthSqr(to_curve));
-}
-
-inline ImRect get_containing_rect_for_bezier_curve(const BezierCurve& bezier)
-{
- const ImVec2 min = ImVec2(ImMin(bezier.p0.x, bezier.p3.x), ImMin(bezier.p0.y, bezier.p3.y));
- const ImVec2 max = ImVec2(ImMax(bezier.p0.x, bezier.p3.x), ImMax(bezier.p0.y, bezier.p3.y));
-
- const float hover_distance = g->style.link_hover_distance;
-
- ImRect rect(min, max);
- rect.Add(bezier.p1);
- rect.Add(bezier.p2);
- rect.Expand(ImVec2(hover_distance, hover_distance));
-
- return rect;
-}
-
-inline LinkBezierData get_link_renderable(
- ImVec2 start,
- ImVec2 end,
- const AttributeType start_type,
- const float line_segments_per_length)
-{
- assert((start_type == AttributeType_Input) || (start_type == AttributeType_Output));
- if (start_type == AttributeType_Input)
- {
- ImSwap(start, end);
- }
-
- const float link_length = ImSqrt(ImLengthSqr(end - start));
- const ImVec2 offset = ImVec2(0.25f * link_length, 0.f);
- LinkBezierData link_data;
- link_data.bezier.p0 = start;
- link_data.bezier.p1 = start + offset;
- link_data.bezier.p2 = end - offset;
- link_data.bezier.p3 = end;
- link_data.num_segments = ImMax(static_cast<int>(link_length * line_segments_per_length), 1);
- return link_data;
-}
-
-inline float eval_implicit_line_eq(const ImVec2& p1, const ImVec2& p2, const ImVec2& p)
-{
- return (p2.y - p1.y) * p.x + (p1.x - p2.x) * p.y + (p2.x * p1.y - p1.x * p2.y);
-}
-
-inline int sign(float val) { return int(val > 0.0f) - int(val < 0.0f); }
-
-inline bool rectangle_overlaps_line_segment(const ImRect& rect, const ImVec2& p1, const ImVec2& p2)
-{
- // Trivial case: rectangle contains an endpoint
- if (rect.Contains(p1) || rect.Contains(p2))
- {
- return true;
- }
-
- // Flip rectangle if necessary
- ImRect flip_rect = rect;
-
- if (flip_rect.Min.x > flip_rect.Max.x)
- {
- ImSwap(flip_rect.Min.x, flip_rect.Max.x);
- }
-
- if (flip_rect.Min.y > flip_rect.Max.y)
- {
- ImSwap(flip_rect.Min.y, flip_rect.Max.y);
- }
-
- // Trivial case: line segment lies to one particular side of rectangle
- if ((p1.x < flip_rect.Min.x && p2.x < flip_rect.Min.x) ||
- (p1.x > flip_rect.Max.x && p2.x > flip_rect.Max.x) ||
- (p1.y < flip_rect.Min.y && p2.y < flip_rect.Min.y) ||
- (p1.y > flip_rect.Max.y && p2.y > flip_rect.Max.y))
- {
- return false;
- }
-
- const int corner_signs[4] = {
- sign(eval_implicit_line_eq(p1, p2, flip_rect.Min)),
- sign(eval_implicit_line_eq(p1, p2, ImVec2(flip_rect.Max.x, flip_rect.Min.y))),
- sign(eval_implicit_line_eq(p1, p2, ImVec2(flip_rect.Min.x, flip_rect.Max.y))),
- sign(eval_implicit_line_eq(p1, p2, flip_rect.Max))};
-
- int sum = 0;
- int sum_abs = 0;
-
- for (int i = 0; i < 4; ++i)
- {
- sum += corner_signs[i];
- sum_abs += abs(corner_signs[i]);
- }
-
- // At least one corner of rectangle lies on a different side of line segment
- return abs(sum) != sum_abs;
-}
-
-inline bool rectangle_overlaps_bezier(const ImRect& rectangle, const LinkBezierData& link_data)
-{
- ImVec2 current = eval_bezier(0.f, link_data.bezier);
- const float dt = 1.0f / link_data.num_segments;
- for (int s = 0; s < link_data.num_segments; ++s)
- {
- ImVec2 next = eval_bezier(static_cast<float>((s + 1) * dt), link_data.bezier);
- if (rectangle_overlaps_line_segment(rectangle, current, next))
- {
- return true;
- }
- current = next;
- }
- return false;
-}
-
-inline bool rectangle_overlaps_link(
- const ImRect& rectangle,
- const ImVec2& start,
- const ImVec2& end,
- const AttributeType start_type)
-{
- // First level: simple rejection test via rectangle overlap:
-
- ImRect lrect = ImRect(start, end);
- if (lrect.Min.x > lrect.Max.x)
- {
- ImSwap(lrect.Min.x, lrect.Max.x);
- }
-
- if (lrect.Min.y > lrect.Max.y)
- {
- ImSwap(lrect.Min.y, lrect.Max.y);
- }
-
- if (rectangle.Overlaps(lrect))
- {
- // First, check if either one or both endpoinds are trivially contained
- // in the rectangle
-
- if (rectangle.Contains(start) || rectangle.Contains(end))
- {
- return true;
- }
-
- // Second level of refinement: do a more expensive test against the
- // link
-
- const LinkBezierData link_data =
- get_link_renderable(start, end, start_type, g->style.link_line_segments_per_length);
- return rectangle_overlaps_bezier(rectangle, link_data);
- }
-
- return false;
-}
-} // namespace
-
-// [SECTION] editor context definition
-
-struct EditorContext
-{
- ObjectPool<NodeData> nodes;
- ObjectPool<PinData> pins;
- ObjectPool<LinkData> links;
-
- ImVector<int> node_depth_order;
-
- // ui related fields
- ImVec2 panning;
-
- ImVector<int> selected_node_indices;
- ImVector<int> selected_link_indices;
-
- ClickInteractionType click_interaction_type;
- ClickInteractionState click_interaction_state;
-
- EditorContext()
- : nodes(), pins(), links(), panning(0.f, 0.f), selected_node_indices(),
- selected_link_indices(), click_interaction_type(ClickInteractionType_None),
- click_interaction_state()
- {
- }
-};
-
-namespace
-{
-// [SECTION] draw list helper
-
-void ImDrawList_grow_channels(ImDrawList* draw_list, const int num_channels)
-{
- ImDrawListSplitter& splitter = draw_list->_Splitter;
-
- if (splitter._Count == 1)
- {
- splitter.Split(draw_list, num_channels + 1);
- return;
- }
-
- // 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 + num_channels;
- if (old_channel_capacity < old_channel_count + num_channels)
- {
- 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 = draw_list->_ClipRectStack.back();
- draw_cmd.TextureId = draw_list->_TextureIdStack.back();
- channel._CmdBuffer.push_back(draw_cmd);
- }
- }
-}
-
-void ImDrawListSplitter_swap_channels(
- ImDrawListSplitter& splitter,
- const int lhs_idx,
- const int rhs_idx)
-{
- if (lhs_idx == rhs_idx)
- {
- return;
- }
-
- assert(lhs_idx >= 0 && lhs_idx < splitter._Count);
- assert(rhs_idx >= 0 && rhs_idx < splitter._Count);
-
- ImDrawChannel& lhs_channel = splitter._Channels[lhs_idx];
- ImDrawChannel& rhs_channel = splitter._Channels[rhs_idx];
- lhs_channel._CmdBuffer.swap(rhs_channel._CmdBuffer);
- lhs_channel._IdxBuffer.swap(rhs_channel._IdxBuffer);
-
- const int current_channel = splitter._Current;
-
- if (current_channel == lhs_idx)
- {
- splitter._Current = rhs_idx;
- }
- else if (current_channel == rhs_idx)
- {
- splitter._Current = lhs_idx;
- }
-}
-
-void draw_list_set(ImDrawList* window_draw_list)
-{
- g->canvas_draw_list = window_draw_list;
- g->node_idx_to_submission_idx.Clear();
- g->node_idx_submission_order.clear();
-}
-
-// The draw list channels are structured as follows. First we have our base channel, the canvas grid
-// on which we render the grid lines in BeginNodeEditor(). The base channel is the reason
-// draw_list_submission_idx_to_background_channel_idx offsets the index by one. Each BeginNode()
-// call appends two new draw channels, for the node background and foreground. The node foreground
-// is the channel into which the node's ImGui content is rendered. Finally, in EndNodeEditor() we
-// append one last draw channel for rendering the selection box and the incomplete link on top of
-// everything else.
-//
-// +----------+----------+----------+----------+----------+----------+
-// | | | | | | |
-// |canvas |node |node |... |... |click |
-// |grid |background|foreground| | |interaction
-// | | | | | | |
-// +----------+----------+----------+----------+----------+----------+
-// | |
-// | submission idx |
-// | |
-// -----------------------
-
-void draw_list_add_node(const int node_idx)
-{
- g->node_idx_to_submission_idx.SetInt(
- static_cast<ImGuiID>(node_idx), g->node_idx_submission_order.Size);
- g->node_idx_submission_order.push_back(node_idx);
- ImDrawList_grow_channels(g->canvas_draw_list, 2);
-}
-
-void draw_list_append_click_interaction_channel()
-{
- // NOTE: don't use this function outside of EndNodeEditor. Using this before all nodes have been
- // added will screw up the node draw order.
- ImDrawList_grow_channels(g->canvas_draw_list, 1);
-}
-
-int draw_list_submission_idx_to_background_channel_idx(const int submission_idx)
-{
- // NOTE: the first channel is the canvas background, i.e. the grid
- return 1 + 2 * submission_idx;
-}
-
-int draw_list_submission_idx_to_foreground_channel_idx(const int submission_idx)
-{
- return draw_list_submission_idx_to_background_channel_idx(submission_idx) + 1;
-}
-
-void draw_list_activate_click_interaction_channel()
-{
- g->canvas_draw_list->_Splitter.SetCurrentChannel(
- g->canvas_draw_list, g->canvas_draw_list->_Splitter._Count - 1);
-}
-
-void draw_list_activate_current_node_foreground()
-{
- const int foreground_channel_idx =
- draw_list_submission_idx_to_foreground_channel_idx(g->node_idx_submission_order.Size - 1);
- g->canvas_draw_list->_Splitter.SetCurrentChannel(g->canvas_draw_list, foreground_channel_idx);
-}
-
-void draw_list_activate_node_background(const int node_idx)
-{
- const int submission_idx =
- g->node_idx_to_submission_idx.GetInt(static_cast<ImGuiID>(node_idx), -1);
- // There is a discrepancy in the submitted node count and the rendered node count! Did you call
- // one of the following functions
- // * EditorContextMoveToNode
- // * SetNodeScreenSpacePos
- // * SetNodeGridSpacePos
- // * SetNodeDraggable
- // after the BeginNode/EndNode function calls?
- assert(submission_idx != -1);
- const int background_channel_idx =
- draw_list_submission_idx_to_background_channel_idx(submission_idx);
- g->canvas_draw_list->_Splitter.SetCurrentChannel(g->canvas_draw_list, background_channel_idx);
-}
-
-void draw_list_swap_submission_indices(const int lhs_idx, const int rhs_idx)
-{
- assert(lhs_idx != rhs_idx);
-
- const int lhs_foreground_channel_idx =
- draw_list_submission_idx_to_foreground_channel_idx(lhs_idx);
- const int lhs_background_channel_idx =
- draw_list_submission_idx_to_background_channel_idx(lhs_idx);
- const int rhs_foreground_channel_idx =
- draw_list_submission_idx_to_foreground_channel_idx(rhs_idx);
- const int rhs_background_channel_idx =
- draw_list_submission_idx_to_background_channel_idx(rhs_idx);
-
- ImDrawListSplitter_swap_channels(
- g->canvas_draw_list->_Splitter, lhs_background_channel_idx, rhs_background_channel_idx);
- ImDrawListSplitter_swap_channels(
- g->canvas_draw_list->_Splitter, lhs_foreground_channel_idx, rhs_foreground_channel_idx);
-}
-
-void draw_list_sort_channels_by_depth(const ImVector<int>& node_idx_depth_order)
-{
- if (g->node_idx_to_submission_idx.Data.Size < 2)
- {
- return;
- }
-
- assert(node_idx_depth_order.Size == g->node_idx_submission_order.Size);
-
- int start_idx = node_idx_depth_order.Size - 1;
-
- while (node_idx_depth_order[start_idx] == g->node_idx_submission_order[start_idx])
- {
- if (--start_idx == 0)
- {
- // early out if submission order and depth order are the same
- return;
- }
- }
-
- // TODO: this is an O(N^2) algorithm. It might be worthwhile revisiting this to see if the time
- // complexity can be reduced.
-
- for (int depth_idx = start_idx; depth_idx > 0; --depth_idx)
- {
- const int node_idx = node_idx_depth_order[depth_idx];
-
- // Find the current index of the node_idx in the submission order array
- int submission_idx = -1;
- for (int i = 0; i < g->node_idx_submission_order.Size; ++i)
- {
- if (g->node_idx_submission_order[i] == node_idx)
- {
- submission_idx = i;
- break;
- }
- }
- assert(submission_idx >= 0);
-
- if (submission_idx == depth_idx)
- {
- continue;
- }
-
- for (int j = submission_idx; j < depth_idx; ++j)
- {
- draw_list_swap_submission_indices(j, j + 1);
- ImSwap(g->node_idx_submission_order[j], g->node_idx_submission_order[j + 1]);
- }
- }
-}
-
-// [SECTION] ObjectPool implementation
-
-template<typename T>
-int object_pool_find(ObjectPool<T>& objects, const int id)
-{
- const int index = objects.id_map.GetInt(static_cast<ImGuiID>(id), -1);
- return index;
-}
-
-template<typename T>
-void object_pool_update(ObjectPool<T>& objects)
-{
- objects.free_list.clear();
- for (int i = 0; i < objects.in_use.size(); ++i)
- {
- if (!objects.in_use[i])
- {
- objects.id_map.SetInt(objects.pool[i].id, -1);
- objects.free_list.push_back(i);
- (objects.pool.Data + i)->~T();
- }
- }
-}
-
-template<>
-void object_pool_update(ObjectPool<NodeData>& nodes)
-{
- nodes.free_list.clear();
- for (int i = 0; i < nodes.in_use.size(); ++i)
- {
- if (nodes.in_use[i])
- {
- nodes.pool[i].pin_indices.clear();
- }
- else
- {
- const int previous_id = nodes.pool[i].id;
- const int previous_idx = nodes.id_map.GetInt(previous_id, -1);
-
- if (previous_idx != -1)
- {
- assert(previous_idx == i);
- // Remove node idx form depth stack the first time we detect that this idx slot is
- // unused
- ImVector<int>& depth_stack = editor_context_get().node_depth_order;
- const int* const elem = depth_stack.find(i);
- assert(elem != depth_stack.end());
- depth_stack.erase(elem);
- }
-
- nodes.id_map.SetInt(previous_id, -1);
- nodes.free_list.push_back(i);
- (nodes.pool.Data + i)->~NodeData();
- }
- }
-}
-
-template<typename T>
-void object_pool_reset(ObjectPool<T>& objects)
-{
- if (!objects.in_use.empty())
- {
- memset(objects.in_use.Data, 0, objects.in_use.size_in_bytes());
- }
-}
-
-template<typename T>
-int object_pool_find_or_create_index(ObjectPool<T>& objects, const int id)
-{
- int index = objects.id_map.GetInt(static_cast<ImGuiID>(id), -1);
-
- // Construct new object
- if (index == -1)
- {
- if (objects.free_list.empty())
- {
- index = objects.pool.size();
- IM_ASSERT(objects.pool.size() == objects.in_use.size());
- const int new_size = objects.pool.size() + 1;
- objects.pool.resize(new_size);
- objects.in_use.resize(new_size);
- }
- else
- {
- index = objects.free_list.back();
- objects.free_list.pop_back();
- }
- IM_PLACEMENT_NEW(objects.pool.Data + index) T(id);
- objects.id_map.SetInt(static_cast<ImGuiID>(id), index);
- }
-
- // Flag it as used
- objects.in_use[index] = true;
-
- return index;
-}
-
-template<>
-int object_pool_find_or_create_index(ObjectPool<NodeData>& nodes, const int node_id)
-{
- int node_idx = nodes.id_map.GetInt(static_cast<ImGuiID>(node_id), -1);
-
- // Construct new node
- if (node_idx == -1)
- {
- if (nodes.free_list.empty())
- {
- node_idx = nodes.pool.size();
- IM_ASSERT(nodes.pool.size() == nodes.in_use.size());
- const int new_size = nodes.pool.size() + 1;
- nodes.pool.resize(new_size);
- nodes.in_use.resize(new_size);
- }
- else
- {
- node_idx = nodes.free_list.back();
- nodes.free_list.pop_back();
- }
- IM_PLACEMENT_NEW(nodes.pool.Data + node_idx) NodeData(node_id);
- nodes.id_map.SetInt(static_cast<ImGuiID>(node_id), node_idx);
-
- EditorContext& editor = editor_context_get();
- editor.node_depth_order.push_back(node_idx);
- }
-
- // Flag node as used
- nodes.in_use[node_idx] = true;
-
- return node_idx;
-}
-
-template<typename T>
-T& object_pool_find_or_create_object(ObjectPool<T>& objects, const int id)
-{
- const int index = object_pool_find_or_create_index(objects, id);
- return objects.pool[index];
-}
-
-// [SECTION] ui state logic
-
-ImVec2 get_screen_space_pin_coordinates(
- const ImRect& node_rect,
- const ImRect& attribute_rect,
- const AttributeType type)
-{
- assert(type == AttributeType_Input || type == AttributeType_Output);
- const float x = type == AttributeType_Input ? (node_rect.Min.x - g->style.pin_offset)
- : (node_rect.Max.x + g->style.pin_offset);
- return ImVec2(x, 0.5f * (attribute_rect.Min.y + attribute_rect.Max.y));
-}
-
-ImVec2 get_screen_space_pin_coordinates(const EditorContext& editor, const PinData& pin)
-{
- const ImRect& parent_node_rect = editor.nodes.pool[pin.parent_node_idx].rect;
- return get_screen_space_pin_coordinates(parent_node_rect, pin.attribute_rect, pin.type);
-}
-
-bool mouse_in_canvas()
-{
- // This flag should be true either when hovering or clicking something in the canvas.
- const bool is_window_hovered_or_focused = ImGui::IsWindowHovered() || ImGui::IsWindowFocused();
-
- return is_window_hovered_or_focused &&
- g->canvas_rect_screen_space.Contains(ImGui::GetMousePos());
-}
-
-void begin_node_selection(EditorContext& editor, const int node_idx)
-{
- // Don't start selecting a node if we are e.g. already creating and dragging
- // a new link! New link creation can happen when the mouse is clicked over
- // a node, but within the hover radius of a pin.
- if (editor.click_interaction_type != ClickInteractionType_None)
- {
- return;
- }
-
- editor.click_interaction_type = ClickInteractionType_Node;
- // If the node is not already contained in the selection, then we want only
- // the interaction node to be selected, effective immediately.
- //
- // Otherwise, we want to allow for the possibility of multiple nodes to be
- // moved at once.
- if (!editor.selected_node_indices.contains(node_idx))
- {
- editor.selected_node_indices.clear();
- editor.selected_link_indices.clear();
- editor.selected_node_indices.push_back(node_idx);
-
- // Ensure that individually selected nodes get rendered on top
- ImVector<int>& depth_stack = editor.node_depth_order;
- const int* const elem = depth_stack.find(node_idx);
- assert(elem != depth_stack.end());
- depth_stack.erase(elem);
- depth_stack.push_back(node_idx);
- }
-}
-
-void begin_link_selection(EditorContext& editor, const int link_idx)
-{
- editor.click_interaction_type = ClickInteractionType_Link;
- // When a link is selected, clear all other selections, and insert the link
- // as the sole selection.
- editor.selected_node_indices.clear();
- editor.selected_link_indices.clear();
- editor.selected_link_indices.push_back(link_idx);
-}
-
-void begin_link_detach(EditorContext& editor, const int link_idx, const int detach_pin_idx)
-{
- const LinkData& link = editor.links.pool[link_idx];
- ClickInteractionState& state = editor.click_interaction_state;
- state.link_creation.end_pin_idx.reset();
- state.link_creation.start_pin_idx =
- detach_pin_idx == link.start_pin_idx ? link.end_pin_idx : link.start_pin_idx;
- g->deleted_link_idx = link_idx;
-}
-
-void begin_link_interaction(EditorContext& editor, const int link_idx)
-{
- // First check if we are clicking a link in the vicinity of a pin.
- // This may result in a link detach via click and drag.
- if (editor.click_interaction_type == ClickInteractionType_LinkCreation)
- {
- if ((g->hovered_pin_flags & AttributeFlags_EnableLinkDetachWithDragClick) != 0)
- {
- begin_link_detach(editor, link_idx, g->hovered_pin_idx.value());
- editor.click_interaction_state.link_creation.link_creation_type =
- LinkCreationType_FromDetach;
- }
- }
- // If we aren't near a pin, check if we are clicking the link with the
- // modifier pressed. This may also result in a link detach via clicking.
- else
- {
- const bool modifier_pressed = g->io.link_detach_with_modifier_click.modifier == NULL
- ? false
- : *g->io.link_detach_with_modifier_click.modifier;
-
- if (modifier_pressed)
- {
- const LinkData& link = editor.links.pool[link_idx];
- const PinData& start_pin = editor.pins.pool[link.start_pin_idx];
- const PinData& end_pin = editor.pins.pool[link.end_pin_idx];
- const ImVec2& mouse_pos = g->mouse_pos;
- const float dist_to_start = ImLengthSqr(start_pin.pos - mouse_pos);
- const float dist_to_end = ImLengthSqr(end_pin.pos - mouse_pos);
- const int closest_pin_idx =
- dist_to_start < dist_to_end ? link.start_pin_idx : link.end_pin_idx;
-
- editor.click_interaction_type = ClickInteractionType_LinkCreation;
- begin_link_detach(editor, link_idx, closest_pin_idx);
- editor.click_interaction_state.link_creation.link_creation_type =
- LinkCreationType_FromDetach;
- }
- else
- {
- begin_link_selection(editor, link_idx);
- }
- }
-}
-
-void begin_link_creation(EditorContext& editor, const int hovered_pin_idx)
-{
- editor.click_interaction_type = ClickInteractionType_LinkCreation;
- editor.click_interaction_state.link_creation.start_pin_idx = hovered_pin_idx;
- editor.click_interaction_state.link_creation.end_pin_idx.reset();
- editor.click_interaction_state.link_creation.link_creation_type = LinkCreationType_Standard;
- g->element_state_change |= ElementStateChange_LinkStarted;
-}
-
-void begin_canvas_interaction(EditorContext& editor)
-{
- const bool any_ui_element_hovered = g->hovered_node_idx.has_value() ||
- g->hovered_link_idx.has_value() ||
- g->hovered_pin_idx.has_value() || ImGui::IsAnyItemHovered();
-
- const bool mouse_not_in_canvas = !mouse_in_canvas();
-
- if (editor.click_interaction_type != ClickInteractionType_None || any_ui_element_hovered ||
- mouse_not_in_canvas)
- {
- return;
- }
-
- const bool started_panning = g->middle_mouse_clicked;
-
- if (started_panning)
- {
- editor.click_interaction_type = ClickInteractionType_Panning;
- }
- else if (g->left_mouse_clicked)
- {
- editor.click_interaction_type = ClickInteractionType_BoxSelection;
- editor.click_interaction_state.box_selector.rect.Min = g->mouse_pos;
- }
-}
-
-void box_selector_update_selection(EditorContext& editor, ImRect box_rect)
-{
- // Invert box selector coordinates as needed
-
- if (box_rect.Min.x > box_rect.Max.x)
- {
- ImSwap(box_rect.Min.x, box_rect.Max.x);
- }
-
- if (box_rect.Min.y > box_rect.Max.y)
- {
- ImSwap(box_rect.Min.y, box_rect.Max.y);
- }
-
- // Update node selection
-
- editor.selected_node_indices.clear();
-
- // Test for overlap against node rectangles
-
- for (int node_idx = 0; node_idx < editor.nodes.pool.size(); ++node_idx)
- {
- if (editor.nodes.in_use[node_idx])
- {
- NodeData& node = editor.nodes.pool[node_idx];
- if (box_rect.Overlaps(node.rect))
- {
- editor.selected_node_indices.push_back(node_idx);
- }
- }
- }
-
- // Update link selection
-
- editor.selected_link_indices.clear();
-
- // Test for overlap against links
-
- for (int link_idx = 0; link_idx < editor.links.pool.size(); ++link_idx)
- {
- if (editor.links.in_use[link_idx])
- {
- const LinkData& link = editor.links.pool[link_idx];
-
- const PinData& pin_start = editor.pins.pool[link.start_pin_idx];
- const PinData& pin_end = editor.pins.pool[link.end_pin_idx];
- const ImRect& node_start_rect = editor.nodes.pool[pin_start.parent_node_idx].rect;
- const ImRect& node_end_rect = editor.nodes.pool[pin_end.parent_node_idx].rect;
-
- const ImVec2 start = get_screen_space_pin_coordinates(
- node_start_rect, pin_start.attribute_rect, pin_start.type);
- const ImVec2 end = get_screen_space_pin_coordinates(
- node_end_rect, pin_end.attribute_rect, pin_end.type);
-
- // Test
- if (rectangle_overlaps_link(box_rect, start, end, pin_start.type))
- {
- editor.selected_link_indices.push_back(link_idx);
- }
- }
- }
-}
-
-void translate_selected_nodes(EditorContext& editor)
-{
- if (g->left_mouse_dragging)
- {
- const ImGuiIO& io = ImGui::GetIO();
- for (int i = 0; i < editor.selected_node_indices.size(); ++i)
- {
- const int node_idx = editor.selected_node_indices[i];
- NodeData& node = editor.nodes.pool[node_idx];
- if (node.draggable)
- {
- node.origin += io.MouseDelta;
- }
- }
- }
-}
-
-OptionalIndex find_duplicate_link(
- const EditorContext& editor,
- const int start_pin_idx,
- const int end_pin_idx)
-{
- LinkData test_link(0);
- test_link.start_pin_idx = start_pin_idx;
- test_link.end_pin_idx = end_pin_idx;
- for (int link_idx = 0; link_idx < editor.links.pool.size(); ++link_idx)
- {
- const LinkData& link = editor.links.pool[link_idx];
- if (LinkPredicate()(test_link, link) && editor.links.in_use[link_idx])
- {
- return OptionalIndex(link_idx);
- }
- }
-
- return OptionalIndex();
-}
-
-bool should_link_snap_to_pin(
- const EditorContext& editor,
- const PinData& start_pin,
- const int hovered_pin_idx,
- const OptionalIndex duplicate_link)
-{
- const PinData& end_pin = editor.pins.pool[hovered_pin_idx];
-
- // The end pin must be in a different node
- if (start_pin.parent_node_idx == end_pin.parent_node_idx)
- {
- return false;
- }
-
- // The end pin must be of a different type
- if (start_pin.type == end_pin.type)
- {
- return false;
- }
-
- // The link to be created must not be a duplicate, unless it is the link which was created on
- // snap. In that case we want to snap, since we want it to appear visually as if the created
- // link remains snapped to the pin.
- if (duplicate_link.has_value() && !(duplicate_link == g->snap_link_idx))
- {
- return false;
- }
-
- return true;
-}
-
-void click_interaction_update(EditorContext& editor)
-{
- switch (editor.click_interaction_type)
- {
- case ClickInteractionType_BoxSelection:
- {
- ImRect& box_rect = editor.click_interaction_state.box_selector.rect;
- box_rect.Max = g->mouse_pos;
-
- box_selector_update_selection(editor, box_rect);
-
- const ImU32 box_selector_color = g->style.colors[ColorStyle_BoxSelector];
- const ImU32 box_selector_outline = g->style.colors[ColorStyle_BoxSelectorOutline];
- g->canvas_draw_list->AddRectFilled(box_rect.Min, box_rect.Max, box_selector_color);
- g->canvas_draw_list->AddRect(box_rect.Min, box_rect.Max, box_selector_outline);
-
- if (g->left_mouse_released)
- {
- ImVector<int>& depth_stack = editor.node_depth_order;
- const ImVector<int>& selected_idxs = editor.selected_node_indices;
-
- // Bump the selected node indices, in order, to the top of the depth stack.
- // NOTE: this algorithm has worst case time complexity of O(N^2), if the node selection
- // is ~ N (due to selected_idxs.contains()).
-
- if ((selected_idxs.Size > 0) && (selected_idxs.Size < depth_stack.Size))
- {
- int num_moved = 0; // The number of indices moved. Stop after selected_idxs.Size
- for (int i = 0; i < depth_stack.Size - selected_idxs.Size; ++i)
- {
- for (int node_idx = depth_stack[i]; selected_idxs.contains(node_idx);
- node_idx = depth_stack[i])
- {
- depth_stack.erase(depth_stack.begin() + static_cast<size_t>(i));
- depth_stack.push_back(node_idx);
- ++num_moved;
- }
-
- if (num_moved == selected_idxs.Size)
- {
- break;
- }
- }
- }
-
- editor.click_interaction_type = ClickInteractionType_None;
- }
- }
- break;
- case ClickInteractionType_Node:
- {
- translate_selected_nodes(editor);
-
- if (g->left_mouse_released)
- {
- editor.click_interaction_type = ClickInteractionType_None;
- }
- }
- break;
- case ClickInteractionType_Link:
- {
- if (g->left_mouse_released)
- {
- editor.click_interaction_type = ClickInteractionType_None;
- }
- }
- break;
- case ClickInteractionType_LinkCreation:
- {
- const PinData& start_pin =
- editor.pins.pool[editor.click_interaction_state.link_creation.start_pin_idx];
-
- const OptionalIndex maybe_duplicate_link_idx =
- g->hovered_pin_idx.has_value()
- ? find_duplicate_link(
- editor,
- editor.click_interaction_state.link_creation.start_pin_idx,
- g->hovered_pin_idx.value())
- : OptionalIndex();
-
- const bool should_snap =
- g->hovered_pin_idx.has_value() &&
- should_link_snap_to_pin(
- editor, start_pin, g->hovered_pin_idx.value(), maybe_duplicate_link_idx);
-
- // If we created on snap and the hovered pin is empty or changed, then we need signal that
- // the link's state has changed.
- const bool snapping_pin_changed =
- editor.click_interaction_state.link_creation.end_pin_idx.has_value() &&
- !(g->hovered_pin_idx == editor.click_interaction_state.link_creation.end_pin_idx);
-
- // Detach the link that was created by this link event if it's no longer in snap range
- if (snapping_pin_changed && g->snap_link_idx.has_value())
- {
- begin_link_detach(
- editor,
- g->snap_link_idx.value(),
- editor.click_interaction_state.link_creation.end_pin_idx.value());
- }
-
- const ImVec2 start_pos = get_screen_space_pin_coordinates(editor, start_pin);
- // If we are within the hover radius of a receiving pin, snap the link
- // endpoint to it
- const ImVec2 end_pos = should_snap
- ? get_screen_space_pin_coordinates(
- editor, editor.pins.pool[g->hovered_pin_idx.value()])
- : g->mouse_pos;
-
- const LinkBezierData link_data = get_link_renderable(
- start_pos, end_pos, start_pin.type, g->style.link_line_segments_per_length);
-#if IMGUI_VERSION_NUM < 18000
- g->canvas_draw_list->AddBezierCurve(
-#else
- g->canvas_draw_list->AddBezierCubic(
-#endif
- link_data.bezier.p0,
- link_data.bezier.p1,
- link_data.bezier.p2,
- link_data.bezier.p3,
- g->style.colors[ColorStyle_Link],
- g->style.link_thickness,
- link_data.num_segments);
-
- const bool link_creation_on_snap =
- g->hovered_pin_idx.has_value() && (editor.pins.pool[g->hovered_pin_idx.value()].flags &
- AttributeFlags_EnableLinkCreationOnSnap);
-
- if (!should_snap)
- {
- editor.click_interaction_state.link_creation.end_pin_idx.reset();
- }
-
- const bool create_link = should_snap && (g->left_mouse_released || link_creation_on_snap);
-
- if (create_link && !maybe_duplicate_link_idx.has_value())
- {
- // Avoid send OnLinkCreated() events every frame if the snap link is not saved
- // (only applies for EnableLinkCreationOnSnap)
- if (!g->left_mouse_released &&
- editor.click_interaction_state.link_creation.end_pin_idx == g->hovered_pin_idx)
- {
- break;
- }
-
- g->element_state_change |= ElementStateChange_LinkCreated;
- editor.click_interaction_state.link_creation.end_pin_idx = g->hovered_pin_idx.value();
- }
-
- if (g->left_mouse_released)
- {
- editor.click_interaction_type = ClickInteractionType_None;
- if (!create_link)
- {
- g->element_state_change |= ElementStateChange_LinkDropped;
- }
- }
- }
- break;
- case ClickInteractionType_Panning:
- {
- const bool dragging = g->middle_mouse_dragging;
-
- if (dragging)
- {
- editor.panning += ImGui::GetIO().MouseDelta;
- }
- else
- {
- editor.click_interaction_type = ClickInteractionType_None;
- }
- }
- break;
- case ClickInteractionType_None:
- break;
- default:
- assert(!"Unreachable code!");
- break;
- }
-}
-
-void resolve_occluded_pins(const EditorContext& editor, ImVector<int>& occluded_pin_indices)
-{
- const ImVector<int>& depth_stack = editor.node_depth_order;
-
- occluded_pin_indices.resize(0);
-
- if (depth_stack.Size < 2)
- {
- return;
- }
-
- // For each node in the depth stack
- for (int depth_idx = 0; depth_idx < (depth_stack.Size - 1); ++depth_idx)
- {
- const NodeData& node_below = editor.nodes.pool[depth_stack[depth_idx]];
-
- // Iterate over the rest of the depth stack to find nodes overlapping the pins
- for (int next_depth_idx = depth_idx + 1; next_depth_idx < depth_stack.Size;
- ++next_depth_idx)
- {
- const ImRect& rect_above = editor.nodes.pool[depth_stack[next_depth_idx]].rect;
-
- // Iterate over each pin
- for (int idx = 0; idx < node_below.pin_indices.Size; ++idx)
- {
- const int pin_idx = node_below.pin_indices[idx];
- const ImVec2& pin_pos = editor.pins.pool[pin_idx].pos;
-
- if (rect_above.Contains(pin_pos))
- {
- occluded_pin_indices.push_back(pin_idx);
- }
- }
- }
- }
-}
-
-OptionalIndex resolve_hovered_pin(
- const ObjectPool<PinData>& pins,
- const ImVector<int>& occluded_pin_indices)
-{
- float smallest_distance = FLT_MAX;
- OptionalIndex pin_idx_with_smallest_distance;
-
- const float hover_radius_sqr = g->style.pin_hover_radius * g->style.pin_hover_radius;
-
- for (int idx = 0; idx < pins.pool.Size; ++idx)
- {
- if (!pins.in_use[idx])
- {
- continue;
- }
-
- if (occluded_pin_indices.contains(idx))
- {
- continue;
- }
-
- const ImVec2& pin_pos = pins.pool[idx].pos;
- const float distance_sqr = ImLengthSqr(pin_pos - g->mouse_pos);
-
- // TODO: g->style.pin_hover_radius needs to be copied into pin data and the pin-local value
- // used here. This is no longer called in BeginAttribute/EndAttribute scope and the detected
- // pin might have a different hover radius than what the user had when calling
- // BeginAttribute/EndAttribute.
- if (distance_sqr < hover_radius_sqr && distance_sqr < smallest_distance)
- {
- smallest_distance = distance_sqr;
- pin_idx_with_smallest_distance = idx;
- }
- }
-
- return pin_idx_with_smallest_distance;
-}
-
-OptionalIndex resolve_hovered_node(const ImVector<int>& depth_stack)
-{
- if (g->node_indices_overlapping_with_mouse.size() == 0)
- {
- return OptionalIndex();
- }
-
- if (g->node_indices_overlapping_with_mouse.size() == 1)
- {
- return OptionalIndex(g->node_indices_overlapping_with_mouse[0]);
- }
-
- int largest_depth_idx = -1;
- int node_idx_on_top = -1;
-
- for (int i = 0; i < g->node_indices_overlapping_with_mouse.size(); ++i)
- {
- const int node_idx = g->node_indices_overlapping_with_mouse[i];
- for (int depth_idx = 0; depth_idx < depth_stack.size(); ++depth_idx)
- {
- if (depth_stack[depth_idx] == node_idx && (depth_idx > largest_depth_idx))
- {
- largest_depth_idx = depth_idx;
- node_idx_on_top = node_idx;
- }
- }
- }
-
- assert(node_idx_on_top != -1);
- return OptionalIndex(node_idx_on_top);
-}
-
-OptionalIndex resolve_hovered_link(
- const ObjectPool<LinkData>& links,
- const ObjectPool<PinData>& pins)
-{
- float smallest_distance = FLT_MAX;
- OptionalIndex link_idx_with_smallest_distance;
-
- // There are two ways a link can be detected as "hovered".
- // 1. The link is within hover distance to the mouse. The closest such link is selected as being
- // hovered over.
- // 2. If the link is connected to the currently hovered pin.
- //
- // The latter is a requirement for link detaching with drag click to work, as both a link and
- // pin are required to be hovered over for the feature to work.
-
- for (int idx = 0; idx < links.pool.Size; ++idx)
- {
- if (!links.in_use[idx])
- {
- continue;
- }
-
- const LinkData& link = links.pool[idx];
- const PinData& start_pin = pins.pool[link.start_pin_idx];
- const PinData& end_pin = pins.pool[link.end_pin_idx];
-
- if (g->hovered_pin_idx == link.start_pin_idx || g->hovered_pin_idx == link.end_pin_idx)
- {
- return idx;
- }
-
- // TODO: the calculated LinkBezierDatas could be cached since we generate them again when
- // rendering the links
-
- const LinkBezierData link_data = get_link_renderable(
- start_pin.pos, end_pin.pos, start_pin.type, g->style.link_line_segments_per_length);
-
- // The distance test
- {
- const ImRect link_rect = get_containing_rect_for_bezier_curve(link_data.bezier);
-
- // First, do a simple bounding box test against the box containing the link
- // to see whether calculating the distance to the link is worth doing.
- if (link_rect.Contains(g->mouse_pos))
- {
- const float distance = get_distance_to_cubic_bezier(
- g->mouse_pos, link_data.bezier, link_data.num_segments);
-
- // TODO: g->style.link_hover_distance could be also copied into LinkData, since
- // we're not calling this function in the same scope as ImNodes::Link(). The
- // rendered/detected link might have a different hover distance than what the user
- // had specified when calling Link()
- if (distance < g->style.link_hover_distance)
- {
- smallest_distance = distance;
- link_idx_with_smallest_distance = idx;
- }
- }
- }
- }
-
- return link_idx_with_smallest_distance;
-}
-
-// [SECTION] render helpers
-
-inline ImVec2 screen_space_to_grid_space(const EditorContext& editor, const ImVec2& v)
-{
- return v - g->canvas_origin_screen_space - editor.panning;
-}
-
-inline ImVec2 grid_space_to_screen_space(const EditorContext& editor, const ImVec2& v)
-{
- return v + g->canvas_origin_screen_space + editor.panning;
-}
-
-inline ImVec2 grid_space_to_editor_space(const EditorContext& editor, const ImVec2& v)
-{
- return v + editor.panning;
-}
-
-inline ImVec2 editor_space_to_grid_space(const EditorContext& editor, const ImVec2& v)
-{
- return v - editor.panning;
-}
-
-inline ImVec2 editor_space_to_screen_space(const ImVec2& v)
-{
- return g->canvas_origin_screen_space + v;
-}
-
-inline ImRect get_item_rect() { return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); }
-
-inline ImVec2 get_node_title_bar_origin(const NodeData& node)
-{
- return node.origin + node.layout_style.padding;
-}
-
-inline ImVec2 get_node_content_origin(const NodeData& node)
-{
- const ImVec2 title_bar_height =
- ImVec2(0.f, node.title_bar_content_rect.GetHeight() + 2.0f * node.layout_style.padding.y);
- return node.origin + title_bar_height + node.layout_style.padding;
-}
-
-inline ImRect get_node_title_rect(const NodeData& node)
-{
- ImRect expanded_title_rect = node.title_bar_content_rect;
- expanded_title_rect.Expand(node.layout_style.padding);
-
- return ImRect(
- expanded_title_rect.Min,
- expanded_title_rect.Min + ImVec2(node.rect.GetWidth(), 0.f) +
- ImVec2(0.f, expanded_title_rect.GetHeight()));
-}
-
-void draw_grid(EditorContext& editor, const ImVec2& canvas_size)
-{
- const ImVec2 offset = editor.panning;
-
- for (float x = fmodf(offset.x, g->style.grid_spacing); x < canvas_size.x;
- x += g->style.grid_spacing)
- {
- g->canvas_draw_list->AddLine(
- editor_space_to_screen_space(ImVec2(x, 0.0f)),
- editor_space_to_screen_space(ImVec2(x, canvas_size.y)),
- g->style.colors[ColorStyle_GridLine]);
- }
-
- for (float y = fmodf(offset.y, g->style.grid_spacing); y < canvas_size.y;
- y += g->style.grid_spacing)
- {
- g->canvas_draw_list->AddLine(
- editor_space_to_screen_space(ImVec2(0.0f, y)),
- editor_space_to_screen_space(ImVec2(canvas_size.x, y)),
- g->style.colors[ColorStyle_GridLine]);
- }
-}
-
-struct QuadOffsets
-{
- ImVec2 top_left, bottom_left, bottom_right, top_right;
-};
-
-QuadOffsets calculate_quad_offsets(const float side_length)
-{
- const float half_side = 0.5f * side_length;
-
- QuadOffsets offset;
-
- offset.top_left = ImVec2(-half_side, half_side);
- offset.bottom_left = ImVec2(-half_side, -half_side);
- offset.bottom_right = ImVec2(half_side, -half_side);
- offset.top_right = ImVec2(half_side, half_side);
-
- return offset;
-}
-
-struct TriangleOffsets
-{
- ImVec2 top_left, bottom_left, right;
-};
-
-TriangleOffsets calculate_triangle_offsets(const float side_length)
-{
- // Calculates the Vec2 offsets from an equilateral triangle's midpoint to
- // its vertices. Here is how the left_offset and right_offset are
- // calculated.
- //
- // For an equilateral triangle of side length s, the
- // triangle's height, h, is h = s * sqrt(3) / 2.
- //
- // The length from the base to the midpoint is (1 / 3) * h. The length from
- // the midpoint to the triangle vertex is (2 / 3) * h.
- const float sqrt_3 = sqrtf(3.0f);
- const float left_offset = -0.1666666666667f * sqrt_3 * side_length;
- const float right_offset = 0.333333333333f * sqrt_3 * side_length;
- const float vertical_offset = 0.5f * side_length;
-
- TriangleOffsets offset;
- offset.top_left = ImVec2(left_offset, vertical_offset);
- offset.bottom_left = ImVec2(left_offset, -vertical_offset);
- offset.right = ImVec2(right_offset, 0.f);
-
- return offset;
-}
-
-void draw_pin_shape(const ImVec2& pin_pos, const PinData& pin, const ImU32 pin_color)
-{
- static const int circle_num_segments = 8;
-
- switch (pin.shape)
- {
- case PinShape_Circle:
- {
- g->canvas_draw_list->AddCircle(
- pin_pos,
- g->style.pin_circle_radius,
- pin_color,
- circle_num_segments,
- g->style.pin_line_thickness);
- }
- break;
- case PinShape_CircleFilled:
- {
- g->canvas_draw_list->AddCircleFilled(
- pin_pos, g->style.pin_circle_radius, pin_color, circle_num_segments);
- }
- break;
- case PinShape_Quad:
- {
- const QuadOffsets offset = calculate_quad_offsets(g->style.pin_quad_side_length);
- g->canvas_draw_list->AddQuad(
- pin_pos + offset.top_left,
- pin_pos + offset.bottom_left,
- pin_pos + offset.bottom_right,
- pin_pos + offset.top_right,
- pin_color,
- g->style.pin_line_thickness);
- }
- break;
- case PinShape_QuadFilled:
- {
- const QuadOffsets offset = calculate_quad_offsets(g->style.pin_quad_side_length);
- g->canvas_draw_list->AddQuadFilled(
- pin_pos + offset.top_left,
- pin_pos + offset.bottom_left,
- pin_pos + offset.bottom_right,
- pin_pos + offset.top_right,
- pin_color);
- }
- break;
- case PinShape_Triangle:
- {
- const TriangleOffsets offset =
- calculate_triangle_offsets(g->style.pin_triangle_side_length);
- g->canvas_draw_list->AddTriangle(
- pin_pos + offset.top_left,
- pin_pos + offset.bottom_left,
- pin_pos + offset.right,
- pin_color,
- // NOTE: for some weird reason, the line drawn by AddTriangle is
- // much thinner than the lines drawn by AddCircle or AddQuad.
- // Multiplying the line thickness by two seemed to solve the
- // problem at a few different thickness values.
- 2.f * g->style.pin_line_thickness);
- }
- break;
- case PinShape_TriangleFilled:
- {
- const TriangleOffsets offset =
- calculate_triangle_offsets(g->style.pin_triangle_side_length);
- g->canvas_draw_list->AddTriangleFilled(
- pin_pos + offset.top_left,
- pin_pos + offset.bottom_left,
- pin_pos + offset.right,
- pin_color);
- }
- break;
- default:
- assert(!"Invalid PinShape value!");
- break;
- }
-}
-
-void draw_pin(EditorContext& editor, const int pin_idx, const bool left_mouse_clicked)
-{
- PinData& pin = editor.pins.pool[pin_idx];
- const ImRect& parent_node_rect = editor.nodes.pool[pin.parent_node_idx].rect;
-
- pin.pos = get_screen_space_pin_coordinates(parent_node_rect, pin.attribute_rect, pin.type);
-
- ImU32 pin_color = pin.color_style.background;
-
- const bool pin_hovered = g->hovered_pin_idx == pin_idx &&
- editor.click_interaction_type != ClickInteractionType_BoxSelection;
-
- if (pin_hovered)
- {
- g->hovered_pin_idx = pin_idx;
- g->hovered_pin_flags = pin.flags;
- pin_color = pin.color_style.hovered;
-
- if (left_mouse_clicked)
- {
- begin_link_creation(editor, pin_idx);
- }
- }
-
- draw_pin_shape(pin.pos, pin, pin_color);
-}
-
-void draw_node(EditorContext& editor, const int node_idx)
-{
- const NodeData& node = editor.nodes.pool[node_idx];
- ImGui::SetCursorPos(node.origin + editor.panning);
-
- const bool node_hovered = g->hovered_node_idx == node_idx &&
- editor.click_interaction_type != ClickInteractionType_BoxSelection;
-
- ImU32 node_background = node.color_style.background;
- ImU32 titlebar_background = node.color_style.titlebar;
-
- if (editor.selected_node_indices.contains(node_idx))
- {
- node_background = node.color_style.background_selected;
- titlebar_background = node.color_style.titlebar_selected;
- }
- else if (node_hovered)
- {
- node_background = node.color_style.background_hovered;
- titlebar_background = node.color_style.titlebar_hovered;
- }
-
- {
- // node base
- g->canvas_draw_list->AddRectFilled(
- node.rect.Min, node.rect.Max, node_background, node.layout_style.corner_rounding);
-
- // title bar:
- if (node.title_bar_content_rect.GetHeight() > 0.f)
- {
- ImRect title_bar_rect = get_node_title_rect(node);
-
-#if IMGUI_VERSION_NUM < 18200
- g->canvas_draw_list->AddRectFilled(
- title_bar_rect.Min,
- title_bar_rect.Max,
- titlebar_background,
- node.layout_style.corner_rounding,
- ImDrawCornerFlags_Top);
-#else
- g->canvas_draw_list->AddRectFilled(
- title_bar_rect.Min,
- title_bar_rect.Max,
- titlebar_background,
- node.layout_style.corner_rounding,
- ImDrawFlags_RoundCornersTop);
-
-#endif
- }
-
- if ((g->style.flags & StyleFlags_NodeOutline) != 0)
- {
-#if IMGUI_VERSION_NUM < 18200
- g->canvas_draw_list->AddRect(
- node.rect.Min,
- node.rect.Max,
- node.color_style.outline,
- node.layout_style.corner_rounding,
- ImDrawCornerFlags_All,
- node.layout_style.border_thickness);
-#else
- g->canvas_draw_list->AddRect(
- node.rect.Min,
- node.rect.Max,
- node.color_style.outline,
- node.layout_style.corner_rounding,
- ImDrawFlags_RoundCornersAll,
- node.layout_style.border_thickness);
-#endif
- }
- }
-
- for (int i = 0; i < node.pin_indices.size(); ++i)
- {
- draw_pin(editor, node.pin_indices[i], g->left_mouse_clicked);
- }
-
- if (node_hovered)
- {
- g->hovered_node_idx = node_idx;
- const bool node_free_to_move = g->interactive_node_idx != node_idx;
- if (g->left_mouse_clicked && node_free_to_move)
- {
- begin_node_selection(editor, node_idx);
- }
- }
-}
-
-void draw_link(EditorContext& editor, const int link_idx)
-{
- const LinkData& link = editor.links.pool[link_idx];
- const PinData& start_pin = editor.pins.pool[link.start_pin_idx];
- const PinData& end_pin = editor.pins.pool[link.end_pin_idx];
-
- const LinkBezierData link_data = get_link_renderable(
- start_pin.pos, end_pin.pos, start_pin.type, g->style.link_line_segments_per_length);
-
- const bool link_hovered = g->hovered_link_idx == link_idx &&
- editor.click_interaction_type != ClickInteractionType_BoxSelection;
-
- if (link_hovered)
- {
- g->hovered_link_idx = link_idx;
- if (g->left_mouse_clicked)
- {
- begin_link_interaction(editor, link_idx);
- }
- }
-
- // It's possible for a link to be deleted in begin_link_interaction. A user
- // may detach a link, resulting in the link wire snapping to the mouse
- // position.
- //
- // In other words, skip rendering the link if it was deleted.
- if (g->deleted_link_idx == link_idx)
- {
- return;
- }
-
- ImU32 link_color = link.color_style.base;
- if (editor.selected_link_indices.contains(link_idx))
- {
- link_color = link.color_style.selected;
- }
- else if (link_hovered)
- {
- link_color = link.color_style.hovered;
- }
-
-#if IMGUI_VERSION_NUM < 18000
- g->canvas_draw_list->AddBezierCurve(
-#else
- g->canvas_draw_list->AddBezierCubic(
-#endif
- link_data.bezier.p0,
- link_data.bezier.p1,
- link_data.bezier.p2,
- link_data.bezier.p3,
- link_color,
- g->style.link_thickness,
- link_data.num_segments);
-}
-
-void begin_pin_attribute(
- const int id,
- const AttributeType type,
- const PinShape shape,
- const int node_idx)
-{
- // Make sure to call BeginNode() before calling
- // BeginAttribute()
- assert(g->current_scope == Scope_Node);
- g->current_scope = Scope_Attribute;
-
- ImGui::BeginGroup();
- ImGui::PushID(id);
-
- EditorContext& editor = editor_context_get();
-
- g->current_attribute_id = id;
-
- const int pin_idx = object_pool_find_or_create_index(editor.pins, id);
- g->current_pin_idx = pin_idx;
- PinData& pin = editor.pins.pool[pin_idx];
- pin.id = id;
- pin.parent_node_idx = node_idx;
- pin.type = type;
- pin.shape = shape;
- pin.flags = g->current_attribute_flags;
- pin.color_style.background = g->style.colors[ColorStyle_Pin];
- pin.color_style.hovered = g->style.colors[ColorStyle_PinHovered];
-}
-
-void end_pin_attribute()
-{
- assert(g->current_scope == Scope_Attribute);
- g->current_scope = Scope_Node;
-
- ImGui::PopID();
- ImGui::EndGroup();
-
- if (ImGui::IsItemActive())
- {
- g->active_attribute = true;
- g->active_attribute_id = g->current_attribute_id;
- g->interactive_node_idx = g->current_node_idx;
- }
-
- EditorContext& editor = editor_context_get();
- PinData& pin = editor.pins.pool[g->current_pin_idx];
- NodeData& node = editor.nodes.pool[g->current_node_idx];
- pin.attribute_rect = get_item_rect();
- node.pin_indices.push_back(g->current_pin_idx);
-}
-
-void initialize(Context* context)
-{
- context->canvas_origin_screen_space = ImVec2(0.0f, 0.0f);
- context->canvas_rect_screen_space = ImRect(ImVec2(0.f, 0.f), ImVec2(0.f, 0.f));
- context->current_scope = Scope_None;
-
- context->current_pin_idx = INT_MAX;
- context->current_node_idx = INT_MAX;
-
- context->default_editor_ctx = EditorContextCreate();
- EditorContextSet(g->default_editor_ctx);
-
- context->current_attribute_flags = AttributeFlags_None;
- context->attribute_flag_stack.push_back(g->current_attribute_flags);
-
- StyleColorsDark();
-}
-
-void shutdown(Context* ctx) { EditorContextFree(ctx->default_editor_ctx); }
-} // namespace
-
-// [SECTION] API implementation
-
-IO::EmulateThreeButtonMouse::EmulateThreeButtonMouse() : modifier(NULL) {}
-
-IO::LinkDetachWithModifierClick::LinkDetachWithModifierClick() : modifier(NULL) {}
-
-IO::IO() : emulate_three_button_mouse(), link_detach_with_modifier_click() {}
-
-Style::Style()
- : grid_spacing(32.f), node_corner_rounding(4.f), node_padding_horizontal(8.f),
- node_padding_vertical(8.f), node_border_thickness(1.f), link_thickness(3.f),
- link_line_segments_per_length(0.1f), link_hover_distance(10.f), pin_circle_radius(4.f),
- pin_quad_side_length(7.f), pin_triangle_side_length(9.5), pin_line_thickness(1.f),
- pin_hover_radius(10.f), pin_offset(0.f),
- flags(StyleFlags(StyleFlags_NodeOutline | StyleFlags_GridLines)), colors()
-{
-}
-
-Context* CreateContext()
-{
- Context* ctx = IM_NEW(Context)();
- if (g == NULL)
- SetCurrentContext(ctx);
- initialize(ctx);
- return ctx;
-}
-
-void DestroyContext(Context* ctx)
-{
- if (ctx == NULL)
- ctx = g;
- shutdown(ctx);
- if (g == ctx)
- SetCurrentContext(NULL);
- IM_DELETE(ctx);
-}
-
-Context* GetCurrentContext() { return g; }
-
-void SetCurrentContext(Context* ctx) { g = ctx; }
-
-EditorContext* EditorContextCreate()
-{
- void* mem = ImGui::MemAlloc(sizeof(EditorContext));
- new (mem) EditorContext();
- return (EditorContext*)mem;
-}
-
-void EditorContextFree(EditorContext* ctx)
-{
- ctx->~EditorContext();
- ImGui::MemFree(ctx);
-}
-
-void EditorContextSet(EditorContext* ctx) { g->editor_ctx = ctx; }
-
-ImVec2 EditorContextGetPanning()
-{
- const EditorContext& editor = editor_context_get();
- return editor.panning;
-}
-
-void EditorContextResetPanning(const ImVec2& pos)
-{
- EditorContext& editor = editor_context_get();
- editor.panning = pos;
-}
-
-void EditorContextMoveToNode(const int node_id)
-{
- EditorContext& editor = editor_context_get();
- NodeData& node = object_pool_find_or_create_object(editor.nodes, node_id);
-
- editor.panning.x = -node.origin.x;
- editor.panning.y = -node.origin.y;
-}
-
-void SetImGuiContext(ImGuiContext* ctx) { ImGui::SetCurrentContext(ctx); }
-
-IO& GetIO() { return g->io; }
-
-Style& GetStyle() { return g->style; }
-
-void StyleColorsDark()
-{
- g->style.colors[ColorStyle_NodeBackground] = IM_COL32(50, 50, 50, 255);
- g->style.colors[ColorStyle_NodeBackgroundHovered] = IM_COL32(75, 75, 75, 255);
- g->style.colors[ColorStyle_NodeBackgroundSelected] = IM_COL32(75, 75, 75, 255);
- g->style.colors[ColorStyle_NodeOutline] = IM_COL32(100, 100, 100, 255);
- // title bar colors match ImGui's titlebg colors
- g->style.colors[ColorStyle_TitleBar] = IM_COL32(41, 74, 122, 255);
- g->style.colors[ColorStyle_TitleBarHovered] = IM_COL32(66, 150, 250, 255);
- g->style.colors[ColorStyle_TitleBarSelected] = IM_COL32(66, 150, 250, 255);
- // link colors match ImGui's slider grab colors
- g->style.colors[ColorStyle_Link] = IM_COL32(61, 133, 224, 200);
- g->style.colors[ColorStyle_LinkHovered] = IM_COL32(66, 150, 250, 255);
- g->style.colors[ColorStyle_LinkSelected] = IM_COL32(66, 150, 250, 255);
- // pin colors match ImGui's button colors
- g->style.colors[ColorStyle_Pin] = IM_COL32(53, 150, 250, 180);
- g->style.colors[ColorStyle_PinHovered] = IM_COL32(53, 150, 250, 255);
-
- g->style.colors[ColorStyle_BoxSelector] = IM_COL32(61, 133, 224, 30);
- g->style.colors[ColorStyle_BoxSelectorOutline] = IM_COL32(61, 133, 224, 150);
-
- g->style.colors[ColorStyle_GridBackground] = IM_COL32(40, 40, 50, 200);
- g->style.colors[ColorStyle_GridLine] = IM_COL32(200, 200, 200, 40);
-}
-
-void StyleColorsClassic()
-{
- g->style.colors[ColorStyle_NodeBackground] = IM_COL32(50, 50, 50, 255);
- g->style.colors[ColorStyle_NodeBackgroundHovered] = IM_COL32(75, 75, 75, 255);
- g->style.colors[ColorStyle_NodeBackgroundSelected] = IM_COL32(75, 75, 75, 255);
- g->style.colors[ColorStyle_NodeOutline] = IM_COL32(100, 100, 100, 255);
- g->style.colors[ColorStyle_TitleBar] = IM_COL32(69, 69, 138, 255);
- g->style.colors[ColorStyle_TitleBarHovered] = IM_COL32(82, 82, 161, 255);
- g->style.colors[ColorStyle_TitleBarSelected] = IM_COL32(82, 82, 161, 255);
- g->style.colors[ColorStyle_Link] = IM_COL32(255, 255, 255, 100);
- g->style.colors[ColorStyle_LinkHovered] = IM_COL32(105, 99, 204, 153);
- g->style.colors[ColorStyle_LinkSelected] = IM_COL32(105, 99, 204, 153);
- g->style.colors[ColorStyle_Pin] = IM_COL32(89, 102, 156, 170);
- g->style.colors[ColorStyle_PinHovered] = IM_COL32(102, 122, 179, 200);
- g->style.colors[ColorStyle_BoxSelector] = IM_COL32(82, 82, 161, 100);
- g->style.colors[ColorStyle_BoxSelectorOutline] = IM_COL32(82, 82, 161, 255);
- g->style.colors[ColorStyle_GridBackground] = IM_COL32(40, 40, 50, 200);
- g->style.colors[ColorStyle_GridLine] = IM_COL32(200, 200, 200, 40);
-}
-
-void StyleColorsLight()
-{
- g->style.colors[ColorStyle_NodeBackground] = IM_COL32(240, 240, 240, 255);
- g->style.colors[ColorStyle_NodeBackgroundHovered] = IM_COL32(240, 240, 240, 255);
- g->style.colors[ColorStyle_NodeBackgroundSelected] = IM_COL32(240, 240, 240, 255);
- g->style.colors[ColorStyle_NodeOutline] = IM_COL32(100, 100, 100, 255);
- g->style.colors[ColorStyle_TitleBar] = IM_COL32(248, 248, 248, 255);
- g->style.colors[ColorStyle_TitleBarHovered] = IM_COL32(209, 209, 209, 255);
- g->style.colors[ColorStyle_TitleBarSelected] = IM_COL32(209, 209, 209, 255);
- // original imgui values: 66, 150, 250
- g->style.colors[ColorStyle_Link] = IM_COL32(66, 150, 250, 100);
- // original imgui values: 117, 138, 204
- g->style.colors[ColorStyle_LinkHovered] = IM_COL32(66, 150, 250, 242);
- g->style.colors[ColorStyle_LinkSelected] = IM_COL32(66, 150, 250, 242);
- // original imgui values: 66, 150, 250
- g->style.colors[ColorStyle_Pin] = IM_COL32(66, 150, 250, 160);
- g->style.colors[ColorStyle_PinHovered] = IM_COL32(66, 150, 250, 255);
- g->style.colors[ColorStyle_BoxSelector] = IM_COL32(90, 170, 250, 30);
- g->style.colors[ColorStyle_BoxSelectorOutline] = IM_COL32(90, 170, 250, 150);
- g->style.colors[ColorStyle_GridBackground] = IM_COL32(225, 225, 225, 255);
- g->style.colors[ColorStyle_GridLine] = IM_COL32(180, 180, 180, 100);
- g->style.flags = StyleFlags(StyleFlags_None);
-}
-
-void BeginNodeEditor()
-{
- assert(g->current_scope == Scope_None);
- g->current_scope = Scope_Editor;
-
- // Reset state from previous pass
-
- EditorContext& editor = editor_context_get();
- object_pool_reset(editor.nodes);
- object_pool_reset(editor.pins);
- object_pool_reset(editor.links);
-
- g->hovered_node_idx.reset();
- g->interactive_node_idx.reset();
- g->hovered_link_idx.reset();
- g->hovered_pin_idx.reset();
- g->hovered_pin_flags = AttributeFlags_None;
- g->deleted_link_idx.reset();
- g->snap_link_idx.reset();
-
- g->node_indices_overlapping_with_mouse.clear();
-
- g->element_state_change = ElementStateChange_None;
-
- g->mouse_pos = ImGui::GetIO().MousePos;
- g->left_mouse_clicked = ImGui::IsMouseClicked(0);
- g->left_mouse_released = ImGui::IsMouseReleased(0);
- g->middle_mouse_clicked =
- (g->io.emulate_three_button_mouse.modifier != NULL &&
- *g->io.emulate_three_button_mouse.modifier && g->left_mouse_clicked) ||
- ImGui::IsMouseClicked(2);
- g->left_mouse_dragging = ImGui::IsMouseDragging(0, 0.0f);
- g->middle_mouse_dragging =
- (g->io.emulate_three_button_mouse.modifier != NULL && g->left_mouse_dragging &&
- (*g->io.emulate_three_button_mouse.modifier)) ||
- ImGui::IsMouseDragging(2, 0.0f);
-
- g->active_attribute = false;
-
- ImGui::BeginGroup();
- {
- ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1.f, 1.f));
- ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
- ImGui::PushStyleColor(ImGuiCol_ChildBg, g->style.colors[ColorStyle_GridBackground]);
- ImGui::BeginChild(
- "scrolling_region",
- ImVec2(0.f, 0.f),
- true,
- ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove |
- ImGuiWindowFlags_NoScrollWithMouse);
- g->canvas_origin_screen_space = ImGui::GetCursorScreenPos();
-
- // NOTE: we have to fetch the canvas draw list *after* we call
- // BeginChild(), otherwise the ImGui UI elements are going to be
- // rendered into the parent window draw list.
- draw_list_set(ImGui::GetWindowDrawList());
-
- {
- const ImVec2 canvas_size = ImGui::GetWindowSize();
- g->canvas_rect_screen_space = ImRect(
- editor_space_to_screen_space(ImVec2(0.f, 0.f)),
- editor_space_to_screen_space(canvas_size));
-
- if (g->style.flags & StyleFlags_GridLines)
- {
- draw_grid(editor, canvas_size);
- }
- }
- }
-}
-
-void EndNodeEditor()
-{
- assert(g->current_scope == Scope_Editor);
- g->current_scope = Scope_None;
-
- EditorContext& editor = editor_context_get();
-
- // Detect which UI element is being hovered over. Detection is done in a hierarchical fashion,
- // because a UI element being hovered excludes any other as being hovered over.
-
- if (mouse_in_canvas())
- {
- // Pins needs some special care. We need to check the depth stack to see which pins are
- // being occluded by other nodes.
- resolve_occluded_pins(editor, g->occluded_pin_indices);
-
- g->hovered_pin_idx = resolve_hovered_pin(editor.pins, g->occluded_pin_indices);
-
- if (!g->hovered_pin_idx.has_value())
- {
- // Resolve which node is actually on top and being hovered using the depth stack.
- g->hovered_node_idx = resolve_hovered_node(editor.node_depth_order);
- }
-
- // We don't need to check the depth stack for links. If a node occludes a link and is being
- // hovered, then we would not be able to detect the link anyway.
- if (!g->hovered_node_idx.has_value())
- {
- g->hovered_link_idx = resolve_hovered_link(editor.links, editor.pins);
- }
- }
-
- for (int node_idx = 0; node_idx < editor.nodes.pool.size(); ++node_idx)
- {
- if (editor.nodes.in_use[node_idx])
- {
- draw_list_activate_node_background(node_idx);
- draw_node(editor, node_idx);
- }
- }
-
- // In order to render the links underneath the nodes, we want to first select the bottom draw
- // channel.
- g->canvas_draw_list->ChannelsSetCurrent(0);
-
- for (int link_idx = 0; link_idx < editor.links.pool.size(); ++link_idx)
- {
- if (editor.links.in_use[link_idx])
- {
- draw_link(editor, link_idx);
- }
- }
-
- // Render the click interaction UI elements (partial links, box selector) on top of everything
- // else.
-
- draw_list_append_click_interaction_channel();
- draw_list_activate_click_interaction_channel();
-
- if (g->left_mouse_clicked || g->middle_mouse_clicked)
- {
- begin_canvas_interaction(editor);
- }
-
- click_interaction_update(editor);
-
- // At this point, draw commands have been issued for all nodes (and pins). Update the node pool
- // to detect unused node slots and remove those indices from the depth stack before sorting the
- // node draw commands by depth.
- object_pool_update(editor.nodes);
- object_pool_update(editor.pins);
-
- draw_list_sort_channels_by_depth(editor.node_depth_order);
-
- // After the links have been rendered, the link pool can be updated as well.
- object_pool_update(editor.links);
-
- // Finally, merge the draw channels
- g->canvas_draw_list->ChannelsMerge();
-
- // pop style
- ImGui::EndChild(); // end scrolling region
- ImGui::PopStyleColor(); // pop child window background color
- ImGui::PopStyleVar(); // pop window padding
- ImGui::PopStyleVar(); // pop frame padding
- ImGui::EndGroup();
-}
-
-void BeginNode(const int node_id)
-{
- // Remember to call BeginNodeEditor before calling BeginNode
- assert(g->current_scope == Scope_Editor);
- g->current_scope = Scope_Node;
-
- EditorContext& editor = editor_context_get();
-
- const int node_idx = object_pool_find_or_create_index(editor.nodes, node_id);
- g->current_node_idx = node_idx;
-
- NodeData& node = editor.nodes.pool[node_idx];
- node.color_style.background = g->style.colors[ColorStyle_NodeBackground];
- node.color_style.background_hovered = g->style.colors[ColorStyle_NodeBackgroundHovered];
- node.color_style.background_selected = g->style.colors[ColorStyle_NodeBackgroundSelected];
- node.color_style.outline = g->style.colors[ColorStyle_NodeOutline];
- node.color_style.titlebar = g->style.colors[ColorStyle_TitleBar];
- node.color_style.titlebar_hovered = g->style.colors[ColorStyle_TitleBarHovered];
- node.color_style.titlebar_selected = g->style.colors[ColorStyle_TitleBarSelected];
- node.layout_style.corner_rounding = g->style.node_corner_rounding;
- node.layout_style.padding =
- ImVec2(g->style.node_padding_horizontal, g->style.node_padding_vertical);
- node.layout_style.border_thickness = g->style.node_border_thickness;
-
- // ImGui::SetCursorPos sets the cursor position, local to the current widget
- // (in this case, the child object started in BeginNodeEditor). Use
- // ImGui::SetCursorScreenPos to set the screen space coordinates directly.
- ImGui::SetCursorPos(grid_space_to_editor_space(editor, get_node_title_bar_origin(node)));
-
- draw_list_add_node(node_idx);
- draw_list_activate_current_node_foreground();
-
- ImGui::PushID(node.id);
- ImGui::BeginGroup();
-}
-
-void EndNode()
-{
- assert(g->current_scope == Scope_Node);
- g->current_scope = Scope_Editor;
-
- EditorContext& editor = editor_context_get();
-
- // The node's rectangle depends on the ImGui UI group size.
- ImGui::EndGroup();
- ImGui::PopID();
-
- NodeData& node = editor.nodes.pool[g->current_node_idx];
- node.rect = get_item_rect();
- node.rect.Expand(node.layout_style.padding);
-
- if (node.rect.Contains(g->mouse_pos))
- {
- g->node_indices_overlapping_with_mouse.push_back(g->current_node_idx);
- }
-}
-
-ImVec2 GetNodeDimensions(int node_id)
-{
- EditorContext& editor = editor_context_get();
- const int node_idx = object_pool_find(editor.nodes, node_id);
- assert(node_idx != -1); // invalid node_id
- const NodeData& node = editor.nodes.pool[node_idx];
- return node.rect.GetSize();
-}
-
-void BeginNodeTitleBar()
-{
- assert(g->current_scope == Scope_Node);
- ImGui::BeginGroup();
-}
-
-void EndNodeTitleBar()
-{
- assert(g->current_scope == Scope_Node);
- ImGui::EndGroup();
-
- EditorContext& editor = editor_context_get();
- NodeData& node = editor.nodes.pool[g->current_node_idx];
- node.title_bar_content_rect = get_item_rect();
-
- ImGui::ItemAdd(get_node_title_rect(node), ImGui::GetID("title_bar"));
-
- ImGui::SetCursorPos(grid_space_to_editor_space(editor, get_node_content_origin(node)));
-}
-
-void BeginInputAttribute(const int id, const PinShape shape)
-{
- begin_pin_attribute(id, AttributeType_Input, shape, g->current_node_idx);
-}
-
-void EndInputAttribute() { end_pin_attribute(); }
-
-void BeginOutputAttribute(const int id, const PinShape shape)
-{
- begin_pin_attribute(id, AttributeType_Output, shape, g->current_node_idx);
-}
-
-void EndOutputAttribute() { end_pin_attribute(); }
-
-void BeginStaticAttribute(const int id)
-{
- // Make sure to call BeginNode() before calling BeginAttribute()
- assert(g->current_scope == Scope_Node);
- g->current_scope = Scope_Attribute;
-
- g->current_attribute_id = id;
-
- ImGui::BeginGroup();
- ImGui::PushID(id);
-}
-
-void EndStaticAttribute()
-{
- // Make sure to call BeginNode() before calling BeginAttribute()
- assert(g->current_scope == Scope_Attribute);
- g->current_scope = Scope_Node;
-
- ImGui::PopID();
- ImGui::EndGroup();
-
- if (ImGui::IsItemActive())
- {
- g->active_attribute = true;
- g->active_attribute_id = g->current_attribute_id;
- g->interactive_node_idx = g->current_node_idx;
- }
-}
-
-void PushAttributeFlag(AttributeFlags flag)
-{
- g->current_attribute_flags |= static_cast<int>(flag);
- g->attribute_flag_stack.push_back(g->current_attribute_flags);
-}
-
-void PopAttributeFlag()
-{
- // PopAttributeFlag called without a matching PushAttributeFlag!
- // The bottom value is always the default value, pushed in Initialize().
- assert(g->attribute_flag_stack.size() > 1);
-
- g->attribute_flag_stack.pop_back();
- g->current_attribute_flags = g->attribute_flag_stack.back();
-}
-
-void Link(int id, const int start_attr_id, const int end_attr_id)
-{
- assert(g->current_scope == Scope_Editor);
-
- EditorContext& editor = editor_context_get();
- LinkData& link = object_pool_find_or_create_object(editor.links, id);
- link.id = id;
- link.start_pin_idx = object_pool_find_or_create_index(editor.pins, start_attr_id);
- link.end_pin_idx = object_pool_find_or_create_index(editor.pins, end_attr_id);
- link.color_style.base = g->style.colors[ColorStyle_Link];
- link.color_style.hovered = g->style.colors[ColorStyle_LinkHovered];
- link.color_style.selected = g->style.colors[ColorStyle_LinkSelected];
-
- // Check if this link was created by the current link event
- if ((editor.click_interaction_type == ClickInteractionType_LinkCreation &&
- editor.pins.pool[link.end_pin_idx].flags & AttributeFlags_EnableLinkCreationOnSnap &&
- editor.click_interaction_state.link_creation.start_pin_idx == link.start_pin_idx &&
- editor.click_interaction_state.link_creation.end_pin_idx == link.end_pin_idx) ||
- (editor.click_interaction_state.link_creation.start_pin_idx == link.end_pin_idx &&
- editor.click_interaction_state.link_creation.end_pin_idx == link.start_pin_idx))
- {
- g->snap_link_idx = object_pool_find_or_create_index(editor.links, id);
- }
-}
-
-void PushColorStyle(ColorStyle item, unsigned int color)
-{
- g->color_modifier_stack.push_back(ColorStyleElement(g->style.colors[item], item));
- g->style.colors[item] = color;
-}
-
-void PopColorStyle()
-{
- assert(g->color_modifier_stack.size() > 0);
- const ColorStyleElement elem = g->color_modifier_stack.back();
- g->style.colors[elem.item] = elem.color;
- g->color_modifier_stack.pop_back();
-}
-
-float& lookup_style_var(const StyleVar item)
-{
- // TODO: once the switch gets too big and unwieldy to work with, we could do
- // a byte-offset lookup into the Style struct, using the StyleVar as an
- // index. This is how ImGui does it.
- float* style_var = 0;
- switch (item)
- {
- case StyleVar_GridSpacing:
- style_var = &g->style.grid_spacing;
- break;
- case StyleVar_NodeCornerRounding:
- style_var = &g->style.node_corner_rounding;
- break;
- case StyleVar_NodePaddingHorizontal:
- style_var = &g->style.node_padding_horizontal;
- break;
- case StyleVar_NodePaddingVertical:
- style_var = &g->style.node_padding_vertical;
- break;
- case StyleVar_NodeBorderThickness:
- style_var = &g->style.node_border_thickness;
- break;
- case StyleVar_LinkThickness:
- style_var = &g->style.link_thickness;
- break;
- case StyleVar_LinkLineSegmentsPerLength:
- style_var = &g->style.link_line_segments_per_length;
- break;
- case StyleVar_LinkHoverDistance:
- style_var = &g->style.link_hover_distance;
- break;
- case StyleVar_PinCircleRadius:
- style_var = &g->style.pin_circle_radius;
- break;
- case StyleVar_PinQuadSideLength:
- style_var = &g->style.pin_quad_side_length;
- break;
- case StyleVar_PinTriangleSideLength:
- style_var = &g->style.pin_triangle_side_length;
- break;
- case StyleVar_PinLineThickness:
- style_var = &g->style.pin_line_thickness;
- break;
- case StyleVar_PinHoverRadius:
- style_var = &g->style.pin_hover_radius;
- break;
- case StyleVar_PinOffset:
- style_var = &g->style.pin_offset;
- break;
- default:
- assert(!"Invalid StyleVar value!");
- }
-
- return *style_var;
-}
-
-void PushStyleVar(const StyleVar item, const float value)
-{
- float& style_var = lookup_style_var(item);
- g->style_modifier_stack.push_back(StyleElement(style_var, item));
- style_var = value;
-}
-
-void PopStyleVar()
-{
- assert(g->style_modifier_stack.size() > 0);
- const StyleElement style_elem = g->style_modifier_stack.back();
- g->style_modifier_stack.pop_back();
- float& style_var = lookup_style_var(style_elem.item);
- style_var = style_elem.value;
-}
-
-void SetNodeScreenSpacePos(int node_id, const ImVec2& screen_space_pos)
-{
- EditorContext& editor = editor_context_get();
- NodeData& node = object_pool_find_or_create_object(editor.nodes, node_id);
- node.origin = screen_space_to_grid_space(editor, screen_space_pos);
-}
-
-void SetNodeEditorSpacePos(int node_id, const ImVec2& editor_space_pos)
-{
- EditorContext& editor = editor_context_get();
- NodeData& node = object_pool_find_or_create_object(editor.nodes, node_id);
- node.origin = editor_space_to_grid_space(editor, editor_space_pos);
-}
-
-void SetNodeGridSpacePos(int node_id, const ImVec2& grid_pos)
-{
- EditorContext& editor = editor_context_get();
- NodeData& node = object_pool_find_or_create_object(editor.nodes, node_id);
- node.origin = grid_pos;
-}
-
-void SetNodeDraggable(int node_id, const bool draggable)
-{
- EditorContext& editor = editor_context_get();
- NodeData& node = object_pool_find_or_create_object(editor.nodes, node_id);
- node.draggable = draggable;
-}
-
-ImVec2 GetNodeScreenSpacePos(const int node_id)
-{
- EditorContext& editor = editor_context_get();
- const int node_idx = object_pool_find(editor.nodes, node_id);
- assert(node_idx != -1);
- NodeData& node = editor.nodes.pool[node_idx];
- return grid_space_to_screen_space(editor, node.origin);
-}
-
-ImVec2 GetNodeEditorSpacePos(const int node_id)
-{
- EditorContext& editor = editor_context_get();
- const int node_idx = object_pool_find(editor.nodes, node_id);
- assert(node_idx != -1);
- NodeData& node = editor.nodes.pool[node_idx];
- return grid_space_to_editor_space(editor, node.origin);
-}
-
-ImVec2 GetNodeGridSpacePos(int node_id)
-{
- EditorContext& editor = editor_context_get();
- const int node_idx = object_pool_find(editor.nodes, node_id);
- assert(node_idx != -1);
- NodeData& node = editor.nodes.pool[node_idx];
- return node.origin;
-}
-
-bool IsEditorHovered() { return mouse_in_canvas(); }
-
-bool IsNodeHovered(int* const node_id)
-{
- assert(g->current_scope == Scope_None);
- assert(node_id != NULL);
-
- const bool is_hovered = g->hovered_node_idx.has_value();
- if (is_hovered)
- {
- const EditorContext& editor = editor_context_get();
- *node_id = editor.nodes.pool[g->hovered_node_idx.value()].id;
- }
- return is_hovered;
-}
-
-bool IsLinkHovered(int* const link_id)
-{
- assert(g->current_scope == Scope_None);
- assert(link_id != NULL);
-
- const bool is_hovered = g->hovered_link_idx.has_value();
- if (is_hovered)
- {
- const EditorContext& editor = editor_context_get();
- *link_id = editor.links.pool[g->hovered_link_idx.value()].id;
- }
- return is_hovered;
-}
-
-bool IsPinHovered(int* const attr)
-{
- assert(g->current_scope == Scope_None);
- assert(attr != NULL);
-
- const bool is_hovered = g->hovered_pin_idx.has_value();
- if (is_hovered)
- {
- const EditorContext& editor = editor_context_get();
- *attr = editor.pins.pool[g->hovered_pin_idx.value()].id;
- }
- return is_hovered;
-}
-
-int NumSelectedNodes()
-{
- assert(g->current_scope == Scope_None);
- const EditorContext& editor = editor_context_get();
- return editor.selected_node_indices.size();
-}
-
-int NumSelectedLinks()
-{
- assert(g->current_scope == Scope_None);
- const EditorContext& editor = editor_context_get();
- return editor.selected_link_indices.size();
-}
-
-void GetSelectedNodes(int* node_ids)
-{
- assert(node_ids != NULL);
-
- const EditorContext& editor = editor_context_get();
- for (int i = 0; i < editor.selected_node_indices.size(); ++i)
- {
- const int node_idx = editor.selected_node_indices[i];
- node_ids[i] = editor.nodes.pool[node_idx].id;
- }
-}
-
-void GetSelectedLinks(int* link_ids)
-{
- assert(link_ids != NULL);
-
- const EditorContext& editor = editor_context_get();
- for (int i = 0; i < editor.selected_link_indices.size(); ++i)
- {
- const int link_idx = editor.selected_link_indices[i];
- link_ids[i] = editor.links.pool[link_idx].id;
- }
-}
-
-void ClearNodeSelection()
-{
- EditorContext& editor = editor_context_get();
- editor.selected_node_indices.clear();
-}
-
-void ClearLinkSelection()
-{
- EditorContext& editor = editor_context_get();
- editor.selected_link_indices.clear();
-}
-
-bool IsAttributeActive()
-{
- assert((g->current_scope & Scope_Node) != 0);
-
- if (!g->active_attribute)
- {
- return false;
- }
-
- return g->active_attribute_id == g->current_attribute_id;
-}
-
-bool IsAnyAttributeActive(int* const attribute_id)
-{
- assert((g->current_scope & (Scope_Node | Scope_Attribute)) == 0);
-
- if (!g->active_attribute)
- {
- return false;
- }
-
- if (attribute_id != NULL)
- {
- *attribute_id = g->active_attribute_id;
- }
-
- return true;
-}
-
-bool IsLinkStarted(int* const started_at_id)
-{
- // Call this function after EndNodeEditor()!
- assert(g->current_scope == Scope_None);
- assert(started_at_id != NULL);
-
- const bool is_started = (g->element_state_change & ElementStateChange_LinkStarted) != 0;
- if (is_started)
- {
- const EditorContext& editor = editor_context_get();
- const int pin_idx = editor.click_interaction_state.link_creation.start_pin_idx;
- *started_at_id = editor.pins.pool[pin_idx].id;
- }
-
- return is_started;
-}
-
-bool IsLinkDropped(int* const started_at_id, const bool including_detached_links)
-{
- // Call this function after EndNodeEditor()!
- assert(g->current_scope == Scope_None);
-
- const EditorContext& editor = editor_context_get();
-
- const bool link_dropped = (g->element_state_change & ElementStateChange_LinkDropped) != 0 &&
- (including_detached_links ||
- editor.click_interaction_state.link_creation.link_creation_type !=
- LinkCreationType_FromDetach);
-
- if (link_dropped && started_at_id)
- {
- const int pin_idx = editor.click_interaction_state.link_creation.start_pin_idx;
- *started_at_id = editor.pins.pool[pin_idx].id;
- }
-
- return link_dropped;
-}
-
-bool IsLinkCreated(
- int* const started_at_pin_id,
- int* const ended_at_pin_id,
- bool* const created_from_snap)
-{
- assert(g->current_scope == Scope_None);
- assert(started_at_pin_id != NULL);
- assert(ended_at_pin_id != NULL);
-
- const bool is_created = (g->element_state_change & ElementStateChange_LinkCreated) != 0;
-
- if (is_created)
- {
- const EditorContext& editor = editor_context_get();
- const int start_idx = editor.click_interaction_state.link_creation.start_pin_idx;
- const int end_idx = editor.click_interaction_state.link_creation.end_pin_idx.value();
- const PinData& start_pin = editor.pins.pool[start_idx];
- const PinData& end_pin = editor.pins.pool[end_idx];
-
- if (start_pin.type == AttributeType_Output)
- {
- *started_at_pin_id = start_pin.id;
- *ended_at_pin_id = end_pin.id;
- }
- else
- {
- *started_at_pin_id = end_pin.id;
- *ended_at_pin_id = start_pin.id;
- }
-
- if (created_from_snap)
- {
- *created_from_snap = editor.click_interaction_type == ClickInteractionType_LinkCreation;
- }
- }
-
- return is_created;
-}
-
-bool IsLinkCreated(
- int* started_at_node_id,
- int* started_at_pin_id,
- int* ended_at_node_id,
- int* ended_at_pin_id,
- bool* created_from_snap)
-{
- assert(g->current_scope == Scope_None);
- assert(started_at_node_id != NULL);
- assert(started_at_pin_id != NULL);
- assert(ended_at_node_id != NULL);
- assert(ended_at_pin_id != NULL);
-
- const bool is_created = (g->element_state_change & ElementStateChange_LinkCreated) != 0;
-
- if (is_created)
- {
- const EditorContext& editor = editor_context_get();
- const int start_idx = editor.click_interaction_state.link_creation.start_pin_idx;
- const int end_idx = editor.click_interaction_state.link_creation.end_pin_idx.value();
- const PinData& start_pin = editor.pins.pool[start_idx];
- const NodeData& start_node = editor.nodes.pool[start_pin.parent_node_idx];
- const PinData& end_pin = editor.pins.pool[end_idx];
- const NodeData& end_node = editor.nodes.pool[end_pin.parent_node_idx];
-
- if (start_pin.type == AttributeType_Output)
- {
- *started_at_pin_id = start_pin.id;
- *started_at_node_id = start_node.id;
- *ended_at_pin_id = end_pin.id;
- *ended_at_node_id = end_node.id;
- }
- else
- {
- *started_at_pin_id = end_pin.id;
- *started_at_node_id = end_node.id;
- *ended_at_pin_id = start_pin.id;
- *ended_at_node_id = start_node.id;
- }
-
- if (created_from_snap)
- {
- *created_from_snap = editor.click_interaction_type == ClickInteractionType_LinkCreation;
- }
- }
-
- return is_created;
-}
-
-bool IsLinkDestroyed(int* const link_id)
-{
- assert(g->current_scope == Scope_None);
-
- const bool link_destroyed = g->deleted_link_idx.has_value();
- if (link_destroyed)
- {
- const EditorContext& editor = editor_context_get();
- const int link_idx = g->deleted_link_idx.value();
- *link_id = editor.links.pool[link_idx].id;
- }
-
- return link_destroyed;
-}
-
-namespace
-{
-void node_line_handler(EditorContext& editor, const char* line)
-{
- int id;
- int x, y;
- if (sscanf(line, "[node.%i", &id) == 1)
- {
- const int node_idx = object_pool_find_or_create_index(editor.nodes, id);
- g->current_node_idx = node_idx;
- NodeData& node = editor.nodes.pool[node_idx];
- node.id = id;
- }
- else if (sscanf(line, "origin=%i,%i", &x, &y) == 2)
- {
- NodeData& node = editor.nodes.pool[g->current_node_idx];
- node.origin = ImVec2((float)x, (float)y);
- }
-}
-
-void editor_line_handler(EditorContext& editor, const char* line)
-{
- sscanf(line, "panning=%f,%f", &editor.panning.x, &editor.panning.y);
-}
-} // namespace
-
-const char* SaveCurrentEditorStateToIniString(size_t* const data_size)
-{
- return SaveEditorStateToIniString(&editor_context_get(), data_size);
-}
-
-const char* SaveEditorStateToIniString(
- const EditorContext* const editor_ptr,
- size_t* const data_size)
-{
- assert(editor_ptr != NULL);
- const EditorContext& editor = *editor_ptr;
-
- g->text_buffer.clear();
- // TODO: check to make sure that the estimate is the upper bound of element
- g->text_buffer.reserve(64 * editor.nodes.pool.size());
-
- g->text_buffer.appendf(
- "[editor]\npanning=%i,%i\n", (int)editor.panning.x, (int)editor.panning.y);
-
- for (int i = 0; i < editor.nodes.pool.size(); i++)
- {
- if (editor.nodes.in_use[i])
- {
- const NodeData& node = editor.nodes.pool[i];
- g->text_buffer.appendf("\n[node.%d]\n", node.id);
- g->text_buffer.appendf("origin=%i,%i\n", (int)node.origin.x, (int)node.origin.y);
- }
- }
-
- if (data_size != NULL)
- {
- *data_size = g->text_buffer.size();
- }
-
- return g->text_buffer.c_str();
-}
-
-void LoadCurrentEditorStateFromIniString(const char* const data, const size_t data_size)
-{
- LoadEditorStateFromIniString(&editor_context_get(), data, data_size);
-}
-
-void LoadEditorStateFromIniString(
- EditorContext* const editor_ptr,
- const char* const data,
- const size_t data_size)
-{
- if (data_size == 0u)
- {
- return;
- }
-
- EditorContext& editor = editor_ptr == NULL ? editor_context_get() : *editor_ptr;
-
- char* buf = (char*)ImGui::MemAlloc(data_size + 1);
- const char* buf_end = buf + data_size;
- memcpy(buf, data, data_size);
- buf[data_size] = 0;
-
- void (*line_handler)(EditorContext&, const char*);
- line_handler = NULL;
- char* line_end = NULL;
- for (char* line = buf; line < buf_end; line = line_end + 1)
- {
- while (*line == '\n' || *line == '\r')
- {
- line++;
- }
- line_end = line;
- while (line_end < buf_end && *line_end != '\n' && *line_end != '\r')
- {
- line_end++;
- }
- line_end[0] = 0;
-
- if (*line == ';' || *line == '\0')
- {
- continue;
- }
-
- if (line[0] == '[' && line_end[-1] == ']')
- {
- line_end[-1] = 0;
- if (strncmp(line + 1, "node", 4) == 0)
- {
- line_handler = node_line_handler;
- }
- else if (strcmp(line + 1, "editor") == 0)
- {
- line_handler = editor_line_handler;
- }
- }
-
- if (line_handler != NULL)
- {
- line_handler(editor, line);
- }
- }
- ImGui::MemFree(buf);
-}
-
-void SaveCurrentEditorStateToIniFile(const char* const file_name)
-{
- SaveEditorStateToIniFile(&editor_context_get(), file_name);
-}
-
-void SaveEditorStateToIniFile(const EditorContext* const editor, const char* const file_name)
-{
- size_t data_size = 0u;
- const char* data = SaveEditorStateToIniString(editor, &data_size);
- FILE* file = ImFileOpen(file_name, "wt");
- if (!file)
- {
- return;
- }
-
- fwrite(data, sizeof(char), data_size, file);
- fclose(file);
-}
-
-void LoadCurrentEditorStateFromIniFile(const char* const file_name)
-{
- LoadEditorStateFromIniFile(&editor_context_get(), file_name);
-}
-
-void LoadEditorStateFromIniFile(EditorContext* const editor, const char* const file_name)
-{
- size_t data_size = 0u;
- char* file_data = (char*)ImFileLoadToMemory(file_name, "rb", &data_size);
-
- if (!file_data)
- {
- return;
- }
-
- LoadEditorStateFromIniString(editor, file_data, data_size);
- ImGui::MemFree(file_data);
-}
-} // namespace ImNodes
diff --git a/3rdparty/imnodes/imnodes.h b/3rdparty/imnodes/imnodes.h
deleted file mode 100644
index f7a9df0..0000000
--- a/3rdparty/imnodes/imnodes.h
+++ /dev/null
@@ -1,335 +0,0 @@
-#pragma once
-
-#include <stddef.h>
-
-struct ImGuiContext;
-struct ImVec2;
-
-namespace ImNodes
-{
-enum ColorStyle
-{
- ColorStyle_NodeBackground = 0,
- ColorStyle_NodeBackgroundHovered,
- ColorStyle_NodeBackgroundSelected,
- ColorStyle_NodeOutline,
- ColorStyle_TitleBar,
- ColorStyle_TitleBarHovered,
- ColorStyle_TitleBarSelected,
- ColorStyle_Link,
- ColorStyle_LinkHovered,
- ColorStyle_LinkSelected,
- ColorStyle_Pin,
- ColorStyle_PinHovered,
- ColorStyle_BoxSelector,
- ColorStyle_BoxSelectorOutline,
- ColorStyle_GridBackground,
- ColorStyle_GridLine,
- ColorStyle_Count
-};
-
-enum StyleVar
-{
- StyleVar_GridSpacing = 0,
- StyleVar_NodeCornerRounding,
- StyleVar_NodePaddingHorizontal,
- StyleVar_NodePaddingVertical,
- StyleVar_NodeBorderThickness,
- StyleVar_LinkThickness,
- StyleVar_LinkLineSegmentsPerLength,
- StyleVar_LinkHoverDistance,
- StyleVar_PinCircleRadius,
- StyleVar_PinQuadSideLength,
- StyleVar_PinTriangleSideLength,
- StyleVar_PinLineThickness,
- StyleVar_PinHoverRadius,
- StyleVar_PinOffset
-};
-
-enum StyleFlags
-{
- StyleFlags_None = 0,
- StyleFlags_NodeOutline = 1 << 0,
- StyleFlags_GridLines = 1 << 2
-};
-
-// This enum controls the way attribute pins look.
-enum PinShape
-{
- PinShape_Circle,
- PinShape_CircleFilled,
- PinShape_Triangle,
- PinShape_TriangleFilled,
- PinShape_Quad,
- PinShape_QuadFilled
-};
-
-// This enum controls the way the attribute pins behave.
-enum AttributeFlags
-{
- AttributeFlags_None = 0,
- // Allow detaching a link by left-clicking and dragging the link at a pin it is connected to.
- // NOTE: the user has to actually delete the link for this to work. A deleted link can be
- // detected by calling IsLinkDestroyed() after EndNodeEditor().
- AttributeFlags_EnableLinkDetachWithDragClick = 1 << 0,
- // Visual snapping of an in progress link will trigger IsLink Created/Destroyed events. Allows
- // for previewing the creation of a link while dragging it across attributes. See here for demo:
- // https://github.com/Nelarius/ImNodes/issues/41#issuecomment-647132113 NOTE: the user has to
- // actually delete the link for this to work. A deleted link can be detected by calling
- // IsLinkDestroyed() after EndNodeEditor().
- AttributeFlags_EnableLinkCreationOnSnap = 1 << 1
-};
-
-struct IO
-{
- struct EmulateThreeButtonMouse
- {
- EmulateThreeButtonMouse();
-
- // The keyboard modifier to use in combination with mouse left click to pan the editor view.
- // Set to NULL by default. To enable this feature, set the modifier to point to a boolean
- // indicating the state of a modifier. For example,
- //
- // ImNodes::GetIO().emulate_three_button_mouse.modifier = &ImGui::GetIO().KeyAlt;
- const bool* modifier;
- } emulate_three_button_mouse;
-
- struct LinkDetachWithModifierClick
- {
- LinkDetachWithModifierClick();
-
- // Pointer to a boolean value indicating when the desired modifier is pressed. Set to NULL
- // by default. To enable the feature, set the modifier to point to a boolean indicating the
- // state of a modifier. For example,
- //
- // ImNodes::GetIO().link_detach_with_modifier_click.modifier = &ImGui::GetIO().KeyCtrl;
- //
- // Left-clicking a link with this modifier pressed will detach that link. NOTE: the user has
- // to actually delete the link for this to work. A deleted link can be detected by calling
- // IsLinkDestroyed() after EndNodeEditor().
- const bool* modifier;
- } link_detach_with_modifier_click;
-
- IO();
-};
-
-struct Style
-{
- float grid_spacing;
-
- float node_corner_rounding;
- float node_padding_horizontal;
- float node_padding_vertical;
- float node_border_thickness;
-
- float link_thickness;
- float link_line_segments_per_length;
- float link_hover_distance;
-
- // The following variables control the look and behavior of the pins. The default size of each
- // pin shape is balanced to occupy approximately the same surface area on the screen.
-
- // The circle radius used when the pin shape is either PinShape_Circle or PinShape_CircleFilled.
- float pin_circle_radius;
- // The quad side length used when the shape is either PinShape_Quad or PinShape_QuadFilled.
- float pin_quad_side_length;
- // The equilateral triangle side length used when the pin shape is either PinShape_Triangle or
- // PinShape_TriangleFilled.
- float pin_triangle_side_length;
- // The thickness of the line used when the pin shape is not filled.
- float pin_line_thickness;
- // The radius from the pin's center position inside of which it is detected as being hovered
- // over.
- float pin_hover_radius;
- // Offsets the pins' positions from the edge of the node to the outside of the node.
- float pin_offset;
-
- // By default, StyleFlags_NodeOutline and StyleFlags_Gridlines are enabled.
- StyleFlags flags;
- // Set these mid-frame using Push/PopColorStyle. You can index this color array with with a
- // ColorStyle enum value.
- unsigned int colors[ColorStyle_Count];
-
- Style();
-};
-
-struct Context;
-
-// Call this function if you are compiling ImNodes in to a dll, separate from ImGui. Calling this
-// function sets the GImGui global variable, which is not shared across dll boundaries.
-void SetImGuiContext(ImGuiContext* ctx);
-
-Context* CreateContext();
-void DestroyContext(Context* ctx = NULL); // NULL = destroy current context
-Context* GetCurrentContext();
-void SetCurrentContext(Context* ctx);
-
-// An editor context corresponds to a set of nodes in a single workspace (created with a single
-// Begin/EndNodeEditor pair)
-//
-// By default, the library creates an editor context behind the scenes, so using any of the ImNodes
-// functions doesn't require you to explicitly create a context.
-struct EditorContext;
-
-EditorContext* EditorContextCreate();
-void EditorContextFree(EditorContext*);
-void EditorContextSet(EditorContext*);
-ImVec2 EditorContextGetPanning();
-void EditorContextResetPanning(const ImVec2& pos);
-void EditorContextMoveToNode(const int node_id);
-
-IO& GetIO();
-
-// Returns the global style struct. See the struct declaration for default values.
-Style& GetStyle();
-// Style presets matching the dear imgui styles of the same name.
-void StyleColorsDark(); // on by default
-void StyleColorsClassic();
-void StyleColorsLight();
-
-// The top-level function call. Call this before calling BeginNode/EndNode. Calling this function
-// will result the node editor grid workspace being rendered.
-void BeginNodeEditor();
-void EndNodeEditor();
-
-// Use PushColorStyle and PopColorStyle to modify Style::colors mid-frame.
-void PushColorStyle(ColorStyle item, unsigned int color);
-void PopColorStyle();
-void PushStyleVar(StyleVar style_item, float value);
-void PopStyleVar();
-
-// id can be any positive or negative integer, but INT_MIN is currently reserved for internal use.
-void BeginNode(int id);
-void EndNode();
-
-ImVec2 GetNodeDimensions(int id);
-
-// Place your node title bar content (such as the node title, using ImGui::Text) between the
-// following function calls. These functions have to be called before adding any attributes, or the
-// layout of the node will be incorrect.
-void BeginNodeTitleBar();
-void EndNodeTitleBar();
-
-// Attributes are ImGui UI elements embedded within the node. Attributes can have pin shapes
-// rendered next to them. Links are created between pins.
-//
-// The activity status of an attribute can be checked via the IsAttributeActive() and
-// IsAnyAttributeActive() function calls. This is one easy way of checking for any changes made to
-// an attribute's drag float UI, for instance.
-//
-// Each attribute id must be unique.
-
-// Create an input attribute block. The pin is rendered on left side.
-void BeginInputAttribute(int id, PinShape shape = PinShape_CircleFilled);
-void EndInputAttribute();
-// Create an output attribute block. The pin is rendered on the right side.
-void BeginOutputAttribute(int id, PinShape shape = PinShape_CircleFilled);
-void EndOutputAttribute();
-// Create a static attribute block. A static attribute has no pin, and therefore can't be linked to
-// anything. However, you can still use IsAttributeActive() and IsAnyAttributeActive() to check for
-// attribute activity.
-void BeginStaticAttribute(int id);
-void EndStaticAttribute();
-
-// Push a single AttributeFlags value. By default, only AttributeFlags_None is set.
-void PushAttributeFlag(AttributeFlags flag);
-void PopAttributeFlag();
-
-// Render a link between attributes.
-// The attributes ids used here must match the ids used in Begin(Input|Output)Attribute function
-// calls. The order of start_attr and end_attr doesn't make a difference for rendering the link.
-void Link(int id, int start_attribute_id, int end_attribute_id);
-
-// Enable or disable the ability to click and drag a specific node.
-void SetNodeDraggable(int node_id, const bool draggable);
-
-// The node's position can be expressed in three coordinate systems:
-// * screen space coordinates, -- the origin is the upper left corner of the window.
-// * editor space coordinates -- the origin is the upper left corner of the node editor window
-// * grid space coordinates, -- the origin is the upper left corner of the node editor window,
-// translated by the current editor panning vector (see EditorContextGetPanning() and
-// EditorContextResetPanning())
-
-// Use the following functions to get and set the node's coordinates in these coordinate systems.
-
-void SetNodeScreenSpacePos(int node_id, const ImVec2& screen_space_pos);
-void SetNodeEditorSpacePos(int node_id, const ImVec2& editor_space_pos);
-void SetNodeGridSpacePos(int node_id, const ImVec2& grid_pos);
-
-ImVec2 GetNodeScreenSpacePos(const int node_id);
-ImVec2 GetNodeEditorSpacePos(const int node_id);
-ImVec2 GetNodeGridSpacePos(const int node_id);
-
-// Returns true if the current node editor canvas is being hovered over by the mouse, and is not
-// blocked by any other windows.
-bool IsEditorHovered();
-// The following functions return true if a UI element is being hovered over by the mouse cursor.
-// Assigns the id of the UI element being hovered over to the function argument. Use these functions
-// after EndNodeEditor() has been called.
-bool IsNodeHovered(int* node_id);
-bool IsLinkHovered(int* link_id);
-bool IsPinHovered(int* attribute_id);
-
-// Use The following two functions to query the number of selected nodes or links in the current
-// editor. Use after calling EndNodeEditor().
-int NumSelectedNodes();
-int NumSelectedLinks();
-// Get the selected node/link ids. The pointer argument should point to an integer array with at
-// least as many elements as the respective NumSelectedNodes/NumSelectedLinks function call
-// returned.
-void GetSelectedNodes(int* node_ids);
-void GetSelectedLinks(int* link_ids);
-
-// Clears the list of selected nodes/links. Useful if you want to delete a selected node or link.
-void ClearNodeSelection();
-void ClearLinkSelection();
-
-// Was the previous attribute active? This will continuously return true while the left mouse button
-// is being pressed over the UI content of the attribute.
-bool IsAttributeActive();
-// Was any attribute active? If so, sets the active attribute id to the output function argument.
-bool IsAnyAttributeActive(int* attribute_id = NULL);
-
-// Use the following functions to query a change of state for an existing link, or new link. Call
-// these after EndNodeEditor().
-
-// Did the user start dragging a new link from a pin?
-bool IsLinkStarted(int* started_at_attribute_id);
-// Did the user drop the dragged link before attaching it to a pin?
-// There are two different kinds of situations to consider when handling this event:
-// 1) a link which is created at a pin and then dropped
-// 2) an existing link which is detached from a pin and then dropped
-// Use the including_detached_links flag to control whether this function triggers when the user
-// detaches a link and drops it.
-bool IsLinkDropped(int* started_at_attribute_id = NULL, bool including_detached_links = true);
-// Did the user finish creating a new link?
-bool IsLinkCreated(
- int* started_at_attribute_id,
- int* ended_at_attribute_id,
- bool* created_from_snap = NULL);
-bool IsLinkCreated(
- int* started_at_node_id,
- int* started_at_attribute_id,
- int* ended_at_node_id,
- int* ended_at_attribute_id,
- bool* created_from_snap = NULL);
-
-// Was an existing link detached from a pin by the user? The detached link's id is assigned to the
-// output argument link_id.
-bool IsLinkDestroyed(int* link_id);
-
-// Use the following functions to write the editor context's state to a string, or directly to a
-// file. The editor context is serialized in the INI file format.
-
-const char* SaveCurrentEditorStateToIniString(size_t* data_size = NULL);
-const char* SaveEditorStateToIniString(const EditorContext* editor, size_t* data_size = NULL);
-
-void LoadCurrentEditorStateFromIniString(const char* data, size_t data_size);
-void LoadEditorStateFromIniString(EditorContext* editor, const char* data, size_t data_size);
-
-void SaveCurrentEditorStateToIniFile(const char* file_name);
-void SaveEditorStateToIniFile(const EditorContext* editor, const char* file_name);
-
-void LoadCurrentEditorStateFromIniFile(const char* file_name);
-void LoadEditorStateFromIniFile(EditorContext* editor, const char* file_name);
-} // namespace ImNodes