前后端全部提交
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
#include "Graph_Metadata.hpp"
|
||||
#include <algorithm>
|
||||
namespace {
|
||||
struct State_Field_Metadata {
|
||||
template <std::size_t Index, typename Field> auto operator()(Field field) const {
|
||||
if constexpr (Index == 0) return field.label("Frame sequence").description("Latest completed frame sent by this graph.");
|
||||
if constexpr (Index == 1) return field.label("Input elements").description("Primary published element count read from the graph State.");
|
||||
return field.label("Rendered elements").description("Prepared render element count read from the graph State.");
|
||||
}
|
||||
};
|
||||
}
|
||||
namespace adminive {
|
||||
auto Type_Descriptor<aethera::web::Graph_State_Document>::get() { return reflected_object_with<aethera::web::Graph_State_Document>("graph_state", "Published graph state", State_Field_Metadata{}); }
|
||||
}
|
||||
namespace aethera::web {
|
||||
const std::vector<Graph_Descriptor>& graph_catalog() {
|
||||
static const std::vector<Graph_Descriptor> value{
|
||||
{"spectrum", "Spectrum", "Curves", "Current, maximum and minimum spectrum curves with markers.", false},
|
||||
{"frequency_trace", "Frequency trace", "Curves", "Time ordered frequency samples rendered as a partitioned curve.", false},
|
||||
{"sweep_spectrum", "Sweep spectrum", "Curves", "Incremental sweep blocks composed into one frequency curve.", false},
|
||||
{"afterglow", "Afterglow", "Raster", "Persistent spectrum energy rendered as reusable color blocks.", false},
|
||||
{"waterfall", "Waterfall", "Raster", "Time ordered spectrum rows rendered as a color raster.", false},
|
||||
{"constellation", "Constellation", "Signals", "I/Q samples and modulation anchors.", false},
|
||||
{"selection_overlay", "Selection overlay", "Interaction", "Direct-paint selection rectangle over numeric axes.", false},
|
||||
{"datoviz_point", "Datoviz point", "3D", "Asynchronous Vulkan point visual with GPU readback.", true}
|
||||
};
|
||||
return value;
|
||||
}
|
||||
const Graph_Descriptor* find_graph(std::string_view graph_id) { const auto& catalog = graph_catalog(); const auto found = std::ranges::find(catalog, graph_id, &Graph_Descriptor::id); return found == catalog.end() ? nullptr : &*found; }
|
||||
nlohmann::json graph_catalog_json() { nlohmann::json result = nlohmann::json::array(); for (const auto& graph : graph_catalog()) result.push_back({{"id", graph.id}, {"title", graph.title}, {"category", graph.category}, {"description", graph.description}, {"dimension", graph.three_dimensional ? "3D" : "2D"}, {"websocket", "/ws/graphs/" + graph.id}, {"state", "/api/graphs/" + graph.id + "/state"}, {"descriptor", "/api/graphs/" + graph.id + "/descriptor"}}); return result; }
|
||||
nlohmann::json graph_state_descriptor_json() { return adminive::to_descriptor_json<nlohmann::json, Graph_State_Document>(); }
|
||||
nlohmann::json graph_state_json(const Graph_State_Document& state) { return adminive::model_to_json<nlohmann::json>(state, true); }
|
||||
nlohmann::json graph_prop_descriptor_json(std::string_view graph_id) {
|
||||
using namespace render_2d;
|
||||
if (graph_id == "spectrum") return adminive::to_descriptor_json<nlohmann::json, Spectrum_Editable_Prop_Data>();
|
||||
if (graph_id == "frequency_trace") return adminive::to_descriptor_json<nlohmann::json, Frequency_Trace_Editable_Prop_Data>();
|
||||
if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json<nlohmann::json, Sweep_Spectrum_Editable_Prop_Data>();
|
||||
if (graph_id == "afterglow") return adminive::to_descriptor_json<nlohmann::json, Afterglow_Editable_Prop_Data>();
|
||||
if (graph_id == "waterfall") return adminive::to_descriptor_json<nlohmann::json, Waterfall_Editable_Prop_Data>();
|
||||
if (graph_id == "constellation") return adminive::to_descriptor_json<nlohmann::json, Constellation_Diagram_Editable_Prop_Data>();
|
||||
return {{"fields", nlohmann::json::array()}};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
#include <adminive/adapters/boost_pfr.hpp>
|
||||
#include <adminive/adapters/nlohmann_json.hpp>
|
||||
#include <adminive/adapters/magic_enum.hpp>
|
||||
#include <adminive/adminive.hpp>
|
||||
#include <render_2D/plottable/Plottables.hpp>
|
||||
#include <cstdint>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
namespace aethera::web {
|
||||
namespace detail {
|
||||
struct Editable_Prop_Field_Metadata {
|
||||
template <std::size_t Index, typename Field> auto operator()(Field field) const { return field.editable(); }
|
||||
};
|
||||
}
|
||||
struct Graph_Descriptor {
|
||||
std::string id{}; /* HTTP 与 WebSocket 使用的稳定图标识。 */
|
||||
std::string title{}; /* Gallery 卡片显示名称。 */
|
||||
std::string category{}; /* Gallery 分类筛选值。 */
|
||||
std::string description{}; /* 图用途和数据语义说明。 */
|
||||
bool three_dimensional{}; /* 是否使用 Datoviz 3D 后端。 */
|
||||
};
|
||||
struct Graph_State_Document {
|
||||
std::uint64_t frame_sequence{}; /* 最近完成并发送的帧序号。 */
|
||||
std::uint64_t primary_count{}; /* 图自身发布的主要输入元素数量。 */
|
||||
std::uint64_t rendered_count{}; /* 最近 Prepare 发布的绘制元素数量。 */
|
||||
};
|
||||
[[nodiscard]] const std::vector<Graph_Descriptor>& graph_catalog();
|
||||
[[nodiscard]] const Graph_Descriptor* find_graph(std::string_view graph_id);
|
||||
[[nodiscard]] nlohmann::json graph_catalog_json();
|
||||
[[nodiscard]] nlohmann::json graph_state_descriptor_json();
|
||||
[[nodiscard]] nlohmann::json graph_state_json(const Graph_State_Document& state);
|
||||
[[nodiscard]] nlohmann::json graph_prop_descriptor_json(std::string_view graph_id);
|
||||
}
|
||||
namespace adminive {
|
||||
template <> struct Reflection_Adapter<aethera::web::Graph_State_Document> : Boost_Pfr_Reflection_Adapter<aethera::web::Graph_State_Document> {};
|
||||
template <> struct Type_Descriptor<aethera::web::Graph_State_Document> { static auto get(); };
|
||||
#define AETHERA_DECLARE_PROP_REFLECTION(Type, Id, Label) \
|
||||
template <> struct Reflection_Adapter<aethera::render_2d::Type> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Type> {}; \
|
||||
template <> struct Type_Descriptor<aethera::render_2d::Type> { static auto get() { return reflected_object_with<aethera::render_2d::Type>(Id, Label, aethera::web::detail::Editable_Prop_Field_Metadata{}); } };
|
||||
AETHERA_DECLARE_PROP_REFLECTION(Spectrum_Editable_Prop_Data, "spectrum_prop", "Spectrum properties")
|
||||
AETHERA_DECLARE_PROP_REFLECTION(Frequency_Trace_Editable_Prop_Data, "frequency_trace_prop", "Frequency trace properties")
|
||||
AETHERA_DECLARE_PROP_REFLECTION(Sweep_Spectrum_Editable_Prop_Data, "sweep_spectrum_prop", "Sweep spectrum properties")
|
||||
AETHERA_DECLARE_PROP_REFLECTION(Afterglow_Editable_Prop_Data, "afterglow_prop", "Afterglow properties")
|
||||
AETHERA_DECLARE_PROP_REFLECTION(Waterfall_Editable_Prop_Data, "waterfall_prop", "Waterfall properties")
|
||||
AETHERA_DECLARE_PROP_REFLECTION(Constellation_Diagram_Editable_Prop_Data, "constellation_prop", "Constellation properties")
|
||||
#undef AETHERA_DECLARE_PROP_REFLECTION
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "Graph_Session.hpp"
|
||||
#include <asio/co_spawn.hpp>
|
||||
#include <asio/error_code.hpp>
|
||||
#include <asio/experimental/concurrent_channel.hpp>
|
||||
#include <asio/redirect_error.hpp>
|
||||
#include <asio/strand.hpp>
|
||||
#include <asio/use_awaitable.hpp>
|
||||
#include <render_2D/plottable/Plottables.hpp>
|
||||
#include <render_2D/scene/Render_Scene_2D.hpp>
|
||||
#include <render_3D/Render_3D.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <numbers>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
namespace aethera::web {
|
||||
namespace {
|
||||
using namespace render_2d;
|
||||
using namespace render_3d;
|
||||
using Scene_2D = Impl<Render_Scene_2D>;
|
||||
using Frequency_Axis_Object = Impl<Frequency_Axis>;
|
||||
using Numeric_Axis_Object = Impl<Numeric_Axis>;
|
||||
using Time_Axis_Object = Impl<Time_Axis>;
|
||||
template <typename Object, typename... Arguments>
|
||||
std::unique_ptr<Object> build(Arguments&&... arguments) { typename Object::Builder builder(std::forward<Arguments>(arguments)...); auto result = builder.build(); if (!result) throw std::logic_error("gallery graph dependency graph is invalid"); return std::move(result).value(); }
|
||||
template <typename Axis>
|
||||
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 <typename Integer>
|
||||
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<std::size_t>(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<std::uint32_t>(image.width)); append_binary(output, static_cast<std::uint32_t>(image.height)); append_binary(output, sequence); for (int y = 0; y < image.height; ++y) { const auto* row = reinterpret_cast<const std::uint8_t*>(image.data + static_cast<std::ptrdiff_t>(y) * image.stride); for (int x = 0; x < image.width; ++x) { const auto* pixel = row + x * 4; output.push_back(static_cast<char>(pixel[2])); output.push_back(static_cast<char>(pixel[1])); output.push_back(static_cast<char>(pixel[0])); output.push_back(static_cast<char>(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<const char*>(frame.rgba8.data()), frame.rgba8.size()); return output; }
|
||||
struct State_Query { Graph_Session::State_Handler handler{}; };
|
||||
struct Prop_Query { Graph_Session::Prop_Handler handler{}; };
|
||||
struct Prop_Patch { nlohmann::json patch{}; Graph_Session::Prop_Handler handler{}; };
|
||||
using Session_Event = std::variant<Graph_Event, State_Query, Prop_Query, Prop_Patch>;
|
||||
struct Plot_2D {
|
||||
std::unique_ptr<Scene_2D> scene{}; /* 最终二维 Scene。 */
|
||||
std::unique_ptr<Frequency_Axis_Object> frequency{}; /* 频率轴;不用时仍为空。 */
|
||||
std::unique_ptr<Numeric_Axis_Object> horizontal{}; /* 星座图水平数值轴。 */
|
||||
std::unique_ptr<Numeric_Axis_Object> vertical{}; /* 功率或星座图垂直轴。 */
|
||||
std::unique_ptr<Time_Axis_Object> time{}; /* 时间轴;不用时为空。 */
|
||||
std::unique_ptr<Root> plot{}; /* 具体 Plottable 的唯一所有权。 */
|
||||
std::function<void(double)> update{}; /* 根据浏览器时钟更新权威 Prop。 */
|
||||
std::function<Graph_State_Document(std::uint64_t)> state{}; /* 从具体 State 即时计算 HTTP 文档。 */
|
||||
std::function<nlohmann::json()> prop{};
|
||||
std::function<nlohmann::json(const nlohmann::json&)> patch_prop{};
|
||||
};
|
||||
template <typename Data, auto... Members, typename Object>
|
||||
void bind_prop_api(Plot_2D& plot, Object* object) {
|
||||
plot.prop = [object] { const auto& prop = object->template read_prop<typename Data::Prop_Tag>(); return adminive::model_to_json<nlohmann::json>(static_cast<const Data&>(prop), true); };
|
||||
plot.patch_prop = [object](const nlohmann::json& patch) { Data updated = static_cast<const Data&>(object->template read_prop<typename Data::Prop_Tag>()); const auto result = adminive::apply_frontend_patch<nlohmann::json>(updated, patch); if (!result.success) return result.to_json<nlohmann::json>(); ([&] { if (object->template get<Members>() != updated.*Members) object->template set<Members>(updated.*Members); }(), ...); auto output = result.to_json<nlohmann::json>(); output["prop"] = adminive::model_to_json<nlohmann::json>(updated, true); return output; };
|
||||
}
|
||||
Plot_2D make_plot_2d(std::string_view id) {
|
||||
Plot_2D result;
|
||||
const Size canvas{720, 420};
|
||||
result.scene = build<Scene_2D>();
|
||||
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<Frequency_Axis_Object>(); 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<Numeric_Axis_Object>(); configure_axis(result.vertical.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); result.vertical->set<&Numeric_Axis::Prop::coordinate_range>(range); };
|
||||
if (id == "spectrum") {
|
||||
make_frequency(); make_vertical({-110.0, 0.0}); auto object = build<Impl<Spectrum>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Spectrum_Editable_Prop_Data, &Spectrum_Editable_Prop_Data::center_frequency, &Spectrum_Editable_Prop_Data::partition_count, &Spectrum_Editable_Prop_Data::max_hold_visible, &Spectrum_Editable_Prop_Data::min_hold_visible, &Spectrum_Editable_Prop_Data::max_marker_visible, &Spectrum_Editable_Prop_Data::min_marker_visible, &Spectrum_Editable_Prop_Data::sweep_region_visible, &Spectrum_Editable_Prop_Data::visible_range_only>(result, raw); result.update = [raw](double time) { std::array<double, 256> samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { const double x = static_cast<double>(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.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state<Spectrum::Base_Tag>(); return Graph_State_Document{sequence, state.sample_count, state.rendered_point_count}; }; result.plot = std::move(object);
|
||||
} else if (id == "frequency_trace") {
|
||||
result.time = build<Time_Axis_Object>(); configure_axis(result.time.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); make_vertical({-1.2, 1.2}); auto object = build<Impl<Frequency_Trace>>(result.scene.get(), result.time.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Frequency_Trace_Editable_Prop_Data, &Frequency_Trace_Editable_Prop_Data::partition_count>(result, raw); auto tick = std::make_shared<std::uint64_t>(); 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.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state<Frequency_Trace::Base_Tag>(); return Graph_State_Document{sequence, state.sample_count, state.rendered_point_count}; }; result.plot = std::move(object);
|
||||
} else if (id == "sweep_spectrum") {
|
||||
make_frequency(); make_vertical({-110.0, 0.0}); auto object = build<Impl<Sweep_Spectrum>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Sweep_Spectrum_Editable_Prop_Data, &Sweep_Spectrum_Editable_Prop_Data::bins_per_block, &Sweep_Spectrum_Editable_Prop_Data::block_count, &Sweep_Spectrum_Editable_Prop_Data::partition_count, &Sweep_Spectrum_Editable_Prop_Data::visible_range_only>(result, raw); result.update = [raw](double time) { std::array<double, 64> 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.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state<Sweep_Spectrum::Base_Tag>(); return Graph_State_Document{sequence, state.stored_block_count, state.rendered_point_count}; }; result.plot = std::move(object);
|
||||
} else if (id == "afterglow") {
|
||||
make_frequency(); make_vertical({-110.0, 0.0}); auto object = build<Impl<Afterglow>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Afterglow_Editable_Prop_Data, &Afterglow_Editable_Prop_Data::frequency_point_size, &Afterglow_Editable_Prop_Data::power_point_size, &Afterglow_Editable_Prop_Data::partition_count, &Afterglow_Editable_Prop_Data::interpolate, &Afterglow_Editable_Prop_Data::attenuation_rate>(result, raw); result.update = [raw](double time) { std::array<double, 192> 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<double>(i) / values.size() - 0.5 - 0.18 * std::sin(time * 0.0008), 2.0)); raw->append_spectrum(values); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state<Afterglow::Base_Tag>(); return Graph_State_Document{sequence, state.history_count, state.rendered_cell_count}; }; result.plot = std::move(object);
|
||||
} else if (id == "waterfall") {
|
||||
make_frequency(); result.time = build<Time_Axis_Object>(); configure_axis(result.time.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); auto object = build<Impl<Waterfall>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Waterfall_Editable_Prop_Data, &Waterfall_Editable_Prop_Data::frequency_bin_count, &Waterfall_Editable_Prop_Data::partition_count, &Waterfall_Editable_Prop_Data::visible_range_only>(result, raw); auto tick = std::make_shared<std::uint64_t>(); result.update = [raw, tick](double time) { std::array<double, 192> 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<double>(i) / values.size() - 0.5 - 0.22 * std::sin(time * 0.0006), 2.0)); raw->append_row((*tick)++, values); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state<Waterfall::Base_Tag>(); return Graph_State_Document{sequence, state.row_count, state.rendered_cell_count}; }; result.plot = std::move(object);
|
||||
} else if (id == "constellation") {
|
||||
result.horizontal = build<Numeric_Axis_Object>(); 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<Impl<Constellation_Diagram>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Constellation_Diagram_Editable_Prop_Data, &Constellation_Diagram_Editable_Prop_Data::point_lifetime_ms, &Constellation_Diagram_Editable_Prop_Data::type, &Constellation_Diagram_Editable_Prop_Data::phase_offset_radians>(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.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state<Constellation_Diagram::Base_Tag>(); return Graph_State_Document{sequence, state.point_count, state.point_count}; }; result.plot = std::move(object);
|
||||
} else {
|
||||
result.horizontal = build<Numeric_Axis_Object>(); 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<Impl<Selection_Rectangle_Overlay>>(result.scene.get(), result.horizontal.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); result.prop = [] { return nlohmann::json::object(); }; result.patch_prop = [](const nlohmann::json&) { return nlohmann::json{{"success", true}, {"prop", nlohmann::json::object()}}; }; result.update = [](double) {}; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state<Renderable::Base_Tag>(); return Graph_State_Document{sequence, state.paint_task_count, state.paint_executed ? 1U : 0U}; }; result.plot = std::move(object);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
struct Plot_3D {
|
||||
std::unique_ptr<Impl<Point_Visual>> visual{}; /* 点图元权威对象。 */
|
||||
std::unique_ptr<Impl<Render_Scene_3D>> scene{}; /* 异步 Datoviz Scene。 */
|
||||
};
|
||||
Plot_3D make_plot_3d() { Plot_3D result; result.visual = build<Impl<Point_Visual>>(); static_cast<void>(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}})); result.visual->advance(); result.scene = build<Impl<Render_Scene_3D>>(result.visual.get()); result.scene->activate_view(); return result; }
|
||||
std::variant<Plot_2D, Plot_3D> make_plot(const Graph_Descriptor& graph) { if (graph.three_dimensional) return std::variant<Plot_2D, Plot_3D>{std::in_place_type<Plot_3D>, make_plot_3d()}; return std::variant<Plot_2D, Plot_3D>{std::in_place_type<Plot_2D>, make_plot_2d(graph.id)}; }
|
||||
}
|
||||
struct Graph_Session::Private {
|
||||
asio::strand<asio::any_io_executor> strand; /* 图对象串行访问域。 */
|
||||
asio::experimental::concurrent_channel<void(asio::error_code, Session_Event)> events; /* 浏览器事件与 HTTP 状态读取队列。 */
|
||||
Graph_Descriptor descriptor; /* Catalog 中的不可变图描述。 */
|
||||
std::variant<Plot_2D, Plot_3D> plot; /* 具体二维或三维 Scene 所有权。 */
|
||||
std::mutex handlers_mutex; /* 保护跨 Drogon 线程的连接集合。 */
|
||||
std::unordered_map<const void*, Frame_Handler> handlers; /* 当前订阅该图像素帧的 WebSocket。 */
|
||||
std::atomic_uint64_t frame_sequence{}; /* 传输协议的完成帧序号。 */
|
||||
explicit Private(asio::any_io_executor executor, const Graph_Descriptor& graph) : strand(asio::make_strand(std::move(executor))), events(strand, 32), descriptor(graph), plot(make_plot(graph)) {}
|
||||
void publish(std::string pixels) { std::vector<Frame_Handler> 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); }
|
||||
Graph_State_Document state_document() const { const auto sequence = frame_sequence.load(std::memory_order_acquire); if (const auto* value = std::get_if<Plot_2D>(&plot)) return value->state(sequence); const auto& value = std::get<Plot_3D>(plot); const auto& state = value.visual->read_state<Point_Visual::Base_Tag>(); return {sequence, state.item_count, state.prepared_item_count}; }
|
||||
nlohmann::json prop_document() const { if (const auto* value = std::get_if<Plot_2D>(&plot)) return value->prop(); return nlohmann::json::object(); }
|
||||
nlohmann::json patch_prop(const nlohmann::json& patch) { if (auto* value = std::get_if<Plot_2D>(&plot)) return value->patch_prop(patch); return {{"success", true}, {"prop", nlohmann::json::object()}}; }
|
||||
};
|
||||
Graph_Session::Graph_Session(std::unique_ptr<Private> private_data) : d(std::move(private_data)) {}
|
||||
std::shared_ptr<Graph_Session> Graph_Session::create(asio::any_io_executor executor, const Graph_Descriptor& descriptor) { auto result = std::shared_ptr<Graph_Session>(new Graph_Session(std::make_unique<Private>(std::move(executor), descriptor))); result->start(); return result; }
|
||||
Graph_Session::~Graph_Session() { d->events.close(); }
|
||||
void Graph_Session::start() { auto self = shared_from_this(); if (auto* plot = std::get_if<Plot_2D>(&d->plot)) plot->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<Plot_3D>(d->plot).scene->set_frame_callback([weak = weak_from_this()](std::shared_ptr<const render_3d::Pixel_Frame> 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<void> { for (;;) { asio::error_code error; auto event = co_await self->d->events.async_receive(asio::redirect_error(asio::use_awaitable, error)); if (error) co_return; if (auto* state = std::get_if<State_Query>(&event)) { state->handler(graph_state_json(self->d->state_document())); continue; } if (auto* prop = std::get_if<Prop_Query>(&event)) { prop->handler(self->d->prop_document()); continue; } if (auto* patch = std::get_if<Prop_Patch>(&event)) { patch->handler(self->d->patch_prop(patch->patch)); continue; } const auto value = std::get<Graph_Event>(event); if (auto* plot = std::get_if<Plot_2D>(&self->d->plot)) { const Size viewport{static_cast<int>(std::clamp(value.width, 160U, 1920U)), static_cast<int>(std::clamp(value.height, 120U, 1080U))}; plot->scene->set<&Render_Scene_2D::Prop::viewport>(viewport); if (plot->frequency) plot->frequency->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->horizontal) plot->horizontal->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->vertical) plot->vertical->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->time) plot->time->set<&Abs_Axis::Prop::canvas_size>(viewport); plot->update(value.time_milliseconds); plot->scene->render(); } else { auto& plot_3d = std::get<Plot_3D>(self->d->plot); plot_3d.scene->set<&Render_Scene_3D::Prop::viewport>(render_3d::Extent{std::clamp(value.width, 160U, 1920U), std::clamp(value.height, 120U, 1080U)}); plot_3d.scene->render(); } } }, [](std::exception_ptr exception) { if (exception) std::rethrow_exception(exception); }); }
|
||||
void Graph_Session::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 Graph_Session::detach(const void* owner) { std::lock_guard lock(d->handlers_mutex); d->handlers.erase(owner); }
|
||||
void Graph_Session::submit(Graph_Event event) { static_cast<void>(d->events.try_send(asio::error_code{}, Session_Event{event})); }
|
||||
void Graph_Session::async_state(State_Handler handler) { if (!d->events.try_send(asio::error_code{}, Session_Event{State_Query{std::move(handler)}})) throw std::runtime_error("graph state queue is unavailable"); }
|
||||
void Graph_Session::async_prop(Prop_Handler handler) { if (!d->events.try_send(asio::error_code{}, Session_Event{Prop_Query{std::move(handler)}})) throw std::runtime_error("graph prop queue is unavailable"); }
|
||||
void Graph_Session::async_patch_prop(nlohmann::json patch, Prop_Handler handler) { if (!d->events.try_send(asio::error_code{}, Session_Event{Prop_Patch{std::move(patch), std::move(handler)}})) throw std::runtime_error("graph prop queue is unavailable"); }
|
||||
const Graph_Descriptor& Graph_Session::descriptor() const noexcept { return d->descriptor; }
|
||||
struct Graph_Registry::Private { asio::any_io_executor executor; std::mutex mutex; std::unordered_map<std::string, std::shared_ptr<Graph_Session>> sessions; explicit Private(asio::any_io_executor value) : executor(std::move(value)) {} };
|
||||
Graph_Registry::Graph_Registry(asio::any_io_executor executor) : d(std::make_unique<Private>(std::move(executor))) {}
|
||||
Graph_Registry::~Graph_Registry() = default;
|
||||
std::shared_ptr<Graph_Session> Graph_Registry::acquire(std::string_view graph_id) { const auto* descriptor = find_graph(graph_id); if (!descriptor) return {}; std::lock_guard lock(d->mutex); auto& session = d->sessions[std::string(graph_id)]; if (!session) session = Graph_Session::create(d->executor, *descriptor); return session; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include "Graph_Metadata.hpp"
|
||||
#include <asio/any_io_executor.hpp>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
namespace aethera::web {
|
||||
struct Graph_Event {
|
||||
double time_milliseconds{}; /* 浏览器 performance.now() 时钟值。 */
|
||||
std::uint32_t width{720}; /* 浏览器画布当前像素宽度。 */
|
||||
std::uint32_t height{420}; /* 浏览器画布当前像素高度。 */
|
||||
};
|
||||
class Graph_Session final : public std::enable_shared_from_this<Graph_Session> {
|
||||
public:
|
||||
using Frame_Handler = std::function<void(std::string)>;
|
||||
using State_Handler = std::function<void(nlohmann::json)>;
|
||||
using Prop_Handler = std::function<void(nlohmann::json)>;
|
||||
[[nodiscard]] static std::shared_ptr<Graph_Session> create(asio::any_io_executor executor, const Graph_Descriptor& descriptor);
|
||||
~Graph_Session();
|
||||
Graph_Session(const Graph_Session&) = delete;
|
||||
Graph_Session& operator=(const Graph_Session&) = delete;
|
||||
void attach(const void* owner, Frame_Handler handler);
|
||||
void detach(const void* owner);
|
||||
void submit(Graph_Event event);
|
||||
void async_state(State_Handler handler);
|
||||
void async_prop(Prop_Handler handler);
|
||||
void async_patch_prop(nlohmann::json patch, Prop_Handler handler);
|
||||
[[nodiscard]] const Graph_Descriptor& descriptor() const noexcept;
|
||||
private:
|
||||
struct Private;
|
||||
explicit Graph_Session(std::unique_ptr<Private> private_data);
|
||||
void start();
|
||||
std::unique_ptr<Private> d; /* 单一协程事件循环及图对象所有权。 */
|
||||
};
|
||||
class Graph_Registry final {
|
||||
public:
|
||||
explicit Graph_Registry(asio::any_io_executor executor);
|
||||
~Graph_Registry();
|
||||
[[nodiscard]] std::shared_ptr<Graph_Session> acquire(std::string_view graph_id);
|
||||
private:
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d; /* 图标识到长期 Scene 会话的注册表。 */
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "Graph_WebSocket.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
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)); }
|
||||
}
|
||||
struct Graph_WebSocket::Private {
|
||||
std::weak_ptr<drogon::WebSocketConnection> connection; /* 不延长已关闭 Drogon 连接生命周期。 */
|
||||
std::shared_ptr<Graph_Session> session; /* 连接期间保持图会话存活。 */
|
||||
const void* owner{}; /* 会话订阅表使用的稳定连接身份。 */
|
||||
bool attached{}; /* 是否已安装像素订阅。 */
|
||||
};
|
||||
Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Graph_Session> session) : d(std::make_unique<Private>(Private{connection, std::move(session), connection.get(), false})) {}
|
||||
Graph_WebSocket::~Graph_WebSocket() { close(); }
|
||||
void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->session->attach(d->owner, [weak](std::string pixels) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (connection && connection->connected()) connection->send(pixels.data(), pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; }
|
||||
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; Graph_Event event; if (const auto value = json.find("time"); value != json.end() && value->is_number()) event.time_milliseconds = value->get<double>(); if (const auto value = json.find("width"); value != json.end() && value->is_number_unsigned()) event.width = std::clamp(value->get<std::uint32_t>(), 160U, 1920U); if (const auto value = json.find("height"); value != json.end() && value->is_number_unsigned()) event.height = std::clamp(value->get<std::uint32_t>(), 120U, 1080U); d->session->submit(event); }
|
||||
void Graph_WebSocket::close() { if (!d->attached) return; d->session->detach(d->owner); d->attached = false; }
|
||||
Graph_WebSocket_Controller::Graph_WebSocket_Controller(std::shared_ptr<Graph_Registry> value) : registry(std::move(value)) {}
|
||||
void Graph_WebSocket_Controller::handleNewConnection(const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) { auto session = registry->acquire(graph_id_from_path(request->path())); if (!session) { connection->shutdown(drogon::CloseCode::kViolation, "Unknown Aethera graph"); return; } auto socket = std::make_shared<Graph_WebSocket>(connection, std::move(session)); 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<Graph_WebSocket>()) socket->receive(message); }
|
||||
void Graph_WebSocket_Controller::handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) { if (const auto socket = connection->getContext<Graph_WebSocket>()) socket->close(); connection->clearContext(); }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "Graph_Session.hpp"
|
||||
#include <drogon/WebSocketController.h>
|
||||
#include <memory>
|
||||
namespace aethera::web {
|
||||
class Graph_WebSocket final : public std::enable_shared_from_this<Graph_WebSocket> {
|
||||
public:
|
||||
Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Graph_Session> session);
|
||||
~Graph_WebSocket();
|
||||
Graph_WebSocket(const Graph_WebSocket&) = delete;
|
||||
Graph_WebSocket& operator=(const Graph_WebSocket&) = delete;
|
||||
void start();
|
||||
void receive(std::string_view message);
|
||||
void close();
|
||||
private:
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d; /* Drogon 连接与图会话绑定。 */
|
||||
};
|
||||
class Graph_WebSocket_Controller final : public drogon::WebSocketController<Graph_WebSocket_Controller, false> {
|
||||
public:
|
||||
explicit Graph_WebSocket_Controller(std::shared_ptr<Graph_Registry> 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;
|
||||
void handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) override;
|
||||
WS_PATH_LIST_BEGIN
|
||||
WS_ADD_PATH_VIA_REGEX("^/ws/graphs/[^/]+$");
|
||||
WS_PATH_LIST_END
|
||||
private:
|
||||
std::shared_ptr<Graph_Registry> registry; /* 所有图 WebSocket 共用的会话注册表。 */
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "Web_Server.hpp"
|
||||
#include "Graph_Metadata.hpp"
|
||||
#include "Graph_Session.hpp"
|
||||
#include "Graph_WebSocket.hpp"
|
||||
#include <asio/thread_pool.hpp>
|
||||
#include <drogon/drogon.h>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
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; }
|
||||
}
|
||||
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<asio::thread_pool>(std::min(8U, hardware_threads));
|
||||
auto registry = std::make_shared<Graph_Registry>(graph_pool->get_executor());
|
||||
auto websocket = std::make_shared<Graph_WebSocket_Controller>(registry);
|
||||
auto& app = drogon::app();
|
||||
app.registerHandler("/api/graphs", [](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) { callback(json_response(graph_catalog_json())); }, {drogon::Get});
|
||||
app.registerHandler("/api/graphs/{1}/descriptor", [](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::string graph_id) { if (!find_graph(graph_id)) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } callback(json_response({{"prop", graph_prop_descriptor_json(graph_id)}, {"state", graph_state_descriptor_json()}, {"state_api", "/api/graphs/" + graph_id + "/state"}, {"prop_api", "/api/graphs/" + graph_id + "/prop"}})); }, {drogon::Get});
|
||||
app.registerHandler("/api/graphs/{1}/state", [registry](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::string graph_id) { auto session = registry->acquire(graph_id); if (!session) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } auto output = std::make_shared<std::function<void(const drogon::HttpResponsePtr&)>>(std::move(callback)); session->async_state([output](nlohmann::json state) { (*output)(json_response(std::move(state))); }); }, {drogon::Get});
|
||||
app.registerHandler("/api/graphs/{1}/prop", [registry](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::string graph_id) { auto session = registry->acquire(graph_id); if (!session) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } auto output = std::make_shared<std::function<void(const drogon::HttpResponsePtr&)>>(std::move(callback)); session->async_prop([output](nlohmann::json prop) { (*output)(json_response(std::move(prop))); }); }, {drogon::Get});
|
||||
app.registerHandler("/api/graphs/{1}/prop", [registry](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::string graph_id) { auto session = registry->acquire(graph_id); if (!session) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } nlohmann::json patch; try { patch = nlohmann::json::parse(request->body()); } catch (const nlohmann::json::exception&) { callback(error_response(drogon::k400BadRequest, "invalid JSON patch")); return; } auto output = std::make_shared<std::function<void(const drogon::HttpResponsePtr&)>>(std::move(callback)); session->async_patch_prop(std::move(patch), [output](nlohmann::json result) { (*output)(json_response(std::move(result))); }); }, {drogon::Patch});
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
namespace aethera::web {
|
||||
int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root);
|
||||
}
|
||||
+10
-2
@@ -1,3 +1,11 @@
|
||||
int main() {
|
||||
return 0;
|
||||
#include "Web_Server.hpp"
|
||||
#include <render_common.hpp>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
namespace {
|
||||
std::uint16_t parse_port(int argc, char** argv) { constexpr std::uint16_t fallback = 8848; if (argc == 1) return fallback; if (argc != 3 || std::string_view(argv[1]) != "--port") return 0; unsigned value{}; const std::string_view text(argv[2]); const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); return error == std::errc{} && end == text.data() + text.size() && value > 0 && value <= 65535 ? static_cast<std::uint16_t>(value) : 0; }
|
||||
}
|
||||
int main(int argc, char** argv) { const auto port = parse_port(argc, argv); if (port == 0) return 2; aethera::initialize_runtime(); const auto executable = std::filesystem::absolute(argv[0]); std::cout << "Aethera Gallery http://127.0.0.1:" << port << std::endl; return aethera::web::run_web_server(port, executable.parent_path() / "webapp_gallery"); }
|
||||
|
||||
Reference in New Issue
Block a user