#include "Plot.hpp" #include "Renderable_Adapter.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace aethera::web { namespace { using namespace render_2d; using namespace render_3d; using Scene_2D = Impl; using Scene_3D = Impl; using Frequency_Axis_Object = Impl; using Numeric_Axis_Object = Impl; using Time_Axis_Object = Impl; template void append_binary(std::string& output, Integer value) { const auto start = output.size(); output.resize(start + sizeof(Integer)); std::memcpy(output.data() + start, &value, sizeof(Integer)); } std::string encode_frame(Image_View image, std::uint64_t sequence) { std::string output; output.reserve(24 + static_cast(image.width) * image.height * 4); append_binary(output, std::uint32_t{0x41544852}); append_binary(output, std::uint16_t{1}); append_binary(output, std::uint16_t{}); append_binary(output, static_cast(image.width)); append_binary(output, static_cast(image.height)); append_binary(output, sequence); for (int y = 0; y < image.height; ++y) { const auto* row = reinterpret_cast( image.data + static_cast(y) * image.stride); for (int x = 0; x < image.width; ++x) { const auto* pixel = row + x * 4; output.push_back(static_cast(pixel[2])); output.push_back(static_cast(pixel[1])); output.push_back(static_cast(pixel[0])); output.push_back(static_cast(pixel[3])); } } return output; } std::string encode_frame(const Pixel_Frame& frame, std::uint64_t sequence) { std::string output; output.reserve(24 + frame.rgba8.size()); append_binary(output, std::uint32_t{0x41544852}); append_binary(output, std::uint16_t{1}); append_binary(output, std::uint16_t{}); append_binary(output, frame.extent.width); append_binary(output, frame.extent.height); append_binary(output, sequence); output.append(reinterpret_cast(frame.rgba8.data()), frame.rgba8.size()); return output; } struct Schema_Query { Plot::Json_Handler handler; }; struct Prop_Write { std::string key; nlohmann::json value; Plot::Json_Handler handler; }; using Plot_Input = std::variant; template class Scene_View_Model final : public Plot::Scene_View { public: 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 { return descriptor->write_prop(key, value); } void update(const Plot_Event& event) override { update_scene(event); } private: std::unique_ptr descriptor; 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_scene_view( Object& object, 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...>>( detail::make_renderable_descriptor(Adapter{object}), std::move(update), std::forward(owned_objects)...); } Frequency_Axis_Object::Builder frequency_axis_builder(Size canvas) { 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(&Abs_Axis::Prop::canvas_size, canvas) .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, Size canvas) { 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(&Abs_Axis::Prop::canvas_size, canvas) .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, Size canvas) { 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::canvas_size, canvas); return builder; } template void resize_axes(Size viewport, Axes*... axes) { (axes->template set<&Abs_Axis::Prop::canvas_size>(viewport), ...); } } std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; 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(); 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(); 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()); auto scene = *scene_builder.build(); 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) { 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, 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 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(time.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(trace.get()); auto scene = *scene_builder.build(); auto update = [raw = trace.get(), time = time.get(), vertical = vertical.get(), tick = std::uint64_t{}](const Plot_Event& event) mutable { 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 + 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, 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 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(); Impl::Builder sweep_builder(frequency.get(), vertical.get()); sweep_builder.set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}); auto sweep = *sweep_builder.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(sweep.get()); auto scene = *scene_builder.build(); 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); }; 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", "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 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(); 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(); 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()); auto scene = *scene_builder.build(); 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) 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, 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 frequency = *frequency_axis_builder(canvas).build(); auto time = *time_axis_builder( Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas).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(); 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()); auto scene = *scene_builder.build(); 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); }; 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, 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 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(); 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(); 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()); auto scene = *scene_builder.build(); 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)}); }; 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, 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 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(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 = [horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { resize_axes({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, 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>; asio::strand strand; asio::experimental::concurrent_channel inputs; Scene scene; std::unique_ptr view; std::once_flag start_once; std::mutex handlers_mutex; std::unordered_map handlers; std::atomic_uint64_t frame_sequence{}; 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), scene(std::move(value_scene)), view(std::move(value_view)) {} void publish(std::string pixels) { std::vector outputs; { std::lock_guard lock(handlers_mutex); outputs.reserve(handlers.size()); for (const auto& [owner, handler] : handlers) outputs.push_back(handler); } for (auto& output : outputs) output(pixels); } }; 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(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(); } void Plot::ensure_started() { std::call_once(d->start_once, [this] { auto self = shared_from_this(); if (auto* scene = std::get_if>(&d->scene)) { (*scene)->set_frame_callback([weak = weak_from_this()](Image_View image) { if (auto owner = weak.lock()) { const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; owner->d->publish(encode_frame(image, sequence)); } }); } else { std::get>(d->scene)->set_frame_callback( [weak = weak_from_this()](std::shared_ptr frame) { if (auto owner = weak.lock()) { const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; owner->d->publish(encode_frame(*frame, sequence)); } }); } asio::co_spawn(d->strand, [self]() -> asio::awaitable { for (;;) { asio::error_code error; auto input = co_await self->d->inputs.async_receive( asio::redirect_error(asio::use_awaitable, error)); if (error) co_return; if (auto* query = std::get_if(&input)) { query->handler(self->d->view->schema()); continue; } if (auto* write = std::get_if(&input)) { write->handler(self->d->view->write_prop(write->key, write->value)); continue; } auto event = std::get(input); event.width = std::clamp(event.width, 160U, 1920U); event.height = std::clamp(event.height, 120U, 1080U); self->d->view->update(event); if (auto* scene = std::get_if>(&self->d->scene)) { (*scene)->set<&Render_Scene_2D::Prop::viewport>( Size{static_cast(event.width), static_cast(event.height)}); (*scene)->render(); } else { auto& scene_3d = std::get>(self->d->scene); scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{event.width, event.height}); scene_3d->render(); } } }, [](std::exception_ptr exception) { if (exception) std::rethrow_exception(exception); }); }); } void Plot::attach(const void* owner, Frame_Handler handler) { ensure_started(); { std::lock_guard lock(d->handlers_mutex); d->handlers.insert_or_assign(owner, std::move(handler)); } submit({}); } void Plot::detach(const void* owner) { std::lock_guard lock(d->handlers_mutex); d->handlers.erase(owner); } void Plot::submit(Plot_Event event) { ensure_started(); static_cast(d->inputs.try_send(asio::error_code{}, Plot_Input{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"); } void Plot::async_write_prop(std::string key, nlohmann::json value, Json_Handler handler) { ensure_started(); if (!d->inputs.try_send(asio::error_code{}, Plot_Input{ Prop_Write{std::move(key), std::move(value), std::move(handler)}})) 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}})); visual->advance(); auto scene = *Scene_3D::Builder(visual.get()) .set(&Render_Scene_3D::Prop::viewport, Extent{720, 420}) .set(&Render_Scene_3D::Prop::view_active, true) .build(); 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>; 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)); } }