Browse Source

Finished node editor functionality

main
Annwan 3 months ago
parent
commit
1f9e9f6d5c
  1. 2
      .clang-format
  2. 4
      .gitignore
  3. 33
      CMakeLists.txt
  4. 163
      src/Application.cppm
  5. 52
      src/Base.cppm
  6. 32
      src/Button.cppm
  7. 46
      src/Node.cppm
  8. 136
      src/Port.cppm
  9. 14
      src/main.cpp

2
.clang-format

@ -104,7 +104,7 @@ Cpp11BracedListStyle: true
ContinuationIndentWidth: 4
ConstructorInitializerIndentWidth: 4
CompactNamespaces: false
ColumnLimit: 80
ColumnLimit: 120
BreakStringLiterals: true
BreakInheritanceList: BeforeComma
AlignArrayOfStructures: Left

4
.gitignore

@ -1,5 +1,5 @@
build/*
/ffmpegraph
/.idea
/cmake-build-debug
/cmake-build-release
/out
/release

33
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
$<$<CONFIG:DEBUG>:-O0 -g3 -glldb>
$<$<CONFIG:RELEASE>:-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")

163
src/Application.cppm

@ -1,89 +1,114 @@
module;
#include <algorithm>
#include <cmath>
#include <memory>
#include <print>
#include <ranges>
#include <raylib.h>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <memory>
#include <cmath>
#include <deque>
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<std::string> message_queue;
std::string title;
bool should_close;
std::vector<std::unique_ptr<Node>> nodes;
Node *selected_node = nullptr;
OutputPort *selected_port = nullptr;
Button run_button;
u64 framecounter = 0;
};
}
} // namespace ffmpegraph
module :private;
namespace ffmpegraph {
Application::Application(std::string _title): title(std::move(_title)), should_close(false) {
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<InputNode>());
nodes.push_back(std::make_unique<StringConstantNode>());
nodes.push_back(std::make_unique<InputNode>());
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) {
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<InputPort *>(port);
if (iport) {
selected_port->Connect(*iport);
}
if (iport) { selected_port->TryConnect(*iport); }
selected_port = nullptr;
}
else {
} else if (run_button.IsHovered()) {
ExecuteGraph();
} else {
i32 x = GetMouseX();
i32 y = GetMouseY();
auto port = GetPort();
auto oport = dynamic_cast<OutputPort *>(port);
if (oport) {
if (oport->GetConnected()) {
oport->Disconnect();
} else {
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()) {
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;
}
@ -91,26 +116,88 @@ void Application::ProcessEvents() {
}
}
}
void Application::ExecuteGraph() {
std::unordered_map<Node *, isz> index_map;
std::unordered_map<isz, std::unordered_set<isz>> 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<InputNode *>(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<StringConstantNode *>(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
}
void Application::Render() const {
BeginDrawing();
ClearBackground(RAYWHITE);
if (selected_port) {
DrawLine(selected_port->pos_x, selected_port->pos_y, GetMouseX(), GetMouseY(), BLACK);
std::vector<Node *> 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();
}
for (auto &node : nodes) {
node->Render();
}
void Application::OnKeyPressed(i32 key) {
auto selected = dynamic_cast<StringConstantNode *>(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);
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();
this->Tick();
this->Render();
}
}
Application::~Application(){
CloseWindow();
}
}
Application::~Application() { CloseWindow(); }
} // namespace ffmpegraph

52
src/Base.cppm

@ -1,7 +1,12 @@
module;
#include <cstdint>
#include <type_traits>
#include <format>
#include <ranges>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <vector>
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 <typename... Types> struct Overloaded : Types... {
using Types::operator()...;
};
template <typename... Types> Overloaded(Types...) -> Overloaded<Types...>;
}
template <class T> struct std::formatter<std::vector<T>> : std::formatter<std::string> {
template <class FormatContext> auto format(std::vector<T> 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<std::string>::format(repr, ctx);
}
};
template <class T> struct std::formatter<std::unordered_set<T>> : std::formatter<std::string> {
template <class FormatContext> auto format(std::unordered_set<T> 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<std::string>::format(repr, ctx);
}
};
template <class T, class U> struct std::formatter<std::unordered_map<T, U>> : std::formatter<std::string> {
template <class FormatContext> auto format(std::unordered_map<T, U> 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<std::string>::format(repr, ctx);
}
};

32
src/Button.cppm

@ -0,0 +1,32 @@
module;
#include <string>
#include <raylib.h>
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;
}
}

46
src/Node.cppm

@ -1,10 +1,10 @@
module;
#include <raylib.h>
#include <vector>
#include <string>
#include <vector>
export module Node;
export import :Port;
import Base;
import Port;
export namespace ffmpegraph {
@ -15,14 +15,25 @@ struct Node {
i32 Height() const;
i32 pos_x{}, pos_y{};
std::vector<Port*> ports;
virtual void Run() = 0;
protected:
i32 width = min_port_width;
};
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;
};
}
@ -30,24 +41,39 @@ module : private;
namespace ffmpegraph {
i32 Node::Width() const {return 100;}
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);
i32 new_width = min_port_width;
for (auto* port : ports) {
port->Render(pos_x, port_y);
new_width = std::max(port->Render(pos_x, port_y, width), new_width);
port_y += 20;
};
}
width = new_width;
}
} // namespace ffmpegraph

136
src/Port.cppm

@ -1,87 +1,141 @@
module;
#include <optional>
#include <string>
#include <format>
#include <limits>
#include <raylib.h>
export module Port;
#include <string>
#include <variant>
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<i32, std::string, std::monostate>;
}
template <> struct std::formatter<ffmpegraph::PortData> : std::formatter<std::string> {
template <class FormatContext> 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<std::string>::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<int> value = std::nullopt;
PortType type;
PortData value = std::monostate{};
public:
explicit InputPort(std::string name): Port(std::move(name)) {}
std::optional<int> 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<int>);
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<i32>::min();
}
};
std::optional<int> InputPort::GetValue(){
return value;
}
void OutputPort::SetValue(std::optional<int> 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<i32>::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){
bool OutputPort::TryConnect(InputPort& ip) {
if (ip.type == type) {
connected = &ip;
return true;
}
void OutputPort::Disconnect(){
connected = nullptr;
return false;
}
void OutputPort::Render(i32 x, i32 y) {
pos_x = x+100; pos_y = y+10;
DrawRectangleLines(x, y, 100, 20, BLACK);
void OutputPort::Disconnect() { connected = nullptr; }
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);
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);
}
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

14
src/main.cpp

@ -1,8 +1,20 @@
#include <clopts.hh>
#include <print>
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;

Loading…
Cancel
Save