#include "Graph_WebSocket.hpp" #include "Exclusive_Page.hpp" #include #include #include #include #include #include #include namespace aethera::web { namespace { std::string graph_id_from_path(std::string_view path) { const auto split = path.find_last_of('/'); return split == std::string_view::npos ? std::string{} : std::string(path.substr(split + 1)); } std::uint32_t input_dimension(std::uint32_t value, std::uint32_t minimum, std::uint32_t maximum) { return std::clamp(value, minimum, maximum); } } struct Graph_WebSocket::Private { std::weak_ptr connection; std::shared_ptr plot; Plot::Stream_Id stream{}; std::atomic_bool attached{}; std::mutex viewport_mutex; std::uint32_t width{320}; /* 当前图集槽位对应的输入坐标宽度。 */ std::uint32_t height{192}; /* 当前图集槽位对应的输入坐标高度。 */ std::string page_id; /* 与页面级媒体连接共用的独占租约。 */ }; Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr plot, std::string page_id) : d(std::make_unique()) { d->connection = std::move(connection); d->plot = std::move(plot); d->page_id = std::move(page_id); } Graph_WebSocket::~Graph_WebSocket() { close(); } void Graph_WebSocket::start() { if (d->attached.exchange(true, std::memory_order_acq_rel)) return; const auto weak = weak_from_this(); d->stream = d->plot->subscribe( [weak](std::shared_ptr frame) { if (const auto socket = weak.lock()) socket->deliver_frame(std::move(frame)); }); } void Graph_WebSocket::deliver_frame( std::shared_ptr frame) { if (!frame || frame->notification.empty() || !d->attached.load(std::memory_order_acquire)) return; const auto connection = d->connection.lock(); if (connection && connection->connected()) { try { connection->send(frame->notification, drogon::WebSocketMessageType::Text); } catch (...) {} } } void Graph_WebSocket::receive(std::string_view message) { const auto json = nlohmann::json::parse(message, nullptr, false); if (json.is_discarded() || !json.is_object()) return; const auto kind = json.value("kind", std::string{}); try { if (kind == "stream") { std::uint32_t width{320}; std::uint32_t height{192}; if (const auto viewport = json.find("viewport"); viewport != json.end() && viewport->is_object()) { width = input_dimension(viewport->value("width", 320U), 160U, 1920U); height = input_dimension(viewport->value("height", 192U), 120U, 1080U); } std::lock_guard lock(d->viewport_mutex); d->width = width; d->height = height; return; } if (kind == "manual_render") { d->plot->render_once(); return; } if (kind != "input") return; const auto input = json.find("event"); if (input == json.end() || !input->is_object()) return; const auto type = magic_enum::enum_cast( input->value("type", std::string{})); if (!type) return; std::uint32_t width{}; std::uint32_t height{}; { std::lock_guard lock(d->viewport_mutex); width = d->width; height = d->height; } Plot_Input_Event decoded; decoded.type = *type; const auto read_point = [&](std::string_view key, render_2d::Point_F& point, bool viewport_relative) { const auto value = input->find(key); if (value == input->end() || !value->is_object()) return; const auto x = value->value("x", 0.0); const auto y = value->value("y", 0.0); point.x = viewport_relative ? std::clamp(x, 0.0, static_cast(width)) : x; point.y = viewport_relative ? std::clamp(y, 0.0, static_cast(height)) : y; }; read_point("position", decoded.position, true); read_point("global_position", decoded.global_position, false); decoded.button = magic_enum::enum_cast( input->value("button", std::string{"none"})).value_or(Mouse_Button::none); decoded.buttons = static_cast( std::clamp(input->value("buttons", 0), 0, 255)); decoded.modifiers = static_cast( std::clamp(input->value("modifiers", 0), 0, 15)); decoded.pixel_delta_x = std::clamp( input->value("pixel_delta_x", 0.0), -4096.0, 4096.0); decoded.pixel_delta_y = std::clamp( input->value("pixel_delta_y", 0.0), -4096.0, 4096.0); decoded.angle_delta_x = std::clamp( input->value("angle_delta_x", 0.0), -120.0, 120.0); decoded.angle_delta_y = std::clamp( input->value("angle_delta_y", 0.0), -120.0, 120.0); decoded.key = magic_enum::enum_cast( input->value("key", std::string{"unknown"})).value_or(Key::unknown); decoded.native_key = input->value("native_key", 0U); decoded.auto_repeat = input->value("auto_repeat", false); d->plot->submit_input(std::move(decoded)); } catch (const nlohmann::json::exception&) {} catch (...) {} } void Graph_WebSocket::close() noexcept { if (!d->attached.exchange(false, std::memory_order_acq_rel)) return; try { d->plot->unsubscribe(d->stream); } catch (...) {} try { release_exclusive_page(d->page_id); } catch (...) {} } Graph_WebSocket_Controller::Graph_WebSocket_Controller(Plot_Resolver resolver) : resolve_plot(std::move(resolver)) {} void Graph_WebSocket_Controller::initPathRouting() { drogon::app().registerWebSocketControllerRegex( "^/ws/plot/[^/]+$", classTypeName()); } void Graph_WebSocket_Controller::handleNewConnection( const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) { try { auto plot = resolve_plot(graph_id_from_path(request->path())); if (!plot) { try { connection->shutdown(drogon::CloseCode::kViolation, "Unknown Aethera plot"); } catch (...) {} return; } const auto page_id = request->getParameter("page"); if (!valid_page_id(page_id) || !acquire_exclusive_page(page_id)) { try { connection->send(nlohmann::json{{"kind", "exclusive_page_rejected"}, {"message", "Aethera Gallery is already owned by another page instance"}}.dump(), drogon::WebSocketMessageType::Text); } catch (...) {} try { connection->shutdown(drogon::CloseCode::kViolation, "Aethera Gallery allows one page instance"); } catch (...) {} return; } try { auto socket = std::make_shared( connection, std::move(plot), page_id); connection->setContext(socket); connection->setPingMessage( "aethera-gallery-plot", std::chrono::seconds(20)); socket->start(); } catch (...) { if (const auto socket = connection->getContext()) socket->close(); else release_exclusive_page(page_id); try { connection->shutdown(drogon::CloseCode::kViolation, "Aethera plot setup failed"); } catch (...) {} } } catch (...) { try { connection->shutdown(drogon::CloseCode::kViolation, "Aethera plot controller failed"); } catch (...) {} } } void Graph_WebSocket_Controller::handleNewMessage( const drogon::WebSocketConnectionPtr& connection, std::string&& message, const drogon::WebSocketMessageType& type) { try { if (type != drogon::WebSocketMessageType::Text || message.size() > 64 * 1024) return; if (const auto socket = connection->getContext()) socket->receive(message); } catch (...) {} } void Graph_WebSocket_Controller::handleConnectionClosed( const drogon::WebSocketConnectionPtr& connection) { try { if (const auto socket = connection->getContext()) socket->close(); connection->clearContext(); } catch (...) {} } }