From 424784f47df96bc2a30ce4e4903a59f7212b10b4 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sat, 22 Aug 2026 01:25:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E9=93=BE=E5=BC=8F=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency_Graph_Storage.hpp | 47 +- kernel/src/kernel/double_buffer/model.hpp | 6 +- render_2D/render_2D/scene/Render_Scene_2D.hpp | 6 +- render_2D/render_2D/scene/Render_Scene_2D.ipp | 4 +- web_server/src/Gallery_Plots.hpp | 3 +- web_server/src/Plot.cpp | 1023 ++++++++--------- 6 files changed, 535 insertions(+), 554 deletions(-) diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp index 38fa598..349ebc7 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp @@ -238,6 +238,7 @@ struct Root::Builder { using Prop = typename Object::Prop; using Buffer_Build = detail::Buffer_Build_Storage; using Dependency_Graph_Build = detail::Dependency_Graph_Build_Storage; + using Final_Builder = typename Object::Builder; std::unique_ptr object; /* 尚未挂接最终 Private 的对象外壳。 */ Prop prop{}; /* build() 前暂存的发布属性写入面。 */ Buffer_Build buffer_storage; /* build() 前暂存的普通 Buffer 初值。 */ @@ -245,33 +246,33 @@ struct Root::Builder { template explicit Builder(Args&&... args) : object(new Object(std::forward(args)...)), dependency_graph_storage(object->memory_resource()) {} - Builder& set_pmr(Pmr pmr) { + Final_Builder& set_pmr(Pmr pmr) { object->set_pmr_resource(pmr); - return *this; + return static_cast(*this); } - Builder& set_pmr(Pmr::Resource& resource) { + Final_Builder& set_pmr(Pmr::Resource& resource) { return set_pmr(Pmr{resource}); } - Builder& set_pmr(Pmr::Resource* resource) { + Final_Builder& set_pmr(Pmr::Resource* resource) { return set_pmr(Pmr{resource}); } template Value> - Builder& set(Member Owner::* member, Value&& value) { + Final_Builder& set(Member Owner::* member, Value&& value) { prop.*member = std::forward(value); - return *this; + return static_cast(*this); } template Tag, detail::Buffer_Value_Settable Value> - Builder& set(Value&& value) { + Final_Builder& set(Value&& value) { buffer_storage.template set(std::forward(value)); - return *this; + return static_cast(*this); } template ... Dependency_Graph_Tags, detail::Dependency_Graph_Edit_Callback_For Callback> - Builder& edit_dependency_graph(Callback&& callback) { + Final_Builder& edit_dependency_graph(Callback&& callback) { dependency_graph_storage.template edit(std::forward(callback)); - return *this; + return static_cast(*this); } template Dependency_Graph_Tag, detail::Bound_Dependency_Graph_Target> Node_Object> - Builder& add_dependency_node(Node_Object* node) { + Final_Builder& add_dependency_node(Node_Object* node) { return edit_dependency_graph( [&](auto& editor) { editor.add(node); @@ -279,7 +280,7 @@ struct Root::Builder { ); } template Dependency_Graph_Tag, Root_Derived Node_Object> - Builder& remove_dependency_node(Node_Object* node) { + Final_Builder& remove_dependency_node(Node_Object* node) { return edit_dependency_graph( [&](auto& editor) { editor.remove(node); @@ -287,15 +288,15 @@ struct Root::Builder { ); } template Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target> Target, detail::Prop_Dependency_Source Source> - Builder& add_prop_dependency(Target* target, Source* source) { + Final_Builder& add_prop_dependency(Target* target, Source* source) { return edit_dependency_graph([&](auto& editor) { editor.template add_prop_dependency(target, source); }); } template Dependency_Graph_Tag, typename Prop_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Prop_Layer_Dependency_Source Source> - Builder& add_prop_dependency(Target* target, Source* source) { + Final_Builder& add_prop_dependency(Target* target, Source* source) { return edit_dependency_graph([&](auto& editor) { editor.template add_prop_dependency(target, source); }); } template Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target> Target, detail::State_Dependency_Source Source> - Builder& add_dependency(Target* target, Source* source) { + Final_Builder& add_dependency(Target* target, Source* source) { return edit_dependency_graph( [&](auto& editor) { editor.template add_dependency(target, source); @@ -304,7 +305,7 @@ struct Root::Builder { } /* 在构造期依赖图中添加指定 State Tag 的状态层级依赖。 */ template Dependency_Graph_Tag, typename State_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::State_Layer_Dependency_Source Source> - Builder& add_state_dependency(Target* target, Source* source) { + Final_Builder& add_state_dependency(Target* target, Source* source) { return edit_dependency_graph( [&](auto& editor) { editor.template add_state_dependency(target, source); @@ -312,7 +313,7 @@ struct Root::Builder { ); } template Dependency_Graph_Tag, typename Buffer_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Buffer_Dependency_Source Source> - Builder& add_dependency(Target* target, Source* source) { + Final_Builder& add_dependency(Target* target, Source* source) { return edit_dependency_graph( [&](auto& editor) { editor.template add_dependency(target, source); @@ -320,7 +321,7 @@ struct Root::Builder { ); } template Dependency_Graph_Tag, typename Source_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Dependency_Object Source> - Builder& add_dirty_dependency(Target* target, Source* source) { + Final_Builder& add_dirty_dependency(Target* target, Source* source) { return edit_dependency_graph( [&](auto& editor) { editor.template add_dirty_dependency(target, source); @@ -328,7 +329,7 @@ struct Root::Builder { ); } template Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target> Target, detail::State_Dependency_Source Source> - Builder& remove_dependency(Target* target, Source* source) { + Final_Builder& remove_dependency(Target* target, Source* source) { return edit_dependency_graph( [&](auto& editor) { editor.template remove_dependency(target, source); @@ -336,16 +337,16 @@ struct Root::Builder { ); } template Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target> Target, detail::Prop_Dependency_Source Source> - Builder& remove_prop_dependency(Target* target, Source* source) { + Final_Builder& remove_prop_dependency(Target* target, Source* source) { return edit_dependency_graph([&](auto& editor) { editor.template remove_prop_dependency(target, source); }); } template Dependency_Graph_Tag, typename Prop_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Prop_Layer_Dependency_Source Source> - Builder& remove_prop_dependency(Target* target, Source* source) { + Final_Builder& remove_prop_dependency(Target* target, Source* source) { return edit_dependency_graph([&](auto& editor) { editor.template remove_prop_dependency(target, source); }); } /* 从构造期依赖图中移除指定 State Tag 的状态层级依赖。 */ template Dependency_Graph_Tag, typename State_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::State_Layer_Dependency_Source Source> - Builder& remove_state_dependency(Target* target, Source* source) { + Final_Builder& remove_state_dependency(Target* target, Source* source) { return edit_dependency_graph( [&](auto& editor) { editor.template remove_state_dependency(target, source); @@ -353,7 +354,7 @@ struct Root::Builder { ); } template Dependency_Graph_Tag, typename Buffer_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Buffer_Dependency_Source Source> - Builder& remove_dependency(Target* target, Source* source) { + Final_Builder& remove_dependency(Target* target, Source* source) { return edit_dependency_graph( [&](auto& editor) { editor.template remove_dependency(target, source); diff --git a/kernel/src/kernel/double_buffer/model.hpp b/kernel/src/kernel/double_buffer/model.hpp index 73275fe..1ee946f 100644 --- a/kernel/src/kernel/double_buffer/model.hpp +++ b/kernel/src/kernel/double_buffer/model.hpp @@ -119,7 +119,11 @@ struct Impl : Obj { using Buffers = typename Obj::Buffers; using Dependency_Graph_Types = typename Obj::Dependency_Graph_Types; using States = typename Obj::States; - using Builder = typename Obj::template Builder; + /* CRTP 最终 Builder:所有基类流式接口都返回本类型,确保 build() 分派到最派生业务 Builder。 */ + struct Builder : Obj::template Builder { + using Base = typename Obj::template Builder; + using Base::Base; + }; using Attached_Object = Obj; struct Private : Obj::Private, Publish_Double_Buffer { using Allocator = std::pmr::polymorphic_allocator; diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index 6d5e27e..f811091 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -9,8 +9,7 @@ namespace aethera::render_2d { /* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */ struct Render_Scene_2D : Def, - Dependency_Graph_Type -> { + Dependency_Graph_Type> { struct Prop : Prev_Prop { Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */ Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */ @@ -25,10 +24,11 @@ struct Render_Scene_2D : Def struct Builder : Prev_Builder { using Base = Prev_Builder; + using Final_Builder = typename Object::Builder; Builder(); template requires std::derived_from - Builder& add_renderable(Renderable_Object* renderable); + Final_Builder& add_renderable(Renderable_Object* renderable); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: std::vector(Object*)>> attachments{}; /* 仅在 build 期间绑定已构造 Renderable。 */ diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index 5427f3f..726bcf7 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -10,7 +10,7 @@ Render_Scene_2D::Builder::Builder() : Base() {} template template requires std::derived_from -Render_Scene_2D::Builder& Render_Scene_2D::Builder::add_renderable(Renderable_Object* renderable) { +typename Render_Scene_2D::Builder::Final_Builder& Render_Scene_2D::Builder::add_renderable(Renderable_Object* renderable) { attachments.emplace_back([renderable](Object* scene) { auto& data = Base::private_access(renderable).template get(); if (!data.scene_attach) @@ -18,7 +18,7 @@ Render_Scene_2D::Builder& Render_Scene_2D::Builder::add_renderab auto attach = std::move(data.scene_attach); return attach(scene); }); - return *this; + return static_cast(*this); } template std::expected, Dependency_Graph_Error> Render_Scene_2D::Builder::build() { diff --git a/web_server/src/Gallery_Plots.hpp b/web_server/src/Gallery_Plots.hpp index ea84f8c..9b3cfbd 100644 --- a/web_server/src/Gallery_Plots.hpp +++ b/web_server/src/Gallery_Plots.hpp @@ -1,7 +1,7 @@ #pragma once #include "Plot.hpp" - namespace aethera::web { +// 2D [[nodiscard]] std::shared_ptr make_spectrum_plot(asio::any_io_executor executor); [[nodiscard]] std::shared_ptr make_axes_plot(asio::any_io_executor executor); [[nodiscard]] std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor); @@ -10,5 +10,6 @@ namespace aethera::web { [[nodiscard]] std::shared_ptr make_waterfall_plot(asio::any_io_executor executor); [[nodiscard]] std::shared_ptr make_constellation_plot(asio::any_io_executor executor); [[nodiscard]] std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor); +// 3D [[nodiscard]] std::shared_ptr make_datoviz_point_plot(asio::any_io_executor executor); } diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index 4dac740..6a15ef7 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -23,7 +23,6 @@ #include #include #include - namespace aethera::web { namespace { using namespace render_2d; @@ -35,30 +34,26 @@ using Numeric_Axis_Object = Impl; using Time_Axis_Object = Impl; using Selection_Object = Impl; constexpr std::uint16_t frame_protocol_version{4}; - enum class Frame_Pacing_Mode : std::uint32_t { manual, fixed_rate, minimum_latency, maximum_rate }; - struct Frame_Pacing_Properties { Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; /* 控制后继 render 请求节奏的策略。 */ - double fixed_rate_fps{30.0}; /* 固定频率策略的目标帧率,单位为 FPS。 */ - double minimum_latency_headroom{1.25}; /* 最低延迟策略相对 P95 生成耗时的安全系数。 */ + double fixed_rate_fps{30.0}; /* 固定频率策略的目标帧率,单位为 FPS。 */ + double minimum_latency_headroom{1.25}; /* 最低延迟策略相对 P95 生成耗时的安全系数。 */ }; - class Frame_Policy final { public: [[nodiscard]] Frame_Pacing_Properties snapshot() const; [[nodiscard]] nlohmann::json schema() const; [[nodiscard]] nlohmann::json write_prop(std::string_view key, const nlohmann::json& value); private: - mutable std::mutex mutex; /* 保护异步帧发布与 HTTP 属性编辑读取。 */ - Frame_Pacing_Properties pacing{}; /* 可编辑帧调度属性的唯一权威来源。 */ + mutable std::mutex mutex; /* 保护异步帧发布与 HTTP 属性编辑读取。 */ + Frame_Pacing_Properties pacing{}; /* 可编辑帧调度属性的唯一权威来源。 */ }; - std::string_view pacing_mode_name(Frame_Pacing_Mode mode) { switch (mode) { case Frame_Pacing_Mode::manual: return "manual"; @@ -68,7 +63,6 @@ std::string_view pacing_mode_name(Frame_Pacing_Mode mode) { } throw std::logic_error("unknown frame pacing mode"); } - std::optional parse_pacing_mode(std::string_view value) { if (value == "manual") return Frame_Pacing_Mode::manual; if (value == "fixed_rate") return Frame_Pacing_Mode::fixed_rate; @@ -76,8 +70,10 @@ std::optional parse_pacing_mode(std::string_view value) { if (value == "maximum_rate") return Frame_Pacing_Mode::maximum_rate; return std::nullopt; } - -Frame_Pacing_Properties Frame_Policy::snapshot() const { std::lock_guard lock(mutex); return pacing; } +Frame_Pacing_Properties Frame_Policy::snapshot() const { + std::lock_guard lock(mutex); + return pacing; +} nlohmann::json Frame_Policy::schema() const { std::lock_guard lock(mutex); nlohmann::json fields = nlohmann::json::array(); @@ -86,7 +82,6 @@ nlohmann::json Frame_Policy::schema() const { fields.push_back({{"key", "minimum_latency_headroom"}, {"label", "最低延迟余量"}, {"editor", "number"}, {"editable", true}, {"description", "最低延迟策略使用的浏览器端 P95 端到端耗时安全系数。"}, {"technical_description", "Multiplier applied to browser-computed P95 request-to-pixel latency before scheduling the next render request."}, {"value", pacing.minimum_latency_headroom}}); return {{"id", "frame-runtime"}, {"label", "帧策略与诊断"}, {"kind", "runtime"}, {"fields", std::move(fields)}, {"state", nlohmann::json::object()}}; } - nlohmann::json Frame_Policy::write_prop(std::string_view key, const nlohmann::json& value) { std::lock_guard lock(mutex); if (key == "pacing_mode") { @@ -112,21 +107,21 @@ nlohmann::json Frame_Policy::write_prop(std::string_view key, const nlohmann::js } return {{"success", false}, {"error", "unknown frame runtime property"}}; } - nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, std::uint32_t height, std::size_t byte_length, const Frame_Pacing_Properties& pacing) { nlohmann::json markers = nlohmann::json::object(); for (const auto& point : frame.trace_points()) markers[std::string(magic_enum::enum_name(point.marker))] = point.elapsed_ns; nlohmann::json measurements = nlohmann::json::object(); for (const auto& value : frame.trace_values()) measurements[std::string(magic_enum::enum_name(value.measurement))] = value.value_ns; const auto identity = frame.identity(); - return {{"kind", "frame_metadata"}, {"protocol", "aethera.frame"}, {"version", frame_protocol_version}, - {"sequence", identity.sequence}, {"correlation_id", identity.correlation_id}, - {"created_time_unix_ms", static_cast(frame.created_time_unix_ns()) / 1'000'000.0}, - {"pixel", {{"width", width}, {"height", height}, {"format", "rgba8"}, {"byte_length", byte_length}}}, - {"pacing", {{"mode", pacing_mode_name(pacing.mode)}, {"fixed_rate_fps", pacing.fixed_rate_fps}, {"minimum_latency_headroom", pacing.minimum_latency_headroom}}}, - {"trace", {{"clock", "steady_elapsed_ns"}, {"markers", std::move(markers)}, {"measurements", std::move(measurements)}}}}; + return { + {"kind", "frame_metadata"}, {"protocol", "aethera.frame"}, {"version", frame_protocol_version}, + {"sequence", identity.sequence}, {"correlation_id", identity.correlation_id}, + {"created_time_unix_ms", static_cast(frame.created_time_unix_ns()) / 1'000'000.0}, + {"pixel", {{"width", width}, {"height", height}, {"format", "rgba8"}, {"byte_length", byte_length}}}, + {"pacing", {{"mode", pacing_mode_name(pacing.mode)}, {"fixed_rate_fps", pacing.fixed_rate_fps}, {"minimum_latency_headroom", pacing.minimum_latency_headroom}}}, + {"trace", {{"clock", "steady_elapsed_ns"}, {"markers", std::move(markers)}, {"measurements", std::move(measurements)}}} + }; } - std::shared_ptr encode_frame(Frame_2D* frame, const Frame_Pacing_Properties& pacing) { frame->mark(Frame_Trace_Marker::websocket_publish_started); const Image_View image = frame->image(); @@ -149,7 +144,6 @@ std::shared_ptr encode_frame(Frame_2D* frame, const Fr message->metadata = frame_metadata(*frame, static_cast(image.width), static_cast(image.height), message->pixels.size(), pacing).dump(); return message; } - std::shared_ptr encode_frame(Frame_3D* frame, const Frame_Pacing_Properties& pacing) { frame->mark(Frame_Trace_Marker::websocket_publish_started); const auto pixels = frame->pixels(); @@ -160,11 +154,12 @@ std::shared_ptr encode_frame(Frame_3D* frame, const Fr message->metadata = frame_metadata(*frame, extent.width, extent.height, message->pixels.size(), pacing).dump(); return message; } - -struct Schema_Query { Plot::Json_Handler handler; }; +struct Schema_Query { + Plot::Json_Handler handler; +}; struct Frame_Submission { - const void* owner{}; /* 只向发起 render 的 WebSocket 连接返回完成帧。 */ - Plot_Frame_Request request{}; /* 浏览器帧请求及关联标识。 */ + const void* owner{}; /* 只向发起 render 的 WebSocket 连接返回完成帧。 */ + Plot_Frame_Request request{}; /* 浏览器帧请求及关联标识。 */ }; struct Prop_Write { std::string component; @@ -173,43 +168,40 @@ struct Prop_Write { Plot::Json_Handler handler; }; using Plot_Input = std::variant; - template class Scene_View_Model final : public Plot::Scene_View { public: Scene_View_Model(std::vector> value_descriptors, std::function value_update, - Owned_Objects... owned_objects) - : descriptors(std::move(value_descriptors)), - update_scene(std::move(value_update)), - objects(std::move(owned_objects)...) {} - + Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)), + update_scene(std::move(value_update)), + objects(std::move(owned_objects)...) {} 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", 2}, {"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; }); + 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_Frame_Request& request) override { update_scene(request); } - + void update(const Plot_Frame_Request& request) override { + update_scene(request); + } private: std::vector> descriptors; std::function update_scene; std::tuple objects; }; - template using Prop_Field = detail::Prop_Field; - template using State_Field = detail::State_Field; - template std::unique_ptr make_renderable_component( std::string id, std::string label, std::string kind, Object& object) { @@ -217,100 +209,95 @@ std::unique_ptr make_renderable_component( using Tag = typename Definition::Base_Tag; using State = typename Definition::State; using Adapter = detail::Renderable_Adapter, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field>; + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field>; return detail::make_renderable_descriptor(std::move(id), std::move(label), std::move(kind), Adapter{object}); } - template std::unique_ptr make_scene_component(Scene_Object& scene) { using Definition = typename Scene_Object::Attached_Object; using Prop = typename Definition::Prop; using Adapter = detail::Renderable_Adapter, - 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, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field>; + detail::Prop_Field<&Prop::viewport, "viewport", "Final scene viewport in physical pixels.">, + 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, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field>; return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene}); } - template <> std::unique_ptr make_scene_component(Scene_3D& scene) { using Adapter = detail::Renderable_Adapter, - detail::Prop_Field<&Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field>; + 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, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field>; return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene}); } - template std::unique_ptr make_axis_component( std::string id, std::string label, Axis_Object& axis) { return make_renderable_component, - 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); + Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">, + 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 make_axis_component( std::string id, std::string label, Time_Axis_Object& axis) { return make_renderable_component, - 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, - State_Field>( - std::move(id), std::move(label), "axis", axis); + Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">, + 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, + State_Field>( + std::move(id), std::move(label), "axis", axis); } - template std::unique_ptr make_scene_view( Object& object, @@ -327,18 +314,19 @@ std::unique_ptr make_scene_view( const auto append_owned = [&](const auto& owned) { using Owned = std::remove_cvref_t; if constexpr (std::same_as - || std::same_as - || std::same_as) { + || std::same_as + || std::same_as) { const auto id = axis_index++ == 0 ? "axis-x" : "axis-y"; components.push_back(make_axis_component(id, id == std::string_view{"axis-x"} ? "横向坐标轴" : "纵向坐标轴", *owned)); - } else if constexpr (std::same_as && !std::same_as) { + } + else if constexpr (std::same_as && !std::same_as) { components.push_back(make_renderable_component, - Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">, - State_Field>("selection", "矩形选区", "overlay", *owned)); + Prop_Field<&Selection_Rectangle_Overlay::Prop::label_font, "label_font", "Font used for labels attached to selected regions.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">, + State_Field>("selection", "矩形选区", "overlay", *owned)); } }; (append_owned(owned_objects), ...); @@ -347,52 +335,48 @@ std::unique_ptr make_scene_view( std::move(update), std::forward(owned_objects)...); } - Frequency_Axis_Object::Builder frequency_axis_builder() { Frequency_Axis_Object::Builder builder; builder - .set(&Abs_Axis::Prop::orientation, Axis_Orientation::horizontal) - .set(&Abs_Axis::Prop::position, Point_F{64.0, 370.0}) - .set(&Abs_Axis::Prop::pixel_length, 620.0) - .set(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0}); + .set(&Abs_Axis::Prop::orientation, Axis_Orientation::horizontal) + .set(&Abs_Axis::Prop::position, Point_F{64.0, 370.0}) + .set(&Abs_Axis::Prop::pixel_length, 620.0) + .set(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0}); return builder; } - Numeric_Axis_Object::Builder numeric_axis_builder( Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Axis_Range range) { Numeric_Axis_Object::Builder builder; builder - .set(&Abs_Axis::Prop::orientation, orientation) - .set(&Abs_Axis::Prop::position, position) - .set(&Abs_Axis::Prop::pixel_length, length) - .set(&Numeric_Axis::Prop::coordinate_range, range); + .set(&Abs_Axis::Prop::orientation, orientation) + .set(&Abs_Axis::Prop::position, position) + .set(&Abs_Axis::Prop::pixel_length, length) + .set(&Numeric_Axis::Prop::coordinate_range, range); return builder; } - Time_Axis_Object::Builder time_axis_builder( Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length) { Time_Axis_Object::Builder builder; builder - .set(&Abs_Axis::Prop::orientation, orientation) - .set(&Abs_Axis::Prop::position, position) - .set(&Abs_Axis::Prop::pixel_length, length); + .set(&Abs_Axis::Prop::orientation, orientation) + .set(&Abs_Axis::Prop::position, position) + .set(&Abs_Axis::Prop::pixel_length, length); return builder; } - template std::unique_ptr selection_overlay(Horizontal_Axis* horizontal_axis, Vertical_Axis* vertical_axis) { auto result = Selection_Object::Builder(horizontal_axis, vertical_axis).build(); if (!result) throw std::logic_error("selection overlay axes form an invalid dependency graph"); return std::move(result).value(); } - template void place_selection_over(Scene_2D* scene, Selection_Object* selection, Plot_Object* plot) { - const auto result = scene->template edit_dependency_graph([&](auto& paint) { paint.add_dependency(selection, plot); }); + const auto result = scene->template edit_dependency_graph([&](auto& paint) { + paint.add_dependency(selection, plot); + }); if (!result) throw std::logic_error("selection overlay paint order is invalid"); } - template void resize_axes(Scene_2D* scene, Size viewport, Axes*... axes) { const Size previous_viewport = scene->template read_prop().viewport; @@ -403,16 +387,18 @@ void resize_axes(Scene_2D* scene, Size viewport, Axes*... axes) { const double vertical_scale = static_cast(viewport.height) / previous_viewport.height; axis->template set<&Abs_Axis::Prop::position>(Point_F{ layout.position.x * horizontal_scale, - layout.position.y * vertical_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 void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { - const auto dispatch = [&](auto event) { scene.dispatch_event(std::move(event)); }; + const auto dispatch = [&](auto event) { + scene.dispatch_event(std::move(event)); + }; const auto apply_pointer = [&](auto& event) { event.position = input.position; event.global_position = input.global_position; @@ -455,9 +441,7 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { } } } - } - std::shared_ptr make_axes_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; auto frequency = *frequency_axis_builder().build(); @@ -470,19 +454,25 @@ std::shared_ptr make_axes_plot(asio::any_io_executor executor) { time->template set<&Abs_Axis::Prop::unit_text>("Time"); Scene_2D::Builder scene_builder; scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true); + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true); auto scene = *scene_builder.build(); const auto topology = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { - prepare.add(frequency.get()); prepare.add(numeric.get()); prepare.add(time.get()); - paint.add(frequency.get()); paint.add(numeric.get()); paint.add(time.get()); + prepare.add(frequency.get()); + prepare.add(numeric.get()); + prepare.add(time.get()); + paint.add(frequency.get()); + paint.add(numeric.get()); + paint.add(time.get()); }); if (!topology) throw std::logic_error("axis gallery topology is invalid"); auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Frame_Request& event) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, numeric, time); constexpr double day_milliseconds = 86'400'000.0; - time->append_time(Time_Of_Day{static_cast( - std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))}); + time->append_time(Time_Of_Day{ + static_cast( + std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) + }); }; std::vector> components; components.push_back(make_scene_component(*scene)); @@ -493,360 +483,353 @@ std::shared_ptr make_axes_plot(asio::any_io_executor executor) { std::move(components), std::move(update), std::move(frequency), std::move(numeric), std::move(time)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } - std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto frequency = *frequency_axis_builder().build(); - auto vertical = *numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}).build(); - Impl::Builder spectrum_builder(frequency.get(), vertical.get()); - spectrum_builder.set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) - .set(&Spectrum::Prop::max_hold_visible, true); - auto spectrum = *spectrum_builder.build(); - auto selection = selection_overlay(frequency.get(), vertical.get()); - Scene_2D::Builder scene_builder; - scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true); - scene_builder.add_renderable(spectrum.get()); - scene_builder.add_renderable(selection.get()); - auto scene = *scene_builder.build(); - place_selection_over(scene.get(), selection.get(), spectrum.get()); - auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); - std::array samples{}; - for (std::size_t i = 0; i < samples.size(); ++i) { - const double x = static_cast(i) / samples.size(); - samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(event.time_milliseconds * 0.001), 2.0)) - + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) - + 2.5 * std::sin(i * 0.31 + event.time_milliseconds * 0.004); - } - raw->update_samples(samples); - }; - auto view = make_scene_view< - Prop_Field<&Spectrum::Prop::center_frequency, "center_frequency", "Frequency placed at the visual center of the spectrum axis.">, - Prop_Field<&Spectrum::Prop::partition_count, "partition_count", "Number of partitions used to prepare and render spectrum samples.">, - Prop_Field<&Spectrum::Prop::max_hold_visible, "max_hold_visible", "Shows the accumulated maximum-hold spectrum curve when enabled.">, - Prop_Field<&Spectrum::Prop::min_hold_visible, "min_hold_visible", "Shows the accumulated minimum-hold spectrum curve when enabled.">, - Prop_Field<&Spectrum::Prop::max_marker_visible, "max_marker_visible", "Displays the marker attached to the strongest visible sample.">, - Prop_Field<&Spectrum::Prop::min_marker_visible, "min_marker_visible", "Displays the marker attached to the weakest visible sample.">, - Prop_Field<&Spectrum::Prop::sweep_region_visible, "sweep_region_visible", "Highlights the configured sweep-frequency interval on the plot.">, - Prop_Field<&Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts sample preparation to the frequency range currently visible on the axis.">, - Prop_Field<&Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete input sample span onto frequency coordinates.">, - Prop_Field<&Spectrum::Prop::sweep_frequency_range, "sweep_frequency_range", "Defines the frequency interval rendered as the sweep region.">, - Prop_Field<&Spectrum::Prop::partition_mode, "partition_mode", "Selects how samples are divided between preparation tasks.">, - Prop_Field<&Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects the interpolation algorithm used between adjacent spectrum samples.">, - Prop_Field<&Spectrum::Prop::max_brush, "max_brush", "Fill brush used for the maximum-hold area.">, - Prop_Field<&Spectrum::Prop::current_brush, "current_brush", "Fill brush used for the current spectrum area.">, - Prop_Field<&Spectrum::Prop::min_brush, "min_brush", "Fill brush used for the minimum-hold area.">, - Prop_Field<&Spectrum::Prop::max_pen, "max_pen", "Stroke style used for the maximum-hold curve.">, - Prop_Field<&Spectrum::Prop::current_pen, "current_pen", "Stroke style used for the current spectrum curve.">, - Prop_Field<&Spectrum::Prop::min_pen, "min_pen", "Stroke style used for the minimum-hold curve.">, - Prop_Field<&Spectrum::Prop::selected_marker_pen, "selected_marker_pen", "Stroke style used to emphasize the currently selected marker.">, - Prop_Field<&Spectrum::Prop::marker_pen, "marker_pen", "Default stroke style used for unselected spectrum markers.">, - Prop_Field<&Spectrum::Prop::middle_frequency_pen, "middle_frequency_pen", "Stroke style used for the center-frequency indicator.">, - Prop_Field<&Spectrum::Prop::sweep_region_brush, "sweep_region_brush", "Fill brush used to highlight the sweep-frequency interval.">, - Prop_Field<&Spectrum::Prop::custom_markers, "custom_markers", "User-defined marker positions and presentation data.">, - Prop_Field<&Spectrum::Prop::selected_marker, "selected_marker", "Index of the custom marker currently selected for interaction.">, - State_Field, - State_Field, - State_Field>( - *spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + constexpr Size canvas{720, 420}; + auto frequency = *frequency_axis_builder().build(); + auto vertical = *numeric_axis_builder(Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}).build(); + Impl::Builder spectrum_builder(frequency.get(), vertical.get()); + spectrum_builder.set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .set(&Spectrum::Prop::max_hold_visible, true); + auto spectrum = *spectrum_builder.build(); + auto selection = selection_overlay(frequency.get(), vertical.get()); + auto scene = *Scene_2D::Builder() + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_renderable(selection.get()) + .build(); + place_selection_over(scene.get(), selection.get(), spectrum.get()); + auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); + std::array samples{}; + for (std::size_t i = 0; i < samples.size(); ++i) { + const double x = static_cast(i) / samples.size(); + samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(event.time_milliseconds * 0.001), 2.0)) + + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) + + 2.5 * std::sin(i * 0.31 + event.time_milliseconds * 0.004); + } + raw->update_samples(samples); + }; + auto view = make_scene_view< + Prop_Field<&Spectrum::Prop::center_frequency, "center_frequency", "Frequency placed at the visual center of the spectrum axis.">, + Prop_Field<&Spectrum::Prop::partition_count, "partition_count", "Number of partitions used to prepare and render spectrum samples.">, + Prop_Field<&Spectrum::Prop::max_hold_visible, "max_hold_visible", "Shows the accumulated maximum-hold spectrum curve when enabled.">, + Prop_Field<&Spectrum::Prop::min_hold_visible, "min_hold_visible", "Shows the accumulated minimum-hold spectrum curve when enabled.">, + Prop_Field<&Spectrum::Prop::max_marker_visible, "max_marker_visible", "Displays the marker attached to the strongest visible sample.">, + Prop_Field<&Spectrum::Prop::min_marker_visible, "min_marker_visible", "Displays the marker attached to the weakest visible sample.">, + Prop_Field<&Spectrum::Prop::sweep_region_visible, "sweep_region_visible", "Highlights the configured sweep-frequency interval on the plot.">, + Prop_Field<&Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts sample preparation to the frequency range currently visible on the axis.">, + Prop_Field<&Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete input sample span onto frequency coordinates.">, + Prop_Field<&Spectrum::Prop::sweep_frequency_range, "sweep_frequency_range", "Defines the frequency interval rendered as the sweep region.">, + Prop_Field<&Spectrum::Prop::partition_mode, "partition_mode", "Selects how samples are divided between preparation tasks.">, + Prop_Field<&Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects the interpolation algorithm used between adjacent spectrum samples.">, + Prop_Field<&Spectrum::Prop::max_brush, "max_brush", "Fill brush used for the maximum-hold area.">, + Prop_Field<&Spectrum::Prop::current_brush, "current_brush", "Fill brush used for the current spectrum area.">, + Prop_Field<&Spectrum::Prop::min_brush, "min_brush", "Fill brush used for the minimum-hold area.">, + Prop_Field<&Spectrum::Prop::max_pen, "max_pen", "Stroke style used for the maximum-hold curve.">, + Prop_Field<&Spectrum::Prop::current_pen, "current_pen", "Stroke style used for the current spectrum curve.">, + Prop_Field<&Spectrum::Prop::min_pen, "min_pen", "Stroke style used for the minimum-hold curve.">, + Prop_Field<&Spectrum::Prop::selected_marker_pen, "selected_marker_pen", "Stroke style used to emphasize the currently selected marker.">, + Prop_Field<&Spectrum::Prop::marker_pen, "marker_pen", "Default stroke style used for unselected spectrum markers.">, + Prop_Field<&Spectrum::Prop::middle_frequency_pen, "middle_frequency_pen", "Stroke style used for the center-frequency indicator.">, + Prop_Field<&Spectrum::Prop::sweep_region_brush, "sweep_region_brush", "Fill brush used to highlight the sweep-frequency interval.">, + Prop_Field<&Spectrum::Prop::custom_markers, "custom_markers", "User-defined marker positions and presentation data.">, + Prop_Field<&Spectrum::Prop::selected_marker, "selected_marker", "Index of the custom marker currently selected for interaction.">, + State_Field, + State_Field, + State_Field>( + *spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } - std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto time = *time_axis_builder( - Axis_Orientation::horizontal, {64.0, 370.0}, 620.0).build(); - auto vertical = *numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}).build(); - auto trace = *Impl::Builder(time.get(), vertical.get()).build(); - auto selection = selection_overlay(time.get(), vertical.get()); - Scene_2D::Builder scene_builder; - scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true); - scene_builder.add_renderable(trace.get()); - scene_builder.add_renderable(selection.get()); - auto scene = *scene_builder.build(); - place_selection_over(scene.get(), selection.get(), trace.get()); - auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, time, vertical); - constexpr double day_milliseconds = 86'400'000.0; - const auto tick = time->append_time(Time_Of_Day{static_cast( - std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))}); - raw->append_sample(tick, - std::sin(event.time_milliseconds * 0.0025) * 0.8 - + std::sin(event.time_milliseconds * 0.0007) * 0.2); - }; - auto view = make_scene_view< - Prop_Field<&Frequency_Trace::Prop::partition_count, "partition_count", "Number of partitions used to prepare the time-ordered trace.">, - Prop_Field<&Frequency_Trace::Prop::pen, "pen", "Stroke style used to draw the frequency trace.">, - Prop_Field<&Frequency_Trace::Prop::partition_mode, "partition_mode", "Selects how trace samples are divided between preparation tasks.">, - Prop_Field<&Frequency_Trace::Prop::samples, "samples", "Complete time-ordered collection of frequency trace samples.">, - State_Field, - State_Field>( - *trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + constexpr Size canvas{720, 420}; + auto time = *time_axis_builder( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0).build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}).build(); + auto trace = *Impl::Builder(time.get(), vertical.get()).build(); + auto selection = selection_overlay(time.get(), vertical.get()); + Scene_2D::Builder scene_builder; + scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true); + scene_builder.add_renderable(trace.get()); + scene_builder.add_renderable(selection.get()); + auto scene = *scene_builder.build(); + place_selection_over(scene.get(), selection.get(), trace.get()); + auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, time, vertical); + constexpr double day_milliseconds = 86'400'000.0; + const auto tick = time->append_time(Time_Of_Day{ + static_cast( + std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) + }); + raw->append_sample(tick, + std::sin(event.time_milliseconds * 0.0025) * 0.8 + + std::sin(event.time_milliseconds * 0.0007) * 0.2); + }; + auto view = make_scene_view< + Prop_Field<&Frequency_Trace::Prop::partition_count, "partition_count", "Number of partitions used to prepare the time-ordered trace.">, + Prop_Field<&Frequency_Trace::Prop::pen, "pen", "Stroke style used to draw the frequency trace.">, + Prop_Field<&Frequency_Trace::Prop::partition_mode, "partition_mode", "Selects how trace samples are divided between preparation tasks.">, + Prop_Field<&Frequency_Trace::Prop::samples, "samples", "Complete time-ordered collection of frequency trace samples.">, + State_Field, + State_Field>( + *trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } - std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto frequency = *frequency_axis_builder().build(); - auto vertical = *numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}).build(); - Impl::Builder sweep_builder(frequency.get(), vertical.get()); - 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(); - auto selection = selection_overlay(frequency.get(), vertical.get()); - Scene_2D::Builder scene_builder; - scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true); - scene_builder.add_renderable(sweep.get()); - scene_builder.add_renderable(selection.get()); - auto scene = *scene_builder.build(); - place_selection_over(scene.get(), selection.get(), sweep.get()); - auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); - const auto& state = raw->template read_prop(); - const std::size_t block_count = std::max(1, state.block_count); - const std::size_t bins_per_block = std::max(1, state.bins_per_block); - const std::size_t block_index = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % block_count; - std::vector 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); - }; - 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.">, - Prop_Field<&Sweep_Spectrum::Prop::block_count, "block_count", "Number of blocks required to compose one complete sweep.">, - Prop_Field<&Sweep_Spectrum::Prop::partition_count, "partition_count", "Number of partitions used during sweep preparation.">, - Prop_Field<&Sweep_Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts preparation to the frequency interval visible on the axis.">, - Prop_Field<&Sweep_Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete sweep span onto frequency coordinates.">, - Prop_Field<&Sweep_Spectrum::Prop::partition_mode, "partition_mode", "Selects how sweep blocks are divided between preparation tasks.">, - Prop_Field<&Sweep_Spectrum::Prop::pen, "pen", "Stroke style used for the completed sweep curve.">, - Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen", "Stroke style used for the current sweep-frequency indicator.">, - Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation between adjacent sweep bins.">, - Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Latest data stored in each fixed frequency-segment slot.">, - State_Field, - State_Field, - State_Field>( - *sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + constexpr Size canvas{720, 420}; + auto frequency = *frequency_axis_builder().build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}).build(); + Impl::Builder sweep_builder(frequency.get(), vertical.get()); + 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(); + auto selection = selection_overlay(frequency.get(), vertical.get()); + Scene_2D::Builder scene_builder; + scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true); + scene_builder.add_renderable(sweep.get()); + scene_builder.add_renderable(selection.get()); + auto scene = *scene_builder.build(); + place_selection_over(scene.get(), selection.get(), sweep.get()); + auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); + const auto& state = raw->template read_prop(); + const std::size_t block_count = std::max(1, state.block_count); + const std::size_t bins_per_block = std::max(1, state.bins_per_block); + const std::size_t block_index = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % block_count; + std::vector 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); + }; + 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.">, + Prop_Field<&Sweep_Spectrum::Prop::block_count, "block_count", "Number of blocks required to compose one complete sweep.">, + Prop_Field<&Sweep_Spectrum::Prop::partition_count, "partition_count", "Number of partitions used during sweep preparation.">, + Prop_Field<&Sweep_Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts preparation to the frequency interval visible on the axis.">, + Prop_Field<&Sweep_Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete sweep span onto frequency coordinates.">, + Prop_Field<&Sweep_Spectrum::Prop::partition_mode, "partition_mode", "Selects how sweep blocks are divided between preparation tasks.">, + Prop_Field<&Sweep_Spectrum::Prop::pen, "pen", "Stroke style used for the completed sweep curve.">, + Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen", "Stroke style used for the current sweep-frequency indicator.">, + Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation between adjacent sweep bins.">, + Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Latest data stored in each fixed frequency-segment slot.">, + State_Field, + State_Field, + State_Field>( + *sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } - std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto frequency = *frequency_axis_builder().build(); - auto vertical = *numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}).build(); - Impl::Builder afterglow_builder(frequency.get(), vertical.get()); - afterglow_builder.set(&Afterglow::Prop::frequency_range, Axis_Range{0.0, 100.0}) - .set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0}) - .set(&Afterglow::Prop::power_point_size, 96); - auto afterglow = *afterglow_builder.build(); - auto selection = selection_overlay(frequency.get(), vertical.get()); - Scene_2D::Builder scene_builder; - scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true); - scene_builder.add_renderable(afterglow.get()); - scene_builder.add_renderable(selection.get()); - auto scene = *scene_builder.build(); - place_selection_over(scene.get(), selection.get(), afterglow.get()); - auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); - std::array values{}; - for (std::size_t i = 0; i < values.size(); ++i) - values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow( - static_cast(i) / values.size() - 0.5 - - 0.18 * std::sin(event.time_milliseconds * 0.0008), 2.0)); - raw->append_spectrum(values); - }; - auto view = make_scene_view< - Prop_Field<&Afterglow::Prop::frequency_point_size, "frequency_point_size", "Number of frequency cells allocated across each afterglow row.">, - Prop_Field<&Afterglow::Prop::power_point_size, "power_point_size", "Number of power cells allocated along the vertical afterglow range.">, - Prop_Field<&Afterglow::Prop::partition_count, "partition_count", "Number of partitions used to prepare afterglow history.">, - Prop_Field<&Afterglow::Prop::interpolate, "interpolate", "Enables interpolation when mapping samples into the afterglow grid.">, - Prop_Field<&Afterglow::Prop::attenuation_rate, "attenuation_rate", "Controls how quickly historical energy fades between updates.">, - Prop_Field<&Afterglow::Prop::frequency_range, "frequency_range", "Maps input samples onto the afterglow frequency axis.">, - Prop_Field<&Afterglow::Prop::power_range, "power_range", "Defines the minimum and maximum power represented by the color grid.">, - Prop_Field<&Afterglow::Prop::partition_mode, "partition_mode", "Selects how afterglow cells are divided between preparation tasks.">, - Prop_Field<&Afterglow::Prop::color_map, "color_map", "Maps accumulated energy values to rendered colors.">, - Prop_Field<&Afterglow::Prop::spectra, "spectra", "Spectrum history currently retained for afterglow rendering.">, - State_Field, - State_Field, - State_Field>( - *afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + constexpr Size canvas{720, 420}; + auto frequency = *frequency_axis_builder().build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}).build(); + Impl::Builder afterglow_builder(frequency.get(), vertical.get()); + afterglow_builder.set(&Afterglow::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0}) + .set(&Afterglow::Prop::power_point_size, 96); + auto afterglow = *afterglow_builder.build(); + auto selection = selection_overlay(frequency.get(), vertical.get()); + Scene_2D::Builder scene_builder; + scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true); + scene_builder.add_renderable(afterglow.get()); + scene_builder.add_renderable(selection.get()); + auto scene = *scene_builder.build(); + place_selection_over(scene.get(), selection.get(), afterglow.get()); + auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); + std::array values{}; + for (std::size_t i = 0; i < values.size(); ++i) + values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow( + static_cast(i) / values.size() - 0.5 + - 0.18 * std::sin(event.time_milliseconds * 0.0008), 2.0)); + raw->append_spectrum(values); + }; + auto view = make_scene_view< + Prop_Field<&Afterglow::Prop::frequency_point_size, "frequency_point_size", "Number of frequency cells allocated across each afterglow row.">, + Prop_Field<&Afterglow::Prop::power_point_size, "power_point_size", "Number of power cells allocated along the vertical afterglow range.">, + Prop_Field<&Afterglow::Prop::partition_count, "partition_count", "Number of partitions used to prepare afterglow history.">, + Prop_Field<&Afterglow::Prop::interpolate, "interpolate", "Enables interpolation when mapping samples into the afterglow grid.">, + Prop_Field<&Afterglow::Prop::attenuation_rate, "attenuation_rate", "Controls how quickly historical energy fades between updates.">, + Prop_Field<&Afterglow::Prop::frequency_range, "frequency_range", "Maps input samples onto the afterglow frequency axis.">, + Prop_Field<&Afterglow::Prop::power_range, "power_range", "Defines the minimum and maximum power represented by the color grid.">, + Prop_Field<&Afterglow::Prop::partition_mode, "partition_mode", "Selects how afterglow cells are divided between preparation tasks.">, + Prop_Field<&Afterglow::Prop::color_map, "color_map", "Maps accumulated energy values to rendered colors.">, + Prop_Field<&Afterglow::Prop::spectra, "spectra", "Spectrum history currently retained for afterglow rendering.">, + State_Field, + State_Field, + State_Field>( + *afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } - std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto frequency = *frequency_axis_builder().build(); - auto time = *time_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0).build(); - Impl::Builder waterfall_builder(frequency.get(), time.get()); - waterfall_builder.set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0}) - .set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0}); - auto waterfall = *waterfall_builder.build(); - auto selection = selection_overlay(frequency.get(), time.get()); - Scene_2D::Builder scene_builder; - scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true); - scene_builder.add_renderable(waterfall.get()); - scene_builder.add_renderable(selection.get()); - auto scene = *scene_builder.build(); - place_selection_over(scene.get(), selection.get(), waterfall.get()); - auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, time); - std::array values{}; - for (std::size_t i = 0; i < values.size(); ++i) - values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow( - static_cast(i) / values.size() - 0.5 - - 0.22 * std::sin(event.time_milliseconds * 0.0006), 2.0)); - constexpr double day_milliseconds = 86'400'000.0; - const auto tick = time->append_time(Time_Of_Day{static_cast( - std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))}); - raw->append_row(tick, values); - }; - auto view = make_scene_view< - Prop_Field<&Waterfall::Prop::tooltip_enabled, "tooltip_enabled", "Enables value inspection tooltips over waterfall cells.">, - Prop_Field<&Waterfall::Prop::tooltip_font, "tooltip_font", "Font used to render waterfall tooltip text.">, - Prop_Field<&Waterfall::Prop::tooltip_text_pen, "tooltip_text_pen", "Pen used to draw tooltip text and its foreground color.">, - Prop_Field<&Waterfall::Prop::tooltip_background_brush, "tooltip_background_brush", "Brush used to fill the tooltip background panel.">, - Prop_Field<&Waterfall::Prop::frequency_bin_count, "frequency_bin_count", "Number of frequency bins expected in each waterfall row.">, - Prop_Field<&Waterfall::Prop::partition_count, "partition_count", "Number of partitions used to prepare waterfall cells.">, - Prop_Field<&Waterfall::Prop::visible_range_only, "visible_range_only", "Restricts preparation to frequencies visible on the current axis.">, - Prop_Field<&Waterfall::Prop::frequency_range, "frequency_range", "Maps row samples onto waterfall frequency coordinates.">, - Prop_Field<&Waterfall::Prop::power_range, "power_range", "Defines the power interval mapped through the waterfall color map.">, - Prop_Field<&Waterfall::Prop::partition_mode, "partition_mode", "Selects how waterfall rows are divided between preparation tasks.">, - Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation when samples are mapped to raster cells.">, - Prop_Field<&Waterfall::Prop::color_map, "color_map", "Maps sample power values to waterfall colors.">, - Prop_Field<&Waterfall::Prop::rows, "rows", "Time-ordered collection of spectrum rows retained by the waterfall.">, - State_Field, - State_Field, - State_Field>( - *waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + constexpr Size canvas{720, 420}; + auto frequency = *frequency_axis_builder().build(); + auto time = *time_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0).build(); + Impl::Builder waterfall_builder(frequency.get(), time.get()); + waterfall_builder.set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0}); + auto waterfall = *waterfall_builder.build(); + auto selection = selection_overlay(frequency.get(), time.get()); + Scene_2D::Builder scene_builder; + scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true); + scene_builder.add_renderable(waterfall.get()); + scene_builder.add_renderable(selection.get()); + auto scene = *scene_builder.build(); + place_selection_over(scene.get(), selection.get(), waterfall.get()); + auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, time); + std::array values{}; + for (std::size_t i = 0; i < values.size(); ++i) + values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow( + static_cast(i) / values.size() - 0.5 + - 0.22 * std::sin(event.time_milliseconds * 0.0006), 2.0)); + constexpr double day_milliseconds = 86'400'000.0; + const auto tick = time->append_time(Time_Of_Day{ + static_cast( + std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) + }); + raw->append_row(tick, values); + }; + auto view = make_scene_view< + Prop_Field<&Waterfall::Prop::tooltip_enabled, "tooltip_enabled", "Enables value inspection tooltips over waterfall cells.">, + Prop_Field<&Waterfall::Prop::tooltip_font, "tooltip_font", "Font used to render waterfall tooltip text.">, + Prop_Field<&Waterfall::Prop::tooltip_text_pen, "tooltip_text_pen", "Pen used to draw tooltip text and its foreground color.">, + Prop_Field<&Waterfall::Prop::tooltip_background_brush, "tooltip_background_brush", "Brush used to fill the tooltip background panel.">, + Prop_Field<&Waterfall::Prop::frequency_bin_count, "frequency_bin_count", "Number of frequency bins expected in each waterfall row.">, + Prop_Field<&Waterfall::Prop::partition_count, "partition_count", "Number of partitions used to prepare waterfall cells.">, + Prop_Field<&Waterfall::Prop::visible_range_only, "visible_range_only", "Restricts preparation to frequencies visible on the current axis.">, + Prop_Field<&Waterfall::Prop::frequency_range, "frequency_range", "Maps row samples onto waterfall frequency coordinates.">, + Prop_Field<&Waterfall::Prop::power_range, "power_range", "Defines the power interval mapped through the waterfall color map.">, + Prop_Field<&Waterfall::Prop::partition_mode, "partition_mode", "Selects how waterfall rows are divided between preparation tasks.">, + Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation when samples are mapped to raster cells.">, + Prop_Field<&Waterfall::Prop::color_map, "color_map", "Maps sample power values to waterfall colors.">, + Prop_Field<&Waterfall::Prop::rows, "rows", "Time-ordered collection of spectrum rows retained by the waterfall.">, + State_Field, + State_Field, + State_Field>( + *waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } - std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto horizontal = *numeric_axis_builder( - Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}).build(); - auto vertical = *numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}).build(); - Impl::Builder constellation_builder(horizontal.get(), vertical.get()); - constellation_builder.set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2}) - .set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2}); - auto constellation = *constellation_builder.build(); - auto selection = selection_overlay(horizontal.get(), vertical.get()); - Scene_2D::Builder scene_builder; - scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true); - scene_builder.add_renderable(constellation.get()); - scene_builder.add_renderable(selection.get()); - auto scene = *scene_builder.build(); - place_selection_over(scene.get(), selection.get(), constellation.get()); - auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); - const auto& state = raw->template read_prop(); - const int anchor_count = static_cast(state.type); - const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; - const double phase = event.time_milliseconds * 0.001; - for (int index = 0; index < anchor_count; ++index) { - const double angle = state.phase_offset_radians - + 2.0 * std::numbers::pi * static_cast(index) / anchor_count; - const double noise_i = 0.025 * std::sin(phase * 11.0 + index * 1.73) - + 0.012 * std::cos(phase * 23.0 + index * 0.61); - const double noise_q = 0.025 * std::cos(phase * 13.0 + index * 1.37) - + 0.012 * std::sin(phase * 19.0 + index * 0.47); - raw->append_point({state.i_range.center() + std::cos(angle) * radius + noise_i, - state.q_range.center() + std::sin(angle) * radius + noise_q}); - } - }; - auto view = make_scene_view< - Prop_Field<&Constellation_Diagram::Prop::point_lifetime_ms, "point_lifetime_ms", "Time in milliseconds that an appended constellation point remains visible.">, - Prop_Field<&Constellation_Diagram::Prop::type, "type", "Selects the modulation constellation used to generate reference anchors.">, - Prop_Field<&Constellation_Diagram::Prop::phase_offset_radians, "phase_offset_radians", "Rotates constellation points and anchors by the specified phase angle.">, - Prop_Field<&Constellation_Diagram::Prop::i_range, "i_range", "Defines the horizontal in-phase coordinate interval.">, - Prop_Field<&Constellation_Diagram::Prop::q_range, "q_range", "Defines the vertical quadrature coordinate interval.">, - Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color", "Color used to render received I/Q samples.">, - 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, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + constexpr Size canvas{720, 420}; + auto horizontal = *numeric_axis_builder( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}).build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}).build(); + Impl::Builder constellation_builder(horizontal.get(), vertical.get()); + constellation_builder.set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2}) + .set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2}); + auto constellation = *constellation_builder.build(); + auto selection = selection_overlay(horizontal.get(), vertical.get()); + Scene_2D::Builder scene_builder; + scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true); + scene_builder.add_renderable(constellation.get()); + scene_builder.add_renderable(selection.get()); + auto scene = *scene_builder.build(); + place_selection_over(scene.get(), selection.get(), constellation.get()); + auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); + const auto& state = raw->template read_prop(); + const int anchor_count = static_cast(state.type); + const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; + const double phase = event.time_milliseconds * 0.001; + for (int index = 0; index < anchor_count; ++index) { + const double angle = state.phase_offset_radians + + 2.0 * std::numbers::pi * static_cast(index) / anchor_count; + const double noise_i = 0.025 * std::sin(phase * 11.0 + index * 1.73) + + 0.012 * std::cos(phase * 23.0 + index * 0.61); + const double noise_q = 0.025 * std::cos(phase * 13.0 + index * 1.37) + + 0.012 * std::sin(phase * 19.0 + index * 0.47); + raw->append_point({ + state.i_range.center() + std::cos(angle) * radius + noise_i, + state.q_range.center() + std::sin(angle) * radius + noise_q + }); + } + }; + auto view = make_scene_view< + Prop_Field<&Constellation_Diagram::Prop::point_lifetime_ms, "point_lifetime_ms", "Time in milliseconds that an appended constellation point remains visible.">, + Prop_Field<&Constellation_Diagram::Prop::type, "type", "Selects the modulation constellation used to generate reference anchors.">, + Prop_Field<&Constellation_Diagram::Prop::phase_offset_radians, "phase_offset_radians", "Rotates constellation points and anchors by the specified phase angle.">, + Prop_Field<&Constellation_Diagram::Prop::i_range, "i_range", "Defines the horizontal in-phase coordinate interval.">, + Prop_Field<&Constellation_Diagram::Prop::q_range, "q_range", "Defines the vertical quadrature coordinate interval.">, + Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color", "Color used to render received I/Q samples.">, + 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, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } - std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto horizontal = *numeric_axis_builder( - Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}).build(); - auto vertical = *numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}).build(); - auto selection = *Impl::Builder(horizontal.get(), vertical.get()).build(); - Scene_2D::Builder scene_builder; - scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true); - scene_builder.add_renderable(selection.get()); - auto scene = *scene_builder.build(); - auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); - }; - auto view = make_scene_view< - Prop_Field<&Selection_Rectangle_Overlay::Prop::label_font, "label_font", "Font used for labels attached to selected regions.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">, - State_Field>( - *selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + constexpr Size canvas{720, 420}; + auto horizontal = *numeric_axis_builder( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}).build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}).build(); + auto selection = *Impl::Builder(horizontal.get(), vertical.get()).build(); + auto scene = *Scene_2D::Builder{} + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_renderable(selection.get()) + .build(); + auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); + }; + auto view = make_scene_view< + Prop_Field<&Selection_Rectangle_Overlay::Prop::label_font, "label_font", "Font used for labels attached to selected regions.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">, + State_Field>( + *selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } - struct Plot::Private { using Scene = std::variant, std::unique_ptr>; using Frame = std::variant, std::unique_ptr>; struct Managed_Frame { - const void* owner{}; /* 发起本帧的 WebSocket 连接身份。 */ - Plot_Frame_Request request{}; /* 创建本帧的浏览器请求及 viewport。 */ - Frame frame{}; /* Web 层唯一拥有并传给 Scene 的外部帧。 */ + const void* owner{}; /* 发起本帧的 WebSocket 连接身份。 */ + Plot_Frame_Request request{}; /* 创建本帧的浏览器请求及 viewport。 */ + Frame frame{}; /* Web 层唯一拥有并传给 Scene 的外部帧。 */ }; - asio::strand strand; /* 串行执行帧请求、属性写入和 Schema 查询。 */ + asio::strand strand; /* 串行执行帧请求、属性写入和 Schema 查询。 */ asio::experimental::concurrent_channel inputs; /* 不承载输入事件的异步命令通道。 */ - std::unique_ptr view; /* 使用层数据推进及反射描述实现;拥有 Scene 引用的图元。 */ - Scene scene; /* 当前 Plot 唯一拥有的 2D 或 3D Scene;析构先于 view 所有图元。 */ - std::once_flag start_once; /* 保证回调与协程只安装一次。 */ - std::mutex handlers_mutex; /* 保护跨 Drogon 连接线程修改的订阅表。 */ - std::unordered_map handlers; /* 以连接身份索引的完成帧订阅。 */ - std::uint64_t next_frame_sequence{1}; /* 下一外部帧使用的单调序号;只在 strand 访问。 */ - Frame_Policy frame_policy{}; /* 仅保存可编辑刷新策略,不保存衍生统计。 */ - std::optional active_frame{}; /* 当前由 Scene/异步后端借用指针的外部帧。 */ - std::deque pending_order{}; /* 按首次等待顺序保存连接身份,避免连接间饥饿。 */ - std::unordered_map pending_frames{}; /* 每个连接只保留最新一个尚未提交 Scene 的外部帧。 */ - bool frame_in_flight{}; /* Scene 是否已有一次尚未完成回调的帧。 */ - + std::unique_ptr view; /* 使用层数据推进及反射描述实现;拥有 Scene 引用的图元。 */ + Scene scene; /* 当前 Plot 唯一拥有的 2D 或 3D Scene;析构先于 view 所有图元。 */ + std::once_flag start_once; /* 保证回调与协程只安装一次。 */ + std::mutex handlers_mutex; /* 保护跨 Drogon 连接线程修改的订阅表。 */ + std::unordered_map handlers; /* 以连接身份索引的完成帧订阅。 */ + std::uint64_t next_frame_sequence{1}; /* 下一外部帧使用的单调序号;只在 strand 访问。 */ + Frame_Policy frame_policy{}; /* 仅保存可编辑刷新策略,不保存衍生统计。 */ + std::optional active_frame{}; /* 当前由 Scene/异步后端借用指针的外部帧。 */ + std::deque pending_order{}; /* 按首次等待顺序保存连接身份,避免连接间饥饿。 */ + std::unordered_map pending_frames{}; /* 每个连接只保留最新一个尚未提交 Scene 的外部帧。 */ + bool frame_in_flight{}; /* Scene 是否已有一次尚未完成回调的帧。 */ template Private(asio::any_io_executor executor, std::unique_ptr value_scene, - std::unique_ptr value_view) - : strand(asio::make_strand(std::move(executor))), inputs(strand, 32), - view(std::move(value_view)), scene(std::move(value_scene)) {} - + std::unique_ptr value_view) : strand(asio::make_strand(std::move(executor))), inputs(strand, 32), + view(std::move(value_view)), scene(std::move(value_scene)) {} void publish(const void* owner, std::shared_ptr frame) { Frame_Handler output; { @@ -856,7 +839,6 @@ struct Plot::Private { } if (output) output(std::move(frame)); } - [[nodiscard]] nlohmann::json schema() const; [[nodiscard]] Managed_Frame make_frame(Frame_Submission submission); void request_frame(Frame_Submission submission); @@ -864,19 +846,16 @@ struct Plot::Private { void publish_completed_frame(Render_Frame* frame); void frame_completed(); }; - nlohmann::json Plot::Private::schema() const { auto result = view->schema(); result["components"].push_back(frame_policy.schema()); return result; } - Plot::Private::Managed_Frame Plot::Private::make_frame(Frame_Submission submission) { const Frame_Identity identity{next_frame_sequence++, submission.request.correlation_id}; if (std::holds_alternative>(scene)) return {submission.owner, std::move(submission.request), std::make_unique(identity)}; return {submission.owner, std::move(submission.request), std::make_unique(identity)}; } - void Plot::Private::request_frame(Frame_Submission submission) { auto frame = make_frame(std::move(submission)); if (frame_in_flight) { @@ -891,7 +870,6 @@ void Plot::Private::request_frame(Frame_Submission submission) { } render_frame(std::move(frame)); } - void Plot::Private::render_frame(Managed_Frame frame) { frame_in_flight = true; active_frame = std::move(frame); @@ -902,15 +880,20 @@ void Plot::Private::render_frame(Managed_Frame frame) { if (auto* scene_2d = std::get_if>(&scene)) { (*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(Size{static_cast(request.width), static_cast(request.height)}); const auto result = (*scene_2d)->render(std::get>(active_frame->frame).get()); - if (result != Render_Scene_2D::Render_Result::completed) asio::post(strand, [this] { frame_completed(); }); + if (result != Render_Scene_2D::Render_Result::completed) + asio::post(strand, [this] { + frame_completed(); + }); return; } auto& scene_3d = std::get>(scene); scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{request.width, request.height}); const auto result = scene_3d->render(std::get>(active_frame->frame).get()); - if (result != Render_Scene_3D::Render_Result::submitted) asio::post(strand, [this] { frame_completed(); }); + if (result != Render_Scene_3D::Render_Result::submitted) + asio::post(strand, [this] { + frame_completed(); + }); } - void Plot::Private::publish_completed_frame(Render_Frame* frame) { if (!active_frame) throw std::logic_error("frame callback has no externally owned active frame"); const auto pacing = frame_policy.snapshot(); @@ -919,7 +902,6 @@ void Plot::Private::publish_completed_frame(Render_Frame* frame) { else throw std::logic_error("frame callback does not match the externally owned active frame"); frame_completed(); } - void Plot::Private::frame_completed() { active_frame.reset(); frame_in_flight = false; @@ -932,19 +914,15 @@ void Plot::Private::frame_completed() { return; } } - Plot::Plot(asio::any_io_executor executor, std::unique_ptr scene, - std::unique_ptr view) - : d(std::make_unique(std::move(executor), std::move(scene), std::move(view))) {} - + std::unique_ptr view) : d(std::make_unique(std::move(executor), std::move(scene), std::move(view))) {} Plot::Plot(asio::any_io_executor executor, std::unique_ptr scene, - std::unique_ptr view) - : d(std::make_unique(std::move(executor), std::move(scene), std::move(view))) {} - -Plot::~Plot() { d->inputs.close(); } - + std::unique_ptr view) : d(std::make_unique(std::move(executor), std::move(scene), std::move(view))) {} +Plot::~Plot() { + d->inputs.close(); +} void Plot::ensure_started() { std::call_once(d->start_once, [this] { auto self = shared_from_this(); @@ -952,15 +930,20 @@ void Plot::ensure_started() { (*scene)->set_frame_callback([weak = weak_from_this()](Frame_2D* frame) { if (auto owner = weak.lock()) { frame->mark(Frame_Trace_Marker::callback_finished); - asio::post(owner->d->strand, [weak, frame] { if (auto next_owner = weak.lock()) next_owner->d->publish_completed_frame(frame); }); + asio::post(owner->d->strand, [weak, frame] { + if (auto next_owner = weak.lock()) next_owner->d->publish_completed_frame(frame); + }); } }); - } else { + } + else { std::get>(d->scene)->set_frame_callback( [weak = weak_from_this()](Frame_3D* frame) { if (auto owner = weak.lock()) { frame->mark(Frame_Trace_Marker::callback_finished); - asio::post(owner->d->strand, [weak, frame] { if (auto next_owner = weak.lock()) next_owner->d->publish_completed_frame(frame); }); + asio::post(owner->d->strand, [weak, frame] { + if (auto next_owner = weak.lock()) next_owner->d->publish_completed_frame(frame); + }); } }); } @@ -976,8 +959,8 @@ void Plot::ensure_started() { } if (auto* write = std::get_if(&input)) { write->handler(write->component == "frame-runtime" - ? self->d->frame_policy.write_prop(write->key, write->value) - : self->d->view->write_prop(write->component, write->key, write->value)); + ? self->d->frame_policy.write_prop(write->key, write->value) + : self->d->view->write_prop(write->component, write->key, write->value)); continue; } auto submission = std::get(input); @@ -988,7 +971,6 @@ void Plot::ensure_started() { }); }); } - void Plot::attach(const void* owner, Frame_Handler handler) { ensure_started(); { @@ -996,60 +978,53 @@ void Plot::attach(const void* owner, Frame_Handler handler) { d->handlers.insert_or_assign(owner, std::move(handler)); } } - void Plot::detach(const void* owner) { std::lock_guard lock(d->handlers_mutex); d->handlers.erase(owner); } - void Plot::submit_frame(const void* owner, Plot_Frame_Request request) { ensure_started(); static_cast(d->inputs.try_send(asio::error_code{}, Plot_Input{Frame_Submission{owner, std::move(request)}})); } - void Plot::submit_input(Plot_Input_Event event) { ensure_started(); if (auto* scene = std::get_if>(&d->scene)) dispatch_plot_input(**scene, event); else dispatch_plot_input(*std::get>(d->scene), event); } - void Plot::async_schema(Json_Handler handler) { ensure_started(); - if (!d->inputs.try_send(asio::error_code{}, Plot_Input{Schema_Query{std::move(handler)}})) - throw std::runtime_error("plot input queue is unavailable"); + 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 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(component), 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"); } - std::shared_ptr make_datoviz_point_plot(asio::any_io_executor executor) { using Visual_Object = Impl; auto visual = *Visual_Object::Builder{}.build(); static_cast(visual->update_items({ render_3d::Point{.position = {-0.55F, -0.2F, 0.0F}, .color = Color::red_color(), .diameter_px = 24.0F}, render_3d::Point{.position = {0.0F, 0.5F, 0.0F}, .color = Color::green_color(), .diameter_px = 30.0F}, - render_3d::Point{.position = {0.55F, -0.1F, 0.0F}, .color = Color{42, 120, 255, 255}, .diameter_px = 26.0F}})); + render_3d::Point{.position = {0.55F, -0.1F, 0.0F}, .color = Color{42, 120, 255, 255}, .diameter_px = 26.0F} + })); visual->advance(); - /* 保留派生 Builder 身份;链式 set() 返回公共 Builder,不能用其 build() 绕过 3D 后端挂接。 */ - Scene_3D::Builder scene_builder(visual.get()); - scene_builder - .set(&Render_Scene_3D::Prop::viewport, Extent{720, 420}) - .set(&Render_Scene_3D::Prop::view_active, true); - auto scene_result = scene_builder.build(); + auto scene_result = Scene_3D::Builder(visual.get()) + .set(&Render_Scene_3D::Prop::viewport, Extent{720, 420}) + .set(&Render_Scene_3D::Prop::view_active, true) + .build(); if (!scene_result) throw std::logic_error("Datoviz point plot dependency graph is invalid"); auto scene = std::move(scene_result).value(); using Adapter = detail::Renderable_Adapter, - detail::Prop_Field<&Point_Visual::Prop::visible, "visible", "Controls whether the point visual participates in scene rendering.">, - detail::Prop_Field<&Point_Visual::Prop::depth_test, "depth_test", "Enables depth testing when point fragments are rendered.">, - detail::Prop_Field<&Point_Visual::Prop::items, "items", "Complete collection of 3D points submitted to the visual.">, - detail::State_Field, - detail::State_Field, - detail::State_Field>; + detail::Prop_Field<&Point_Visual::Prop::transform, "transform", "World transform applied to every point in the visual.">, + detail::Prop_Field<&Point_Visual::Prop::visible, "visible", "Controls whether the point visual participates in scene rendering.">, + detail::Prop_Field<&Point_Visual::Prop::depth_test, "depth_test", "Enables depth testing when point fragments are rendered.">, + detail::Prop_Field<&Point_Visual::Prop::items, "items", "Complete collection of 3D points submitted to the visual.">, + detail::State_Field, + detail::State_Field, + detail::State_Field>; std::vector> components; components.push_back(make_scene_component(*scene)); components.push_back(detail::make_renderable_descriptor("plot", "主绘图组件", "visual", Adapter{*visual}));