From 2912ae0df46f27d670244c5013df215e09c9dd96 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 21 Aug 2026 13:50:32 +0800 Subject: [PATCH] =?UTF-8?q?web=5Fserver=20=E5=88=9D=E6=AD=A5=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_server/src/Gallery_Plots.hpp | 13 + web_server/src/Graph_WebSocket.cpp | 4 +- web_server/src/Graph_WebSocket.hpp | 20 +- web_server/src/Plot.cpp | 659 ++++++++++++++++++++++------- web_server/src/Plot.hpp | 42 +- web_server/src/Web_Server.cpp | 109 ++++- 6 files changed, 659 insertions(+), 188 deletions(-) create mode 100644 web_server/src/Gallery_Plots.hpp diff --git a/web_server/src/Gallery_Plots.hpp b/web_server/src/Gallery_Plots.hpp new file mode 100644 index 0000000..2752a46 --- /dev/null +++ b/web_server/src/Gallery_Plots.hpp @@ -0,0 +1,13 @@ +#pragma once +#include "Plot.hpp" + +namespace aethera::web { +[[nodiscard]] std::shared_ptr make_spectrum_plot(asio::any_io_executor executor); +[[nodiscard]] std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor); +[[nodiscard]] std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor); +[[nodiscard]] std::shared_ptr make_afterglow_plot(asio::any_io_executor executor); +[[nodiscard]] std::shared_ptr make_waterfall_plot(asio::any_io_executor executor); +[[nodiscard]] std::shared_ptr make_constellation_plot(asio::any_io_executor executor); +[[nodiscard]] std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor); +[[nodiscard]] std::shared_ptr make_datoviz_point_plot(asio::any_io_executor executor); +} diff --git a/web_server/src/Graph_WebSocket.cpp b/web_server/src/Graph_WebSocket.cpp index 1e7d3b2..7bfd0ea 100644 --- a/web_server/src/Graph_WebSocket.cpp +++ b/web_server/src/Graph_WebSocket.cpp @@ -21,8 +21,8 @@ void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_ 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; Plot_Event event; if (const auto value = json.find("time"); value != json.end() && value->is_number()) event.time_milliseconds = value->get(); if (const auto value = json.find("width"); value != json.end() && value->is_number_unsigned()) event.width = std::clamp(value->get(), 160U, 1920U); if (const auto value = json.find("height"); value != json.end() && value->is_number_unsigned()) event.height = std::clamp(value->get(), 120U, 1080U); d->plot->submit(event); } void Graph_WebSocket::close() { if (!d->attached) return; d->plot->detach(d->owner); d->attached = false; } -Graph_WebSocket_Controller::Graph_WebSocket_Controller(std::shared_ptr value) : registry(std::move(value)) {} -void Graph_WebSocket_Controller::handleNewConnection(const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) { auto plot = registry->acquire(graph_id_from_path(request->path())); if (!plot) { connection->shutdown(drogon::CloseCode::kViolation, "Unknown Aethera plot"); return; } auto socket = std::make_shared(connection, std::move(plot)); connection->setContext(socket); connection->setPingMessage("aethera-gallery", std::chrono::seconds(20)); socket->start(); } +Graph_WebSocket_Controller::Graph_WebSocket_Controller(Plot_Resolver resolver) : resolve_plot(std::move(resolver)) {} +void Graph_WebSocket_Controller::handleNewConnection(const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) { auto plot = resolve_plot(graph_id_from_path(request->path())); if (!plot) { connection->shutdown(drogon::CloseCode::kViolation, "Unknown Aethera plot"); return; } auto socket = std::make_shared(connection, std::move(plot)); connection->setContext(socket); connection->setPingMessage("aethera-gallery", std::chrono::seconds(20)); socket->start(); } void Graph_WebSocket_Controller::handleNewMessage(const drogon::WebSocketConnectionPtr& connection, std::string&& message, const drogon::WebSocketMessageType& type) { if (type != drogon::WebSocketMessageType::Text || message.size() > 64 * 1024) return; if (const auto socket = connection->getContext()) socket->receive(message); } void Graph_WebSocket_Controller::handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) { if (const auto socket = connection->getContext()) socket->close(); connection->clearContext(); } } diff --git a/web_server/src/Graph_WebSocket.hpp b/web_server/src/Graph_WebSocket.hpp index 88188b3..7b523b3 100644 --- a/web_server/src/Graph_WebSocket.hpp +++ b/web_server/src/Graph_WebSocket.hpp @@ -4,6 +4,8 @@ #include #include #include +#include + namespace aethera::web { class Graph_WebSocket final : public std::enable_shared_from_this { public: @@ -16,18 +18,24 @@ public: void close(); private: struct Private; - std::unique_ptr d; /* Drogon 连接与图会话绑定。 */ + std::unique_ptr d; }; -class Graph_WebSocket_Controller final : public drogon::WebSocketController { + +class Graph_WebSocket_Controller final + : public drogon::WebSocketController { public: - explicit Graph_WebSocket_Controller(std::shared_ptr registry); - void handleNewMessage(const drogon::WebSocketConnectionPtr& connection, std::string&& message, const drogon::WebSocketMessageType& type) override; - void handleNewConnection(const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) override; + using Plot_Resolver = std::function(std::string_view)>; + explicit Graph_WebSocket_Controller(Plot_Resolver resolver); + void handleNewMessage(const drogon::WebSocketConnectionPtr& connection, + std::string&& message, + const drogon::WebSocketMessageType& type) override; + void handleNewConnection(const drogon::HttpRequestPtr& request, + const drogon::WebSocketConnectionPtr& connection) override; void handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) override; WS_PATH_LIST_BEGIN WS_ADD_PATH_VIA_REGEX("^/ws/plot/[^/]+$"); WS_PATH_LIST_END private: - std::shared_ptr registry; /* 所有 WebSocket 共用的 Plot 注册表。 */ + Plot_Resolver resolve_plot; }; } diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index 876fd23..4e1374f 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -15,43 +14,114 @@ #include #include #include -#include -#include #include #include #include #include +#include + namespace aethera::web { namespace { using namespace render_2d; using namespace render_3d; using Scene_2D = Impl; +using Scene_3D = Impl; using Frequency_Axis_Object = Impl; using Numeric_Axis_Object = Impl; using Time_Axis_Object = Impl; -template -std::unique_ptr build(Arguments&&... arguments) { typename Object::Builder builder(std::forward(arguments)...); auto result = builder.build(); if (!result) throw std::logic_error("gallery graph dependency graph is invalid"); return std::move(result).value(); } -template -void configure_axis(Axis* axis, Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Size canvas) { axis->template set<&Abs_Axis::Prop::orientation>(orientation); axis->template set<&Abs_Axis::Prop::position>(position); axis->template set<&Abs_Axis::Prop::pixel_length>(length); axis->template set<&Abs_Axis::Prop::canvas_size>(canvas); } + +template +auto finish_build(Builder&& builder) { + auto result = builder.build(); + if (!result) throw std::logic_error("gallery scene topology validation failed"); + return std::move(result).value(); +} + +template +std::shared_ptr finish_shared(Builder&& builder) { + return std::shared_ptr(finish_build(std::forward(builder))); +} + template -void append_binary(std::string& output, Integer value) { const auto start = output.size(); output.resize(start + sizeof(Integer)); std::memcpy(output.data() + start, &value, sizeof(Integer)); } -std::string encode_frame(Image_View image, std::uint64_t sequence) { std::string output; output.reserve(24 + static_cast(image.width) * image.height * 4); append_binary(output, std::uint32_t{0x41544852}); append_binary(output, std::uint16_t{1}); append_binary(output, std::uint16_t{}); append_binary(output, static_cast(image.width)); append_binary(output, static_cast(image.height)); append_binary(output, sequence); for (int y = 0; y < image.height; ++y) { const auto* row = reinterpret_cast(image.data + static_cast(y) * image.stride); for (int x = 0; x < image.width; ++x) { const auto* pixel = row + x * 4; output.push_back(static_cast(pixel[2])); output.push_back(static_cast(pixel[1])); output.push_back(static_cast(pixel[0])); output.push_back(static_cast(pixel[3])); } } return output; } -std::string encode_frame(const render_3d::Pixel_Frame& frame, std::uint64_t sequence) { std::string output; output.reserve(24 + frame.rgba8.size()); append_binary(output, std::uint32_t{0x41544852}); append_binary(output, std::uint16_t{1}); append_binary(output, std::uint16_t{}); append_binary(output, frame.extent.width); append_binary(output, frame.extent.height); append_binary(output, sequence); output.append(reinterpret_cast(frame.rgba8.data()), frame.rgba8.size()); return output; } -struct Schema_Query { Plot::Json_Handler handler{}; }; -struct Prop_Write { std::string key{}; nlohmann::json value{}; Plot::Json_Handler handler{}; }; -using Plot_Input = std::variant; -struct Plot_2D { - std::unique_ptr scene{}; /* 鏈€缁堜簩缁?Scene銆?*/ - std::unique_ptr frequency{}; /* 棰戠巼杞达紱涓嶇敤鏃朵粛涓虹┖銆?*/ - std::unique_ptr horizontal{}; /* 鏄熷骇鍥炬按骞虫暟鍊艰酱銆?*/ - std::unique_ptr vertical{}; /* 鍔熺巼鎴栨槦搴у浘鍨傜洿杞淬€?*/ - std::unique_ptr time{}; /* 鏃堕棿杞达紱涓嶇敤鏃朵负绌恒€?*/ - std::unique_ptr plot{}; /* 鍏蜂綋 Plottable 鐨勫敮涓€鎵€鏈夋潈銆?*/ - std::function update{}; /* 鏍规嵁娴忚鍣ㄦ椂閽熸洿鏂版潈濞?Prop銆?*/ - std::unique_ptr descriptor{}; /* 鐩存帴璇诲啓 plot 鐨?Structive 鍗忚瑙嗗浘銆?*/ +void append_binary(std::string& output, Integer value) { + const auto start = output.size(); + output.resize(start + sizeof(Integer)); + std::memcpy(output.data() + start, &value, sizeof(Integer)); +} + +std::string encode_frame(Image_View image, std::uint64_t sequence) { + std::string output; + output.reserve(24 + static_cast(image.width) * image.height * 4); + append_binary(output, std::uint32_t{0x41544852}); + append_binary(output, std::uint16_t{1}); + append_binary(output, std::uint16_t{}); + append_binary(output, static_cast(image.width)); + append_binary(output, static_cast(image.height)); + append_binary(output, sequence); + for (int y = 0; y < image.height; ++y) { + const auto* row = reinterpret_cast( + image.data + static_cast(y) * image.stride); + for (int x = 0; x < image.width; ++x) { + const auto* pixel = row + x * 4; + output.push_back(static_cast(pixel[2])); + output.push_back(static_cast(pixel[1])); + output.push_back(static_cast(pixel[0])); + output.push_back(static_cast(pixel[3])); + } + } + return output; +} + +std::string encode_frame(const Pixel_Frame& frame, std::uint64_t sequence) { + std::string output; + output.reserve(24 + frame.rgba8.size()); + append_binary(output, std::uint32_t{0x41544852}); + append_binary(output, std::uint16_t{1}); + append_binary(output, std::uint16_t{}); + append_binary(output, frame.extent.width); + append_binary(output, frame.extent.height); + append_binary(output, sequence); + output.append(reinterpret_cast(frame.rgba8.data()), frame.rgba8.size()); + return output; +} + +struct Schema_Query { Plot::Json_Handler handler; }; +struct Prop_Write { + std::string key; + nlohmann::json value; + Plot::Json_Handler handler; }; -template -void bind_renderable_adapter(Holder& plot, Object& object) { +using Plot_Input = std::variant; + +class Scene_View_Model final : public Plot::Scene_View { +public: + Scene_View_Model(std::vector> owned_objects, + std::unique_ptr value_descriptor, + std::function value_update) + : objects(std::move(owned_objects)), + descriptor(std::move(value_descriptor)), + update_scene(std::move(value_update)) {} + + nlohmann::json schema() const override { return descriptor->schema(); } + nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override { + return descriptor->write_prop(key, value); + } + void update(const Plot_Event& event) override { update_scene(event); } + +private: + std::vector> objects; + std::unique_ptr descriptor; + std::function update_scene; +}; + +#define AETHERA_PROP(Type, Name) detail::Prop_Field<&Type::Prop::Name, #Name> +#define AETHERA_STATE(Type, Name) detail::State_Field + +template +std::unique_ptr make_scene_view( + Object& object, + std::vector> owners, + std::function update) { using Tag = typename Definition::Base_Tag; using State = typename Definition::State; using Adapter = detail::Renderable_Adapter, detail::State_Field, detail::State_Field>; - plot.descriptor = detail::make_renderable_descriptor(Adapter{object}); + return std::make_unique( + std::move(owners), + detail::make_renderable_descriptor(Adapter{object}), + std::move(update)); } -#define AETHERA_PROP(Type, Name) detail::Prop_Field<&Type::Prop::Name, #Name> -#define AETHERA_STATE(Type, Name) detail::State_Field + +Frequency_Axis_Object::Builder frequency_axis_builder(Size canvas) { + Frequency_Axis_Object::Builder builder; + builder + .set(&Abs_Axis::Prop::orientation, Axis_Orientation::horizontal) + .set(&Abs_Axis::Prop::position, Point_F{64.0, 370.0}) + .set(&Abs_Axis::Prop::pixel_length, 620.0) + .set(&Abs_Axis::Prop::canvas_size, canvas) + .set(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0}); + return builder; +} + +Numeric_Axis_Object::Builder numeric_axis_builder( + Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, + Axis_Range range, Size canvas) { + Numeric_Axis_Object::Builder builder; + builder + .set(&Abs_Axis::Prop::orientation, orientation) + .set(&Abs_Axis::Prop::position, position) + .set(&Abs_Axis::Prop::pixel_length, length) + .set(&Abs_Axis::Prop::canvas_size, canvas) + .set(&Numeric_Axis::Prop::coordinate_range, range); + return builder; +} + +Time_Axis_Object::Builder time_axis_builder( + Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Size canvas) { + Time_Axis_Object::Builder builder; + builder + .set(&Abs_Axis::Prop::orientation, orientation) + .set(&Abs_Axis::Prop::position, position) + .set(&Abs_Axis::Prop::pixel_length, length) + .set(&Abs_Axis::Prop::canvas_size, canvas); + return builder; +} + +template +void resize_axes(Size viewport, Axes*... axes) { + (axes->template set<&Abs_Axis::Prop::canvas_size>(viewport), ...); +} + template -Plot_2D make_plot_2d() { - Plot_2D result; - const Size canvas{720, 420}; - result.scene = build(); - result.scene->set<&Render_Scene_2D::Prop::viewport>(canvas); - result.scene->set<&Render_Scene_2D::Prop::background>(Color{7, 13, 24, 255}); - result.scene->activate_view(); - auto make_frequency = [&] { result.frequency = build(); configure_axis(result.frequency.get(), Axis_Orientation::horizontal, Point_F{64.0, 370.0}, 620.0, canvas); result.frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); }; - auto make_vertical = [&](Axis_Range range) { - result.vertical = build(); - configure_axis(result.vertical.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); result.vertical->set<&Numeric_Axis::Prop::coordinate_range>(range); - }; +std::shared_ptr build_2d_plot(asio::any_io_executor executor) { + constexpr Size canvas{720, 420}; + Scene_2D::Builder scene_builder; + scene_builder + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true); + auto scene = finish_build(std::move(scene_builder)); + if constexpr (std::same_as) { - make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Spectrum::Prop::max_hold_visible>(true); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_renderable_adapter(result, *raw); result.update = [raw](double time) { std::array samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { const double x = static_cast(i) / samples.size(); samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(time * 0.001), 2.0)) + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) + 2.5 * std::sin(i * 0.31 + time * 0.004); } raw->update_samples(samples); }; result.plot = std::move(object); + auto frequency = finish_shared(frequency_axis_builder(canvas)); + auto vertical = finish_shared(numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas)); + Impl::Builder builder(scene.get(), frequency.get(), vertical.get()); + builder + .set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .set(&Spectrum::Prop::max_hold_visible, true); + auto object = finish_shared>(std::move(builder)); + object->mark_dirty(); + object->mark_dirty(); + auto update = [raw = object.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, vertical); + std::array samples{}; + for (std::size_t i = 0; i < samples.size(); ++i) { + const double x = static_cast(i) / samples.size(); + samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(event.time_milliseconds * 0.001), 2.0)) + + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) + + 2.5 * std::sin(i * 0.31 + event.time_milliseconds * 0.004); + } + raw->update_samples(samples); + }; + std::vector> owners{frequency, vertical, object}; + auto view = make_scene_view(*object, std::move(owners), std::move(update)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } else if constexpr (std::same_as) { - result.time = build(); configure_axis(result.time.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); make_vertical({-1.2, 1.2}); auto object = build>(result.scene.get(), result.time.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_renderable_adapter(result, *raw); auto tick = std::make_shared(); result.update = [raw, tick](double time) { raw->append_sample((*tick)++, std::sin(time * 0.0025) * 0.8 + std::sin(time * 0.0007) * 0.2); }; result.plot = std::move(object); + auto time = finish_shared(time_axis_builder( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas)); + auto vertical = finish_shared(numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas)); + Impl::Builder builder(scene.get(), time.get(), vertical.get()); + auto object = finish_shared>(std::move(builder)); + object->mark_dirty(); + object->mark_dirty(); + auto tick = std::make_shared(); + auto update = [raw = object.get(), time = time.get(), vertical = vertical.get(), tick](const Plot_Event& event) { + resize_axes({static_cast(event.width), static_cast(event.height)}, time, vertical); + raw->append_sample((*tick)++, std::sin(event.time_milliseconds * 0.0025) * 0.8 + + std::sin(event.time_milliseconds * 0.0007) * 0.2); + }; + std::vector> owners{time, vertical, object, tick}; + auto view = make_scene_view( + *object, std::move(owners), std::move(update)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } else if constexpr (std::same_as) { - make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Sweep_Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_renderable_adapter(result, *raw); result.update = [raw](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -90.0 + 35.0 * std::sin(i * 0.08 + time * 0.002); raw->append_block(values); }; result.plot = std::move(object); + auto frequency = finish_shared(frequency_axis_builder(canvas)); + auto vertical = finish_shared(numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas)); + Impl::Builder builder(scene.get(), frequency.get(), vertical.get()); + builder.set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}); + auto object = finish_shared>(std::move(builder)); + object->mark_dirty(); + object->mark_dirty(); + auto update = [raw = object.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, vertical); + std::array values{}; + for (std::size_t i = 0; i < values.size(); ++i) + values[i] = -90.0 + 35.0 * std::sin(i * 0.08 + event.time_milliseconds * 0.002); + raw->append_block(values); + }; + std::vector> owners{frequency, vertical, object}; + auto view = make_scene_view(*object, std::move(owners), std::move(update)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } else if constexpr (std::same_as) { - make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Afterglow::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Afterglow::Prop::power_range>(Axis_Range{-110.0, 0.0}); object->set<&Afterglow::Prop::power_point_size>(96); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_renderable_adapter(result, *raw); result.update = [raw](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow(static_cast(i) / values.size() - 0.5 - 0.18 * std::sin(time * 0.0008), 2.0)); raw->append_spectrum(values); }; result.plot = std::move(object); + auto frequency = finish_shared(frequency_axis_builder(canvas)); + auto vertical = finish_shared(numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas)); + Impl::Builder builder(scene.get(), frequency.get(), vertical.get()); + builder + .set(&Afterglow::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0}) + .set(&Afterglow::Prop::power_point_size, 96); + auto object = finish_shared>(std::move(builder)); + object->mark_dirty(); + object->mark_dirty(); + auto update = [raw = object.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, vertical); + std::array values{}; + for (std::size_t i = 0; i < values.size(); ++i) + values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow( + static_cast(i) / values.size() - 0.5 + - 0.18 * std::sin(event.time_milliseconds * 0.0008), 2.0)); + raw->append_spectrum(values); + }; + std::vector> owners{frequency, vertical, object}; + auto view = make_scene_view(*object, std::move(owners), std::move(update)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } else if constexpr (std::same_as) { - make_frequency(); result.time = build(); configure_axis(result.time.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); auto object = build>(result.scene.get(), result.frequency.get(), result.time.get()); object->set<&Waterfall::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Waterfall::Prop::power_range>(Axis_Range{-110.0, 0.0}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_renderable_adapter(result, *raw); auto tick = std::make_shared(); result.update = [raw, tick](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow(static_cast(i) / values.size() - 0.5 - 0.22 * std::sin(time * 0.0006), 2.0)); raw->append_row((*tick)++, values); }; result.plot = std::move(object); + auto frequency = finish_shared(frequency_axis_builder(canvas)); + auto time = finish_shared(time_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas)); + Impl::Builder builder(scene.get(), frequency.get(), time.get()); + builder + .set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0}); + auto object = finish_shared>(std::move(builder)); + object->mark_dirty(); + object->mark_dirty(); + auto tick = std::make_shared(); + auto update = [raw = object.get(), frequency = frequency.get(), time = time.get(), tick](const Plot_Event& event) { + resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, time); + std::array values{}; + for (std::size_t i = 0; i < values.size(); ++i) + values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow( + static_cast(i) / values.size() - 0.5 + - 0.22 * std::sin(event.time_milliseconds * 0.0006), 2.0)); + raw->append_row((*tick)++, values); + }; + std::vector> owners{frequency, time, object, tick}; + auto view = make_scene_view( + *object, std::move(owners), std::move(update)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } else if constexpr (std::same_as) { - result.horizontal = build(); configure_axis(result.horizontal.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); result.horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.2, 1.2}); make_vertical({-1.2, 1.2}); auto object = build>(result.scene.get(), result.horizontal.get(), result.vertical.get()); object->set<&Constellation_Diagram::Prop::i_range>(Axis_Range{-1.2, 1.2}); object->set<&Constellation_Diagram::Prop::q_range>(Axis_Range{-1.2, 1.2}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_renderable_adapter(result, *raw); result.update = [raw](double time) { const double phase = time * 0.003; raw->append_point({std::cos(phase) * 0.82 + 0.04 * std::sin(phase * 7.0), std::sin(phase) * 0.82 + 0.04 * std::cos(phase * 5.0)}); }; result.plot = std::move(object); + auto horizontal = finish_shared(numeric_axis_builder( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}, canvas)); + auto vertical = finish_shared(numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas)); + Impl::Builder builder(scene.get(), horizontal.get(), vertical.get()); + builder + .set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2}) + .set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2}); + auto object = finish_shared>(std::move(builder)); + object->mark_dirty(); + object->mark_dirty(); + auto update = [raw = object.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes({static_cast(event.width), static_cast(event.height)}, horizontal, vertical); + const double phase = event.time_milliseconds * 0.003; + raw->append_point({std::cos(phase) * 0.82 + 0.04 * std::sin(phase * 7.0), + std::sin(phase) * 0.82 + 0.04 * std::cos(phase * 5.0)}); + }; + std::vector> owners{horizontal, vertical, object}; + auto view = make_scene_view(*object, std::move(owners), std::move(update)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } else if constexpr (std::same_as) { - result.horizontal = build(); configure_axis(result.horizontal.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); result.horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); make_vertical({0.0, 100.0}); auto object = build>(result.scene.get(), result.horizontal.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_renderable_adapter(result, *raw); result.update = [](double) {}; result.plot = std::move(object); - } else { - static_assert(std::same_as, "unsupported 2D Plot definition"); + auto horizontal = finish_shared(numeric_axis_builder( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}, canvas)); + auto vertical = finish_shared(numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}, canvas)); + Impl::Builder builder(scene.get(), horizontal.get(), vertical.get()); + auto object = finish_shared>(std::move(builder)); + object->mark_dirty(); + object->mark_dirty(); + auto update = [horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes({static_cast(event.width), static_cast(event.height)}, horizontal, vertical); + }; + std::vector> owners{horizontal, vertical, object}; + auto view = make_scene_view( + *object, std::move(owners), std::move(update)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } - return result; } -struct Plot_3D { - std::unique_ptr> visual{}; /* 鐐瑰浘鍏冩潈濞佸璞°€?*/ - std::unique_ptr> scene{}; /* 寮傛 Datoviz Scene銆?*/ - std::unique_ptr descriptor{}; /* 鐩存帴璇诲啓 visual 鐨?Structive 鍗忚瑙嗗浘銆?*/ -}; -Plot_3D make_plot_3d() { Plot_3D result; result.visual = build>(); static_cast(result.visual->update_items({render_3d::Point{.position = {-0.55F, -0.2F, 0.0F}, .color = Color::red_color(), .diameter_px = 24.0F}, render_3d::Point{.position = {0.0F, 0.5F, 0.0F}, .color = Color::green_color(), .diameter_px = 30.0F}, render_3d::Point{.position = {0.55F, -0.1F, 0.0F}, .color = Color{42, 120, 255, 255}, .diameter_px = 26.0F}})); bind_renderable_adapter(result, *result.visual); result.visual->advance(); result.scene = build>(result.visual.get()); result.scene->activate_view(); return result; } -using Plot_Engine = std::variant; -template -Plot_Engine make_plot_engine_2d() { return Plot_Engine{std::in_place_type, make_plot_2d()}; } -Plot_Engine make_plot_engine_3d() { return Plot_Engine{std::in_place_type, make_plot_3d()}; } + #undef AETHERA_STATE #undef AETHERA_PROP } -namespace { -struct Plot_Catalog_Entry { - std::string_view id; /* URL 与 WebSocket 共用的稳定标识。 */ - std::string_view title; /* Gallery 展示名称。 */ - std::string_view category; /* Gallery 分类。 */ - std::string_view description; /* Gallery 用途说明。 */ - std::string_view dimension; /* Gallery 维度标签。 */ - Plot_Engine (*build)(); /* 直接构造完整 Scene/Renderable/descriptor 的 typed factory。 */ -}; -constexpr std::array plot_catalog{ - Plot_Catalog_Entry{"spectrum", "Spectrum", "Curves", "Current, maximum and minimum spectrum curves with markers.", "2D", &make_plot_engine_2d}, - Plot_Catalog_Entry{"frequency_trace", "Frequency trace", "Curves", "Time ordered frequency samples rendered as a partitioned curve.", "2D", &make_plot_engine_2d}, - Plot_Catalog_Entry{"sweep_spectrum", "Sweep spectrum", "Curves", "Incremental sweep blocks composed into one frequency curve.", "2D", &make_plot_engine_2d}, - Plot_Catalog_Entry{"afterglow", "Afterglow", "Raster", "Persistent spectrum energy rendered as reusable color blocks.", "2D", &make_plot_engine_2d}, - Plot_Catalog_Entry{"waterfall", "Waterfall", "Raster", "Time ordered spectrum rows rendered as a color raster.", "2D", &make_plot_engine_2d}, - Plot_Catalog_Entry{"constellation", "Constellation", "Signals", "I/Q samples and modulation anchors.", "2D", &make_plot_engine_2d}, - Plot_Catalog_Entry{"selection_overlay", "Selection overlay", "Interaction", "Direct-paint selection rectangle over numeric axes.", "2D", &make_plot_engine_2d}, - Plot_Catalog_Entry{"datoviz_point", "Datoviz point", "3D", "Asynchronous Vulkan point visual with GPU readback.", "3D", &make_plot_engine_3d} -}; -const Plot_Catalog_Entry* find_plot(std::string_view id) { - const auto found = std::ranges::find(plot_catalog, id, &Plot_Catalog_Entry::id); - return found == plot_catalog.end() ? nullptr : &*found; -} -} struct Plot::Private { - asio::strand strand; /* 寮曟搸鍙?descriptor 鐨勪覆琛岃闂煙銆?*/ - asio::experimental::concurrent_channel inputs; /* UI 杈撳叆闃熷垪銆?*/ - Plot_Engine engine; /* Builder 已完成的 Scene、Renderable 与 adapter 唯一所有权。 */ - std::mutex handlers_mutex; /* 淇濇姢璺?Drogon 绾跨▼鐨勫抚璁㈤槄闆嗗悎銆?*/ - std::unordered_map handlers; /* 褰撳墠 WebSocket 甯ф秷璐硅€呫€?*/ - std::atomic_uint64_t frame_sequence{}; /* 浜岃繘鍒跺抚鍗忚搴忓彿銆?*/ - explicit Private(asio::any_io_executor executor, Plot_Engine value) : strand(asio::make_strand(std::move(executor))), inputs(strand, 32), engine(std::move(value)) {} - void publish(std::string pixels) { std::vector outputs; { std::lock_guard lock(handlers_mutex); outputs.reserve(handlers.size()); for (const auto& [owner, handler] : handlers) outputs.push_back(handler); } for (auto& output : outputs) output(pixels); } - detail::Renderable_Descriptor& descriptor() { return std::visit([](auto& value) -> detail::Renderable_Descriptor& { return *value.descriptor; }, engine); } + using Scene = std::variant, std::unique_ptr>; + asio::strand strand; + asio::experimental::concurrent_channel inputs; + Scene scene; + std::unique_ptr view; + std::once_flag start_once; + std::mutex handlers_mutex; + std::unordered_map handlers; + std::atomic_uint64_t frame_sequence{}; + + template + Private(asio::any_io_executor executor, + std::unique_ptr value_scene, + std::unique_ptr value_view) + : strand(asio::make_strand(std::move(executor))), inputs(strand, 32), + scene(std::move(value_scene)), view(std::move(value_view)) {} + + void publish(std::string pixels) { + std::vector outputs; + { + std::lock_guard lock(handlers_mutex); + outputs.reserve(handlers.size()); + for (const auto& [owner, handler] : handlers) outputs.push_back(handler); + } + for (auto& output : outputs) output(pixels); + } }; -Plot::Plot(std::unique_ptr private_data) : d(std::move(private_data)) {} +Plot::Plot(asio::any_io_executor executor, + std::unique_ptr scene, + std::unique_ptr view) + : d(std::make_unique(std::move(executor), std::move(scene), std::move(view))) {} + +Plot::Plot(asio::any_io_executor executor, + std::unique_ptr scene, + std::unique_ptr view) + : d(std::make_unique(std::move(executor), std::move(scene), std::move(view))) {} + Plot::~Plot() { d->inputs.close(); } -void Plot::start() { - auto self = shared_from_this(); - if (auto* engine_2d = std::get_if(&d->engine)) { - engine_2d->scene->set_frame_callback([weak = weak_from_this()](Image_View image) { - if (auto owner = weak.lock()) { - const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; - owner->d->publish(encode_frame(image, sequence)); - } - }); - } else { - std::get(d->engine).scene->set_frame_callback([weak = weak_from_this()](std::shared_ptr frame) { - if (auto owner = weak.lock()) { - const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; - owner->d->publish(encode_frame(*frame, sequence)); - } - }); - } - asio::co_spawn(d->strand, [self]() -> asio::awaitable { - for (;;) { - asio::error_code error; - auto input = co_await self->d->inputs.async_receive(asio::redirect_error(asio::use_awaitable, error)); - if (error) co_return; - if (auto* query = std::get_if(&input)) { - query->handler(self->d->descriptor().schema()); - continue; - } - if (auto* write = std::get_if(&input)) { - write->handler(self->d->descriptor().write_prop(write->key, write->value)); - continue; - } - const auto value = std::get(input); - if (auto* engine_2d = std::get_if(&self->d->engine)) { - const Size viewport{static_cast(std::clamp(value.width, 160U, 1920U)), static_cast(std::clamp(value.height, 120U, 1080U))}; - engine_2d->scene->set<&Render_Scene_2D::Prop::viewport>(viewport); - if (engine_2d->frequency) engine_2d->frequency->set<&Abs_Axis::Prop::canvas_size>(viewport); - if (engine_2d->horizontal) engine_2d->horizontal->set<&Abs_Axis::Prop::canvas_size>(viewport); - if (engine_2d->vertical) engine_2d->vertical->set<&Abs_Axis::Prop::canvas_size>(viewport); - if (engine_2d->time) engine_2d->time->set<&Abs_Axis::Prop::canvas_size>(viewport); - engine_2d->update(value.time_milliseconds); - engine_2d->scene->render(); - } else { - auto& engine_3d = std::get(self->d->engine); - engine_3d.scene->set<&Render_Scene_3D::Prop::viewport>(render_3d::Extent{std::clamp(value.width, 160U, 1920U), std::clamp(value.height, 120U, 1080U)}); - engine_3d.scene->render(); - } + +void Plot::ensure_started() { + std::call_once(d->start_once, [this] { + auto self = shared_from_this(); + if (auto* scene = std::get_if>(&d->scene)) { + (*scene)->set_frame_callback([weak = weak_from_this()](Image_View image) { + if (auto owner = weak.lock()) { + const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; + owner->d->publish(encode_frame(image, sequence)); + } + }); + } else { + std::get>(d->scene)->set_frame_callback( + [weak = weak_from_this()](std::shared_ptr frame) { + if (auto owner = weak.lock()) { + const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; + owner->d->publish(encode_frame(*frame, sequence)); + } + }); } - }, [](std::exception_ptr exception) { - if (exception) std::rethrow_exception(exception); + asio::co_spawn(d->strand, [self]() -> asio::awaitable { + for (;;) { + asio::error_code error; + auto input = co_await self->d->inputs.async_receive( + asio::redirect_error(asio::use_awaitable, error)); + if (error) co_return; + if (auto* query = std::get_if(&input)) { + query->handler(self->d->view->schema()); + continue; + } + if (auto* write = std::get_if(&input)) { + write->handler(self->d->view->write_prop(write->key, write->value)); + continue; + } + auto event = std::get(input); + event.width = std::clamp(event.width, 160U, 1920U); + event.height = std::clamp(event.height, 120U, 1080U); + self->d->view->update(event); + if (auto* scene = std::get_if>(&self->d->scene)) { + (*scene)->set<&Render_Scene_2D::Prop::viewport>( + Size{static_cast(event.width), static_cast(event.height)}); + (*scene)->render(); + } else { + auto& scene_3d = std::get>(self->d->scene); + scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{event.width, event.height}); + scene_3d->render(); + } + } + }, [](std::exception_ptr exception) { + if (exception) std::rethrow_exception(exception); + }); }); } -void Plot::attach(const void* owner, Frame_Handler handler) { { std::lock_guard lock(d->handlers_mutex); d->handlers.insert_or_assign(owner, std::move(handler)); } submit({}); } -void Plot::detach(const void* owner) { std::lock_guard lock(d->handlers_mutex); d->handlers.erase(owner); } -void Plot::submit(Plot_Event event) { static_cast(d->inputs.try_send(asio::error_code{}, Plot_Input{event})); } -void Plot::async_schema(Json_Handler handler) { if (!d->inputs.try_send(asio::error_code{}, Plot_Input{Schema_Query{std::move(handler)}})) throw std::runtime_error("plot input queue is unavailable"); } -void Plot::async_write_prop(std::string key, nlohmann::json value, Json_Handler handler) { if (!d->inputs.try_send(asio::error_code{}, Plot_Input{Prop_Write{std::move(key), std::move(value), std::move(handler)}})) throw std::runtime_error("plot input queue is unavailable"); } -struct Plot_Registry::Private { asio::any_io_executor executor; std::mutex mutex; std::unordered_map> plots; explicit Private(asio::any_io_executor value) : executor(std::move(value)) {} }; -Plot_Registry::Plot_Registry(asio::any_io_executor executor) : d(std::make_unique(std::move(executor))) {} -Plot_Registry::~Plot_Registry() = default; -std::shared_ptr Plot_Registry::acquire(std::string_view plot_id) { const auto* entry = find_plot(plot_id); if (!entry) return {}; std::lock_guard lock(d->mutex); auto& plot = d->plots[std::string(plot_id)]; if (!plot) { plot = std::shared_ptr(new Plot(std::make_unique(d->executor, entry->build()))); plot->start(); } return plot; } -nlohmann::json Plot_Registry::catalog() const { nlohmann::json result = nlohmann::json::array(); for (const auto& entry : plot_catalog) result.push_back({{"id", entry.id}, {"title", entry.title}, {"category", entry.category}, {"description", entry.description}, {"dimension", entry.dimension}, {"websocket", "/ws/plot/" + std::string(entry.id)}, {"schema", "/plot/" + std::string(entry.id) + "/schema"}}); return result; } +void Plot::attach(const void* owner, Frame_Handler handler) { + ensure_started(); + { + std::lock_guard lock(d->handlers_mutex); + d->handlers.insert_or_assign(owner, std::move(handler)); + } + submit({}); +} + +void Plot::detach(const void* owner) { + std::lock_guard lock(d->handlers_mutex); + d->handlers.erase(owner); +} + +void Plot::submit(Plot_Event event) { + ensure_started(); + static_cast(d->inputs.try_send(asio::error_code{}, Plot_Input{event})); +} + +void Plot::async_schema(Json_Handler handler) { + ensure_started(); + if (!d->inputs.try_send(asio::error_code{}, Plot_Input{Schema_Query{std::move(handler)}})) + throw std::runtime_error("plot input queue is unavailable"); +} + +void Plot::async_write_prop(std::string key, nlohmann::json value, Json_Handler handler) { + ensure_started(); + if (!d->inputs.try_send(asio::error_code{}, Plot_Input{ + Prop_Write{std::move(key), std::move(value), std::move(handler)}})) + throw std::runtime_error("plot input queue is unavailable"); +} + +std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { + return build_2d_plot(std::move(executor)); +} +std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) { + return build_2d_plot(std::move(executor)); +} +std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { + return build_2d_plot(std::move(executor)); +} +std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { + return build_2d_plot(std::move(executor)); +} +std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { + return build_2d_plot(std::move(executor)); +} +std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { + return build_2d_plot(std::move(executor)); +} +std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor) { + return build_2d_plot(std::move(executor)); +} + +std::shared_ptr make_datoviz_point_plot(asio::any_io_executor executor) { + using Visual_Object = Impl; + Visual_Object::Builder visual_builder; + auto visual = finish_shared(std::move(visual_builder)); + static_cast(visual->update_items({ + render_3d::Point{.position = {-0.55F, -0.2F, 0.0F}, .color = Color::red_color(), .diameter_px = 24.0F}, + render_3d::Point{.position = {0.0F, 0.5F, 0.0F}, .color = Color::green_color(), .diameter_px = 30.0F}, + render_3d::Point{.position = {0.55F, -0.1F, 0.0F}, .color = Color{42, 120, 255, 255}, .diameter_px = 26.0F}})); + visual->advance(); + Scene_3D::Builder scene_builder(visual.get()); + scene_builder + .set(&Render_Scene_3D::Prop::viewport, Extent{720, 420}) + .set(&Render_Scene_3D::Prop::view_active, true); + auto scene = finish_build(std::move(scene_builder)); + using Adapter = detail::Renderable_Adapter, + detail::Prop_Field<&Point_Visual::Prop::visible, "visible">, + detail::Prop_Field<&Point_Visual::Prop::depth_test, "depth_test">, + detail::Prop_Field<&Point_Visual::Prop::items, "items">, + detail::State_Field, + detail::State_Field, + detail::State_Field>; + std::vector> owners{visual}; + auto view = std::make_unique( + std::move(owners), detail::make_renderable_descriptor(Adapter{*visual}), + [](const Plot_Event&) {}); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); +} } diff --git a/web_server/src/Plot.hpp b/web_server/src/Plot.hpp index b0e28a9..a02cfe4 100644 --- a/web_server/src/Plot.hpp +++ b/web_server/src/Plot.hpp @@ -1,24 +1,37 @@ #pragma once #include #include +#include +#include #include #include #include #include #include -#include - namespace aethera::web { struct Plot_Event { - double time_milliseconds{}; /* 浏览器 performance.now() 时钟值。 */ - std::uint32_t width{720}; /* 目标帧像素宽度。 */ - std::uint32_t height{420}; /* 目标帧像素高度。 */ + double time_milliseconds{}; + std::uint32_t width{720}; + std::uint32_t height{420}; }; - class Plot final : public std::enable_shared_from_this { public: using Frame_Handler = std::function; using Json_Handler = std::function; + class Scene_View { + public: + virtual ~Scene_View() = default; + [[nodiscard]] virtual nlohmann::json schema() const = 0; + [[nodiscard]] virtual nlohmann::json write_prop(std::string_view key, + const nlohmann::json& value) = 0; + virtual void update(const Plot_Event& event) = 0; + }; + Plot(asio::any_io_executor executor, + std::unique_ptr> scene, + std::unique_ptr view); + Plot(asio::any_io_executor executor, + std::unique_ptr> scene, + std::unique_ptr view); ~Plot(); Plot(const Plot&) = delete; Plot& operator=(const Plot&) = delete; @@ -29,20 +42,7 @@ public: void async_write_prop(std::string key, nlohmann::json value, Json_Handler handler); private: struct Private; - explicit Plot(std::unique_ptr private_data); - void start(); - friend class Plot_Registry; - std::unique_ptr d; /* 引擎对象、协议 adapter、事件队列和帧订阅的唯一所有者。 */ -}; - -class Plot_Registry final { -public: - explicit Plot_Registry(asio::any_io_executor executor); - ~Plot_Registry(); - [[nodiscard]] std::shared_ptr acquire(std::string_view plot_id); - [[nodiscard]] nlohmann::json catalog() const; -private: - struct Private; - std::unique_ptr d; /* Plot id 到长生命周期引擎桥接对象的注册表。 */ + void ensure_started(); + std::unique_ptr d; }; } diff --git a/web_server/src/Web_Server.cpp b/web_server/src/Web_Server.cpp index a04a3b3..0f2f894 100644 --- a/web_server/src/Web_Server.cpp +++ b/web_server/src/Web_Server.cpp @@ -1,28 +1,119 @@ #include "Web_Server.hpp" #include "Graph_WebSocket.hpp" -#include "Plot.hpp" +#include "Gallery_Plots.hpp" #include #include #include #include #include #include +#include #include +#include + namespace aethera::web { namespace { -drogon::HttpResponsePtr json_response(nlohmann::json value) { auto response = drogon::HttpResponse::newHttpResponse(); response->setContentTypeCode(drogon::CT_APPLICATION_JSON); response->setBody(value.dump()); return response; } -drogon::HttpResponsePtr error_response(drogon::HttpStatusCode status, std::string message) { auto response = json_response({{"error", std::move(message)}}); response->setStatusCode(status); return response; } +using Plot_Map = std::unordered_map>; + +drogon::HttpResponsePtr json_response(nlohmann::json value) { + auto response = drogon::HttpResponse::newHttpResponse(); + response->setContentTypeCode(drogon::CT_APPLICATION_JSON); + response->setBody(value.dump()); + return response; } + +drogon::HttpResponsePtr error_response(drogon::HttpStatusCode status, std::string message) { + auto response = json_response({{"error", std::move(message)}}); + response->setStatusCode(status); + return response; +} + +std::shared_ptr find_plot(const Plot_Map& plots, std::string_view id) { + const auto found = plots.find(std::string(id)); + return found == plots.end() ? nullptr : found->second; +} +} + int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) { const auto hardware_threads = std::max(2U, std::thread::hardware_concurrency()); auto graph_pool = std::make_shared(std::min(8U, hardware_threads)); - auto registry = std::make_shared(graph_pool->get_executor()); - auto websocket = std::make_shared(registry); + const auto executor = graph_pool->get_executor(); + + auto plots = std::make_shared(); + plots->emplace("spectrum", make_spectrum_plot(executor)); + plots->emplace("frequency_trace", make_frequency_trace_plot(executor)); + plots->emplace("sweep_spectrum", make_sweep_spectrum_plot(executor)); + plots->emplace("afterglow", make_afterglow_plot(executor)); + plots->emplace("waterfall", make_waterfall_plot(executor)); + plots->emplace("constellation", make_constellation_plot(executor)); + plots->emplace("selection_overlay", make_selection_overlay_plot(executor)); + plots->emplace("datoviz_point", make_datoviz_point_plot(executor)); + + auto resolve_plot = [plots](std::string_view id) { return find_plot(*plots, id); }; + auto websocket = std::make_shared(resolve_plot); auto& app = drogon::app(); - app.registerHandler("/plot", [registry](const drogon::HttpRequestPtr&, std::function&& callback) { callback(json_response(registry->catalog())); }, {drogon::Get}); - app.registerHandler("/plot/{1}/schema", [registry](const drogon::HttpRequestPtr&, std::function&& callback, std::string plot_id) { auto plot = registry->acquire(plot_id); if (!plot) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; } auto output = std::make_shared>(std::move(callback)); plot->async_schema([output](nlohmann::json schema) { (*output)(json_response(std::move(schema))); }); }, {drogon::Get}); - app.registerHandler("/plot/{1}/prop/{2}", [registry](const drogon::HttpRequestPtr& request, std::function&& callback, std::string plot_id, std::string key) { auto plot = registry->acquire(plot_id); if (!plot) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; } nlohmann::json value; try { value = nlohmann::json::parse(request->body()); } catch (const nlohmann::json::exception&) { callback(error_response(drogon::k400BadRequest, "invalid JSON value")); return; } auto output = std::make_shared>(std::move(callback)); plot->async_write_prop(std::move(key), std::move(value), [output](nlohmann::json result) { (*output)(json_response(std::move(result))); }); }, {drogon::Put}); - app.registerController(websocket).setDocumentRoot(asset_root.string()).setHomePage("index.html").setStaticFileHeaders({{"Cache-Control", "no-store"}}).addListener("127.0.0.1", port).setThreadNum(std::min(8U, hardware_threads)).setIdleConnectionTimeout(90).run(); + + app.registerHandler("/plot", [plots](const drogon::HttpRequestPtr&, + std::function&& callback) { + nlohmann::json result = nlohmann::json::array(); + for (const auto& [id, plot] : *plots) { + static_cast(plot); + result.push_back({ + {"id", id}, {"title", id}, {"category", "Plots"}, {"description", ""}, + {"dimension", ""}, {"websocket", "/ws/plot/" + id}, + {"schema", "/plot/" + id + "/schema"}}); + } + callback(json_response(std::move(result))); + }, {drogon::Get}); + + app.registerHandler("/plot/{1}/schema", [plots]( + const drogon::HttpRequestPtr&, + std::function&& callback, + std::string plot_id) { + auto plot = find_plot(*plots, plot_id); + if (!plot) { + callback(error_response(drogon::k404NotFound, "unknown plot")); + return; + } + auto output = std::make_shared>( + std::move(callback)); + plot->async_schema([output](nlohmann::json schema) { + (*output)(json_response(std::move(schema))); + }); + }, {drogon::Get}); + + app.registerHandler("/plot/{1}/prop/{2}", [plots]( + const drogon::HttpRequestPtr& request, + std::function&& callback, + std::string plot_id, + std::string key) { + auto plot = find_plot(*plots, plot_id); + if (!plot) { + callback(error_response(drogon::k404NotFound, "unknown plot")); + return; + } + nlohmann::json value; + try { + value = nlohmann::json::parse(request->body()); + } catch (const nlohmann::json::exception&) { + callback(error_response(drogon::k400BadRequest, "invalid JSON value")); + return; + } + auto output = std::make_shared>( + std::move(callback)); + plot->async_write_prop(std::move(key), std::move(value), [output](nlohmann::json result) { + (*output)(json_response(std::move(result))); + }); + }, {drogon::Put}); + + app.registerController(websocket) + .setDocumentRoot(asset_root.string()) + .setHomePage("index.html") + .setStaticFileHeaders({{"Cache-Control", "no-store"}}) + .addListener("127.0.0.1", port) + .setThreadNum(std::min(8U, hardware_threads)) + .setIdleConnectionTimeout(90) + .run(); graph_pool->stop(); graph_pool->join(); return 0;