Annwan
3 months ago
9 changed files with 411 additions and 129 deletions
-
2.clang-format
-
4.gitignore
-
33CMakeLists.txt
-
179src/Application.cppm
-
52src/Base.cppm
-
32src/Button.cppm
-
56src/Node.cppm
-
138src/Port.cppm
-
14src/main.cpp
@ -1,5 +1,5 @@ |
|||
build/* |
|||
/ffmpegraph |
|||
/.idea |
|||
/cmake-build-debug |
|||
/cmake-build-release |
|||
/out |
|||
/release |
@ -1,116 +1,203 @@ |
|||
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; |
|||
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<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) { |
|||
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); |
|||
} |
|||
auto iport = dynamic_cast<InputPort *>(port); |
|||
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); |
|||
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()) { |
|||
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::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 |
|||
} |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
|
|||
void Application::Render() const { |
|||
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); |
|||
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 |
@ -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; |
|||
} |
|||
|
|||
} |
@ -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; |
|||
} |
|||
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); |
|||
} |
|||
|
|||
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 |
@ -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; |
|||
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue