feat: full feature set - filters, context menu, patchbay, position persistence

Backend:
- GET/PUT /api/positions - server-side node position persistence (~/.config/pwweb/)
- GET/PUT /api/patchbay - save/load connection snapshots
- POST /api/patchbay/apply - apply saved connections

Frontend (custom SVG canvas):
- Right-click wire to disconnect (context menu)
- Port type filter toolbar (audio/midi/video/other toggles)
- Save/Load patchbay buttons
- Node positions persisted to localStorage + server
- Node border color by mode (green=output, red=input)
- Type indicator in node header [audio] [midi]
- Selected wire highlight (white, thicker)
- Select wire + Delete to disconnect
- Drag output port to input port to connect
- Pan (drag bg) and zoom (scroll wheel)
- Filtered ports shown as dim dots when hidden
This commit is contained in:
joren
2026-03-29 22:45:10 +02:00
parent f8c57fbdd3
commit 77e2fdca14
3 changed files with 388 additions and 286 deletions

View File

@@ -4,9 +4,40 @@
#include <cstdio>
#include <algorithm>
#include <condition_variable>
#include <fstream>
#include <sys/stat.h>
#include <pwd.h>
using namespace pwgraph;
// ============================================================================
// File I/O helpers
// ============================================================================
std::string WebServer::dataDir() const {
const char *home = getenv("HOME");
if (!home) {
auto *pw = getpwuid(getuid());
if (pw) home = pw->pw_dir;
}
std::string dir = (home ? home : "/tmp");
dir += "/.config/pwweb";
mkdir(dir.c_str(), 0755);
return dir;
}
std::string WebServer::readFile(const std::string &path) const {
std::ifstream f(path);
if (!f.is_open()) return "{}";
return std::string((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
}
void WebServer::writeFile(const std::string &path, const std::string &data) const {
std::ofstream f(path);
if (f.is_open()) f << data;
}
// ============================================================================
// JSON serialization helpers
// ============================================================================
@@ -309,6 +340,69 @@ void WebServer::setupRoutes() {
};
m_http.Options("/api/connect", cors_handler);
m_http.Options("/api/disconnect", cors_handler);
m_http.Options("/api/positions", cors_handler);
m_http.Options("/api/patchbay", cors_handler);
// Positions persistence: GET /api/positions
m_http.Get("/api/positions", [this](const httplib::Request &, httplib::Response &res) {
std::string path = dataDir() + "/positions.json";
res.set_content(readFile(path), "application/json");
res.set_header("Access-Control-Allow-Origin", "*");
});
// Positions persistence: PUT /api/positions
m_http.Put("/api/positions", [this](const httplib::Request &req, httplib::Response &res) {
std::string path = dataDir() + "/positions.json";
writeFile(path, req.body);
res.set_content("{\"ok\":true}", "application/json");
res.set_header("Access-Control-Allow-Origin", "*");
});
// Patchbay: GET /api/patchbay
m_http.Get("/api/patchbay", [this](const httplib::Request &, httplib::Response &res) {
std::string path = dataDir() + "/patchbay.json";
res.set_content(readFile(path), "application/json");
res.set_header("Access-Control-Allow-Origin", "*");
});
// Patchbay: PUT /api/patchbay (save current connections as a named snapshot)
m_http.Put("/api/patchbay", [this](const httplib::Request &req, httplib::Response &res) {
std::string path = dataDir() + "/patchbay.json";
writeFile(path, req.body);
res.set_content("{\"ok\":true}", "application/json");
res.set_header("Access-Control-Allow-Origin", "*");
});
// Patchbay: POST /api/patchbay/apply (apply saved connections to current graph)
m_http.Post("/api/patchbay/apply", [this](const httplib::Request &req, httplib::Response &res) {
// req.body is a JSON array of {output_port_id, input_port_id} pairs
// We just parse it and connect each pair
// Simple parsing: find all pairs
std::string body = req.body;
size_t pos = 0;
int connected = 0;
while (pos < body.size()) {
uint32_t out_id = 0, in_id = 0;
int chars = 0;
if (sscanf(body.c_str() + pos,
"{\"output_port_id\":%u,\"input_port_id\":%u}%n",
&out_id, &in_id, &chars) == 2 ||
sscanf(body.c_str() + pos,
"{\"output_port_id\":%u, \"input_port_id\":%u}%n",
&out_id, &in_id, &chars) == 2)
{
if (m_engine.connectPorts(out_id, in_id))
connected++;
}
pos = body.find('{', pos + 1);
if (pos == std::string::npos) break;
}
if (connected > 0) broadcastGraph();
char buf[64];
snprintf(buf, sizeof(buf), "{\"ok\":true,\"connected\":%d}", connected);
res.set_content(buf, "application/json");
res.set_header("Access-Control-Allow-Origin", "*");
});
}
// end of web_server.cpp