From 8d5407ea900353cf013faa0ca53f16c71fa6f7be Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 21 Aug 2026 14:24:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_server/src/Plot.cpp | 486 +++++++++++++------------- web_server/src/Renderable_Adapter.hpp | 4 +- web_server/src/Renderable_Adapter.ipp | 31 +- webapp_gallery/src/app.tsx | 12 +- 4 files changed, 265 insertions(+), 268 deletions(-) diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index 1e3a1ff..521d8cd 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -30,18 +31,6 @@ using Frequency_Axis_Object = Impl; using Numeric_Axis_Object = Impl; using Time_Axis_Object = Impl; -template -auto finish_build(Builder&& builder) { - auto result = builder.build(); - if (!result) throw std::logic_error("gallery scene topology validation failed"); - return std::move(result).value(); -} - -template -std::shared_ptr finish_shared(Builder&& builder) { - return std::shared_ptr(finish_build(std::forward(builder))); -} - template void append_binary(std::string& output, Integer value) { const auto start = output.size(); @@ -93,14 +82,15 @@ struct Prop_Write { }; using Plot_Input = std::variant; +template class Scene_View_Model final : public Plot::Scene_View { public: - Scene_View_Model(std::vector> owned_objects, - std::unique_ptr value_descriptor, - std::function value_update) - : objects(std::move(owned_objects)), - descriptor(std::move(value_descriptor)), - update_scene(std::move(value_update)) {} + Scene_View_Model(std::unique_ptr value_descriptor, + std::function value_update, + Owned_Objects... owned_objects) + : descriptor(std::move(value_descriptor)), + update_scene(std::move(value_update)), + objects(std::move(owned_objects)...) {} nlohmann::json schema() const override { return descriptor->schema(); } nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override { @@ -109,40 +99,40 @@ public: void update(const Plot_Event& event) override { update_scene(event); } private: - std::vector> objects; std::unique_ptr descriptor; std::function update_scene; + std::tuple objects; }; -template -using Prop_Field = detail::Prop_Field; +template +using Prop_Field = detail::Prop_Field; -template -using State_Field = detail::State_Field; +template +using State_Field = detail::State_Field; -template +template std::unique_ptr make_scene_view( Object& object, - std::vector> owners, - std::function update) { + std::function update, + Owned_Objects&&... owned_objects) { using Definition = typename Object::Attached_Object; 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>; - return std::make_unique( - std::move(owners), + 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 std::make_unique...>>( detail::make_renderable_descriptor(Adapter{object}), - std::move(update)); + std::move(update), + std::forward(owned_objects)...); } Frequency_Axis_Object::Builder frequency_axis_builder(Size canvas) { @@ -185,31 +175,25 @@ void resize_axes(Size viewport, Axes*... axes) { (axes->template set<&Abs_Axis::Prop::canvas_size>(viewport), ...); } -std::unique_ptr make_scene_2d() { - constexpr Size canvas{720, 420}; - Scene_2D::Builder scene_builder; - scene_builder - .set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true); - return finish_build(std::move(scene_builder)); -} } std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; - auto scene = make_scene_2d(); - auto frequency = finish_shared(frequency_axis_builder(canvas)); - auto vertical = finish_shared(numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas)); - Impl::Builder builder(scene.get(), frequency.get(), vertical.get()); - builder + 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) + .build(); + auto frequency = *frequency_axis_builder(canvas).build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build(); + auto spectrum = *Impl::Builder(scene.get(), frequency.get(), vertical.get()) .set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) - .set(&Spectrum::Prop::max_hold_visible, true); - auto object = finish_shared>(std::move(builder)); - object->mark_dirty(); - object->mark_dirty(); - auto update = [raw = object.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { + .set(&Spectrum::Prop::max_hold_visible, true) + .build(); + spectrum->mark_dirty(); + spectrum->mark_dirty(); + auto update = [raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, vertical); std::array samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { @@ -220,120 +204,126 @@ std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { } raw->update_samples(samples); }; - std::vector> owners{frequency, vertical, object}; auto view = make_scene_view< - Prop_Field<&Spectrum::Prop::center_frequency, "center_frequency">, - Prop_Field<&Spectrum::Prop::partition_count, "partition_count">, - Prop_Field<&Spectrum::Prop::max_hold_visible, "max_hold_visible">, - Prop_Field<&Spectrum::Prop::min_hold_visible, "min_hold_visible">, - Prop_Field<&Spectrum::Prop::max_marker_visible, "max_marker_visible">, - Prop_Field<&Spectrum::Prop::min_marker_visible, "min_marker_visible">, - Prop_Field<&Spectrum::Prop::sweep_region_visible, "sweep_region_visible">, - Prop_Field<&Spectrum::Prop::visible_range_only, "visible_range_only">, - Prop_Field<&Spectrum::Prop::frequency_range, "frequency_range">, - Prop_Field<&Spectrum::Prop::sweep_frequency_range, "sweep_frequency_range">, - Prop_Field<&Spectrum::Prop::partition_mode, "partition_mode">, - Prop_Field<&Spectrum::Prop::interpolation_mode, "interpolation_mode">, - Prop_Field<&Spectrum::Prop::max_brush, "max_brush">, - Prop_Field<&Spectrum::Prop::current_brush, "current_brush">, - Prop_Field<&Spectrum::Prop::min_brush, "min_brush">, - Prop_Field<&Spectrum::Prop::max_pen, "max_pen">, - Prop_Field<&Spectrum::Prop::current_pen, "current_pen">, - Prop_Field<&Spectrum::Prop::min_pen, "min_pen">, - Prop_Field<&Spectrum::Prop::selected_marker_pen, "selected_marker_pen">, - Prop_Field<&Spectrum::Prop::marker_pen, "marker_pen">, - Prop_Field<&Spectrum::Prop::middle_frequency_pen, "middle_frequency_pen">, - Prop_Field<&Spectrum::Prop::sweep_region_brush, "sweep_region_brush">, - Prop_Field<&Spectrum::Prop::custom_markers, "custom_markers">, - Prop_Field<&Spectrum::Prop::selected_marker, "selected_marker">, - State_Field, - State_Field, - State_Field>( - *object, std::move(owners), std::move(update)); + 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, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum)); 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 scene = make_scene_2d(); - auto time = finish_shared(time_axis_builder( - Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas)); - auto vertical = finish_shared(numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas)); - Impl::Builder builder(scene.get(), time.get(), vertical.get()); - auto object = finish_shared>(std::move(builder)); - object->mark_dirty(); - object->mark_dirty(); - auto tick = std::make_shared(); - auto update = [raw = object.get(), time = time.get(), vertical = vertical.get(), tick](const Plot_Event& event) { + 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) + .build(); + auto time = *time_axis_builder( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas).build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas).build(); + auto trace = *Impl::Builder(scene.get(), time.get(), vertical.get()).build(); + trace->mark_dirty(); + trace->mark_dirty(); + auto update = [raw = trace.get(), time = time.get(), vertical = vertical.get(), tick = std::uint64_t{}](const Plot_Event& event) mutable { resize_axes({static_cast(event.width), static_cast(event.height)}, time, vertical); - raw->append_sample((*tick)++, std::sin(event.time_milliseconds * 0.0025) * 0.8 + raw->append_sample(tick++, std::sin(event.time_milliseconds * 0.0025) * 0.8 + std::sin(event.time_milliseconds * 0.0007) * 0.2); }; - std::vector> owners{time, vertical, object, tick}; auto view = make_scene_view< - Prop_Field<&Frequency_Trace::Prop::partition_count, "partition_count">, - Prop_Field<&Frequency_Trace::Prop::pen, "pen">, - Prop_Field<&Frequency_Trace::Prop::partition_mode, "partition_mode">, - Prop_Field<&Frequency_Trace::Prop::samples, "samples">, - State_Field, - State_Field>( - *object, std::move(owners), std::move(update)); + 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, std::move(update), std::move(time), std::move(vertical), std::move(trace)); 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 scene = make_scene_2d(); - auto frequency = finish_shared(frequency_axis_builder(canvas)); - auto vertical = finish_shared(numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas)); - Impl::Builder builder(scene.get(), frequency.get(), vertical.get()); - builder.set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}); - auto object = finish_shared>(std::move(builder)); - object->mark_dirty(); - object->mark_dirty(); - auto update = [raw = object.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { + 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) + .build(); + auto frequency = *frequency_axis_builder(canvas).build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build(); + auto sweep = *Impl::Builder(scene.get(), frequency.get(), vertical.get()) + .set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .build(); + sweep->mark_dirty(); + sweep->mark_dirty(); + auto update = [raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, vertical); std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -90.0 + 35.0 * std::sin(i * 0.08 + event.time_milliseconds * 0.002); raw->append_block(values); }; - std::vector> owners{frequency, vertical, object}; auto view = make_scene_view< - Prop_Field<&Sweep_Spectrum::Prop::bins_per_block, "bins_per_block">, - Prop_Field<&Sweep_Spectrum::Prop::block_count, "block_count">, - Prop_Field<&Sweep_Spectrum::Prop::partition_count, "partition_count">, - Prop_Field<&Sweep_Spectrum::Prop::visible_range_only, "visible_range_only">, - Prop_Field<&Sweep_Spectrum::Prop::frequency_range, "frequency_range">, - Prop_Field<&Sweep_Spectrum::Prop::partition_mode, "partition_mode">, - Prop_Field<&Sweep_Spectrum::Prop::pen, "pen">, - Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen">, - Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode">, - Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks">, - State_Field, - State_Field, - State_Field>( - *object, std::move(owners), std::move(update)); + 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", "Current collection of incremental sweep blocks.">, + State_Field, + State_Field, + State_Field>( + *sweep, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep)); 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 scene = make_scene_2d(); - auto frequency = finish_shared(frequency_axis_builder(canvas)); - auto vertical = finish_shared(numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas)); - Impl::Builder builder(scene.get(), frequency.get(), vertical.get()); - builder + 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) + .build(); + auto frequency = *frequency_axis_builder(canvas).build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build(); + auto afterglow = *Impl::Builder(scene.get(), frequency.get(), vertical.get()) .set(&Afterglow::Prop::frequency_range, Axis_Range{0.0, 100.0}) .set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0}) - .set(&Afterglow::Prop::power_point_size, 96); - auto object = finish_shared>(std::move(builder)); - object->mark_dirty(); - object->mark_dirty(); - auto update = [raw = object.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { + .set(&Afterglow::Prop::power_point_size, 96) + .build(); + afterglow->mark_dirty(); + afterglow->mark_dirty(); + auto update = [raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, vertical); std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) @@ -342,130 +332,135 @@ std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { - 0.18 * std::sin(event.time_milliseconds * 0.0008), 2.0)); raw->append_spectrum(values); }; - std::vector> owners{frequency, vertical, object}; auto view = make_scene_view< - Prop_Field<&Afterglow::Prop::frequency_point_size, "frequency_point_size">, - Prop_Field<&Afterglow::Prop::power_point_size, "power_point_size">, - Prop_Field<&Afterglow::Prop::partition_count, "partition_count">, - Prop_Field<&Afterglow::Prop::interpolate, "interpolate">, - Prop_Field<&Afterglow::Prop::attenuation_rate, "attenuation_rate">, - Prop_Field<&Afterglow::Prop::frequency_range, "frequency_range">, - Prop_Field<&Afterglow::Prop::power_range, "power_range">, - Prop_Field<&Afterglow::Prop::partition_mode, "partition_mode">, - Prop_Field<&Afterglow::Prop::color_map, "color_map">, - Prop_Field<&Afterglow::Prop::spectra, "spectra">, - State_Field, - State_Field, - State_Field>( - *object, std::move(owners), std::move(update)); + 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, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow)); 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 scene = make_scene_2d(); - auto frequency = finish_shared(frequency_axis_builder(canvas)); - auto time = finish_shared(time_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas)); - Impl::Builder builder(scene.get(), frequency.get(), time.get()); - builder + 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) + .build(); + auto frequency = *frequency_axis_builder(canvas).build(); + auto time = *time_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas).build(); + auto waterfall = *Impl::Builder(scene.get(), frequency.get(), time.get()) .set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0}) - .set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0}); - auto object = finish_shared>(std::move(builder)); - object->mark_dirty(); - object->mark_dirty(); - auto tick = std::make_shared(); - auto update = [raw = object.get(), frequency = frequency.get(), time = time.get(), tick](const Plot_Event& event) { + .set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0}) + .build(); + waterfall->mark_dirty(); + waterfall->mark_dirty(); + auto update = [raw = waterfall.get(), frequency = frequency.get(), time = time.get(), tick = std::uint64_t{}](const Plot_Event& event) mutable { resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, time); std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow( static_cast(i) / values.size() - 0.5 - 0.22 * std::sin(event.time_milliseconds * 0.0006), 2.0)); - raw->append_row((*tick)++, values); + raw->append_row(tick++, values); }; - std::vector> owners{frequency, time, object, tick}; auto view = make_scene_view< - Prop_Field<&Waterfall::Prop::tooltip_enabled, "tooltip_enabled">, - Prop_Field<&Waterfall::Prop::tooltip_font, "tooltip_font">, - Prop_Field<&Waterfall::Prop::tooltip_text_pen, "tooltip_text_pen">, - Prop_Field<&Waterfall::Prop::tooltip_background_brush, "tooltip_background_brush">, - Prop_Field<&Waterfall::Prop::frequency_bin_count, "frequency_bin_count">, - Prop_Field<&Waterfall::Prop::partition_count, "partition_count">, - Prop_Field<&Waterfall::Prop::visible_range_only, "visible_range_only">, - Prop_Field<&Waterfall::Prop::frequency_range, "frequency_range">, - Prop_Field<&Waterfall::Prop::power_range, "power_range">, - Prop_Field<&Waterfall::Prop::partition_mode, "partition_mode">, - Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode">, - Prop_Field<&Waterfall::Prop::color_map, "color_map">, - Prop_Field<&Waterfall::Prop::rows, "rows">, - State_Field, - State_Field, - State_Field>( - *object, std::move(owners), std::move(update)); + 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, std::move(update), std::move(frequency), std::move(time), std::move(waterfall)); 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 scene = make_scene_2d(); - auto horizontal = finish_shared(numeric_axis_builder( - Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}, canvas)); - auto vertical = finish_shared(numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas)); - Impl::Builder builder(scene.get(), horizontal.get(), vertical.get()); - builder + 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) + .build(); + auto horizontal = *numeric_axis_builder( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}, canvas).build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas).build(); + auto constellation = *Impl::Builder(scene.get(), horizontal.get(), vertical.get()) .set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2}) - .set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2}); - auto object = finish_shared>(std::move(builder)); - object->mark_dirty(); - object->mark_dirty(); - auto update = [raw = object.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { + .set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2}) + .build(); + constellation->mark_dirty(); + constellation->mark_dirty(); + auto update = [raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { resize_axes({static_cast(event.width), static_cast(event.height)}, horizontal, vertical); const double phase = event.time_milliseconds * 0.003; raw->append_point({std::cos(phase) * 0.82 + 0.04 * std::sin(phase * 7.0), std::sin(phase) * 0.82 + 0.04 * std::cos(phase * 5.0)}); }; - std::vector> owners{horizontal, vertical, object}; auto view = make_scene_view< - Prop_Field<&Constellation_Diagram::Prop::point_lifetime_ms, "point_lifetime_ms">, - Prop_Field<&Constellation_Diagram::Prop::type, "type">, - Prop_Field<&Constellation_Diagram::Prop::phase_offset_radians, "phase_offset_radians">, - Prop_Field<&Constellation_Diagram::Prop::i_range, "i_range">, - Prop_Field<&Constellation_Diagram::Prop::q_range, "q_range">, - Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color">, - Prop_Field<&Constellation_Diagram::Prop::anchor_color, "anchor_color">, - Prop_Field<&Constellation_Diagram::Prop::points, "points">, - State_Field>( - *object, std::move(owners), std::move(update)); + 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, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation)); 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 scene = make_scene_2d(); - auto horizontal = finish_shared(numeric_axis_builder( - Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}, canvas)); - auto vertical = finish_shared(numeric_axis_builder( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}, canvas)); - Impl::Builder builder(scene.get(), horizontal.get(), vertical.get()); - auto object = finish_shared>(std::move(builder)); - object->mark_dirty(); - object->mark_dirty(); + auto 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) + .build(); + auto horizontal = *numeric_axis_builder( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}, canvas).build(); + auto vertical = *numeric_axis_builder( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}, canvas).build(); + auto selection = *Impl::Builder(scene.get(), horizontal.get(), vertical.get()).build(); + selection->mark_dirty(); + selection->mark_dirty(); auto update = [horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { resize_axes({static_cast(event.width), static_cast(event.height)}, horizontal, vertical); }; - std::vector> owners{horizontal, vertical, object}; auto view = make_scene_view< - Prop_Field<&Selection_Rectangle_Overlay::Prop::label_font, "label_font">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions">, + 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>( - *object, std::move(owners), std::move(update)); + "selected_region_count", + "Number of rectangular regions currently selected.">>( + *selection, 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)); } @@ -597,30 +592,27 @@ void Plot::async_write_prop(std::string key, nlohmann::json value, Json_Handler std::shared_ptr make_datoviz_point_plot(asio::any_io_executor executor) { using Visual_Object = Impl; - Visual_Object::Builder visual_builder; - auto visual = finish_shared(std::move(visual_builder)); + 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}})); visual->advance(); - Scene_3D::Builder scene_builder(visual.get()); - scene_builder + auto scene = *Scene_3D::Builder(visual.get()) .set(&Render_Scene_3D::Prop::viewport, Extent{720, 420}) - .set(&Render_Scene_3D::Prop::view_active, true); - auto scene = finish_build(std::move(scene_builder)); + .set(&Render_Scene_3D::Prop::view_active, true) + .build(); using Adapter = detail::Renderable_Adapter, - detail::Prop_Field<&Point_Visual::Prop::visible, "visible">, - detail::Prop_Field<&Point_Visual::Prop::depth_test, "depth_test">, - detail::Prop_Field<&Point_Visual::Prop::items, "items">, - detail::State_Field, - detail::State_Field, - detail::State_Field>; - std::vector> owners{visual}; - auto view = std::make_unique( - std::move(owners), detail::make_renderable_descriptor(Adapter{*visual}), - [](const Plot_Event&) {}); + 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>; + auto view = std::make_unique>( + detail::make_renderable_descriptor(Adapter{*visual}), + [](const Plot_Event&) {}, std::move(visual)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } } diff --git a/web_server/src/Renderable_Adapter.hpp b/web_server/src/Renderable_Adapter.hpp index d7a7a1d..434b2bb 100644 --- a/web_server/src/Renderable_Adapter.hpp +++ b/web_server/src/Renderable_Adapter.hpp @@ -13,10 +13,10 @@ enum class Renderable_Field_Role { template struct Renderable_Field_Role_Attribute; -template +template struct Prop_Field; -template +template struct State_Field; template diff --git a/web_server/src/Renderable_Adapter.ipp b/web_server/src/Renderable_Adapter.ipp index 13ce7ee..e8b813a 100644 --- a/web_server/src/Renderable_Adapter.ipp +++ b/web_server/src/Renderable_Adapter.ipp @@ -89,18 +89,20 @@ struct Renderable_Field_Role_Attribute { template inline constexpr Renderable_Field_Role_Attribute renderable_field_role{}; -template +template struct Prop_Field { static constexpr auto member = Member; static constexpr auto key = Key; + static constexpr auto description = Description; static constexpr auto role = Renderable_Field_Role::prop; }; -template +template struct State_Field { using tag_type = Tag; static constexpr auto member = Member; static constexpr auto key = Key; + static constexpr auto description = Description; static constexpr auto role = Renderable_Field_Role::state; }; @@ -128,12 +130,13 @@ private: template struct Renderable_Field_Accessor; -template -struct Renderable_Field_Accessor, Prop_Field> { +template +struct Renderable_Field_Accessor, Prop_Field> { using object_type = Renderable_Adapter; using value_type = typename structive::Member_Pointer_Traits::value_type; using storage_identity = void; using dependency_spec = structive::No_Property_Dependencies; + static constexpr auto description = Description; static constexpr bool readable = true; static constexpr bool writable = true; static constexpr bool synchronized_view_read = false; @@ -142,12 +145,13 @@ struct Renderable_Field_Accessor, Prop_Fie void write(object_type& adapter, value_type value) const; }; -template -struct Renderable_Field_Accessor, State_Field> { +template +struct Renderable_Field_Accessor, State_Field> { using object_type = Renderable_Adapter; using value_type = typename structive::Member_Pointer_Traits::value_type; using storage_identity = void; using dependency_spec = structive::No_Property_Dependencies; + static constexpr auto description = Description; static constexpr bool readable = true; static constexpr bool writable = false; static constexpr bool synchronized_view_read = false; @@ -170,18 +174,18 @@ namespace aethera::web::detail { template Renderable_Adapter::Renderable_Adapter(Object& value) : object(&value) {} -template -auto Renderable_Field_Accessor, Prop_Field>::read(const object_type& adapter) const -> value_type { +template +auto Renderable_Field_Accessor, Prop_Field>::read(const object_type& adapter) const -> value_type { return adapter.object->template get(); } -template -void Renderable_Field_Accessor, Prop_Field>::write(object_type& adapter, value_type value) const { +template +void Renderable_Field_Accessor, Prop_Field>::write(object_type& adapter, value_type value) const { adapter.object->template set(std::move(value)); } -template -auto Renderable_Field_Accessor, State_Field>::read(const object_type& adapter) const -> value_type { +template +auto Renderable_Field_Accessor, State_Field>::read(const object_type& adapter) const -> value_type { value_type result{}; adapter.object->template access_state([&](const auto& state) { result = state.*Member; }); return result; @@ -225,7 +229,8 @@ template using Field = std::remove_cvref_t; using Value = typename Field::value_type; const auto role = field.template attribute().value; - nlohmann::json item{{"key", field.key()}, {"type", json_value_type()}}; + nlohmann::json item{{"key", field.key()}, {"type", json_value_type()}, + {"description", std::string(Field::accessor_type::description.view())}}; const auto value = field.accessor.read(adapter); item["value"] = adminive::encode_json_value(value); if constexpr (std::is_enum_v) { diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 00766de..d82f9e9 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -10,7 +10,7 @@ type Plot = { schema: string; }; type Option = {value: string; label: string}; -type Field = {key: string; type: "boolean" | "integer" | "number" | "select" | "array" | "object"; value: unknown; options?: Option[]}; +type Field = {key: string; description: string; type: "boolean" | "integer" | "number" | "select" | "array" | "object"; value: unknown; options?: Option[]}; type Schema = {prop: Field[]; state: Field[]}; const protocolHeaderSize = 24; @@ -65,14 +65,14 @@ function JsonControl({field, onChange}: {field: Field; onChange: (value: unknown try { onChange(JSON.parse(draft)); setInvalid(false); } catch { setInvalid(true); } }; - return