web_server 初步改

This commit is contained in:
2026-08-21 12:52:27 +08:00
parent d1aa763fcd
commit c41a8ff62d
14 changed files with 792 additions and 421 deletions
-41
View File
@@ -1,41 +0,0 @@
#include "Graph_Metadata.hpp" /* PFR 属性与状态描述。 */
#include <algorithm>
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(std::string_view graph_id) {
using namespace render_2d;
if (graph_id == "spectrum") return adminive::to_descriptor_json<nlohmann::json, Spectrum::State>();
if (graph_id == "frequency_trace") return adminive::to_descriptor_json<nlohmann::json, Frequency_Trace::State>();
if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json<nlohmann::json, Sweep_Spectrum::State>();
if (graph_id == "afterglow") return adminive::to_descriptor_json<nlohmann::json, Afterglow::State>();
if (graph_id == "waterfall") return adminive::to_descriptor_json<nlohmann::json, Waterfall::State>();
if (graph_id == "constellation") return adminive::to_descriptor_json<nlohmann::json, Constellation_Diagram::State>();
if (graph_id == "selection_overlay") return adminive::to_descriptor_json<nlohmann::json, Selection_Rectangle_Overlay::State>();
return {{"fields", nlohmann::json::array()}};
}
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::Prop>();
if (graph_id == "frequency_trace") return adminive::to_descriptor_json<nlohmann::json, Frequency_Trace::Prop>();
if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json<nlohmann::json, Sweep_Spectrum::Prop>();
if (graph_id == "afterglow") return adminive::to_descriptor_json<nlohmann::json, Afterglow::Prop>();
if (graph_id == "waterfall") return adminive::to_descriptor_json<nlohmann::json, Waterfall::Prop>();
if (graph_id == "constellation") return adminive::to_descriptor_json<nlohmann::json, Constellation_Diagram::Prop>();
if (graph_id == "selection_overlay") return adminive::to_descriptor_json<nlohmann::json, Selection_Rectangle_Overlay::Prop>();
return {{"fields", nlohmann::json::array()}};
}
}
-25
View File
@@ -1,25 +0,0 @@
#pragma once
#include <adminive/adapters/boost_pfr.hpp>
#include <adminive/adapters/magic_enum.hpp>
#include <adminive/adapters/nlohmann_json.hpp>
#include <adminive/adminive.hpp>
#include <render_2D/plottable/Plottables.hpp>
#include <nlohmann/json.hpp>
#include <string>
#include <string_view>
#include <vector>
namespace aethera::web {
struct Graph_Descriptor {
std::string id{}; /* HTTP 与 WebSocket 使用的稳定图标识。 */
std::string title{}; /* Gallery 卡片显示名称。 */
std::string category{}; /* Gallery 分类筛选值。 */
std::string description{}; /* 图用途和数据语义说明。 */
bool three_dimensional{}; /* 是否使用 Datoviz 3D 后端。 */
};
[[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(std::string_view graph_id);
[[nodiscard]] nlohmann::json graph_prop_descriptor_json(std::string_view graph_id);
}
#include "Graph_Metadata.ipp" /* 模板和反射特化实现。 */
-113
View File
@@ -1,113 +0,0 @@
#pragma once
#include <boost/pfr.hpp>
#include <stdexcept>
#include <type_traits>
#include <utility>
namespace aethera::web::detail {
struct Editable_Field_Metadata {
template <std::size_t Index, typename Field> auto operator()(Field field) const;
};
template <typename Object, typename Members>
struct Member_Pfr_Reflection_Adapter {
static constexpr std::size_t field_count = boost::pfr::tuple_size_v<Members>;
template <std::size_t Index> static decltype(auto) get(Object& value) noexcept;
template <std::size_t Index> static decltype(auto) get(const Object& value) noexcept;
template <std::size_t Index> static constexpr std::string_view name() noexcept;
};
#define AETHERA_MEMBER(Type, Name) decltype(&Type::Name) Name{&Type::Name}
using namespace render_2d;
struct Spectrum_Prop_Members { AETHERA_MEMBER(Spectrum::Prop, center_frequency); AETHERA_MEMBER(Spectrum::Prop, partition_count); AETHERA_MEMBER(Spectrum::Prop, max_hold_visible); AETHERA_MEMBER(Spectrum::Prop, min_hold_visible); AETHERA_MEMBER(Spectrum::Prop, max_marker_visible); AETHERA_MEMBER(Spectrum::Prop, min_marker_visible); AETHERA_MEMBER(Spectrum::Prop, sweep_region_visible); AETHERA_MEMBER(Spectrum::Prop, visible_range_only); AETHERA_MEMBER(Spectrum::Prop, frequency_range); AETHERA_MEMBER(Spectrum::Prop, sweep_frequency_range); AETHERA_MEMBER(Spectrum::Prop, partition_mode); AETHERA_MEMBER(Spectrum::Prop, interpolation_mode); AETHERA_MEMBER(Spectrum::Prop, max_brush); AETHERA_MEMBER(Spectrum::Prop, current_brush); AETHERA_MEMBER(Spectrum::Prop, min_brush); AETHERA_MEMBER(Spectrum::Prop, max_pen); AETHERA_MEMBER(Spectrum::Prop, current_pen); AETHERA_MEMBER(Spectrum::Prop, min_pen); AETHERA_MEMBER(Spectrum::Prop, selected_marker_pen); AETHERA_MEMBER(Spectrum::Prop, marker_pen); AETHERA_MEMBER(Spectrum::Prop, middle_frequency_pen); AETHERA_MEMBER(Spectrum::Prop, sweep_region_brush); AETHERA_MEMBER(Spectrum::Prop, custom_markers); AETHERA_MEMBER(Spectrum::Prop, selected_marker); };
struct Frequency_Trace_Prop_Members { AETHERA_MEMBER(Frequency_Trace::Prop, partition_count); AETHERA_MEMBER(Frequency_Trace::Prop, pen); AETHERA_MEMBER(Frequency_Trace::Prop, partition_mode); AETHERA_MEMBER(Frequency_Trace::Prop, samples); };
struct Sweep_Spectrum_Prop_Members { AETHERA_MEMBER(Sweep_Spectrum::Prop, bins_per_block); AETHERA_MEMBER(Sweep_Spectrum::Prop, block_count); AETHERA_MEMBER(Sweep_Spectrum::Prop, partition_count); AETHERA_MEMBER(Sweep_Spectrum::Prop, visible_range_only); AETHERA_MEMBER(Sweep_Spectrum::Prop, frequency_range); AETHERA_MEMBER(Sweep_Spectrum::Prop, partition_mode); AETHERA_MEMBER(Sweep_Spectrum::Prop, pen); AETHERA_MEMBER(Sweep_Spectrum::Prop, current_frequency_pen); AETHERA_MEMBER(Sweep_Spectrum::Prop, interpolation_mode); AETHERA_MEMBER(Sweep_Spectrum::Prop, blocks); };
struct Afterglow_Prop_Members { AETHERA_MEMBER(Afterglow::Prop, frequency_point_size); AETHERA_MEMBER(Afterglow::Prop, power_point_size); AETHERA_MEMBER(Afterglow::Prop, partition_count); AETHERA_MEMBER(Afterglow::Prop, interpolate); AETHERA_MEMBER(Afterglow::Prop, attenuation_rate); AETHERA_MEMBER(Afterglow::Prop, frequency_range); AETHERA_MEMBER(Afterglow::Prop, power_range); AETHERA_MEMBER(Afterglow::Prop, partition_mode); AETHERA_MEMBER(Afterglow::Prop, color_map); AETHERA_MEMBER(Afterglow::Prop, spectra); };
struct Waterfall_Prop_Members { AETHERA_MEMBER(Waterfall::Prop, tooltip_enabled); AETHERA_MEMBER(Waterfall::Prop, tooltip_font); AETHERA_MEMBER(Waterfall::Prop, tooltip_text_pen); AETHERA_MEMBER(Waterfall::Prop, tooltip_background_brush); AETHERA_MEMBER(Waterfall::Prop, frequency_bin_count); AETHERA_MEMBER(Waterfall::Prop, partition_count); AETHERA_MEMBER(Waterfall::Prop, visible_range_only); AETHERA_MEMBER(Waterfall::Prop, frequency_range); AETHERA_MEMBER(Waterfall::Prop, power_range); AETHERA_MEMBER(Waterfall::Prop, partition_mode); AETHERA_MEMBER(Waterfall::Prop, interpolation_mode); AETHERA_MEMBER(Waterfall::Prop, color_map); AETHERA_MEMBER(Waterfall::Prop, rows); };
struct Constellation_Prop_Members { AETHERA_MEMBER(Constellation_Diagram::Prop, point_lifetime_ms); AETHERA_MEMBER(Constellation_Diagram::Prop, type); AETHERA_MEMBER(Constellation_Diagram::Prop, phase_offset_radians); AETHERA_MEMBER(Constellation_Diagram::Prop, i_range); AETHERA_MEMBER(Constellation_Diagram::Prop, q_range); AETHERA_MEMBER(Constellation_Diagram::Prop, point_color); AETHERA_MEMBER(Constellation_Diagram::Prop, anchor_color); AETHERA_MEMBER(Constellation_Diagram::Prop, points); };
struct Selection_Prop_Members { AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, label_font); AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, label_pen); AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, selection_brush); AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, selection_border_pen); AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, selected_regions); };
struct Color_Map_Members { AETHERA_MEMBER(Color_Map, stops); };
#define AETHERA_RENDER_STATE_MEMBERS(Type) AETHERA_MEMBER(Type, prepare_dirty); AETHERA_MEMBER(Type, paint_dirty); AETHERA_MEMBER(Type, prepare_executed); AETHERA_MEMBER(Type, paint_executed); AETHERA_MEMBER(Type, prepare_graph_rebuilt); AETHERA_MEMBER(Type, paint_graph_rebuilt); AETHERA_MEMBER(Type, prepare_task_count); AETHERA_MEMBER(Type, paint_task_count); AETHERA_MEMBER(Type, prepare_execution_time_ns); AETHERA_MEMBER(Type, paint_execution_time_ns)
struct Spectrum_State_Members { AETHERA_RENDER_STATE_MEMBERS(Spectrum::State); AETHERA_MEMBER(Spectrum::State, sample_count); AETHERA_MEMBER(Spectrum::State, rendered_point_count); AETHERA_MEMBER(Spectrum::State, selectable_marker_count); };
struct Frequency_Trace_State_Members { AETHERA_RENDER_STATE_MEMBERS(Frequency_Trace::State); AETHERA_MEMBER(Frequency_Trace::State, sample_count); AETHERA_MEMBER(Frequency_Trace::State, rendered_point_count); };
struct Sweep_Spectrum_State_Members { AETHERA_RENDER_STATE_MEMBERS(Sweep_Spectrum::State); AETHERA_MEMBER(Sweep_Spectrum::State, stored_block_count); AETHERA_MEMBER(Sweep_Spectrum::State, stored_point_count); AETHERA_MEMBER(Sweep_Spectrum::State, rendered_point_count); };
struct Afterglow_State_Members { AETHERA_RENDER_STATE_MEMBERS(Afterglow::State); AETHERA_MEMBER(Afterglow::State, history_count); AETHERA_MEMBER(Afterglow::State, latest_spectrum_point_count); AETHERA_MEMBER(Afterglow::State, rendered_cell_count); };
struct Waterfall_State_Members { AETHERA_RENDER_STATE_MEMBERS(Waterfall::State); AETHERA_MEMBER(Waterfall::State, row_count); AETHERA_MEMBER(Waterfall::State, stored_point_count); AETHERA_MEMBER(Waterfall::State, rendered_cell_count); };
struct Constellation_State_Members { AETHERA_RENDER_STATE_MEMBERS(Constellation_Diagram::State); AETHERA_MEMBER(Constellation_Diagram::State, point_count); };
struct Selection_State_Members { AETHERA_RENDER_STATE_MEMBERS(Selection_Rectangle_Overlay::State); AETHERA_MEMBER(Selection_Rectangle_Overlay::State, selected_region_count); };
#undef AETHERA_RENDER_STATE_MEMBERS
#undef AETHERA_MEMBER
}
namespace adminive {
template <typename Value, Json_Type Json> struct Value_Adapter<std::vector<Value>, Json> {
using value_type = std::vector<Value>;
static constexpr std::string_view type_name{"array"};
static Json encode(const value_type& values);
static void decode(value_type& target, const Json& value);
};
#define AETHERA_PLAIN_REFLECTION(Type, Id, Label) template <> struct Reflection_Adapter<Type> : Boost_Pfr_Reflection_Adapter<Type> {}; template <> struct Type_Descriptor<Type> { static auto get(); };
AETHERA_PLAIN_REFLECTION(aethera::Color, "color", "Color")
AETHERA_PLAIN_REFLECTION(aethera::render_2d::Point_F, "point", "Point")
AETHERA_PLAIN_REFLECTION(aethera::render_2d::Rect_F, "rectangle", "Rectangle")
AETHERA_PLAIN_REFLECTION(aethera::render_2d::Axis_Range, "axis_range", "Axis range")
AETHERA_PLAIN_REFLECTION(aethera::render_2d::Pen, "pen", "Pen")
AETHERA_PLAIN_REFLECTION(aethera::render_2d::Brush, "brush", "Brush")
AETHERA_PLAIN_REFLECTION(aethera::render_2d::Font, "font", "Font")
AETHERA_PLAIN_REFLECTION(aethera::render_2d::Frequency_Trace_Sample, "frequency_trace_sample", "Frequency trace sample")
AETHERA_PLAIN_REFLECTION(aethera::render_2d::Waterfall_Row, "waterfall_row", "Waterfall row")
AETHERA_PLAIN_REFLECTION(aethera::render_2d::Constellation_Point, "constellation_point", "Constellation point")
#undef AETHERA_PLAIN_REFLECTION
#define AETHERA_MEMBER_REFLECTION(Type, Members, Id, Label, Customizer) template <> struct Reflection_Adapter<Type> : aethera::web::detail::Member_Pfr_Reflection_Adapter<Type, aethera::web::detail::Members> {}; template <> struct Type_Descriptor<Type> { static auto get(); };
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Color_Map, Color_Map_Members, "color_map", "Color map", aethera::web::detail::Editable_Field_Metadata)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Spectrum::Prop, Spectrum_Prop_Members, "spectrum_prop", "Spectrum properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Frequency_Trace::Prop, Frequency_Trace_Prop_Members, "frequency_trace_prop", "Frequency trace properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Sweep_Spectrum::Prop, Sweep_Spectrum_Prop_Members, "sweep_spectrum_prop", "Sweep spectrum properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Afterglow::Prop, Afterglow_Prop_Members, "afterglow_prop", "Afterglow properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Waterfall::Prop, Waterfall_Prop_Members, "waterfall_prop", "Waterfall properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Constellation_Diagram::Prop, Constellation_Prop_Members, "constellation_prop", "Constellation properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Selection_Rectangle_Overlay::Prop, Selection_Prop_Members, "selection_prop", "Selection properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Spectrum::State, Spectrum_State_Members, "spectrum_state", "Spectrum state", Identity_Field_Customizer)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Frequency_Trace::State, Frequency_Trace_State_Members, "frequency_trace_state", "Frequency trace state", Identity_Field_Customizer)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Sweep_Spectrum::State, Sweep_Spectrum_State_Members, "sweep_spectrum_state", "Sweep spectrum state", Identity_Field_Customizer)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Afterglow::State, Afterglow_State_Members, "afterglow_state", "Afterglow state", Identity_Field_Customizer)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Waterfall::State, Waterfall_State_Members, "waterfall_state", "Waterfall state", Identity_Field_Customizer)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Constellation_Diagram::State, Constellation_State_Members, "constellation_state", "Constellation state", Identity_Field_Customizer)
AETHERA_MEMBER_REFLECTION(aethera::render_2d::Selection_Rectangle_Overlay::State, Selection_State_Members, "selection_state", "Selection state", Identity_Field_Customizer)
#undef AETHERA_MEMBER_REFLECTION
}
namespace aethera::web::detail {
template <std::size_t Index, typename Field> auto Editable_Field_Metadata::operator()(Field field) const { return field.editable(); }
template <typename Object, typename Members> template <std::size_t Index> decltype(auto) Member_Pfr_Reflection_Adapter<Object, Members>::get(Object& value) noexcept { return value.*boost::pfr::get<Index>(Members{}); }
template <typename Object, typename Members> template <std::size_t Index> decltype(auto) Member_Pfr_Reflection_Adapter<Object, Members>::get(const Object& value) noexcept { return value.*boost::pfr::get<Index>(Members{}); }
template <typename Object, typename Members> template <std::size_t Index> constexpr std::string_view Member_Pfr_Reflection_Adapter<Object, Members>::name() noexcept { return boost::pfr::get_name<Index, Members>(); }
}
namespace adminive {
template <typename Value, Json_Type Json> Json Value_Adapter<std::vector<Value>, Json>::encode(const value_type& values) { Json result = json_array<Json>(); for (const auto& value : values) json_append(result, encode_json_value<Json>(value)); return result; }
template <typename Value, Json_Type Json> void Value_Adapter<std::vector<Value>, Json>::decode(value_type& target, const Json& value) { if (!Json_Adapter<Json>::is_array(value)) throw std::invalid_argument("value must be an array"); value_type updated; updated.reserve(Json_Adapter<Json>::size(value)); for (std::size_t index = 0; index < Json_Adapter<Json>::size(value); ++index) { Value item{}; assign_json_value<Json>(item, Json_Adapter<Json>::at(value, index)); updated.push_back(std::move(item)); } target = std::move(updated); }
#define AETHERA_DEFINE_PLAIN_DESCRIPTOR(Type, Id, Label) inline auto Type_Descriptor<Type>::get() { return reflected_object_with<Type>(Id, Label, aethera::web::detail::Editable_Field_Metadata{}); }
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::Color, "color", "Color")
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Point_F, "point", "Point")
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Rect_F, "rectangle", "Rectangle")
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Axis_Range, "axis_range", "Axis range")
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Pen, "pen", "Pen")
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Brush, "brush", "Brush")
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Font, "font", "Font")
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Frequency_Trace_Sample, "frequency_trace_sample", "Frequency trace sample")
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Waterfall_Row, "waterfall_row", "Waterfall row")
AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Constellation_Point, "constellation_point", "Constellation point")
#undef AETHERA_DEFINE_PLAIN_DESCRIPTOR
#define AETHERA_DEFINE_MEMBER_DESCRIPTOR(Type, Id, Label, Customizer) inline auto Type_Descriptor<Type>::get() { return reflected_object_with<Type>(Id, Label, Customizer{}); }
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Color_Map, "color_map", "Color map", aethera::web::detail::Editable_Field_Metadata)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Spectrum::Prop, "spectrum_prop", "Spectrum properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Frequency_Trace::Prop, "frequency_trace_prop", "Frequency trace properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Sweep_Spectrum::Prop, "sweep_spectrum_prop", "Sweep spectrum properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Afterglow::Prop, "afterglow_prop", "Afterglow properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Waterfall::Prop, "waterfall_prop", "Waterfall properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Constellation_Diagram::Prop, "constellation_prop", "Constellation properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Selection_Rectangle_Overlay::Prop, "selection_prop", "Selection properties", aethera::web::detail::Editable_Field_Metadata)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Spectrum::State, "spectrum_state", "Spectrum state", Identity_Field_Customizer)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Frequency_Trace::State, "frequency_trace_state", "Frequency trace state", Identity_Field_Customizer)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Sweep_Spectrum::State, "sweep_spectrum_state", "Sweep spectrum state", Identity_Field_Customizer)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Afterglow::State, "afterglow_state", "Afterglow state", Identity_Field_Customizer)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Waterfall::State, "waterfall_state", "Waterfall state", Identity_Field_Customizer)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Constellation_Diagram::State, "constellation_state", "Constellation state", Identity_Field_Customizer)
AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Selection_Rectangle_Overlay::State, "selection_state", "Selection state", Identity_Field_Customizer)
#undef AETHERA_DEFINE_MEMBER_DESCRIPTOR
}
-127
View File
@@ -1,127 +0,0 @@
#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<nlohmann::json()> state{}; /* 直接遍历具体图完整 State 的即时 HTTP 文档。 */
std::function<nlohmann::json()> prop{};
std::function<nlohmann::json(const nlohmann::json&)> patch_prop{};
};
template <typename Definition, auto... Members, typename Object>
void bind_prop_api(Plot_2D& plot, Object* object) {
using Prop = typename Definition::Prop;
plot.prop = [object] { return adminive::model_to_json<nlohmann::json>(static_cast<const Prop&>(object->template read_prop<typename Definition::Base_Tag>()), true); };
plot.state = [object] { return adminive::model_to_json<nlohmann::json>(static_cast<const typename Definition::State&>(object->template read_state<typename Definition::Base_Tag>()), true); };
plot.patch_prop = [object](const nlohmann::json& patch) { Prop updated = static_cast<const Prop&>(object->template read_prop<typename Definition::Base_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>(static_cast<const Prop&>(object->template read_prop<typename Definition::Base_Tag>()), 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, &Spectrum::Prop::center_frequency, &Spectrum::Prop::partition_count, &Spectrum::Prop::max_hold_visible, &Spectrum::Prop::min_hold_visible, &Spectrum::Prop::max_marker_visible, &Spectrum::Prop::min_marker_visible, &Spectrum::Prop::sweep_region_visible, &Spectrum::Prop::visible_range_only, &Spectrum::Prop::frequency_range, &Spectrum::Prop::sweep_frequency_range, &Spectrum::Prop::partition_mode, &Spectrum::Prop::interpolation_mode, &Spectrum::Prop::max_brush, &Spectrum::Prop::current_brush, &Spectrum::Prop::min_brush, &Spectrum::Prop::max_pen, &Spectrum::Prop::current_pen, &Spectrum::Prop::min_pen, &Spectrum::Prop::selected_marker_pen, &Spectrum::Prop::marker_pen, &Spectrum::Prop::middle_frequency_pen, &Spectrum::Prop::sweep_region_brush, &Spectrum::Prop::custom_markers, &Spectrum::Prop::selected_marker>(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.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, &Frequency_Trace::Prop::partition_count, &Frequency_Trace::Prop::pen, &Frequency_Trace::Prop::partition_mode, &Frequency_Trace::Prop::samples>(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.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, &Sweep_Spectrum::Prop::bins_per_block, &Sweep_Spectrum::Prop::block_count, &Sweep_Spectrum::Prop::partition_count, &Sweep_Spectrum::Prop::visible_range_only, &Sweep_Spectrum::Prop::frequency_range, &Sweep_Spectrum::Prop::partition_mode, &Sweep_Spectrum::Prop::pen, &Sweep_Spectrum::Prop::current_frequency_pen, &Sweep_Spectrum::Prop::interpolation_mode, &Sweep_Spectrum::Prop::blocks>(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.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, &Afterglow::Prop::frequency_point_size, &Afterglow::Prop::power_point_size, &Afterglow::Prop::partition_count, &Afterglow::Prop::interpolate, &Afterglow::Prop::attenuation_rate, &Afterglow::Prop::frequency_range, &Afterglow::Prop::power_range, &Afterglow::Prop::partition_mode, &Afterglow::Prop::color_map, &Afterglow::Prop::spectra>(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.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, &Waterfall::Prop::tooltip_enabled, &Waterfall::Prop::tooltip_font, &Waterfall::Prop::tooltip_text_pen, &Waterfall::Prop::tooltip_background_brush, &Waterfall::Prop::frequency_bin_count, &Waterfall::Prop::partition_count, &Waterfall::Prop::visible_range_only, &Waterfall::Prop::frequency_range, &Waterfall::Prop::power_range, &Waterfall::Prop::partition_mode, &Waterfall::Prop::interpolation_mode, &Waterfall::Prop::color_map, &Waterfall::Prop::rows>(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.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, &Constellation_Diagram::Prop::point_lifetime_ms, &Constellation_Diagram::Prop::type, &Constellation_Diagram::Prop::phase_offset_radians, &Constellation_Diagram::Prop::i_range, &Constellation_Diagram::Prop::q_range, &Constellation_Diagram::Prop::point_color, &Constellation_Diagram::Prop::anchor_color, &Constellation_Diagram::Prop::points>(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);
} 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>(); bind_prop_api<Selection_Rectangle_Overlay, &Selection_Rectangle_Overlay::Prop::label_font, &Selection_Rectangle_Overlay::Prop::label_pen, &Selection_Rectangle_Overlay::Prop::selection_brush, &Selection_Rectangle_Overlay::Prop::selection_border_pen, &Selection_Rectangle_Overlay::Prop::selected_regions>(result, raw); result.update = [](double) {}; 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); }
nlohmann::json state_document() const { if (const auto* value = std::get_if<Plot_2D>(&plot)) return value->state(); const auto& state = std::get<Plot_3D>(plot).visual->read_state<Point_Visual::Base_Tag>(); return {{"prepare_dirty", state.prepare_dirty}, {"paint_dirty", state.paint_dirty}, {"prepare_executed", state.prepare_executed}, {"paint_executed", state.paint_executed}, {"prepare_graph_rebuilt", state.prepare_graph_rebuilt}, {"paint_graph_rebuilt", state.paint_graph_rebuilt}, {"prepare_task_count", state.prepare_task_count}, {"paint_task_count", state.paint_task_count}, {"prepare_execution_time_ns", state.prepare_execution_time_ns}, {"paint_execution_time_ns", state.paint_execution_time_ns}, {"item_count", state.item_count}, {"prepared_item_count", state.prepared_item_count}, {"prepared_revision", state.prepared_revision}}; }
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(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; }
}
-45
View File
@@ -1,45 +0,0 @@
#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 会话的注册表。 */
};
}
+7 -27
View File
@@ -11,38 +11,18 @@ std::string graph_id_from_path(std::string_view path) { const auto split = path.
} }
struct Graph_WebSocket::Private { struct Graph_WebSocket::Private {
std::weak_ptr<drogon::WebSocketConnection> connection; /* 不延长已关闭 Drogon 连接生命周期。 */ std::weak_ptr<drogon::WebSocketConnection> connection; /* 不延长已关闭 Drogon 连接生命周期。 */
std::shared_ptr<Graph_Session> session; /* 连接期间保持图会话存活。 */ std::shared_ptr<Plot> plot; /* 连接期间保持引擎桥接对象存活。 */
const void* owner{}; /* 会话订阅表使用的稳定连接身份。 */ const void* owner{}; /* 会话订阅表使用的稳定连接身份。 */
bool attached{}; /* 是否已安装像素订阅。 */ bool attached{}; /* 是否已安装像素订阅。 */
std::mutex callback_mutex;
std::unordered_map<std::string, Callback> callbacks;
}; };
Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Graph_Session> session) : d(std::make_unique<Private>()) { d->connection = connection; d->session = std::move(session); d->owner = connection.get(); } Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Plot> plot) : d(std::make_unique<Private>()) { d->connection = connection; d->plot = std::move(plot); d->owner = connection.get(); }
Graph_WebSocket::~Graph_WebSocket() { close(); } 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::start() { if (d->attached) return; const auto weak = weak_from_this(); d->plot->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::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<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->plot->submit(event); }
void Graph_WebSocket::close() { if (!d->attached) return; d->session->detach(d->owner); d->attached = false; } void Graph_WebSocket::close() { if (!d->attached) return; d->plot->detach(d->owner); d->attached = false; }
void Graph_WebSocket::register_callback(std::string key, Callback callback) {
std::lock_guard lock(d->callback_mutex);
d->callbacks.insert_or_assign(std::move(key), std::move(callback));
}
void Graph_WebSocket::unregister_callback(std::string_view key) {
std::lock_guard lock(d->callback_mutex);
d->callbacks.erase(std::string(key));
}
void Graph_WebSocket::send(std::string_view key, std::string_view frame) {
Callback callback;
{
std::lock_guard lock(d->callback_mutex);
const auto iterator = d->callbacks.find(std::string(key));
if (iterator == d->callbacks.end()) return;
callback = iterator->second;
}
callback(frame);
}
Graph_WebSocket_Controller::Graph_WebSocket_Controller(std::shared_ptr<Graph_Registry> value) : registry(std::move(value)) {} Graph_WebSocket_Controller::Graph_WebSocket_Controller(std::shared_ptr<Plot_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::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<Graph_WebSocket>(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<Graph_WebSocket>()) socket->receive(message); } 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(); } void Graph_WebSocket_Controller::handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) { if (const auto socket = connection->getContext<Graph_WebSocket>()) socket->close(); connection->clearContext(); }
} }
+5 -9
View File
@@ -1,5 +1,5 @@
#pragma once #pragma once
#include "Graph_Session.hpp" #include "Plot.hpp"
#include <drogon/WebSocketController.h> #include <drogon/WebSocketController.h>
#include <functional> #include <functional>
#include <memory> #include <memory>
@@ -7,31 +7,27 @@
namespace aethera::web { namespace aethera::web {
class Graph_WebSocket final : public std::enable_shared_from_this<Graph_WebSocket> { class Graph_WebSocket final : public std::enable_shared_from_this<Graph_WebSocket> {
public: public:
Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Graph_Session> session); Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Plot> plot);
~Graph_WebSocket(); ~Graph_WebSocket();
Graph_WebSocket(const Graph_WebSocket&) = delete; Graph_WebSocket(const Graph_WebSocket&) = delete;
Graph_WebSocket& operator=(const Graph_WebSocket&) = delete; Graph_WebSocket& operator=(const Graph_WebSocket&) = delete;
using Callback = std::function<void(std::string_view)>;
void start(); void start();
void receive(std::string_view message); void receive(std::string_view message);
void close(); void close();
void register_callback(std::string key, Callback callback);
void unregister_callback(std::string_view key);
void send(std::string_view key, std::string_view frame);
private: private:
struct Private; struct Private;
std::unique_ptr<Private> d; /* Drogon 连接与图会话绑定。 */ std::unique_ptr<Private> d; /* Drogon 连接与图会话绑定。 */
}; };
class Graph_WebSocket_Controller final : public drogon::WebSocketController<Graph_WebSocket_Controller, false> { class Graph_WebSocket_Controller final : public drogon::WebSocketController<Graph_WebSocket_Controller, false> {
public: public:
explicit Graph_WebSocket_Controller(std::shared_ptr<Graph_Registry> registry); explicit Graph_WebSocket_Controller(std::shared_ptr<Plot_Registry> registry);
void handleNewMessage(const drogon::WebSocketConnectionPtr& connection, std::string&& message, const drogon::WebSocketMessageType& type) override; 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 handleNewConnection(const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) override;
void handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) override; void handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) override;
WS_PATH_LIST_BEGIN WS_PATH_LIST_BEGIN
WS_ADD_PATH_VIA_REGEX("^/ws/graphs/[^/]+$"); WS_ADD_PATH_VIA_REGEX("^/ws/plot/[^/]+$");
WS_PATH_LIST_END WS_PATH_LIST_END
private: private:
std::shared_ptr<Graph_Registry> registry; /* 所有 WebSocket 共用的会话注册表。 */ std::shared_ptr<Plot_Registry> registry; /* 所有 WebSocket 共用的 Plot 注册表。 */
}; };
} }
+214 -3
View File
@@ -1,6 +1,217 @@
#include "Plot.hpp" #include "Plot.hpp"
#include "Renderable_Adapter.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 aethera::web {
Plot::Plot(std::string key_value, std::shared_ptr<Graph_WebSocket> websocket_value) : websocket(std::move(websocket_value)), key(std::move(key_value)) { websocket->register_callback(key, [this](std::string_view event) { on_event(event); }); } namespace {
Plot::~Plot() { websocket->unregister_callback(key); } using namespace render_2d;
void Plot::send_frame(std::string_view frame) { websocket->send(key, frame); } 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 Schema_Query { Plot::Json_Handler handler{}; };
struct Prop_Write { std::string key{}; nlohmann::json value{}; Plot::Json_Handler handler{}; };
using Plot_Input = std::variant<Plot_Event, Schema_Query, Prop_Write>;
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::unique_ptr<detail::Renderable_Descriptor> descriptor{}; /* 鐩存帴璇诲啓 plot 鐨?Structive 鍗忚瑙嗗浘銆?*/
};
template <typename Definition, typename... Fields, typename Holder, typename Object>
void bind_renderable_adapter(Holder& plot, Object& object) {
using Tag = typename Definition::Base_Tag;
using State = typename Definition::State;
using Adapter = detail::Renderable_Adapter<Object, Fields...,
detail::State_Field<Tag, &State::prepare_dirty, "prepare_dirty">,
detail::State_Field<Tag, &State::paint_dirty, "paint_dirty">,
detail::State_Field<Tag, &State::prepare_executed, "prepare_executed">,
detail::State_Field<Tag, &State::paint_executed, "paint_executed">,
detail::State_Field<Tag, &State::prepare_graph_rebuilt, "prepare_graph_rebuilt">,
detail::State_Field<Tag, &State::paint_graph_rebuilt, "paint_graph_rebuilt">,
detail::State_Field<Tag, &State::prepare_task_count, "prepare_task_count">,
detail::State_Field<Tag, &State::paint_task_count, "paint_task_count">,
detail::State_Field<Tag, &State::prepare_execution_time_ns, "prepare_execution_time_ns">,
detail::State_Field<Tag, &State::paint_execution_time_ns, "paint_execution_time_ns">>;
plot.descriptor = detail::make_renderable_descriptor(Adapter{object});
}
#define AETHERA_PROP(Type, Name) detail::Prop_Field<&Type::Prop::Name, #Name>
#define AETHERA_STATE(Type, Name) detail::State_Field<typename Type::Base_Tag, &Type::State::Name, #Name>
template <typename Definition>
Plot_2D make_plot_2d() {
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 constexpr (std::same_as<Definition, 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_renderable_adapter<Spectrum, AETHERA_PROP(Spectrum, center_frequency), AETHERA_PROP(Spectrum, partition_count), AETHERA_PROP(Spectrum, max_hold_visible), AETHERA_PROP(Spectrum, min_hold_visible), AETHERA_PROP(Spectrum, max_marker_visible), AETHERA_PROP(Spectrum, min_marker_visible), AETHERA_PROP(Spectrum, sweep_region_visible), AETHERA_PROP(Spectrum, visible_range_only), AETHERA_PROP(Spectrum, frequency_range), AETHERA_PROP(Spectrum, sweep_frequency_range), AETHERA_PROP(Spectrum, partition_mode), AETHERA_PROP(Spectrum, interpolation_mode), AETHERA_PROP(Spectrum, max_brush), AETHERA_PROP(Spectrum, current_brush), AETHERA_PROP(Spectrum, min_brush), AETHERA_PROP(Spectrum, max_pen), AETHERA_PROP(Spectrum, current_pen), AETHERA_PROP(Spectrum, min_pen), AETHERA_PROP(Spectrum, selected_marker_pen), AETHERA_PROP(Spectrum, marker_pen), AETHERA_PROP(Spectrum, middle_frequency_pen), AETHERA_PROP(Spectrum, sweep_region_brush), AETHERA_PROP(Spectrum, custom_markers), AETHERA_PROP(Spectrum, selected_marker), AETHERA_STATE(Spectrum, sample_count), AETHERA_STATE(Spectrum, rendered_point_count), AETHERA_STATE(Spectrum, selectable_marker_count)>(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.plot = std::move(object);
} else if constexpr (std::same_as<Definition, 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_renderable_adapter<Frequency_Trace, AETHERA_PROP(Frequency_Trace, partition_count), AETHERA_PROP(Frequency_Trace, pen), AETHERA_PROP(Frequency_Trace, partition_mode), AETHERA_PROP(Frequency_Trace, samples), AETHERA_STATE(Frequency_Trace, sample_count), AETHERA_STATE(Frequency_Trace, rendered_point_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.plot = std::move(object);
} else if constexpr (std::same_as<Definition, 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_renderable_adapter<Sweep_Spectrum, AETHERA_PROP(Sweep_Spectrum, bins_per_block), AETHERA_PROP(Sweep_Spectrum, block_count), AETHERA_PROP(Sweep_Spectrum, partition_count), AETHERA_PROP(Sweep_Spectrum, visible_range_only), AETHERA_PROP(Sweep_Spectrum, frequency_range), AETHERA_PROP(Sweep_Spectrum, partition_mode), AETHERA_PROP(Sweep_Spectrum, pen), AETHERA_PROP(Sweep_Spectrum, current_frequency_pen), AETHERA_PROP(Sweep_Spectrum, interpolation_mode), AETHERA_PROP(Sweep_Spectrum, blocks), AETHERA_STATE(Sweep_Spectrum, stored_block_count), AETHERA_STATE(Sweep_Spectrum, stored_point_count), AETHERA_STATE(Sweep_Spectrum, rendered_point_count)>(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.plot = std::move(object);
} else if constexpr (std::same_as<Definition, 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_renderable_adapter<Afterglow, AETHERA_PROP(Afterglow, frequency_point_size), AETHERA_PROP(Afterglow, power_point_size), AETHERA_PROP(Afterglow, partition_count), AETHERA_PROP(Afterglow, interpolate), AETHERA_PROP(Afterglow, attenuation_rate), AETHERA_PROP(Afterglow, frequency_range), AETHERA_PROP(Afterglow, power_range), AETHERA_PROP(Afterglow, partition_mode), AETHERA_PROP(Afterglow, color_map), AETHERA_PROP(Afterglow, spectra), AETHERA_STATE(Afterglow, history_count), AETHERA_STATE(Afterglow, latest_spectrum_point_count), AETHERA_STATE(Afterglow, rendered_cell_count)>(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.plot = std::move(object);
} else if constexpr (std::same_as<Definition, 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_renderable_adapter<Waterfall, AETHERA_PROP(Waterfall, tooltip_enabled), AETHERA_PROP(Waterfall, tooltip_font), AETHERA_PROP(Waterfall, tooltip_text_pen), AETHERA_PROP(Waterfall, tooltip_background_brush), AETHERA_PROP(Waterfall, frequency_bin_count), AETHERA_PROP(Waterfall, partition_count), AETHERA_PROP(Waterfall, visible_range_only), AETHERA_PROP(Waterfall, frequency_range), AETHERA_PROP(Waterfall, power_range), AETHERA_PROP(Waterfall, partition_mode), AETHERA_PROP(Waterfall, interpolation_mode), AETHERA_PROP(Waterfall, color_map), AETHERA_PROP(Waterfall, rows), AETHERA_STATE(Waterfall, row_count), AETHERA_STATE(Waterfall, stored_point_count), AETHERA_STATE(Waterfall, rendered_cell_count)>(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.plot = std::move(object);
} else if constexpr (std::same_as<Definition, Constellation_Diagram>) {
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_renderable_adapter<Constellation_Diagram, AETHERA_PROP(Constellation_Diagram, point_lifetime_ms), AETHERA_PROP(Constellation_Diagram, type), AETHERA_PROP(Constellation_Diagram, phase_offset_radians), AETHERA_PROP(Constellation_Diagram, i_range), AETHERA_PROP(Constellation_Diagram, q_range), AETHERA_PROP(Constellation_Diagram, point_color), AETHERA_PROP(Constellation_Diagram, anchor_color), AETHERA_PROP(Constellation_Diagram, points), AETHERA_STATE(Constellation_Diagram, point_count)>(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);
} else if constexpr (std::same_as<Definition, Selection_Rectangle_Overlay>) {
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>(); bind_renderable_adapter<Selection_Rectangle_Overlay, AETHERA_PROP(Selection_Rectangle_Overlay, label_font), AETHERA_PROP(Selection_Rectangle_Overlay, label_pen), AETHERA_PROP(Selection_Rectangle_Overlay, selection_brush), AETHERA_PROP(Selection_Rectangle_Overlay, selection_border_pen), AETHERA_PROP(Selection_Rectangle_Overlay, selected_regions), AETHERA_STATE(Selection_Rectangle_Overlay, selected_region_count)>(result, *raw); result.update = [](double) {}; result.plot = std::move(object);
} else {
static_assert(std::same_as<Definition, void>, "unsupported 2D Plot definition");
}
return result;
}
struct Plot_3D {
std::unique_ptr<Impl<Point_Visual>> visual{}; /* 鐐瑰浘鍏冩潈濞佸璞°€?*/
std::unique_ptr<Impl<Render_Scene_3D>> scene{}; /* 寮傛 Datoviz Scene銆?*/
std::unique_ptr<detail::Renderable_Descriptor> descriptor{}; /* 鐩存帴璇诲啓 visual 鐨?Structive 鍗忚瑙嗗浘銆?*/
};
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}})); bind_renderable_adapter<Point_Visual, AETHERA_PROP(Point_Visual, transform), AETHERA_PROP(Point_Visual, visible), AETHERA_PROP(Point_Visual, depth_test), AETHERA_PROP(Point_Visual, items), AETHERA_STATE(Point_Visual, item_count), AETHERA_STATE(Point_Visual, prepared_item_count), AETHERA_STATE(Point_Visual, prepared_revision)>(result, *result.visual); result.visual->advance(); result.scene = build<Impl<Render_Scene_3D>>(result.visual.get()); result.scene->activate_view(); return result; }
using Plot_Engine = std::variant<Plot_2D, Plot_3D>;
template <typename Definition>
Plot_Engine make_plot_engine_2d() { return Plot_Engine{std::in_place_type<Plot_2D>, make_plot_2d<Definition>()}; }
Plot_Engine make_plot_engine_3d() { return Plot_Engine{std::in_place_type<Plot_3D>, 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<Spectrum>},
Plot_Catalog_Entry{"frequency_trace", "Frequency trace", "Curves", "Time ordered frequency samples rendered as a partitioned curve.", "2D", &make_plot_engine_2d<Frequency_Trace>},
Plot_Catalog_Entry{"sweep_spectrum", "Sweep spectrum", "Curves", "Incremental sweep blocks composed into one frequency curve.", "2D", &make_plot_engine_2d<Sweep_Spectrum>},
Plot_Catalog_Entry{"afterglow", "Afterglow", "Raster", "Persistent spectrum energy rendered as reusable color blocks.", "2D", &make_plot_engine_2d<Afterglow>},
Plot_Catalog_Entry{"waterfall", "Waterfall", "Raster", "Time ordered spectrum rows rendered as a color raster.", "2D", &make_plot_engine_2d<Waterfall>},
Plot_Catalog_Entry{"constellation", "Constellation", "Signals", "I/Q samples and modulation anchors.", "2D", &make_plot_engine_2d<Constellation_Diagram>},
Plot_Catalog_Entry{"selection_overlay", "Selection overlay", "Interaction", "Direct-paint selection rectangle over numeric axes.", "2D", &make_plot_engine_2d<Selection_Rectangle_Overlay>},
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<asio::any_io_executor> strand; /* 寮曟搸鍙?descriptor 鐨勪覆琛岃闂煙銆?*/
asio::experimental::concurrent_channel<void(asio::error_code, Plot_Input)> inputs; /* UI 杈撳叆闃熷垪銆?*/
Plot_Engine engine; /* Builder 已完成的 Scene、Renderable 与 adapter 唯一所有权。 */
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, Plot_Engine value) : strand(asio::make_strand(std::move(executor))), inputs(strand, 32), engine(std::move(value)) {}
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); }
detail::Renderable_Descriptor& descriptor() { return std::visit([](auto& value) -> detail::Renderable_Descriptor& { return *value.descriptor; }, engine); }
};
Plot::Plot(std::unique_ptr<Private> private_data) : d(std::move(private_data)) {}
Plot::~Plot() { d->inputs.close(); }
void Plot::start() {
auto self = shared_from_this();
if (auto* engine_2d = std::get_if<Plot_2D>(&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<Plot_3D>(d->engine).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 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<Schema_Query>(&input)) {
query->handler(self->d->descriptor().schema());
continue;
}
if (auto* write = std::get_if<Prop_Write>(&input)) {
write->handler(self->d->descriptor().write_prop(write->key, write->value));
continue;
}
const auto value = std::get<Plot_Event>(input);
if (auto* engine_2d = std::get_if<Plot_2D>(&self->d->engine)) {
const Size viewport{static_cast<int>(std::clamp(value.width, 160U, 1920U)), static_cast<int>(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<Plot_3D>(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();
}
}
}, [](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<void>(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<std::string, std::shared_ptr<Plot>> 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<Private>(std::move(executor))) {}
Plot_Registry::~Plot_Registry() = default;
std::shared_ptr<Plot> 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<Plot>(new Plot(std::make_unique<Plot::Private>(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; }
} }
+39 -9
View File
@@ -1,18 +1,48 @@
#pragma once #pragma once
#include "Graph_WebSocket.hpp" #include <asio/any_io_executor.hpp>
#include <nlohmann/json_fwd.hpp>
#include <cstdint>
#include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
#include <string_view>
#include <vector>
namespace aethera::web { namespace aethera::web {
class Plot { struct Plot_Event {
double time_milliseconds{}; /* 浏览器 performance.now() 时钟值。 */
std::uint32_t width{720}; /* 目标帧像素宽度。 */
std::uint32_t height{420}; /* 目标帧像素高度。 */
};
class Plot final : public std::enable_shared_from_this<Plot> {
public: public:
Plot(std::string key, std::shared_ptr<Graph_WebSocket> websocket); using Frame_Handler = std::function<void(std::string)>;
virtual ~Plot(); using Json_Handler = std::function<void(nlohmann::json)>;
~Plot();
Plot(const Plot&) = delete; Plot(const Plot&) = delete;
Plot& operator=(const Plot&) = delete; Plot& operator=(const Plot&) = delete;
protected: void attach(const void* owner, Frame_Handler handler);
virtual void on_event(std::string_view event) = 0; void detach(const void* owner);
void send_frame(std::string_view frame); void submit(Plot_Event event);
std::shared_ptr<Graph_WebSocket> websocket; void async_schema(Json_Handler handler);
std::string key; void async_write_prop(std::string key, nlohmann::json value, Json_Handler handler);
private:
struct Private;
explicit Plot(std::unique_ptr<Private> private_data);
void start();
friend class Plot_Registry;
std::unique_ptr<Private> d; /* 引擎对象、协议 adapter、事件队列和帧订阅的唯一所有者。 */
};
class Plot_Registry final {
public:
explicit Plot_Registry(asio::any_io_executor executor);
~Plot_Registry();
[[nodiscard]] std::shared_ptr<Plot> acquire(std::string_view plot_id);
[[nodiscard]] nlohmann::json catalog() const;
private:
struct Private;
std::unique_ptr<Private> d; /* Plot id 到长生命周期引擎桥接对象的注册表。 */
}; };
} }
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <nlohmann/json_fwd.hpp>
#include <structive/property/property.hpp>
#include <memory>
#include <string_view>
namespace aethera::web::detail {
enum class Renderable_Field_Role {
prop,
state
};
template <Renderable_Field_Role Role>
struct Renderable_Field_Role_Attribute;
template <auto Member, structive::Fixed_String Key>
struct Prop_Field;
template <typename Tag, auto Member, structive::Fixed_String Key>
struct State_Field;
template <typename Object, typename... Fields>
class Renderable_Adapter;
class Renderable_Descriptor {
public:
virtual ~Renderable_Descriptor() = default;
[[nodiscard]] virtual nlohmann::json schema() const = 0;
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) = 0;
};
template <typename Adapter>
class Renderable_Descriptor_Model final : public Renderable_Descriptor {
public:
explicit Renderable_Descriptor_Model(Adapter adapter);
[[nodiscard]] nlohmann::json schema() const override;
[[nodiscard]] nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override;
private:
Adapter adapter; /* 与 Plot 内真实 Renderable 同生共死的无所有权协议视图。 */
};
template <typename Adapter>
[[nodiscard]] std::unique_ptr<Renderable_Descriptor> make_renderable_descriptor(Adapter adapter);
}
#include "Renderable_Adapter.ipp"
+340
View File
@@ -0,0 +1,340 @@
#pragma once
#include <adminive/adapters/boost_pfr.hpp>
#include <adminive/adapters/magic_enum.hpp>
#include <adminive/adapters/nlohmann_json.hpp>
#include <adminive/json.hpp>
#include <boost/pfr.hpp>
#include <render_2D/plottable/Plottables.hpp>
#include <render_3D/Render_3D.hpp>
#include <array>
#include <type_traits>
#include <utility>
#include <vector>
namespace aethera::web::detail {
template <typename Object, typename Members>
struct Json_Member_Reflection_Adapter {
static constexpr std::size_t field_count = boost::pfr::tuple_size_v<Members>;
template <std::size_t Index> static decltype(auto) get(Object& value) noexcept;
template <std::size_t Index> static decltype(auto) get(const Object& value) noexcept;
template <std::size_t Index> static constexpr std::string_view name() noexcept;
};
struct Color_Map_Json_Members {
decltype(&render_2d::Color_Map::stops) stops{&render_2d::Color_Map::stops}; /* 唯一需要显式适配的非聚合值字段。 */
};
}
namespace adminive {
template <typename Value, Json_Type Json>
struct Value_Adapter<std::vector<Value>, Json> {
using value_type = std::vector<Value>;
static constexpr std::string_view type_name{"array"};
static Json encode(const value_type& values);
static void decode(value_type& target, const Json& value);
};
template <typename Value, std::size_t Size, Json_Type Json>
struct Value_Adapter<std::array<Value, Size>, Json> {
using value_type = std::array<Value, Size>;
static constexpr std::string_view type_name{"array"};
static Json encode(const value_type& values);
static void decode(value_type& target, const Json& value);
};
#define AETHERA_WEB_JSON_REFLECTION(Type) \
template <> struct Reflection_Adapter<Type> : Boost_Pfr_Reflection_Adapter<Type> {}; \
template <> struct Type_Descriptor<Type> { static auto get(); };
AETHERA_WEB_JSON_REFLECTION(aethera::Color)
AETHERA_WEB_JSON_REFLECTION(aethera::render_2d::Point_F)
AETHERA_WEB_JSON_REFLECTION(aethera::render_2d::Rect_F)
AETHERA_WEB_JSON_REFLECTION(aethera::render_2d::Axis_Range)
AETHERA_WEB_JSON_REFLECTION(aethera::render_2d::Pen)
AETHERA_WEB_JSON_REFLECTION(aethera::render_2d::Brush)
AETHERA_WEB_JSON_REFLECTION(aethera::render_2d::Font)
AETHERA_WEB_JSON_REFLECTION(aethera::render_2d::Frequency_Trace_Sample)
AETHERA_WEB_JSON_REFLECTION(aethera::render_2d::Waterfall_Row)
AETHERA_WEB_JSON_REFLECTION(aethera::render_2d::Constellation_Point)
AETHERA_WEB_JSON_REFLECTION(aethera::render_3d::Vec3)
AETHERA_WEB_JSON_REFLECTION(aethera::render_3d::Matrix4)
AETHERA_WEB_JSON_REFLECTION(aethera::render_3d::Point)
#undef AETHERA_WEB_JSON_REFLECTION
template <>
struct Reflection_Adapter<aethera::render_2d::Color_Map>
: aethera::web::detail::Json_Member_Reflection_Adapter<aethera::render_2d::Color_Map, aethera::web::detail::Color_Map_Json_Members> {};
template <>
struct Type_Descriptor<aethera::render_2d::Color_Map> { static auto get(); };
}
namespace aethera::web::detail {
struct Renderable_Field_Role_Category {};
template <Renderable_Field_Role Role>
struct Renderable_Field_Role_Attribute {
using attribute_category = Renderable_Field_Role_Category;
static constexpr bool single_valued = true;
static constexpr bool inheritable = false;
static constexpr auto value = Role;
};
template <Renderable_Field_Role Role>
inline constexpr Renderable_Field_Role_Attribute<Role> renderable_field_role{};
template <auto Member, structive::Fixed_String Key>
struct Prop_Field {
static constexpr auto member = Member;
static constexpr auto key = Key;
static constexpr auto role = Renderable_Field_Role::prop;
};
template <typename Tag, auto Member, structive::Fixed_String Key>
struct State_Field {
using tag_type = Tag;
static constexpr auto member = Member;
static constexpr auto key = Key;
static constexpr auto role = Renderable_Field_Role::state;
};
template <typename Field>
concept Prop_Field_Type = requires {
requires Field::role == Renderable_Field_Role::prop;
};
template <typename Field>
concept State_Field_Type = requires {
typename Field::tag_type;
requires Field::role == Renderable_Field_Role::state;
};
template <typename Object, typename... Fields>
class Renderable_Adapter final : public structive::Property_Object<Renderable_Adapter<Object, Fields...>, structive::No_Lock_Policy> {
public:
explicit Renderable_Adapter(Object& object);
private:
template <typename Adapter, typename Field>
friend struct Renderable_Field_Accessor;
Object* object; /* Plot 拥有且析构晚于本 adapter 的真实引擎对象。 */
};
template <typename Adapter, typename Field>
struct Renderable_Field_Accessor;
template <typename Object, typename... Fields, auto Member, structive::Fixed_String Key>
struct Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, Prop_Field<Member, Key>> {
using object_type = Renderable_Adapter<Object, Fields...>;
using value_type = typename structive::Member_Pointer_Traits<decltype(Member)>::value_type;
using storage_identity = void;
using dependency_spec = structive::No_Property_Dependencies;
static constexpr bool readable = true;
static constexpr bool writable = true;
static constexpr bool synchronized_view_read = false;
static constexpr bool trusted_object_access = true;
[[nodiscard]] value_type read(const object_type& adapter) const;
void write(object_type& adapter, value_type value) const;
};
template <typename Object, typename... Fields, typename Tag, auto Member, structive::Fixed_String Key>
struct Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, State_Field<Tag, Member, Key>> {
using object_type = Renderable_Adapter<Object, Fields...>;
using value_type = typename structive::Member_Pointer_Traits<decltype(Member)>::value_type;
using storage_identity = void;
using dependency_spec = structive::No_Property_Dependencies;
static constexpr bool readable = true;
static constexpr bool writable = false;
static constexpr bool synchronized_view_read = false;
static constexpr bool trusted_object_access = true;
[[nodiscard]] value_type read(const object_type& adapter) const;
};
template <typename Adapter, typename Field>
constexpr auto renderable_field_descriptor();
}
namespace structive {
template <typename Object, typename... Fields>
struct Type_Descriptor<aethera::web::detail::Renderable_Adapter<Object, Fields...>> {
static auto get();
};
}
namespace aethera::web::detail {
template <typename Object, typename... Fields>
Renderable_Adapter<Object, Fields...>::Renderable_Adapter(Object& value) : object(&value) {}
template <typename Object, typename... Fields, auto Member, structive::Fixed_String Key>
auto Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, Prop_Field<Member, Key>>::read(const object_type& adapter) const -> value_type {
return adapter.object->template get<Member>();
}
template <typename Object, typename... Fields, auto Member, structive::Fixed_String Key>
void Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, Prop_Field<Member, Key>>::write(object_type& adapter, value_type value) const {
adapter.object->template set<Member>(std::move(value));
}
template <typename Object, typename... Fields, typename Tag, auto Member, structive::Fixed_String Key>
auto Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, State_Field<Tag, Member, Key>>::read(const object_type& adapter) const -> value_type {
value_type result{};
adapter.object->template access_state<Tag>([&](const auto& state) { result = state.*Member; });
return result;
}
template <typename Adapter, typename Field>
constexpr auto renderable_field_descriptor() {
using Accessor = Renderable_Field_Accessor<Adapter, Field>;
using Role = Renderable_Field_Role_Attribute<Field::role>;
using Key = structive::Key_Attribute<Field::key>;
return structive::Property_Descriptor<Accessor, Key, Role>{{}, {Key{}, Role{}}};
}
}
namespace structive {
template <typename Object, typename... Fields>
auto Type_Descriptor<aethera::web::detail::Renderable_Adapter<Object, Fields...>>::get() {
using Adapter = aethera::web::detail::Renderable_Adapter<Object, Fields...>;
return object<Adapter>(synchronization(sync_all_unsynchronized), aethera::web::detail::renderable_field_descriptor<Adapter, Fields>()...);
}
}
namespace aethera::web::detail {
template <typename Value>
[[nodiscard]] std::string_view json_value_type() {
using Type = std::remove_cvref_t<Value>;
if constexpr (std::same_as<Type, bool>) return "boolean";
if constexpr (std::integral<Type>) return "integer";
if constexpr (std::floating_point<Type>) return "number";
if constexpr (std::is_enum_v<Type>) return "select";
if constexpr (requires { typename Type::value_type; }) return "array";
return "object";
}
template <typename Adapter>
[[nodiscard]] nlohmann::json adapter_schema(const Adapter& adapter) {
const auto& schema = structive::type_descriptor<Adapter>();
nlohmann::json prop = nlohmann::json::array();
nlohmann::json state = nlohmann::json::array();
schema.for_each_property([&](auto, const auto& field) {
using Field = std::remove_cvref_t<decltype(field)>;
using Value = typename Field::value_type;
const auto role = field.template attribute<Renderable_Field_Role_Category>().value;
nlohmann::json item{{"key", field.key()}, {"type", json_value_type<Value>()}};
const auto value = field.accessor.read(adapter);
item["value"] = adminive::encode_json_value<nlohmann::json>(value);
if constexpr (std::is_enum_v<Value>) {
item["options"] = nlohmann::json::array();
for (const auto enum_value : adminive::enum_values<Value>()) {
const auto name = adminive::enum_name(enum_value);
item["options"].push_back({{"value", name}, {"label", name}});
}
}
(role == Renderable_Field_Role::prop ? prop : state).push_back(std::move(item));
});
return {{"prop", std::move(prop)}, {"state", std::move(state)}};
}
template <typename Adapter>
[[nodiscard]] nlohmann::json write_adapter_prop(Adapter& adapter, std::string_view key, const nlohmann::json& input) {
const auto& schema = structive::type_descriptor<Adapter>();
nlohmann::json result{{"success", false}, {"key", key}};
const bool found = structive::visit_schema_property(schema, key, [&](auto, const auto& field) {
using Field = std::remove_cvref_t<decltype(field)>;
using Value = typename Field::value_type;
if constexpr (!Field::writable) {
result["error"] = "property is read-only";
} else {
try {
Value value{};
adminive::assign_json_value<nlohmann::json>(value, input);
const auto status = adapter.runtime_write(key, typeid(Value), &value);
if (status != structive::Runtime_Access_Result::ok) {
result["error"] = "property write was rejected";
return;
}
result["success"] = true;
result["value"] = adminive::encode_json_value<nlohmann::json>(field.accessor.read(adapter));
} catch (const std::exception& error) {
result["error"] = error.what();
}
}
});
if (!found) result["error"] = "unknown property";
return result;
}
template <typename Adapter>
Renderable_Descriptor_Model<Adapter>::Renderable_Descriptor_Model(Adapter value) : adapter(std::move(value)) {}
template <typename Adapter>
nlohmann::json Renderable_Descriptor_Model<Adapter>::schema() const { return adapter_schema(adapter); }
template <typename Adapter>
nlohmann::json Renderable_Descriptor_Model<Adapter>::write_prop(std::string_view key, const nlohmann::json& value) { return write_adapter_prop(adapter, key, value); }
template <typename Adapter>
std::unique_ptr<Renderable_Descriptor> make_renderable_descriptor(Adapter adapter) {
return std::make_unique<Renderable_Descriptor_Model<Adapter>>(std::move(adapter));
}
}
namespace aethera::web::detail {
template <typename Object, typename Members>
template <std::size_t Index>
decltype(auto) Json_Member_Reflection_Adapter<Object, Members>::get(Object& value) noexcept { return value.*boost::pfr::get<Index>(Members{}); }
template <typename Object, typename Members>
template <std::size_t Index>
decltype(auto) Json_Member_Reflection_Adapter<Object, Members>::get(const Object& value) noexcept { return value.*boost::pfr::get<Index>(Members{}); }
template <typename Object, typename Members>
template <std::size_t Index>
constexpr std::string_view Json_Member_Reflection_Adapter<Object, Members>::name() noexcept { return boost::pfr::get_name<Index, Members>(); }
}
namespace adminive {
template <typename Value, Json_Type Json>
Json Value_Adapter<std::vector<Value>, Json>::encode(const value_type& values) {
Json result = json_array<Json>();
for (const auto& value : values) json_append(result, encode_json_value<Json>(value));
return result;
}
template <typename Value, Json_Type Json>
void Value_Adapter<std::vector<Value>, Json>::decode(value_type& target, const Json& value) {
if (!Json_Adapter<Json>::is_array(value)) throw std::invalid_argument("value must be an array");
value_type updated;
updated.reserve(Json_Adapter<Json>::size(value));
for (std::size_t index = 0; index < Json_Adapter<Json>::size(value); ++index) {
Value item{};
assign_json_value<Json>(item, Json_Adapter<Json>::at(value, index));
updated.push_back(std::move(item));
}
target = std::move(updated);
}
template <typename Value, std::size_t Size, Json_Type Json>
Json Value_Adapter<std::array<Value, Size>, Json>::encode(const value_type& values) {
Json result = json_array<Json>();
for (const auto& value : values) json_append(result, encode_json_value<Json>(value));
return result;
}
template <typename Value, std::size_t Size, Json_Type Json>
void Value_Adapter<std::array<Value, Size>, Json>::decode(value_type& target, const Json& value) {
if (!Json_Adapter<Json>::is_array(value) || Json_Adapter<Json>::size(value) != Size) throw std::invalid_argument("array has the wrong size");
value_type updated{};
for (std::size_t index = 0; index < Size; ++index) assign_json_value<Json>(updated[index], Json_Adapter<Json>::at(value, index));
target = std::move(updated);
}
#define AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(Type, Id, Label) \
inline auto Type_Descriptor<Type>::get() { return reflected_object_with<Type>(Id, Label, Identity_Field_Customizer{}); }
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::Color, "color", "Color")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Point_F, "point", "Point")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Rect_F, "rectangle", "Rectangle")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Axis_Range, "axis_range", "Axis range")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Pen, "pen", "Pen")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Brush, "brush", "Brush")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Font, "font", "Font")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Frequency_Trace_Sample, "frequency_trace_sample", "Frequency trace sample")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Waterfall_Row, "waterfall_row", "Waterfall row")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Constellation_Point, "constellation_point", "Constellation point")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_2d::Color_Map, "color_map", "Color map")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_3d::Vec3, "vec3", "3D vector")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_3d::Matrix4, "matrix4", "Transform")
AETHERA_WEB_DEFINE_JSON_DESCRIPTOR(aethera::render_3d::Point, "point_3d", "3D point")
#undef AETHERA_WEB_DEFINE_JSON_DESCRIPTOR
}
+6 -8
View File
@@ -1,9 +1,9 @@
#include "Web_Server.hpp" #include "Web_Server.hpp"
#include "Graph_Metadata.hpp" /* 按图请求描述数据。 */
#include "Graph_Session.hpp"
#include "Graph_WebSocket.hpp" #include "Graph_WebSocket.hpp"
#include "Plot.hpp"
#include <asio/thread_pool.hpp> #include <asio/thread_pool.hpp>
#include <drogon/drogon.h> #include <drogon/drogon.h>
#include <nlohmann/json.hpp>
#include <algorithm> #include <algorithm>
#include <functional> #include <functional>
#include <memory> #include <memory>
@@ -16,14 +16,12 @@ drogon::HttpResponsePtr error_response(drogon::HttpStatusCode status, std::strin
int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) { 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()); 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 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 registry = std::make_shared<Plot_Registry>(graph_pool->get_executor());
auto websocket = std::make_shared<Graph_WebSocket_Controller>(registry); auto websocket = std::make_shared<Graph_WebSocket_Controller>(registry);
auto& app = drogon::app(); 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("/plot", [registry](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) { callback(json_response(registry->catalog())); }, {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(graph_id)}, {"state_api", "/api/graphs/" + graph_id + "/state"}, {"prop_api", "/api/graphs/" + graph_id + "/prop"}})); }, {drogon::Get}); app.registerHandler("/plot/{1}/schema", [registry](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& 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::function<void(const drogon::HttpResponsePtr&)>>(std::move(callback)); plot->async_schema([output](nlohmann::json schema) { (*output)(json_response(std::move(schema))); }); }, {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("/plot/{1}/prop/{2}", [registry](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& 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::function<void(const drogon::HttpResponsePtr&)>>(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.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(); 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->stop();
graph_pool->join(); graph_pool->join();
+17 -2
View File
@@ -6,6 +6,21 @@
#include <iostream> #include <iostream>
#include <string_view> #include <string_view>
namespace { 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; } 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");
} }
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"); }
+118 -12
View File
@@ -1,14 +1,120 @@
import {memo, useEffect, useMemo, useRef, useState} from "react"; import {memo, useEffect, useMemo, useRef, useState} from "react";
type Graph = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; state: string; descriptor: string};
type Field = {name: string; value_type: string; editable: boolean; children?: Field[]; presentation: {label: string; description?: string; control: string; options?: Array<{value: string; label: string}>}}; type Plot = {
type Descriptor = {state: {fields: Field[]}; prop: {fields: Field[]}; state_api: string; prop_api: string}; id: string;
title: string;
category: string;
description: string;
dimension: "2D" | "3D";
websocket: string;
schema: string;
};
type Option = {value: string; label: string};
type Field = {key: string; type: "boolean" | "integer" | "number" | "select" | "array" | "object"; value: unknown; options?: Option[]};
type Schema = {prop: Field[]; state: Field[]};
const protocolHeaderSize = 24; const protocolHeaderSize = 24;
function socketUrl(path: string) { return `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${path}`; }
function drawPixels(canvas: HTMLCanvasElement, bytes: ArrayBuffer) { if (bytes.byteLength < protocolHeaderSize) return; const view = new DataView(bytes); if (view.getUint32(0, true) !== 0x41544852 || view.getUint16(4, true) !== 1) return; const width = view.getUint32(8, true); const height = view.getUint32(12, true); const pixels = new Uint8ClampedArray(bytes, protocolHeaderSize); if (pixels.byteLength !== width * height * 4) return; if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } canvas.getContext("2d", {alpha: false})?.putImageData(new ImageData(pixels, width, height), 0, 0); } function socketUrl(path: string) {
function useGraphStream(graph: Graph, canvasRef: React.RefObject<HTMLCanvasElement | null>) { const statusRef = useRef<HTMLSpanElement>(null); useEffect(() => { let stopped = false; let animation = 0; const socket = new WebSocket(socketUrl(graph.websocket)); socket.binaryType = "arraybuffer"; socket.onopen = () => { if (statusRef.current) statusRef.current.textContent = "LIVE"; }; socket.onclose = () => { if (statusRef.current) statusRef.current.textContent = "OFFLINE"; }; socket.onmessage = event => { if (event.data instanceof ArrayBuffer && canvasRef.current) drawPixels(canvasRef.current, event.data); }; const tick = (time: number) => { const canvas = canvasRef.current; if (!stopped && socket.readyState === WebSocket.OPEN && canvas) socket.send(JSON.stringify({time, width: Math.max(320, canvas.clientWidth * devicePixelRatio), height: Math.max(220, canvas.clientHeight * devicePixelRatio)})); if (!stopped) animation = requestAnimationFrame(tick); }; animation = requestAnimationFrame(tick); return () => { stopped = true; cancelAnimationFrame(animation); socket.close(); }; }, [graph.websocket, canvasRef]); return statusRef; } return `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${path}`;
function JsonControl({field, value, onChange}: {field: Field; value: unknown; onChange: (value: unknown) => void}) { const [draft, setDraft] = useState(() => JSON.stringify(value, null, 2)); const [invalid, setInvalid] = useState(false); useEffect(() => setDraft(JSON.stringify(value, null, 2)), [value]); const apply = () => { try { onChange(JSON.parse(draft)); setInvalid(false); } catch { setInvalid(true); } }; return <label className="control controlJson"><span>{field.presentation.label}</span><textarea value={draft} onChange={event => setDraft(event.target.value)} onBlur={apply}/><small className={invalid ? "error" : ""}>{invalid ? "Invalid JSON" : field.presentation.description ?? "Edit as JSON; changes apply when focus leaves the field."}</small></label>; } }
function FieldControl({field, value, onChange}: {field: Field; value: unknown; onChange: (value: unknown) => void}) { if (field.value_type === "object" || field.value_type === "array") return <JsonControl field={field} value={value} onChange={onChange}/>; const control = field.presentation.control === "automatic" ? field.value_type === "boolean" ? "boolean" : field.value_type === "number" || field.value_type === "integer" ? "number" : field.presentation.options ? "select" : "text" : field.presentation.control; if (control === "boolean") return <label className="control controlBoolean"><span>{field.presentation.label}</span><input type="checkbox" checked={Boolean(value)} onChange={event => onChange(event.target.checked)}/></label>; if (control === "select" && field.presentation.options) return <label className="control"><span>{field.presentation.label}</span><select value={String(value ?? "")} onChange={event => onChange(event.target.value)}>{field.presentation.options.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label>; return <label className="control"><span>{field.presentation.label}</span><input type={control === "number" ? "number" : "text"} value={String(value ?? "")} onChange={event => onChange(control === "number" ? Number(event.target.value) : event.target.value)}/><small>{field.presentation.description}</small></label>; }
function StateValue({value}: {value: unknown}) { if (value !== null && typeof value === "object") return <pre>{JSON.stringify(value, null, 2)}</pre>; return <code>{String(value ?? "—")}</code>; } function drawPixels(canvas: HTMLCanvasElement, bytes: ArrayBuffer) {
function InspectorSidebar({graph, onClose}: {graph: Graph; onClose: () => void}) { const [descriptor, setDescriptor] = useState<Descriptor | null>(null); const [tab, setTab] = useState<"prop" | "state">("prop"); const [state, setState] = useState<Record<string, unknown>>({}); const [prop, setProp] = useState<Record<string, unknown>>({}); const [busy, setBusy] = useState(true); useEffect(() => { let active = true; setBusy(true); void fetch(graph.descriptor).then(response => response.json()).then((value: Descriptor) => { if (!active) return; setDescriptor(value); return fetch(value.prop_api); }).then(response => response?.json()).then(value => { if (active && value) setProp(value); }).finally(() => { if (active) setBusy(false); }); return () => { active = false; }; }, [graph]); const loadState = async () => { if (!descriptor) return; setBusy(true); try { setState(await fetch(descriptor.state_api).then(response => response.json())); } finally { setBusy(false); } }; useEffect(() => { if (tab === "state") void loadState(); }, [tab, descriptor]); const update = async (name: string, value: unknown) => { const previous = prop; setProp(current => ({...current, [name]: value})); const result = await fetch(descriptor!.prop_api, {method: "PATCH", headers: {"Content-Type": "application/json"}, body: JSON.stringify({[name]: value})}).then(response => response.json()); if (result.success && result.prop) setProp(result.prop); else setProp(previous); }; const fields = descriptor?.prop.fields ?? []; return <><button className="backdrop" aria-label="Close inspector" onClick={onClose}/><aside className="sidebar" aria-label={`${graph.title} inspector`}><header><div><span className="eyebrow">{graph.category} · {graph.dimension}</span><h2>{graph.title}</h2></div><button className="close" onClick={onClose} aria-label="Close">×</button></header><div className="tabs"><button className={tab === "prop" ? "active" : ""} onClick={() => setTab("prop")}>Editable Prop <span>{fields.length}</span></button><button className={tab === "state" ? "active" : ""} onClick={() => setTab("state")}>Published State <span>{descriptor?.state.fields.length ?? 0}</span></button></div><div className="sidebarBody">{busy && !descriptor ? <p className="muted">Loading descriptor</p> : tab === "prop" ? <section className="propGrid">{fields.map(field => <FieldControl key={field.name} field={field} value={prop[field.name]} onChange={value => void update(field.name, value)}/>)}</section> : <section><div className="stateHeading"><p>Complete published State</p><button onClick={() => void loadState()}>Refresh</button></div><dl className="stateList">{descriptor?.state.fields.map(field => <div key={field.name}><dt>{field.presentation.label}</dt><dd><StateValue value={state[field.name]}/></dd></div>)}</dl></section>}</div></aside></>; } if (bytes.byteLength < protocolHeaderSize) return;
const GraphCard = memo(function GraphCard({graph, onInspect}: {graph: Graph; onInspect: (graph: Graph) => void}) { const canvasRef = useRef<HTMLCanvasElement>(null); const statusRef = useGraphStream(graph, canvasRef); return <article className="card"><header><div><span className="eyebrow">{graph.category} · {graph.dimension}</span><h2>{graph.title}</h2></div><span ref={statusRef} className="status">CONNECTING</span></header><p>{graph.description}</p><canvas ref={canvasRef}/><button className="inspect" onClick={() => onInspect(graph)}>Inspect Prop & State</button></article>; }); const view = new DataView(bytes);
export function App() { const [graphs, setGraphs] = useState<Graph[]>([]); const [category, setCategory] = useState("All"); const [selected, setSelected] = useState<Graph | null>(null); useEffect(() => { void fetch("/api/graphs").then(response => response.json()).then(setGraphs); }, []); useEffect(() => { document.body.classList.toggle("sidebarOpen", selected !== null); return () => document.body.classList.remove("sidebarOpen"); }, [selected]); const categories = useMemo(() => ["All", ...new Set(graphs.map(graph => graph.category))], [graphs]); const visible = category === "All" ? graphs : graphs.filter(graph => graph.category === category); return <><main><section className="hero"><div><span className="eyebrow">AETHERA RENDER LAB</span><h1>Live chart gallery</h1><p>Browser time drives each isolated Scene. Completed pixels return directly from the Scene callback; properties and state are fetched only when inspected.</p></div><div className="heroMetric"><strong>{graphs.length}</strong><span>live components</span></div></section><nav>{categories.map(value => <button key={value} className={category === value ? "active" : ""} onClick={() => setCategory(value)}>{value}</button>)}</nav><section className="grid">{visible.map(graph => <GraphCard key={graph.id} graph={graph} onInspect={setSelected}/>)}</section></main>{selected ? <InspectorSidebar graph={selected} onClose={() => setSelected(null)}/> : null}</>; } if (view.getUint32(0, true) !== 0x41544852 || view.getUint16(4, true) !== 1) return;
const width = view.getUint32(8, true);
const height = view.getUint32(12, true);
const pixels = new Uint8ClampedArray(bytes, protocolHeaderSize);
if (pixels.byteLength !== width * height * 4) return;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
canvas.getContext("2d", {alpha: false})?.putImageData(new ImageData(pixels, width, height), 0, 0);
}
function usePlotStream(plot: Plot, canvasRef: React.RefObject<HTMLCanvasElement | null>) {
const statusRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
let stopped = false;
let animation = 0;
const socket = new WebSocket(socketUrl(plot.websocket));
socket.binaryType = "arraybuffer";
socket.onopen = () => { if (statusRef.current) statusRef.current.textContent = "LIVE"; };
socket.onclose = () => { if (statusRef.current) statusRef.current.textContent = "OFFLINE"; };
socket.onmessage = event => { if (event.data instanceof ArrayBuffer && canvasRef.current) drawPixels(canvasRef.current, event.data); };
const tick = (time: number) => {
const canvas = canvasRef.current;
if (!stopped && socket.readyState === WebSocket.OPEN && canvas) {
socket.send(JSON.stringify({time, width: Math.max(320, canvas.clientWidth * devicePixelRatio), height: Math.max(220, canvas.clientHeight * devicePixelRatio)}));
}
if (!stopped) animation = requestAnimationFrame(tick);
};
animation = requestAnimationFrame(tick);
return () => { stopped = true; cancelAnimationFrame(animation); socket.close(); };
}, [plot.websocket, canvasRef]);
return statusRef;
}
function JsonControl({field, onChange}: {field: Field; onChange: (value: unknown) => void}) {
const [draft, setDraft] = useState(() => JSON.stringify(field.value, null, 2));
const [invalid, setInvalid] = useState(false);
useEffect(() => setDraft(JSON.stringify(field.value, null, 2)), [field.value]);
const apply = () => {
try { onChange(JSON.parse(draft)); setInvalid(false); }
catch { setInvalid(true); }
};
return <label className="control controlJson"><span>{field.key}</span><textarea value={draft} onChange={event => setDraft(event.target.value)} onBlur={apply}/><small className={invalid ? "error" : ""}>{invalid ? "Invalid JSON" : "Changes apply when focus leaves the field."}</small></label>;
}
function FieldControl({field, onChange}: {field: Field; onChange: (value: unknown) => void}) {
if (field.type === "object" || field.type === "array") return <JsonControl field={field} onChange={onChange}/>;
if (field.type === "boolean") return <label className="control controlBoolean"><span>{field.key}</span><input type="checkbox" checked={Boolean(field.value)} onChange={event => onChange(event.target.checked)}/></label>;
if (field.type === "select") return <label className="control"><span>{field.key}</span><select value={String(field.value ?? "")} onChange={event => onChange(event.target.value)}>{field.options?.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label>;
return <label className="control"><span>{field.key}</span><input type="number" value={String(field.value ?? "")} onChange={event => onChange(Number(event.target.value))}/></label>;
}
function StateValue({value}: {value: unknown}) {
if (value !== null && typeof value === "object") return <pre>{JSON.stringify(value, null, 2)}</pre>;
return <code>{String(value ?? "—")}</code>;
}
function InspectorSidebar({plot, onClose}: {plot: Plot; onClose: () => void}) {
const [schema, setSchema] = useState<Schema | null>(null);
const [tab, setTab] = useState<"prop" | "state">("prop");
const [busy, setBusy] = useState(true);
const loadSchema = async () => {
setBusy(true);
try { setSchema(await fetch(plot.schema).then(response => response.json())); }
finally { setBusy(false); }
};
useEffect(() => { void loadSchema(); }, [plot.schema]);
useEffect(() => { if (tab === "state") void loadSchema(); }, [tab]);
const update = async (field: Field, value: unknown) => {
const result = await fetch(`/plot/${encodeURIComponent(plot.id)}/prop/${encodeURIComponent(field.key)}`, {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(value)
}).then(response => response.json());
if (result.success) setSchema(current => current ? {...current, prop: current.prop.map(item => item.key === field.key ? {...item, value: result.value} : item)} : current);
};
return <><button className="backdrop" aria-label="Close inspector" onClick={onClose}/><aside className="sidebar" aria-label={`${plot.title} inspector`}><header><div><span className="eyebrow">{plot.category} · {plot.dimension}</span><h2>{plot.title}</h2></div><button className="close" onClick={onClose} aria-label="Close">×</button></header><div className="tabs"><button className={tab === "prop" ? "active" : ""} onClick={() => setTab("prop")}>Editable Prop <span>{schema?.prop.length ?? 0}</span></button><button className={tab === "state" ? "active" : ""} onClick={() => setTab("state")}>Published State <span>{schema?.state.length ?? 0}</span></button></div><div className="sidebarBody">{busy && !schema ? <p className="muted">Loading schema</p> : tab === "prop" ? <section className="propGrid">{schema?.prop.map(field => <FieldControl key={field.key} field={field} onChange={value => void update(field, value)}/>)}</section> : <section><div className="stateHeading"><p>Current published State</p><button onClick={() => void loadSchema()}>Refresh</button></div><dl className="stateList">{schema?.state.map(field => <div key={field.key}><dt>{field.key}</dt><dd><StateValue value={field.value}/></dd></div>)}</dl></section>}</div></aside></>;
}
const PlotCard = memo(function PlotCard({plot, onInspect}: {plot: Plot; onInspect: (plot: Plot) => void}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const statusRef = usePlotStream(plot, canvasRef);
return <article className="card"><header><div><span className="eyebrow">{plot.category} · {plot.dimension}</span><h2>{plot.title}</h2></div><span ref={statusRef} className="status">CONNECTING</span></header><p>{plot.description}</p><canvas ref={canvasRef}/><button className="inspect" onClick={() => onInspect(plot)}>Inspect Prop & State</button></article>;
});
export function App() {
const [plots, setPlots] = useState<Plot[]>([]);
const [category, setCategory] = useState("All");
const [selected, setSelected] = useState<Plot | null>(null);
useEffect(() => { void fetch("/plot").then(response => response.json()).then(setPlots); }, []);
useEffect(() => { document.body.classList.toggle("sidebarOpen", selected !== null); return () => document.body.classList.remove("sidebarOpen"); }, [selected]);
const categories = useMemo(() => ["All", ...new Set(plots.map(plot => plot.category))], [plots]);
const visible = category === "All" ? plots : plots.filter(plot => plot.category === category);
return <><main><section className="hero"><div><span className="eyebrow">AETHERA RENDER LAB</span><h1>Live chart gallery</h1><p>Each Plot binds a real engine Scene to its WebSocket stream and Structive-generated property schema.</p></div><div className="heroMetric"><strong>{plots.length}</strong><span>live components</span></div></section><nav>{categories.map(value => <button key={value} className={category === value ? "active" : ""} onClick={() => setCategory(value)}>{value}</button>)}</nav><section className="grid">{visible.map(plot => <PlotCard key={plot.id} plot={plot} onInspect={setSelected}/>)}</section></main>{selected ? <InspectorSidebar plot={selected} onClose={() => setSelected(null)}/> : null}</>;
}