From 1f9e9f6d5cc1c36620f09c5f5ef7e22a2377adfe Mon Sep 17 00:00:00 2001 From: Annwan Date: Sat, 28 Sep 2024 01:03:05 +0200 Subject: [PATCH] Finished node editor functionality --- .clang-format | 2 +- .gitignore | 4 +- CMakeLists.txt | 33 +++++-- src/Application.cppm | 203 ++++++++++++++++++++++++++++++------------- src/Base.cppm | 52 ++++++++++- src/Button.cppm | 32 +++++++ src/Node.cppm | 58 +++++++++---- src/Port.cppm | 142 ++++++++++++++++++++---------- src/main.cpp | 14 ++- 9 files changed, 411 insertions(+), 129 deletions(-) create mode 100644 src/Button.cppm diff --git a/.clang-format b/.clang-format index e37a2bd..8a630a1 100644 --- a/.clang-format +++ b/.clang-format @@ -104,7 +104,7 @@ Cpp11BracedListStyle: true ContinuationIndentWidth: 4 ConstructorInitializerIndentWidth: 4 CompactNamespaces: false -ColumnLimit: 80 +ColumnLimit: 120 BreakStringLiterals: true BreakInheritanceList: BeforeComma AlignArrayOfStructures: Left diff --git a/.gitignore b/.gitignore index ff11aeb..39dcbad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ build/* /ffmpegraph /.idea -/cmake-build-debug -/cmake-build-release \ No newline at end of file +/out +/release \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fe66d4..93cec82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,23 +1,36 @@ # Needs CMake 3.28 for module cmake_minimum_required(VERSION 3.28) +message(STATUS "Create project") project(ffmpegraph CXX) set(CMAKE_CXX_STANDARD 26) -# color shit out -if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - add_compile_options(-fdiagnostics-color=always) -elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - add_compile_options(-fcolor-diagnostics) -endif() # Use mold as the default linker, if it exists. if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") find_program(MOLD_LINKER "mold") if (MOLD_LINKER) add_link_options(-fuse-ld=mold) + message(STATUS "Setting linker to mold") endif() endif() +include(FetchContent) +message(STATUS "Fetching raylib") +FetchContent_Declare(raylib + GIT_REPOSITORY https://github.com/raysan5/raylib + GIT_TAG 5.0 + SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libs/raylib" +) +FetchContent_MakeAvailable(raylib) + +message(STATUS "Fetching clopts") +FetchContent_Declare(clopts + GIT_REPOSITORY https://github.com/Sirraide/clopts.git + GIT_TAG master + SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libs/clopts" +) +FetchContent_MakeAvailable(clopts) + # Find all the modules file(GLOB_RECURSE modules src/*.cppm) # Create the executable target @@ -42,5 +55,13 @@ target_compile_options(ffmpegraph PRIVATE $<$:-O0 -g3 -glldb> $<$:-O3 -march=native> ) +# color shit out +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(ffmpegraph PRIVATE -fdiagnostics-color=always) +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(ffmpegraph PRIVATE -fcolor-diagnostics) +endif() + # Link against exernal libraries target_link_libraries(ffmpegraph PRIVATE raylib m) +target_include_directories(ffmpegraph PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/libs/clopts/include") diff --git a/src/Application.cppm b/src/Application.cppm index 9e463f9..b07a86d 100644 --- a/src/Application.cppm +++ b/src/Application.cppm @@ -1,116 +1,203 @@ module; +#include +#include +#include +#include +#include #include #include +#include +#include #include -#include -#include +#include export module Application; import Node; import Base; -import Port; +import Button; namespace ffmpegraph { export class Application { - public: explicit Application(std::string _title = "FFmpeGraph"); + ~Application(); + void Run(); + protected: + void Tick(); + void ProcessEvents(); - void Render() const; + void OnLeftClick(); + void OnKeyPressed(i32 key); + void Render(); + void ExecuteGraph(); + std::deque message_queue; std::string title; bool should_close; std::vector> nodes; - Node* selected_node = nullptr; - OutputPort* selected_port = nullptr; + Node *selected_node = nullptr; + OutputPort *selected_port = nullptr; + Button run_button; + u64 framecounter = 0; }; -} -module : private; -namespace ffmpegraph { +} // namespace ffmpegraph -Application::Application(std::string _title): title(std::move(_title)), should_close(false) { +module :private; +namespace ffmpegraph { +Application::Application(std::string _title) + : title(std::move(_title)), should_close(false), run_button(0, 0, 0, 20, "Run") { SetConfigFlags(FLAG_WINDOW_RESIZABLE); InitWindow(800, 600, this->title.c_str()); SetTargetFPS(60); + SetExitKey(KEY_NULL); nodes.push_back(std::make_unique()); + nodes.push_back(std::make_unique()); nodes.push_back(std::make_unique()); - nodes[0]->pos_x = 10; + nodes[0]->pos_x = 400; nodes[0]->pos_y = 10; nodes[1]->pos_x = 200; nodes[1]->pos_y = 100; + run_button.width = 10 + MeasureText("Run", 10); } - +void Application::Tick() { + if (selected_node) { + selected_node->pos_x = GetMouseX(); + selected_node->pos_y = GetMouseY(); + } + ProcessEvents(); +} void Application::ProcessEvents() { + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { OnLeftClick(); } + if (auto key = GetKeyPressed()) { OnKeyPressed(key); } +} +void Application::OnLeftClick() { auto GetPort = [&] -> Port * { - for(auto& node : nodes) { - for (auto* port: node->ports) { + for (auto &node : nodes) { + for (auto *port : node->ports) { auto distx = double(GetMouseX() - port->pos_x); auto disty = double(GetMouseY() - port->pos_y); - if (std::sqrt(distx * distx + disty * disty) < 10) { - return port; - } + if (std::sqrt(distx * distx + disty * disty) < 10) { return port; } } } return nullptr; }; - if (selected_node) { - selected_node->pos_x = GetMouseX(); - selected_node->pos_y = GetMouseY(); - } - if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { - if (selected_node) { - selected_node = nullptr; - } else if (selected_port) { - auto port = GetPort(); - auto iport = dynamic_cast(port); - if (iport) { - selected_port->Connect(*iport); - } - selected_port = nullptr; - } - else { - i32 x = GetMouseX(); - i32 y = GetMouseY(); - auto port = GetPort(); - auto oport = dynamic_cast(port); - if (oport) { - selected_port = oport; + selected_node = nullptr; + } else if (selected_port) { + auto port = GetPort(); + auto iport = dynamic_cast(port); + if (iport) { selected_port->TryConnect(*iport); } + selected_port = nullptr; + } else if (run_button.IsHovered()) { + ExecuteGraph(); + } else { + i32 x = GetMouseX(); + i32 y = GetMouseY(); + auto port = GetPort(); + auto oport = dynamic_cast(port); + if (oport) { + if (oport->GetConnected()) { + oport->Disconnect(); } else { - for (auto& node : nodes) { - if (node->pos_x < x and x < node->pos_x + node->Width() - and node->pos_y < y and y < node->pos_y + node->Height()) { - selected_node = node.get(); - break; - } + selected_port = oport; + } + } else { + for (auto &node : nodes) { + if (node->pos_x < x and x < node->pos_x + node->Width() and node->pos_y < y + and y < node->pos_y + node->Height()) { + selected_node = node.get(); + break; } } } } } -void Application::Render() const { +void Application::ExecuteGraph() { + std::unordered_map index_map; + std::unordered_map> adj_list; + for (auto [i, node] : vws::enumerate(nodes)) { + index_map[node.get()] = i; + adj_list[i] = {}; + } + for (auto [i, node] : vws::enumerate(nodes)) { + Node *raw_node = node.get(); + if (auto n = dynamic_cast(raw_node)) { + if (auto c = n->output_port.GetConnected()) { adj_list[index_map[c->owner]].insert(index_map[raw_node]); } + } + if (auto n = dynamic_cast(raw_node)) { + if (auto c = n->out.GetConnected()) { adj_list[index_map[c->owner]].insert(index_map[raw_node]); } + } + // TODO add cases for all new types of nodes + } + + std::vector sorted_nodes; + while (not adj_list.empty()) { + bool changed = false; + for (auto &[i, incoming] : adj_list) { + if (incoming.empty()) { + sorted_nodes.push_back(nodes[usz(i)].get()); + for (auto &[_, thing] : adj_list) { thing.erase(i); } + adj_list.erase(i); + changed = true; + break; + } + } + if (not changed) { + message_queue.push_back("ERROR: Graph has cycle"); + return; + } + } + for (auto node : sorted_nodes) { + node->Run(); + } +} + +void Application::OnKeyPressed(i32 key) { + auto selected = dynamic_cast(selected_node); + if (not selected) return; + if (key == KEY_ENTER) { + selected_node = nullptr; + } else if (key == KEY_BACKSPACE) { + if (not selected->in.data.empty()) selected->in.data.pop_back(); + } else if (0x20 <= key and key <= 0x7E) { + if (not IsKeyDown(KEY_LEFT_SHIFT) and not IsKeyDown(KEY_RIGHT_SHIFT) and 'A' <= key and key <= 'Z') { + key += 32; + } + selected->in.data += char(key); + } +} + +void Application::Render() { + framecounter++; + if (not (framecounter%300) and not message_queue.empty()) { + message_queue.pop_back(); + } BeginDrawing(); ClearBackground(RAYWHITE); - if (selected_port) { - DrawLine(selected_port->pos_x, selected_port->pos_y, GetMouseX(), GetMouseY(), BLACK); - } - for (auto &node : nodes) { - node->Render(); + message_queue.resize(std::min(message_queue.size(), usz(10))); + i32 offset = 15; + for (auto& message: message_queue) { + DrawText(message.c_str(), 5, GetScreenHeight() - offset, 10, RED); + offset += 20; } + if (selected_port) { DrawLine(selected_port->pos_x, selected_port->pos_y, GetMouseX(), GetMouseY(), BLACK); } + for (auto &node : nodes) { node->Render(); } + run_button.Render(); EndDrawing(); + } + void Application::Run() { - while(not WindowShouldClose()) { - this->ProcessEvents(); + while (not WindowShouldClose()) { + this->Tick(); this->Render(); } } -Application::~Application(){ - CloseWindow(); -} -} + +Application::~Application() { CloseWindow(); } +} // namespace ffmpegraph diff --git a/src/Base.cppm b/src/Base.cppm index 7907ab5..e8631d0 100644 --- a/src/Base.cppm +++ b/src/Base.cppm @@ -1,7 +1,12 @@ module; #include -#include +#include #include +#include +#include +#include +#include +#include export module Base; export namespace rgs = std::ranges; @@ -24,4 +29,49 @@ export using iptr = std::intptr_t; export using f32 = float; export using f64 = double; +export namespace utils { +template struct Overloaded : Types... { + using Types::operator()...; +}; +template Overloaded(Types...) -> Overloaded; +} + +template struct std::formatter> : std::formatter { + template auto format(std::vector const& data, FormatContext& ctx) const { + std::string repr = "["; + bool first = true; + for (auto& el : data) { + repr += std::format("{}{}",first ? "" : ", ", el); + first = false; + } + repr += "]"; + return formatter::format(repr, ctx); + } +}; + +template struct std::formatter> : std::formatter { + template auto format(std::unordered_set const& data, FormatContext& ctx) const { + std::string repr = "{"; + bool first = true; + for (auto& el : data) { + repr += std::format("{}{}", first ? "" : ", ", el); + first = false; + } + repr += "}"; + return formatter::format(repr, ctx); + } +}; + +template struct std::formatter> : std::formatter { + template auto format(std::unordered_map const& data, FormatContext& ctx) const { + std::string repr = "{"; + bool first = true; + for (auto& [k, el] : data) { + repr += std::format("{}{} → {}", first ? "" : ", ", k, el); + first = false; + } + repr += "}"; + return formatter::format(repr, ctx); + } +}; diff --git a/src/Button.cppm b/src/Button.cppm new file mode 100644 index 0000000..fa1be7a --- /dev/null +++ b/src/Button.cppm @@ -0,0 +1,32 @@ +module; +#include +#include +export module Button; +import Base; + +export namespace ffmpegraph { +struct Button { + i32 pos_x, pos_y; + i32 width, height; + std::string label; + void Render() const; + bool IsHovered() const; + Button(i32 x, i32 y, i32 w, i32 h, std::string label): pos_x(x), pos_y(y), width(w), height(h), label(std::move(label)) {} +}; +} + +module: private; +namespace ffmpegraph { +void Button::Render() const { + auto color = IsHovered()? LIGHTGRAY : WHITE; + DrawRectangle(pos_x, pos_y, width, height, color); + DrawRectangleLines(pos_x, pos_y, width, height, BLACK); + DrawText(label.c_str(), pos_x + 5, pos_y + 5, 10, BLACK); +} + +bool Button::IsHovered() const { + return pos_x <= GetMouseX() and GetMouseX() <= pos_x + width + and pos_y <= GetMouseY() and GetMouseY() <= pos_y + height; +} + +} \ No newline at end of file diff --git a/src/Node.cppm b/src/Node.cppm index fa01d04..482ea84 100644 --- a/src/Node.cppm +++ b/src/Node.cppm @@ -1,10 +1,10 @@ module; #include -#include #include +#include export module Node; +export import :Port; import Base; -import Port; export namespace ffmpegraph { @@ -12,42 +12,68 @@ struct Node { virtual ~Node() = default; void Render(); i32 Width() const; - i32 Height()const; + i32 Height() const; i32 pos_x{}, pos_y{}; std::vector ports; + virtual void Run() = 0; +protected: + i32 width = min_port_width; }; -struct InputNode : Node { +struct InputNode : Node { InputNode(); Label label; InputPort filename_port; OutputPort output_port; + void Run() override; }; +struct StringConstantNode : Node { + StringConstantNode(); + Label label; + StringUserInput in; + OutputPort out; + void Run() override; +}; } -module : private; +module :private; namespace ffmpegraph { -i32 Node::Width() const {return 100;} -i32 Node::Height() const {return 20 * i32(ports.size());} +i32 Node::Width() const { return width; } +i32 Node::Height() const { return 20 * i32(ports.size()); } -InputNode::InputNode(): - label("Input"), - filename_port("filename"), - output_port("output") { +InputNode::InputNode() + : label("Input", this), filename_port("filename", this, PortType::STRING), output_port("output", this, PortType::STRING) { ports.emplace_back(&label); ports.emplace_back(&filename_port); ports.emplace_back(&output_port); } + +void InputNode::Run() { + output_port.SetValue(filename_port.GetValue()); +} + +StringConstantNode::StringConstantNode() : label("String Constant", this), in("Text", this), out("Output", this, PortType::STRING) { + ports.emplace_back(&label); + ports.emplace_back(&in); + ports.emplace_back(&out); +} + +void StringConstantNode::Run() { + out.SetValue(in.data); +} + + void Node::Render() { i32 port_y = pos_y; DrawRectangle(pos_x, pos_y, Width(), Height(), WHITE); - for (auto* port: ports) { - port->Render(pos_x, port_y); + i32 new_width = min_port_width; + for (auto* port : ports) { + new_width = std::max(port->Render(pos_x, port_y, width), new_width); port_y += 20; - }; -} + } + width = new_width; } - +} // namespace ffmpegraph diff --git a/src/Port.cppm b/src/Port.cppm index cc233fd..12ea0ba 100644 --- a/src/Port.cppm +++ b/src/Port.cppm @@ -1,87 +1,141 @@ module; -#include -#include +#include +#include #include -export module Port; +#include +#include +export module Node:Port; import Base; - +using namespace std::literals; export namespace ffmpegraph { +struct Node; + +enum struct PortType { + INT, + STRING, +}; + +constexpr i32 min_port_width = 100; +using PortData = std::variant; +} + +template <> struct std::formatter : std::formatter { + template auto format(ffmpegraph::PortData const& data, FormatContext& ctx) const { + auto repr = std::visit( + utils::Overloaded{ + [](std::monostate) { return "()"s; }, + [](std::string const& x) { return x; }, + [](i32 x) { return std::to_string(x); }, + }, + data + ); + return formatter::format(repr, ctx); + } +}; + +export namespace ffmpegraph { struct Port { protected: std::string name; - explicit Port(std::string name): name(std::move(name)) {} + explicit Port(std::string name, Node* owner) : name(std::move(name)), owner(owner) {} virtual ~Port() = default; public: i32 pos_x{}, pos_y{}; - virtual void Render(i32 x, i32 y) = 0; + Node* owner; + // Returns the desired width for the port. + virtual i32 Render(i32 x, i32 y, i32 width) = 0; }; struct OutputPort; struct InputPort : Port { protected: - std::optional value = std::nullopt; + PortType type; + PortData value = std::monostate{}; public: - explicit InputPort(std::string name): Port(std::move(name)) {} - std::optional GetValue(); - void Render(i32 x, i32 y) override; + InputPort(std::string name, Node* owner, PortType type) : Port(std::move(name), owner), type(type) {} + PortData const& GetValue() const; + i32 Render(i32 x, i32 y, i32 width) override; friend OutputPort; }; struct OutputPort : Port { protected: + PortType type; InputPort* connected = nullptr; public: - explicit OutputPort(std::string name): Port(std::move(name)) {} - void SetValue(std::optional); - void Connect(InputPort& ip); + OutputPort(std::string name, Node* owner, PortType type) : Port(std::move(name), owner), type(type) {} + void SetValue(PortData value); + bool TryConnect(InputPort& ip); + InputPort* GetConnected() const { return connected; } void Disconnect(); - void Render(i32 x, i32 y) override; + i32 Render(i32 x, i32 y, i32 width) override; }; struct Label : Port { - void Render(i32 x, i32 y) override; - explicit Label(std::string name): Port(std::move(name)) { - pos_x = -1; - pos_y = -1; + i32 Render(i32 x, i32 y, i32 width) override; + explicit Label(std::string name, Node* owner) : Port(std::move(name), owner) { + pos_x = pos_y = std::numeric_limits::min(); } }; -std::optional InputPort::GetValue(){ - return value; -} -void OutputPort::SetValue(std::optional val){ - if (connected) { - connected->value = val; +struct StringUserInput : Port { + explicit StringUserInput(std::string name, Node* owner) : Port(std::move(name), owner), data("") { + pos_x = pos_y = std::numeric_limits::min(); } + i32 Render(i32 x, i32 y, i32 width) override; + std::string data; +}; + +PortData const& InputPort::GetValue() const { return value; } + +void OutputPort::SetValue(PortData value) { + if (connected) { connected->value = std::move(value); } } -void OutputPort::Connect(InputPort& ip){ - connected = &ip; +bool OutputPort::TryConnect(InputPort& ip) { + if (ip.type == type) { + connected = &ip; + return true; + } + return false; } -void OutputPort::Disconnect(){ - connected = nullptr; -} +void OutputPort::Disconnect() { connected = nullptr; } -void OutputPort::Render(i32 x, i32 y) { - pos_x = x+100; pos_y = y+10; - DrawRectangleLines(x, y, 100, 20, BLACK); +i32 OutputPort::Render(i32 x, i32 y, i32 width) { + pos_x = x + width; + pos_y = y + 10; + DrawRectangleLines(x, y, width, 20, BLACK); DrawText(name.c_str(), x + 5, y + 5, 10, RED); - DrawCircle(x+100, y+10, 2.5f, BLACK); - if (connected) { - DrawLine(pos_x, pos_y, connected->pos_x, connected->pos_y, BLUE); - } + DrawCircle(x + width, y + 10, 2.5f, BLACK); + if (connected) { DrawLine(pos_x, pos_y, connected->pos_x, connected->pos_y, BLUE); } + return 10 + MeasureText(name.c_str(), 10); } -void InputPort::Render(i32 x, i32 y) { - pos_x = x; pos_y = y+10; - DrawRectangleLines(x, y, 100, 20, BLACK); - DrawText(name.c_str(), x + 5, y + 5, 10, BLUE); - DrawCircle(x, y+10, 2.5f, BLACK); +i32 InputPort::Render(i32 x, i32 y, i32 width) { + pos_x = x; + pos_y = y + 10; + auto formatted = std::format("{}: {}", name, value); + auto new_width = 10 + MeasureText(formatted.c_str(), 10); + DrawRectangleLines(x, y, width, 20, BLACK); + DrawText(formatted.c_str(), x + 5, y + 5, 10, BLUE); + DrawCircle(x, y + 10, 2.5f, BLACK); + return new_width; } -void Label::Render(i32 x, i32 y){ - DrawRectangleLines(x, y, 100, 20, BLACK); +i32 Label::Render(i32 x, i32 y, i32 width) { + DrawRectangleLines(x, y, width, 20, BLACK); DrawText(name.c_str(), x + 5, y + 5, 10, BLACK); + return 10 + MeasureText(name.c_str(), 10); } -} \ No newline at end of file +i32 StringUserInput::Render(i32 x, i32 y, i32 width) { + DrawRectangleLines(x, y, width, 20, BLACK); + if (data.empty()) { + DrawText(name.c_str(), x + 5, y + 5, 10, GRAY); + return 10 + MeasureText(name.c_str(), 10); + } else { + DrawText(data.c_str(), x + 5, y + 5, 10, BLACK); + return 10 + MeasureText(data.c_str(), 10); + } +} +} // namespace ffmpegraph diff --git a/src/main.cpp b/src/main.cpp index 7e8624a..6d8ab39 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,20 @@ +#include +#include import Base; import Application; using namespace ffmpegraph; -int main() { +namespace detail { + using namespace command_line_options; + using options = clopts< + help<>, + option<"--ffmpeg-path", "Path to ffmpeg, default is `ffmpeg'">, + positional<"savefile", "Savefile to load", file<>, false> + >; +} + +int main(int argc, char *argv[]) { + auto opts = detail::options::parse(argc, argv); Application app("FFMpeGraph"); app.Run(); return 0;