功能比较完善的一版

This commit is contained in:
2026-08-21 17:06:38 +08:00
parent b280e2f5ee
commit ade34e18f2
77 changed files with 30093 additions and 624 deletions
+42 -15
View File
@@ -1,5 +1,6 @@
#include "Graph_WebSocket.hpp" /* 图像素流 WebSocket。 */
#include <nlohmann/json.hpp>
#include <magic_enum/magic_enum.hpp>
#include <algorithm>
#include <chrono>
#include <string>
@@ -21,26 +22,52 @@ void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_
void Graph_WebSocket::receive(std::string_view message) {
const auto json = nlohmann::json::parse(message, nullptr, false);
if (json.is_discarded() || !json.is_object()) return;
const auto kind = json.find("kind");
if (kind == json.end() || !kind->is_string()
|| (*kind != "frame" && *kind != "input")) return;
try {
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);
if (const auto value = json.find("wheel"); value != json.end() && value->is_object()) {
const auto x = value->find("x");
const auto y = value->find("y");
const auto delta_y = value->find("delta_y");
if (x != value->end() && x->is_number() && y != value->end() && y->is_number()
&& delta_y != value->end() && delta_y->is_number()) {
event.wheel = Plot_Wheel_Event{
std::clamp(x->get<double>(), 0.0, static_cast<double>(event.width)),
std::clamp(y->get<double>(), 0.0, static_cast<double>(event.height)),
std::clamp(delta_y->get<double>(), -120.0, 120.0)};
}
if (const auto viewport = json.find("viewport"); viewport != json.end() && viewport->is_object()) {
if (const auto value = viewport->find("width"); value != viewport->end() && value->is_number_unsigned())
event.width = std::clamp(value->get<std::uint32_t>(), 160U, 1920U);
if (const auto value = viewport->find("height"); value != viewport->end() && value->is_number_unsigned())
event.height = std::clamp(value->get<std::uint32_t>(), 120U, 1080U);
}
if (*kind == "input") {
const auto input = json.find("event");
if (input == json.end() || !input->is_object()) return;
const auto type = magic_enum::enum_cast<Event_Type>(input->value("type", std::string{}));
if (!type) return;
Plot_Input_Event decoded;
decoded.type = *type;
const auto read_point = [&](std::string_view key, render_2d::Point_F& point, bool viewport_relative) {
const auto value = input->find(key);
if (value == input->end() || !value->is_object()) return;
const auto x = value->value("x", 0.0);
const auto y = value->value("y", 0.0);
point.x = viewport_relative ? std::clamp(x, 0.0, static_cast<double>(event.width)) : x;
point.y = viewport_relative ? std::clamp(y, 0.0, static_cast<double>(event.height)) : y;
};
read_point("position", decoded.position, true);
read_point("global_position", decoded.global_position, false);
decoded.button = magic_enum::enum_cast<Mouse_Button>(input->value("button", std::string{"none"})).value_or(Mouse_Button::none);
decoded.buttons = static_cast<Mouse_Button_Mask>(std::clamp(input->value("buttons", 0), 0, 255));
decoded.modifiers = static_cast<Keyboard_Modifier>(std::clamp(input->value("modifiers", 0), 0, 15));
decoded.pixel_delta_x = std::clamp(input->value("pixel_delta_x", 0.0), -4096.0, 4096.0);
decoded.pixel_delta_y = std::clamp(input->value("pixel_delta_y", 0.0), -4096.0, 4096.0);
decoded.angle_delta_x = std::clamp(input->value("angle_delta_x", 0.0), -120.0, 120.0);
decoded.angle_delta_y = std::clamp(input->value("angle_delta_y", 0.0), -120.0, 120.0);
decoded.key = magic_enum::enum_cast<Key>(input->value("key", std::string{"unknown"})).value_or(Key::unknown);
decoded.native_key = input->value("native_key", 0U);
decoded.auto_repeat = input->value("auto_repeat", false);
event.input = decoded;
}
d->plot->submit(event);
} catch (const nlohmann::json::exception&) {
return;
}
}
void Graph_WebSocket::close() { if (!d->attached) return; d->plot->detach(d->owner); d->attached = false; }
+246 -47
View File
@@ -77,6 +77,7 @@ std::string encode_frame(const Pixel_Frame& frame, std::uint64_t sequence) {
struct Schema_Query { Plot::Json_Handler handler; };
struct Prop_Write {
std::string component;
std::string key;
nlohmann::json value;
Plot::Json_Handler handler;
@@ -86,21 +87,29 @@ using Plot_Input = std::variant<Plot_Event, Schema_Query, Prop_Write>;
template <typename... Owned_Objects>
class Scene_View_Model final : public Plot::Scene_View {
public:
Scene_View_Model(std::unique_ptr<detail::Renderable_Descriptor> value_descriptor,
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
std::function<void(const Plot_Event&)> value_update,
Owned_Objects... owned_objects)
: descriptor(std::move(value_descriptor)),
: descriptors(std::move(value_descriptors)),
update_scene(std::move(value_update)),
objects(std::move(owned_objects)...) {}
nlohmann::json schema() const override { return descriptor->schema(); }
nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override {
return descriptor->write_prop(key, value);
nlohmann::json schema() const override {
nlohmann::json components = nlohmann::json::array();
for (const auto& descriptor : descriptors) components.push_back(descriptor->schema());
return {{"protocol", "aethera.plot.inspector"}, {"version", 1}, {"components", std::move(components)}};
}
nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override {
const auto found = std::ranges::find_if(descriptors, [&](const auto& item) { return item->id() == component; });
if (found == descriptors.end()) return {{"success", false}, {"error", "unknown component"}};
auto result = (*found)->write_prop(key, value);
result["component"] = component;
return result;
}
void update(const Plot_Event& event) override { update_scene(event); }
private:
std::unique_ptr<detail::Renderable_Descriptor> descriptor;
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
std::function<void(const Plot_Event&)> update_scene;
std::tuple<Owned_Objects...> objects;
};
@@ -111,11 +120,9 @@ using Prop_Field = detail::Prop_Field<Member, Key, Description>;
template <typename Definition, auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
using State_Field = detail::State_Field<typename Definition::Base_Tag, Member, Key, Description>;
template <typename... Fields, typename Object, typename... Owned_Objects>
std::unique_ptr<Plot::Scene_View> make_scene_view(
Object& object,
std::function<void(const Plot_Event&)> update,
Owned_Objects&&... owned_objects) {
template <typename Object, typename... Fields>
std::unique_ptr<detail::Renderable_Descriptor> make_renderable_component(
std::string id, std::string label, std::string kind, Object& object) {
using Definition = typename Object::Attached_Object;
using Tag = typename Definition::Base_Tag;
using State = typename Definition::State;
@@ -130,8 +137,134 @@ std::unique_ptr<Plot::Scene_View> make_scene_view(
detail::State_Field<Tag, &State::paint_task_count, "paint_task_count", "Number of tasks in the current paint execution graph.">,
detail::State_Field<Tag, &State::prepare_execution_time_ns, "prepare_execution_time_ns", "Measured prepare-stage execution time in nanoseconds.">,
detail::State_Field<Tag, &State::paint_execution_time_ns, "paint_execution_time_ns", "Measured paint-stage execution time in nanoseconds.">>;
return detail::make_renderable_descriptor(std::move(id), std::move(label), std::move(kind), Adapter{object});
}
template <typename Scene_Object>
std::unique_ptr<detail::Renderable_Descriptor> make_scene_component(Scene_Object& scene) {
using Definition = typename Scene_Object::Attached_Object;
using Prop = typename Definition::Prop;
using Adapter = detail::Renderable_Adapter<Scene_Object,
detail::Prop_Field<&Prop::background, "background", "Scene clear color.">,
detail::Prop_Field<&Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_rebuilt, "taskflow_rebuilt", "Whether the scene task graph was rebuilt.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_dependency_count, "taskflow_dependency_count", "Number of graph dependencies.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
return detail::make_renderable_descriptor("scene", "Scene", "scene", Adapter{scene});
}
template <>
std::unique_ptr<detail::Renderable_Descriptor> make_scene_component<Scene_3D>(Scene_3D& scene) {
using Adapter = detail::Renderable_Adapter<Scene_3D,
detail::Prop_Field<&Render_Scene_3D::Prop::clear_color, "clear_color", "Linear scene clear color.">,
detail::Prop_Field<&Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_rebuilt, "taskflow_rebuilt", "Whether the scene task graph was rebuilt.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_dependency_count, "taskflow_dependency_count", "Number of graph dependencies.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
return detail::make_renderable_descriptor("scene", "Scene", "scene", Adapter{scene});
}
template <typename Axis_Object>
std::unique_ptr<detail::Renderable_Descriptor> make_axis_component(
std::string id, std::string label, Axis_Object& axis) {
using Adapter = detail::Renderable_Adapter<Axis_Object,
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
Prop_Field<&Abs_Axis::Prop::canvas_size, "canvas_size", "Axis canvas extent.">,
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
Prop_Field<&Abs_Axis::Prop::sub_tick_length, "sub_tick_length", "Minor tick length.">,
Prop_Field<&Abs_Axis::Prop::axis_pen, "axis_pen", "Axis line and tick style.">,
Prop_Field<&Abs_Axis::Prop::unit_text, "unit_text", "Axis unit label.">,
Prop_Field<&Abs_Axis::Prop::unit_text_font, "unit_text_font", "Axis label font.">,
Prop_Field<&Abs_Axis::Prop::unit_text_pen, "unit_text_pen", "Axis label foreground.">,
Prop_Field<&Abs_Axis::Prop::unit_text_background_brush, "unit_text_background_brush", "Axis label background.">,
Prop_Field<&Abs_Axis::Prop::label_rotation_degrees, "label_rotation_degrees", "Tick label rotation.">,
Prop_Field<&Numeric_Axis::Prop::coordinate_range, "coordinate_range", "Visible coordinate range.">,
Prop_Field<&Numeric_Axis::Prop::precision, "precision", "Maximum decimal precision.">,
Prop_Field<&Numeric_Axis::Prop::locale, "locale", "Numeric label locale.">,
Prop_Field<&Numeric_Axis::Prop::wheel_enabled, "wheel_enabled", "Allows wheel zoom.">,
Prop_Field<&Numeric_Axis::Prop::drag_enabled, "drag_enabled", "Allows pointer drag panning.">>;
return make_renderable_component<Axis_Object,
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
Prop_Field<&Abs_Axis::Prop::canvas_size, "canvas_size", "Axis canvas extent.">,
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
Prop_Field<&Abs_Axis::Prop::sub_tick_length, "sub_tick_length", "Minor tick length.">,
Prop_Field<&Abs_Axis::Prop::axis_pen, "axis_pen", "Axis line and tick style.">,
Prop_Field<&Abs_Axis::Prop::unit_text, "unit_text", "Axis unit label.">,
Prop_Field<&Abs_Axis::Prop::unit_text_font, "unit_text_font", "Axis label font.">,
Prop_Field<&Abs_Axis::Prop::unit_text_pen, "unit_text_pen", "Axis label foreground.">,
Prop_Field<&Abs_Axis::Prop::unit_text_background_brush, "unit_text_background_brush", "Axis label background.">,
Prop_Field<&Abs_Axis::Prop::label_rotation_degrees, "label_rotation_degrees", "Tick label rotation.">,
Prop_Field<&Numeric_Axis::Prop::coordinate_range, "coordinate_range", "Visible coordinate range.">,
Prop_Field<&Numeric_Axis::Prop::precision, "precision", "Maximum decimal precision.">,
Prop_Field<&Numeric_Axis::Prop::locale, "locale", "Numeric label locale.">,
Prop_Field<&Numeric_Axis::Prop::wheel_enabled, "wheel_enabled", "Allows wheel zoom.">,
Prop_Field<&Numeric_Axis::Prop::drag_enabled, "drag_enabled", "Allows pointer drag panning.">>(
std::move(id), std::move(label), "axis", axis);
}
template <>
std::unique_ptr<detail::Renderable_Descriptor> make_axis_component<Time_Axis_Object>(
std::string id, std::string label, Time_Axis_Object& axis) {
return make_renderable_component<Time_Axis_Object,
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
Prop_Field<&Abs_Axis::Prop::canvas_size, "canvas_size", "Axis canvas extent.">,
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
Prop_Field<&Abs_Axis::Prop::sub_tick_length, "sub_tick_length", "Minor tick length.">,
Prop_Field<&Abs_Axis::Prop::axis_pen, "axis_pen", "Axis line and tick style.">,
Prop_Field<&Abs_Axis::Prop::unit_text, "unit_text", "Axis unit label.">,
Prop_Field<&Abs_Axis::Prop::unit_text_font, "unit_text_font", "Axis label font.">,
Prop_Field<&Abs_Axis::Prop::unit_text_pen, "unit_text_pen", "Axis label foreground.">,
Prop_Field<&Abs_Axis::Prop::unit_text_background_brush, "unit_text_background_brush", "Axis label background.">,
Prop_Field<&Abs_Axis::Prop::label_rotation_degrees, "label_rotation_degrees", "Tick label rotation.">,
Prop_Field<&Time_Axis::Prop::visible_count, "visible_count", "Maximum visible time samples.">,
Prop_Field<&Time_Axis::Prop::tick_label_spacing_px, "tick_label_spacing_px", "Spacing between time labels.">,
Prop_Field<&Time_Axis::Prop::estimated_label_width_px, "estimated_label_width_px", "Estimated time label width.">,
Prop_Field<&Time_Axis::Prop::format, "format", "Time label format.">,
Prop_Field<&Time_Axis::Prop::newest_at_start, "newest_at_start", "Places the newest time at the range origin.">,
State_Field<Time_Axis, &Time_Axis::State::next_tick, "next_tick", "Next allocated time tick.">,
State_Field<Time_Axis, &Time_Axis::State::samples, "samples", "Published time sample window.">>(
std::move(id), std::move(label), "axis", axis);
}
template <typename... Fields, typename Object, typename... Owned_Objects>
std::unique_ptr<Plot::Scene_View> make_scene_view(
Object& object,
Scene_2D& scene,
std::function<void(const Plot_Event&)> update,
Owned_Objects&&... owned_objects) {
using Definition = typename Object::Attached_Object;
using Tag = typename Definition::Base_Tag;
using State = typename Definition::State;
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> components;
components.push_back(make_scene_component(scene));
components.push_back(make_renderable_component<Object, Fields...>("plot", "Plot Renderable", "renderable", object));
std::size_t axis_index{};
const auto append_owned = [&](const auto& owned) {
using Owned = std::remove_cvref_t<decltype(owned)>;
if constexpr (std::same_as<typename Owned::element_type, Frequency_Axis_Object>
|| std::same_as<typename Owned::element_type, Numeric_Axis_Object>
|| std::same_as<typename Owned::element_type, Time_Axis_Object>) {
const auto id = axis_index++ == 0 ? "axis-x" : "axis-y";
components.push_back(make_axis_component(id, id == std::string_view{"axis-x"} ? "Horizontal Axis" : "Vertical Axis", *owned));
}
};
(append_owned(owned_objects), ...);
return std::make_unique<Scene_View_Model<std::remove_cvref_t<Owned_Objects>...>>(
detail::make_renderable_descriptor(Adapter{object}),
std::move(components),
std::move(update),
std::forward<Owned_Objects>(owned_objects)...);
}
@@ -173,23 +306,69 @@ Time_Axis_Object::Builder time_axis_builder(
template <typename... Axes>
void resize_axes(Size viewport, Axes*... axes) {
const double left = std::clamp(static_cast<double>(viewport.width) * 0.10, 42.0, 68.0);
const double right = std::clamp(static_cast<double>(viewport.width) * 0.05, 18.0, 34.0);
const double top = std::clamp(static_cast<double>(viewport.height) * 0.07, 16.0, 30.0);
const double bottom = std::clamp(static_cast<double>(viewport.height) * 0.13, 36.0, 52.0);
const Point_F origin{left, static_cast<double>(viewport.height) - bottom};
const Axis_Pixel_Length horizontal_length = std::max(1.0, static_cast<double>(viewport.width) - left - right);
const Axis_Pixel_Length vertical_length = -std::max(1.0, static_cast<double>(viewport.height) - top - bottom);
const auto resize_axis = [&](auto* axis) {
const auto orientation = axis->template read_prop<Abs_Axis::Base_Tag>().orientation;
const auto layout = axis->template read_prop<Abs_Axis::Base_Tag>();
if (layout.canvas_size == viewport) return;
const double horizontal_scale = layout.canvas_size.width > 0
? static_cast<double>(viewport.width) / layout.canvas_size.width : 1.0;
const double vertical_scale = layout.canvas_size.height > 0
? static_cast<double>(viewport.height) / layout.canvas_size.height : 1.0;
axis->template set<&Abs_Axis::Prop::canvas_size>(viewport);
axis->template set<&Abs_Axis::Prop::position>(origin);
axis->template set<&Abs_Axis::Prop::pixel_length>(
orientation == Axis_Orientation::horizontal ? horizontal_length : vertical_length);
axis->template set<&Abs_Axis::Prop::position>(Point_F{
layout.position.x * horizontal_scale,
layout.position.y * vertical_scale});
axis->template set<&Abs_Axis::Prop::pixel_length>(layout.pixel_length *
(layout.orientation == Axis_Orientation::horizontal ? horizontal_scale : vertical_scale));
};
(resize_axis(axes), ...);
}
template <typename Scene_Object>
void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
const auto apply_pointer = [&](auto& event) {
event.position = input.position;
event.global_position = input.global_position;
event.button = input.button;
event.buttons = input.buttons;
event.modifiers = input.modifiers;
};
switch (input.type) {
case Event_Type::pointer_move:
case Event_Type::pointer_press:
case Event_Type::pointer_release: {
Basic_Pointer_Event<Point_F> event(input.type);
apply_pointer(event);
static_cast<void>(scene.dispatch_event(event));
break;
}
case Event_Type::wheel: {
Basic_Wheel_Event<Point_F> event;
apply_pointer(event);
event.pixel_delta_x = input.pixel_delta_x;
event.pixel_delta_y = input.pixel_delta_y;
event.angle_delta_x = input.angle_delta_x;
event.angle_delta_y = input.angle_delta_y;
static_cast<void>(scene.dispatch_event(event));
break;
}
case Event_Type::key_press:
case Event_Type::key_release: {
Key_Event event(input.type);
event.key = input.key;
event.native_key = input.native_key;
event.modifiers = input.modifiers;
event.auto_repeat = input.auto_repeat;
static_cast<void>(scene.dispatch_event(event));
break;
}
default: {
Event event(input.type);
static_cast<void>(scene.dispatch_event(event));
break;
}
}
}
}
std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
@@ -209,6 +388,7 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
auto scene = *scene_builder.build();
auto update = [raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (event.input) return;
std::array<double, 256> samples{};
for (std::size_t i = 0; i < samples.size(); ++i) {
const double x = static_cast<double>(i) / samples.size();
@@ -246,7 +426,7 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
State_Field<Spectrum, &Spectrum::State::sample_count, "sample_count", "Number of input spectrum samples available in the latest update.">,
State_Field<Spectrum, &Spectrum::State::rendered_point_count, "rendered_point_count", "Number of curve points emitted by the latest render preparation.">,
State_Field<Spectrum, &Spectrum::State::selectable_marker_count, "selectable_marker_count", "Number of markers currently eligible for selection.">>(
*spectrum, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum));
*spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
@@ -263,9 +443,13 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
.set(&Render_Scene_2D::Prop::view_active, true);
scene_builder.add_renderable(trace.get());
auto scene = *scene_builder.build();
auto update = [raw = trace.get(), time = time.get(), vertical = vertical.get(), tick = std::uint64_t{}](const Plot_Event& event) mutable {
auto update = [raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Event& event) {
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
raw->append_sample(tick++, std::sin(event.time_milliseconds * 0.0025) * 0.8
if (event.input) return;
constexpr double day_milliseconds = 86'400'000.0;
raw->append_sample(Time_Of_Day{static_cast<std::int64_t>(
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))},
std::sin(event.time_milliseconds * 0.0025) * 0.8
+ std::sin(event.time_milliseconds * 0.0007) * 0.2);
};
auto view = make_scene_view<
@@ -275,7 +459,7 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
Prop_Field<&Frequency_Trace::Prop::samples, "samples", "Complete time-ordered collection of frequency trace samples.">,
State_Field<Frequency_Trace, &Frequency_Trace::State::sample_count, "sample_count", "Number of samples retained by the current trace.">,
State_Field<Frequency_Trace, &Frequency_Trace::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the latest trace frame.">>(
*trace, std::move(update), std::move(time), std::move(vertical), std::move(trace));
*trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
@@ -285,7 +469,9 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
auto vertical = *numeric_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build();
Impl<Sweep_Spectrum>::Builder sweep_builder(frequency.get(), vertical.get());
sweep_builder.set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0});
sweep_builder.set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0})
.set(&Sweep_Spectrum::Prop::bins_per_block, std::size_t{8})
.set(&Sweep_Spectrum::Prop::block_count, std::size_t{64});
auto sweep = *sweep_builder.build();
Scene_2D::Builder scene_builder;
scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas)
@@ -293,12 +479,23 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
.set(&Render_Scene_2D::Prop::view_active, true);
scene_builder.add_renderable(sweep.get());
auto scene = *scene_builder.build();
auto update = [raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
auto update = [raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get(), block_index = std::size_t{}](const Plot_Event& event) mutable {
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
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 + event.time_milliseconds * 0.002);
if (event.input) return;
const auto& state = raw->template read_prop<Sweep_Spectrum::Base_Tag>();
const std::size_t block_count = std::max<std::size_t>(1, state.block_count);
const std::size_t bins_per_block = std::max<std::size_t>(1, state.bins_per_block);
if (block_index >= block_count) {
raw->template set<&Sweep_Spectrum::Prop::blocks>(std::vector<std::vector<Plot_Value>>{});
block_index = 0;
}
std::vector<double> values(bins_per_block);
for (std::size_t i = 0; i < values.size(); ++i) {
const auto sweep_index = block_index * values.size() + i;
values[i] = -90.0 + 35.0 * std::sin(sweep_index * 0.08 + event.time_milliseconds * 0.002);
}
raw->append_block(values);
++block_index;
};
auto view = make_scene_view<
Prop_Field<&Sweep_Spectrum::Prop::bins_per_block, "bins_per_block", "Number of frequency bins stored in each incoming sweep block.">,
@@ -314,7 +511,7 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_block_count, "stored_block_count", "Number of sweep blocks currently retained.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_point_count, "stored_point_count", "Total number of frequency points retained across all blocks.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the latest sweep frame.">>(
*sweep, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep));
*sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
@@ -336,6 +533,7 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
auto scene = *scene_builder.build();
auto update = [raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (event.input) return;
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(
@@ -357,7 +555,7 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
State_Field<Afterglow, &Afterglow::State::history_count, "history_count", "Number of spectrum frames retained in afterglow history.">,
State_Field<Afterglow, &Afterglow::State::latest_spectrum_point_count, "latest_spectrum_point_count", "Number of samples in the most recently appended spectrum.">,
State_Field<Afterglow, &Afterglow::State::rendered_cell_count, "rendered_cell_count", "Number of colored cells emitted for the latest frame.">>(
*afterglow, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow));
*afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
@@ -378,6 +576,7 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
auto scene = *scene_builder.build();
auto update = [raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Event& event) {
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
if (event.input) return;
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(
@@ -404,7 +603,7 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
State_Field<Waterfall, &Waterfall::State::row_count, "row_count", "Number of waterfall rows currently retained.">,
State_Field<Waterfall, &Waterfall::State::stored_point_count, "stored_point_count", "Total number of spectrum points retained across all rows.">,
State_Field<Waterfall, &Waterfall::State::rendered_cell_count, "rendered_cell_count", "Number of raster cells emitted for the latest frame.">>(
*waterfall, std::move(update), std::move(frequency), std::move(time), std::move(waterfall));
*waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
@@ -426,6 +625,7 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
auto scene = *scene_builder.build();
auto update = [raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
if (event.input) return;
const auto& state = raw->template read_prop<Constellation_Diagram::Base_Tag>();
const int anchor_count = static_cast<int>(state.type);
const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4;
@@ -451,7 +651,7 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
Prop_Field<&Constellation_Diagram::Prop::anchor_color, "anchor_color", "Color used to render ideal modulation anchors.">,
Prop_Field<&Constellation_Diagram::Prop::points, "points", "Current time-stamped collection of received I/Q samples.">,
State_Field<Constellation_Diagram, &Constellation_Diagram::State::point_count, "point_count", "Number of constellation samples currently retained.">>(
*constellation, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation));
*constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
@@ -481,7 +681,7 @@ std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor
&Selection_Rectangle_Overlay::State::selected_region_count,
"selected_region_count",
"Number of rectangular regions currently selected.">>(
*selection, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection));
*selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
@@ -556,7 +756,7 @@ void Plot::ensure_started() {
continue;
}
if (auto* write = std::get_if<Prop_Write>(&input)) {
write->handler(self->d->view->write_prop(write->key, write->value));
write->handler(self->d->view->write_prop(write->component, write->key, write->value));
continue;
}
auto event = std::get<Plot_Event>(input);
@@ -566,16 +766,12 @@ void Plot::ensure_started() {
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&self->d->scene)) {
(*scene)->set<&Render_Scene_2D::Prop::viewport>(
Size{static_cast<int>(event.width), static_cast<int>(event.height)});
if (event.wheel) {
Wheel_Event wheel;
wheel.position = {event.wheel->position_x, event.wheel->position_y};
wheel.angle_delta_y = event.wheel->delta_y;
(*scene)->dispatch_event(wheel);
}
if (event.input) dispatch_plot_input(**scene, *event.input);
(*scene)->render();
} else {
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(self->d->scene);
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{event.width, event.height});
if (event.input) dispatch_plot_input(*scene_3d, *event.input);
scene_3d->render();
}
}
@@ -610,10 +806,10 @@ void Plot::async_schema(Json_Handler handler) {
throw std::runtime_error("plot input queue is unavailable");
}
void Plot::async_write_prop(std::string key, nlohmann::json value, Json_Handler handler) {
void Plot::async_write_prop(std::string component, std::string key, nlohmann::json value, Json_Handler handler) {
ensure_started();
if (!d->inputs.try_send(asio::error_code{}, Plot_Input{
Prop_Write{std::move(key), std::move(value), std::move(handler)}}))
Prop_Write{std::move(component), std::move(key), std::move(value), std::move(handler)}}))
throw std::runtime_error("plot input queue is unavailable");
}
@@ -637,8 +833,11 @@ std::shared_ptr<Plot> make_datoviz_point_plot(asio::any_io_executor executor) {
detail::State_Field<Point_Visual::Base_Tag, &Point_Visual::State::item_count, "item_count", "Number of point items currently published by the visual.">,
detail::State_Field<Point_Visual::Base_Tag, &Point_Visual::State::prepared_item_count, "prepared_item_count", "Number of point items prepared for the latest GPU submission.">,
detail::State_Field<Point_Visual::Base_Tag, &Point_Visual::State::prepared_revision, "prepared_revision", "Property revision represented by the currently prepared GPU data.">>;
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> components;
components.push_back(make_scene_component(*scene));
components.push_back(detail::make_renderable_descriptor("plot", "Point Visual", "visual", Adapter{*visual}));
auto view = std::make_unique<Scene_View_Model<decltype(visual)>>(
detail::make_renderable_descriptor(Adapter{*visual}),
std::move(components),
[](const Plot_Event&) {}, std::move(visual));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
+17 -7
View File
@@ -10,16 +10,26 @@
#include <string>
#include <string_view>
namespace aethera::web {
struct Plot_Wheel_Event {
double position_x{}; /* Pointer position in the rendered viewport, in physical pixels. */
double position_y{}; /* Pointer position in the rendered viewport, in physical pixels. */
double delta_y{}; /* Normalized vertical wheel angle delta. */
struct Plot_Input_Event {
Event_Type type{Event_Type::pointer_move};
render_2d::Point_F position{};
render_2d::Point_F global_position{};
Mouse_Button button{Mouse_Button::none};
Mouse_Button_Mask buttons{};
Keyboard_Modifier modifiers{Keyboard_Modifier::none};
double pixel_delta_x{};
double pixel_delta_y{};
double angle_delta_x{};
double angle_delta_y{};
Key key{Key::unknown};
std::uint32_t native_key{};
bool auto_repeat{};
};
struct Plot_Event {
double time_milliseconds{};
std::uint32_t width{720};
std::uint32_t height{420};
std::optional<Plot_Wheel_Event> wheel{}; /* Canvas interaction attached to this render request. */
std::optional<Plot_Input_Event> input{};
};
class Plot final : public std::enable_shared_from_this<Plot> {
public:
@@ -29,7 +39,7 @@ public:
public:
virtual ~Scene_View() = default;
[[nodiscard]] virtual nlohmann::json schema() const = 0;
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view key,
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view component, std::string_view key,
const nlohmann::json& value) = 0;
virtual void update(const Plot_Event& event) = 0;
};
@@ -46,7 +56,7 @@ public:
void detach(const void* owner);
void submit(Plot_Event event);
void async_schema(Json_Handler handler);
void async_write_prop(std::string key, nlohmann::json value, Json_Handler handler);
void async_write_prop(std::string component, std::string key, nlohmann::json value, Json_Handler handler);
private:
struct Private;
void ensure_started();
+9 -2
View File
@@ -2,6 +2,7 @@
#include <nlohmann/json_fwd.hpp>
#include <structive/property/property.hpp>
#include <memory>
#include <string>
#include <string_view>
namespace aethera::web::detail {
@@ -25,6 +26,7 @@ class Renderable_Adapter;
class Renderable_Descriptor {
public:
virtual ~Renderable_Descriptor() = default;
[[nodiscard]] virtual std::string_view id() const noexcept = 0;
[[nodiscard]] virtual nlohmann::json schema() const = 0;
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) = 0;
};
@@ -32,15 +34,20 @@ public:
template <typename Adapter>
class Renderable_Descriptor_Model final : public Renderable_Descriptor {
public:
explicit Renderable_Descriptor_Model(Adapter adapter);
Renderable_Descriptor_Model(std::string id, std::string label, std::string kind, Adapter adapter);
[[nodiscard]] std::string_view id() const noexcept override;
[[nodiscard]] nlohmann::json schema() const override;
[[nodiscard]] nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override;
private:
std::string component_id;
std::string component_label;
std::string component_kind;
Adapter adapter; /* 与 Plot 内真实 Renderable 同生共死的无所有权协议视图。 */
};
template <typename Adapter>
[[nodiscard]] std::unique_ptr<Renderable_Descriptor> make_renderable_descriptor(Adapter adapter);
[[nodiscard]] std::unique_ptr<Renderable_Descriptor> make_renderable_descriptor(
std::string id, std::string label, std::string kind, Adapter adapter);
}
#include "Renderable_Adapter.ipp"
+169 -248
View File
@@ -1,80 +1,20 @@
#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 <magic_enum/magic_enum.hpp>
#include <nlohmann/json.hpp>
#include <render_2D/plottable/Plottables.hpp>
#include <render_3D/Render_3D.hpp>
#include <array>
#include <deque>
#include <limits>
#include <ranges>
#include <stdexcept>
#include <string>
#include <tuple>
#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);
};
template <> struct Reflection_Adapter<aethera::Color> : Boost_Pfr_Reflection_Adapter<aethera::Color> {};
template <> struct Type_Descriptor<aethera::Color> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_2d::Point_F> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Point_F> {};
template <> struct Type_Descriptor<aethera::render_2d::Point_F> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_2d::Rect_F> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Rect_F> {};
template <> struct Type_Descriptor<aethera::render_2d::Rect_F> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_2d::Axis_Range> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Axis_Range> {};
template <> struct Type_Descriptor<aethera::render_2d::Axis_Range> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_2d::Pen> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Pen> {};
template <> struct Type_Descriptor<aethera::render_2d::Pen> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_2d::Brush> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Brush> {};
template <> struct Type_Descriptor<aethera::render_2d::Brush> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_2d::Font> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Font> {};
template <> struct Type_Descriptor<aethera::render_2d::Font> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_2d::Frequency_Trace_Sample> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Frequency_Trace_Sample> {};
template <> struct Type_Descriptor<aethera::render_2d::Frequency_Trace_Sample> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_2d::Waterfall_Row> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Waterfall_Row> {};
template <> struct Type_Descriptor<aethera::render_2d::Waterfall_Row> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_2d::Constellation_Point> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Constellation_Point> {};
template <> struct Type_Descriptor<aethera::render_2d::Constellation_Point> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_3d::Vec3> : Boost_Pfr_Reflection_Adapter<aethera::render_3d::Vec3> {};
template <> struct Type_Descriptor<aethera::render_3d::Vec3> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_3d::Matrix4> : Boost_Pfr_Reflection_Adapter<aethera::render_3d::Matrix4> {};
template <> struct Type_Descriptor<aethera::render_3d::Matrix4> { static auto get(); };
template <> struct Reflection_Adapter<aethera::render_3d::Point> : Boost_Pfr_Reflection_Adapter<aethera::render_3d::Point> {};
template <> struct Type_Descriptor<aethera::render_3d::Point> { static auto get(); };
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 {};
@@ -86,9 +26,6 @@ struct Renderable_Field_Role_Attribute {
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, structive::Fixed_String Description>
struct Prop_Field {
static constexpr auto member = Member;
@@ -106,25 +43,13 @@ struct State_Field {
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);
explicit Renderable_Adapter(Object& value) : object(&value) {}
private:
template <typename Adapter, typename Field>
friend struct Renderable_Field_Accessor;
Object* object; /* Plot 拥有且析构晚于本 adapter 的真实引擎对象。 */
template <typename Adapter, typename Field> friend struct Renderable_Field_Accessor;
Object* object;
};
template <typename Adapter, typename Field>
@@ -141,8 +66,8 @@ struct Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, Prop_Fie
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;
[[nodiscard]] value_type read(const object_type& adapter) const { return adapter.object->template get<Member>(); }
void 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, structive::Fixed_String Description>
@@ -156,41 +81,13 @@ struct Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, State_Fi
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;
[[nodiscard]] value_type read(const object_type& adapter) const {
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();
}
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, structive::Fixed_String Description>
auto Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, Prop_Field<Member, Key, Description>>::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, structive::Fixed_String Description>
void Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, Prop_Field<Member, Key, Description>>::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, structive::Fixed_String Description>
auto Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, State_Field<Tag, Member, Key, Description>>::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>;
@@ -202,54 +99,163 @@ constexpr auto renderable_field_descriptor() {
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>()...);
}
struct Type_Descriptor<aethera::web::detail::Renderable_Adapter<Object, Fields...>> {
static auto 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 T> struct is_std_array : std::false_type {};
template <typename T, std::size_t Size> struct is_std_array<std::array<T, Size>> : std::true_type {};
template <typename T> inline constexpr bool is_std_array_v = is_std_array<std::remove_cvref_t<T>>::value;
template <typename T> struct is_pair : std::false_type {};
template <typename A, typename B> struct is_pair<std::pair<A, B>> : std::true_type {};
template <typename T> inline constexpr bool is_pair_v = is_pair<std::remove_cvref_t<T>>::value;
template <typename T>
concept Json_Sequence = std::ranges::range<T> && requires(T value, typename T::value_type item) {
value.clear();
value.emplace_back(std::move(item));
} && !std::same_as<std::remove_cvref_t<T>, std::string>;
template <typename Value> nlohmann::json encode_protocol_value(const Value& value);
template <typename Value> void decode_protocol_value(Value& target, const nlohmann::json& input);
template <typename Value, std::size_t... Index>
nlohmann::json encode_aggregate(const Value& value, std::index_sequence<Index...>) {
nlohmann::json result = nlohmann::json::object();
((result[std::string(boost::pfr::get_name<Index, Value>())] =
encode_protocol_value(boost::pfr::get<Index>(value))), ...);
return result;
}
template <typename Value, std::size_t... Index>
void decode_aggregate(Value& target, const nlohmann::json& input, std::index_sequence<Index...>) {
(decode_protocol_value(boost::pfr::get<Index>(target),
input.at(std::string(boost::pfr::get_name<Index, Value>()))), ...);
}
template <typename Value>
[[nodiscard]] std::string_view json_value_type() {
nlohmann::json encode_protocol_value(const Value& value) {
using Type = std::remove_cvref_t<Value>;
if constexpr (std::same_as<Type, bool> || std::integral<Type> || std::floating_point<Type> || std::same_as<Type, std::string>) {
return value;
} else if constexpr (std::same_as<Type, char>) {
return std::string(1, value);
} else if constexpr (std::is_enum_v<Type>) {
return magic_enum::enum_name(value);
} else if constexpr (std::same_as<Type, render_2d::Color_Map>) {
return {{"stops", encode_protocol_value(value.stops)}};
} else if constexpr (is_pair_v<Type>) {
return nlohmann::json::array({encode_protocol_value(value.first), encode_protocol_value(value.second)});
} else if constexpr (is_std_array_v<Type> || Json_Sequence<Type>) {
nlohmann::json result = nlohmann::json::array();
for (const auto& item : value) result.push_back(encode_protocol_value(item));
return result;
} else if constexpr (std::is_aggregate_v<Type>) {
return encode_aggregate(value, std::make_index_sequence<boost::pfr::tuple_size_v<Type>>{});
} else {
static_assert(std::is_aggregate_v<Type>, "Aethera protocol needs an explicit codec for this value type");
}
}
template <typename Value>
void decode_protocol_value(Value& target, const nlohmann::json& input) {
using Type = std::remove_cvref_t<Value>;
if constexpr (std::same_as<Type, bool> || std::integral<Type> || std::floating_point<Type> || std::same_as<Type, std::string>) {
target = input.template get<Type>();
} else if constexpr (std::same_as<Type, char>) {
const auto text = input.template get<std::string>();
if (text.size() != 1) throw std::invalid_argument("character value must contain exactly one character");
target = text.front();
} else if constexpr (std::is_enum_v<Type>) {
const auto value = magic_enum::enum_cast<Type>(input.template get<std::string>());
if (!value) throw std::invalid_argument("unknown enum value");
target = *value;
} else if constexpr (std::same_as<Type, render_2d::Color_Map>) {
decode_protocol_value(target.stops, input.at("stops"));
} else if constexpr (is_pair_v<Type>) {
if (!input.is_array() || input.size() != 2) throw std::invalid_argument("pair value must be a two-item array");
decode_protocol_value(target.first, input[0]);
decode_protocol_value(target.second, input[1]);
} else if constexpr (is_std_array_v<Type>) {
if (!input.is_array() || input.size() != target.size()) throw std::invalid_argument("array has the wrong size");
for (std::size_t index = 0; index < target.size(); ++index) decode_protocol_value(target[index], input[index]);
} else if constexpr (Json_Sequence<Type>) {
if (!input.is_array()) throw std::invalid_argument("value must be an array");
Type updated;
for (const auto& encoded : input) {
typename Type::value_type item{};
decode_protocol_value(item, encoded);
updated.emplace_back(std::move(item));
}
target = std::move(updated);
} else if constexpr (std::is_aggregate_v<Type>) {
if (!input.is_object()) throw std::invalid_argument("value must be an object");
decode_aggregate(target, input, std::make_index_sequence<boost::pfr::tuple_size_v<Type>>{});
} else {
static_assert(std::is_aggregate_v<Type>, "Aethera protocol needs an explicit codec for this value type");
}
}
template <typename Value>
constexpr std::string_view protocol_editor() {
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::same_as<Type, std::string> || std::same_as<Type, char>) return "text";
if constexpr (std::is_enum_v<Type>) return "select";
if constexpr (requires { typename Type::value_type; }) return "array";
return "object";
if constexpr (std::same_as<Type, Color> || std::same_as<Type, render_3d::Linear_Color>) return "color";
if constexpr (std::same_as<Type, render_2d::Point_F>) return "point2";
if constexpr (std::same_as<Type, render_2d::Size> || std::same_as<Type, render_3d::Extent>) return "size";
if constexpr (std::same_as<Type, render_2d::Rect_F>) return "rect";
if constexpr (std::same_as<Type, render_2d::Axis_Range>) return "range";
if constexpr (std::same_as<Type, render_2d::Pen>) return "pen";
if constexpr (std::same_as<Type, render_2d::Brush>) return "brush";
if constexpr (std::same_as<Type, render_2d::Font>) return "font";
if constexpr (std::same_as<Type, render_2d::Color_Map>) return "color-map";
if constexpr (std::same_as<Type, render_3d::Vec3>) return "vector3";
if constexpr (std::same_as<Type, render_3d::Matrix4>) return "matrix4";
return "json";
}
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) {
nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::string_view label, std::string_view kind) {
nlohmann::json properties = nlohmann::json::array();
nlohmann::json state = nlohmann::json::object();
structive::type_descriptor<Adapter>().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>()},
{"description", std::string(Field::accessor_type::description.view())}};
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}});
if (field.template attribute<Renderable_Field_Role_Category>().value == Renderable_Field_Role::prop) {
nlohmann::json item{{"key", field.key()}, {"editor", protocol_editor<Value>()},
{"description", std::string(Field::accessor_type::description.view())},
{"value", encode_protocol_value(value)}};
if constexpr (std::is_enum_v<Value>) {
item["options"] = nlohmann::json::array();
for (const auto enum_value : magic_enum::enum_values<Value>()) {
const auto name = magic_enum::enum_name(enum_value);
item["options"].push_back({{"value", name}, {"label", name}});
}
}
properties.push_back(std::move(item));
} else {
state[field.key()] = encode_protocol_value(value);
}
(role == Renderable_Field_Role::prop ? prop : state).push_back(std::move(item));
});
return {{"prop", std::move(prop)}, {"state", std::move(state)}};
return {{"id", id}, {"label", label}, {"kind", kind},
{"properties", std::move(properties)}, {"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 write_adapter_prop(Adapter& adapter, std::string_view key, const nlohmann::json& input) {
nlohmann::json result{{"success", false}, {"key", key}};
const bool found = structive::visit_schema_property(schema, key, [&](auto, const auto& field) {
const bool found = structive::visit_schema_property(structive::type_descriptor<Adapter>(), key, [&](auto, const auto& field) {
using Field = std::remove_cvref_t<decltype(field)>;
using Value = typename Field::value_type;
if constexpr (!Field::writable) {
@@ -257,14 +263,14 @@ template <typename Adapter>
} else {
try {
Value value{};
adminive::assign_json_value<nlohmann::json>(value, input);
decode_protocol_value(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));
result["value"] = encode_protocol_value(field.accessor.read(adapter));
} catch (const std::exception& error) {
result["error"] = error.what();
}
@@ -275,105 +281,20 @@ template <typename Adapter>
}
template <typename Adapter>
Renderable_Descriptor_Model<Adapter>::Renderable_Descriptor_Model(Adapter value) : adapter(std::move(value)) {}
Renderable_Descriptor_Model<Adapter>::Renderable_Descriptor_Model(
std::string id, std::string label, std::string kind, Adapter value)
: component_id(std::move(id)), component_label(std::move(label)), component_kind(std::move(kind)), adapter(std::move(value)) {}
template <typename Adapter> std::string_view Renderable_Descriptor_Model<Adapter>::id() const noexcept { return component_id; }
template <typename Adapter> nlohmann::json Renderable_Descriptor_Model<Adapter>::schema() const {
return adapter_schema(adapter, component_id, component_label, component_kind);
}
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>
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);
}
inline auto Type_Descriptor<aethera::Color>::get() {
return reflected_object_with<aethera::Color>("color", "Color", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Point_F>::get() {
return reflected_object_with<aethera::render_2d::Point_F>("point", "Point", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Rect_F>::get() {
return reflected_object_with<aethera::render_2d::Rect_F>("rectangle", "Rectangle", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Axis_Range>::get() {
return reflected_object_with<aethera::render_2d::Axis_Range>("axis_range", "Axis range", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Pen>::get() {
return reflected_object_with<aethera::render_2d::Pen>("pen", "Pen", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Brush>::get() {
return reflected_object_with<aethera::render_2d::Brush>("brush", "Brush", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Font>::get() {
return reflected_object_with<aethera::render_2d::Font>("font", "Font", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Frequency_Trace_Sample>::get() {
return reflected_object_with<aethera::render_2d::Frequency_Trace_Sample>("frequency_trace_sample", "Frequency trace sample", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Waterfall_Row>::get() {
return reflected_object_with<aethera::render_2d::Waterfall_Row>("waterfall_row", "Waterfall row", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Constellation_Point>::get() {
return reflected_object_with<aethera::render_2d::Constellation_Point>("constellation_point", "Constellation point", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_2d::Color_Map>::get() {
return reflected_object_with<aethera::render_2d::Color_Map>("color_map", "Color map", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_3d::Vec3>::get() {
return reflected_object_with<aethera::render_3d::Vec3>("vec3", "3D vector", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_3d::Matrix4>::get() {
return reflected_object_with<aethera::render_3d::Matrix4>("matrix4", "Transform", Identity_Field_Customizer{});
}
inline auto Type_Descriptor<aethera::render_3d::Point>::get() {
return reflected_object_with<aethera::render_3d::Point>("point_3d", "3D point", Identity_Field_Customizer{});
std::unique_ptr<Renderable_Descriptor> make_renderable_descriptor(
std::string id, std::string label, std::string kind, Adapter adapter) {
return std::make_unique<Renderable_Descriptor_Model<Adapter>>(
std::move(id), std::move(label), std::move(kind), std::move(adapter));
}
}
+3 -2
View File
@@ -82,10 +82,11 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
});
}, {drogon::Get});
app.registerHandler("/plot/{1}/prop/{2}", [plots](
app.registerHandler("/plot/{1}/component/{2}/prop/{3}", [plots](
const drogon::HttpRequestPtr& request,
std::function<void(const drogon::HttpResponsePtr&)>&& callback,
std::string plot_id,
std::string component,
std::string key) {
auto plot = find_plot(*plots, plot_id);
if (!plot) {
@@ -101,7 +102,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
}
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) {
plot->async_write_prop(std::move(component), std::move(key), std::move(value), [output](nlohmann::json result) {
(*output)(json_response(std::move(result)));
});
}, {drogon::Put});