From d46eea203ec7104b8d11f5a32b4e455ab534499a Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 3 Sep 2026 12:38:51 +0800 Subject: [PATCH] =?UTF-8?q?2D=E7=A7=BB=E6=A4=8D=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/kernel/module/renderable/rely_facade.h | 2 +- .../module/renderable/src/renderable.cpp | 1 + .../render_2D/module/plottable/Afterglow.ipp | 199 +++++++++--- .../plottable/Constellation_Diagram.ipp | 163 +++++++--- .../module/plottable/Frequency_Trace.ipp | 130 +++++--- .../plottable/Selection_Rectangle_Overlay.ipp | 193 ++++++------ .../render_2D/module/plottable/Spectrum.ipp | 284 +++++++++++++----- .../module/plottable/Sweep_Spectrum.ipp | 197 ++++++++---- .../render_2D/module/plottable/Waterfall.ipp | 261 ++++++++++------ .../module/plottable/axis/Abs_Axis.ipp | 114 ++++--- .../module/plottable/axis/Numeric_Axis.ipp | 23 +- .../module/plottable/axis/Time_Axis.ipp | 14 +- .../module/plottable/common/Curve_Plot.cpp | 95 ++---- .../module/plottable/common/Curve_Plot.hpp | 52 ++-- .../module/plottable/common/Raster_Plot.cpp | 103 ++++--- .../module/plottable/common/Raster_Plot.hpp | 60 ++-- .../module/renderable/Renderable_2D.ipp | 56 ++-- .../module/scene/Render_Scene_2D.cpp | 71 ++--- .../module/scene/Render_Scene_2D.ipp | 1 - .../render_2D/module/scene/rely_facade.h | 3 +- .../tests/Async_Render_Contract_Test.cpp | 14 +- render_2D/tests/Model_Adaptation_Test.cpp | 4 +- render_2D/tests/Plottable_Migration_Test.cpp | 36 +-- render_2D/tests/Spectrum_Test.cpp | 14 +- 24 files changed, 1318 insertions(+), 772 deletions(-) diff --git a/kernel/kernel/module/renderable/rely_facade.h b/kernel/kernel/module/renderable/rely_facade.h index 20369b0..4e0e8f2 100644 --- a/kernel/kernel/module/renderable/rely_facade.h +++ b/kernel/kernel/module/renderable/rely_facade.h @@ -1,6 +1,6 @@ #pragma once #include "task_flow/export/export.h" namespace aethera::renderable { -using Task_Graph = task_flow::Task_Graph_Build_Facade; +using Task_Graph = task_flow::Task_Graph_Facade; proxy make_task_graph(std::string name); } // namespace aethera::renderable diff --git a/kernel/kernel/module/renderable/src/renderable.cpp b/kernel/kernel/module/renderable/src/renderable.cpp index 22a9575..4c92566 100644 --- a/kernel/kernel/module/renderable/src/renderable.cpp +++ b/kernel/kernel/module/renderable/src/renderable.cpp @@ -2,6 +2,7 @@ #include "task_flow/export/export.h" #include namespace aethera { +/* Renderable 需要完整建图 facade,以便具体图元保留自己的内部 DAG 依赖。 */ proxy renderable::make_task_graph(std::string name) { return task_flow::make_task_graph(std::move(name)); } diff --git a/render_2D/render_2D/module/plottable/Afterglow.ipp b/render_2D/render_2D/module/plottable/Afterglow.ipp index 94b4f2f..405d859 100644 --- a/render_2D/render_2D/module/plottable/Afterglow.ipp +++ b/render_2D/render_2D/module/plottable/Afterglow.ipp @@ -1,64 +1,167 @@ #pragma once +#include "common/Curve_Plot.hpp" #include "common/Raster_Plot.hpp" #include -#include #include +#include +#include namespace aethera::render_2d { struct Afterglow::Private : Prev_Private { + struct Prepared { + std::vector>> source_spectra{}; + detail::Raster_Layout layout{}; + std::vector intensity{}; + std::vector pixels{}; + std::vector partition_maxima{}; + Size canvas{}; + Plot_Ratio maximum{1.0}; + bool valid{}; + }; + Private(Frequency_Axis& frequency_axis, Numeric_Axis& power_axis) : frequency_axis(std::addressof(frequency_axis)), power_axis(std::addressof(power_axis)) {} + void advance() override; + void build_graph(Plot_Partition_Grid partition_grid); + void prepare_frame(); + void accumulate_partition(Plot_Partition_Count index); + void normalize_frame(); + void color_partition(Plot_Partition_Count index); + void paint_partition(Plot_Partition_Count index); +private: + Frequency_Axis* frequency_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + Numeric_Axis* power_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + std::deque>> spectra{}; + std::vector>> current_spectra{}; + Prepared prepared{}; + std::optional graph_partition_grid{}; +}; - void advance() override { - concurrent().internal.advance(); - auto stream = concurrent(); - stream.internal.advance(); - for (const auto& spectrum : *stream.internal.use()) - if (spectrum) spectra.push_back(spectrum); - while (spectra.size() > 64) spectra.pop_front(); - auto state = concurrent(); - state.internal.use()->spectrum_count = spectra.size(); - state.internal.advance(); - Prev_Private::advance(); +inline void Afterglow::Private::advance() { + auto properties = concurrent(); + auto stream = concurrent(); + auto state = concurrent(); + properties.internal.advance(); + stream.internal.advance(); + current_spectra.assign(stream.internal.use()->begin(), stream.internal.use()->end()); + for (const auto& spectrum : *stream.internal.use()) + if (spectrum) spectra.push_back(spectrum); + while (spectra.size() > 64) spectra.pop_front(); + state.internal.use()->spectrum_count = spectra.size(); + state.internal.use()->raster_cell_count = prepared.pixels.size(); + state.internal.advance(); + const auto partition_grid = static_cast(*properties.internal.use()).partition_grid; + if (!graph_partition_grid || *graph_partition_grid != partition_grid) build_graph(partition_grid); +} + +inline void Afterglow::Private::build_graph(Plot_Partition_Grid partition_grid) { + graph_partition_grid = partition_grid; + const Plot_Partition_Count partition_count = detail::raster_partition_count(partition_grid); + Task_Graph graph{this}; + auto begin = graph.add("prepare.frame", [this] { prepare_frame(); }); + auto normalize = graph.add("prepare.normalize", [this] { normalize_frame(); }); + auto colored = graph.add("prepare.color.complete", [] {}); + if (partition_count == 0) begin.precede(normalize); + normalize.precede(colored); + for (Plot_Partition_Count index = 0; index < partition_count; ++index) { + auto accumulate = graph.add("prepare.accumulate", [this, index] { accumulate_partition(index); }); + auto color = graph.add("prepare.color", [this, index] { color_partition(index); }); + auto paint = graph.add("paint.partition", [this, index] { paint_partition(index); }); + begin.precede(accumulate); + accumulate.precede(normalize); + normalize.precede(color); + color.precede(colored); + colored.precede(paint); } +} - void paint(detail::Painter& painter, Size) override { - auto state = concurrent(); - state.internal.use()->raster_cell_count = 0; - const auto& properties = static_cast(*concurrent().internal.use()); - const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); - const auto& power_layout = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); - if (!properties.partition_grid.valid() || spectra.empty() || frequency_layout.orientation == power_layout.orientation) { - state.internal.advance(); - return; +inline void Afterglow::Private::prepare_frame() { + const auto& state = static_cast(*concurrent().internal.use()); + const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + const auto& power_layout = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); + prepared.source_spectra = current_spectra; + const std::size_t available = prepared.source_spectra.empty() ? 0 : prepared.source_spectra.back()->size(); + const int columns = static_cast(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available); + const int rows = static_cast(state.power_point_size ? state.power_point_size : std::max(1.0, std::abs(power_layout.pixel_length))); + const Size canvas = paint_viewport(); + const auto layout = detail::raster_layout(frequency_axis, state.frequency_range, columns, power_axis, state.power_range, rows, frequency_layout.orientation, power_layout.orientation); + if (canvas.empty() || !layout.valid()) { + prepared.valid = false; + return; + } + const std::size_t cells = static_cast(columns) * rows; + const bool layout_changed = prepared.canvas != canvas || prepared.layout.width != layout.width || prepared.layout.height != layout.height || prepared.layout.target != layout.target || prepared.layout.first_reversed != layout.first_reversed || prepared.layout.second_reversed != layout.second_reversed || prepared.layout.first_horizontal != layout.first_horizontal; + prepared.canvas = canvas; + prepared.layout = layout; + if (layout_changed || prepared.intensity.size() != cells) { + prepared.intensity.assign(cells, 0.0); + prepared.pixels.assign(cells, 0); + } + prepared.partition_maxima.assign(detail::raster_partition_count(*graph_partition_grid), 1.0); + prepared.valid = true; +} + +inline void Afterglow::Private::accumulate_partition(Plot_Partition_Count index) { + if (!prepared.valid) return; + const auto& state = static_cast(*concurrent().internal.use()); + const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; + const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; + const detail::Raster_Partition partition = detail::raster_partition(prepared.layout, index, *graph_partition_grid); + if (partition.empty()) return; + Plot_Ratio attenuation{1.0}; + const Plot_Ratio decay = 1.0 - std::clamp(state.attenuation_rate, 0.0, 1.0); + const auto& source_spectra = prepared.source_spectra; + const Plot_Ratio retained = std::pow(decay, static_cast(source_spectra.size())); + for (int y = partition.first_y; y < partition.last_y; ++y) + for (int x = partition.first_x; x < partition.last_x; ++x) { + const auto [column, row] = detail::raster_coordinates(prepared.layout, x, y); + prepared.intensity[static_cast(row) * columns + column] *= retained; } - const std::size_t available = spectra.back()->size(); - const int columns = static_cast(properties.frequency_point_size ? std::min(properties.frequency_point_size, available) : available); - const int rows = static_cast(properties.power_point_size ? properties.power_point_size : std::max(1.0, std::abs(power_layout.pixel_length))); - const auto layout = detail::raster_layout(frequency_axis, properties.frequency_range, columns, power_axis, properties.power_range, rows, frequency_layout.orientation, power_layout.orientation); - if (!layout.valid()) { - state.internal.advance(); - return; - } - std::vector intensity(static_cast(layout.width) * layout.height); - const Plot_Ratio decay = 1.0 - std::clamp(properties.attenuation_rate, 0.0, 1.0); - Plot_Ratio attenuation{1.0}; - for (auto spectrum = spectra.rbegin(); spectrum != spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { - const std::size_t count = std::min(columns, (*spectrum)->size()); - for (std::size_t column = 0; column < count; ++column) { - const int row = std::clamp(static_cast(detail::normalized_plot_value((**spectrum)[column], properties.power_range) * (rows - 1)), 0, rows - 1); - intensity[layout.index(static_cast(column), row)] += attenuation; - if (properties.interpolate && row + 1 < rows) intensity[layout.index(static_cast(column), row + 1)] += attenuation * 0.35; + for (auto spectrum = source_spectra.rbegin(); spectrum != source_spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { + const std::size_t count = std::min(columns, (*spectrum)->size()); + for (std::size_t column = 0; column < count; ++column) { + const int row = std::clamp(static_cast(detail::normalized_plot_value((**spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1); + const std::size_t pixel = prepared.layout.index(static_cast(column), row); + const int x = static_cast(pixel % prepared.layout.width); + const int y = static_cast(pixel / prepared.layout.width); + if (partition.contains(x, y)) prepared.intensity[static_cast(row) * columns + column] += attenuation; + if (state.interpolate && row + 1 < rows) { + const std::size_t adjacent = prepared.layout.index(static_cast(column), row + 1); + const int adjacent_x = static_cast(adjacent % prepared.layout.width); + const int adjacent_y = static_cast(adjacent / prepared.layout.width); + if (partition.contains(adjacent_x, adjacent_y)) prepared.intensity[static_cast(row + 1) * columns + column] += attenuation * 0.35; } } - const Plot_Ratio maximum = std::max(1.0, *std::max_element(intensity.begin(), intensity.end())); - std::vector pixels(intensity.size()); - for (std::size_t index = 0; index < pixels.size(); ++index) pixels[index] = premultiply(properties.color_map.sample(intensity[index] / maximum)); - detail::paint_raster(painter, layout, pixels, Image_Interpolation_Mode::bilinear); - state.internal.use()->raster_cell_count = pixels.size(); - state.internal.advance(); } + Plot_Ratio maximum{1.0}; + for (int y = partition.first_y; y < partition.last_y; ++y) + for (int x = partition.first_x; x < partition.last_x; ++x) { + const auto [column, row] = detail::raster_coordinates(prepared.layout, x, y); + maximum = std::max(maximum, prepared.intensity[static_cast(row) * columns + column]); + } + prepared.partition_maxima[index] = maximum; +} - Frequency_Axis* frequency_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - Numeric_Axis* power_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - std::deque>> spectra{}; /* advance 线程唯一拥有的衰减历史。 */ -}; +inline void Afterglow::Private::normalize_frame() { + prepared.maximum = prepared.valid && !prepared.partition_maxima.empty() ? *std::max_element(prepared.partition_maxima.begin(), prepared.partition_maxima.end()) : 1.0; +} + +inline void Afterglow::Private::color_partition(Plot_Partition_Count index) { + if (!prepared.valid) return; + const auto& state = static_cast(*concurrent().internal.use()); + const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; + const detail::Raster_Partition partition = detail::raster_partition(prepared.layout, index, *graph_partition_grid); + for (int y = partition.first_y; y < partition.last_y; ++y) + for (int x = partition.first_x; x < partition.last_x; ++x) { + const auto [column, row] = detail::raster_coordinates(prepared.layout, x, y); + const std::size_t cell = static_cast(row) * columns + column; + prepared.pixels[static_cast(y) * prepared.layout.width + x] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); + } +} + +inline void Afterglow::Private::paint_partition(Plot_Partition_Count index) { + if (!prepared.valid || index >= detail::raster_partition_count(*graph_partition_grid)) return; + const Rect_F region = detail::raster_paint_region(prepared.layout, index, *graph_partition_grid); + if (region.empty()) return; + detail::Painter painter(paint_surface(), prepared.canvas, region); + detail::paint_raster(painter, prepared.layout, prepared.pixels, Image_Interpolation_Mode::bilinear); +} } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/plottable/Constellation_Diagram.ipp b/render_2D/render_2D/module/plottable/Constellation_Diagram.ipp index 26192ab..d555d6c 100644 --- a/render_2D/render_2D/module/plottable/Constellation_Diagram.ipp +++ b/render_2D/render_2D/module/plottable/Constellation_Diagram.ipp @@ -4,58 +4,121 @@ #include #include #include +#include namespace aethera::render_2d { struct Constellation_Diagram::Private : Prev_Private { + struct Prepared { + std::vector points{}; + std::vector anchors{}; + Size canvas{}; + bool valid{}; + }; + Private(Numeric_Axis& i_axis, Numeric_Axis& q_axis) : i_axis(std::addressof(i_axis)), q_axis(std::addressof(q_axis)) {} - - void advance() override { - concurrent().internal.advance(); - auto stream = concurrent(); - stream.internal.advance(); - for (const auto& point : *stream.internal.use()) points.push_back(point); - const auto& properties = static_cast(*concurrent().internal.use()); - const auto current = current_milliseconds(); - while (!points.empty() && current - points.front().submitted_at_ms > properties.point_lifetime_ms) points.pop_front(); - auto state = concurrent(); - state.internal.use()->point_count = points.size(); - state.internal.use()->anchor_count = static_cast(properties.type); - state.internal.advance(); - Prev_Private::advance(); - } - - void paint(detail::Painter& painter, Size) override { - const auto& properties = static_cast(*concurrent().internal.use()); - const auto& i_layout = static_cast(*aethera::detail::model_private(*i_axis).concurrent().internal.use()); - const auto& q_layout = static_cast(*aethera::detail::model_private(*q_axis).concurrent().internal.use()); - if (i_layout.orientation == q_layout.orientation) return; - std::vector anchors; - const int anchor_count = static_cast(properties.type); - const Plot_Coordinate center_i = properties.i_range.center(); - const Plot_Coordinate center_q = properties.q_range.center(); - const Plot_Coordinate radius = std::min(properties.i_range.size(), properties.q_range.size()) * 0.4; - anchors.reserve(static_cast(anchor_count)); - for (int index = 0; index < anchor_count; ++index) { - const Plot_Ratio angle = properties.phase_offset_radians + 2.0 * std::numbers::pi * index / anchor_count; - anchors.push_back(detail::map_plot_point(i_axis, center_i + std::cos(angle) * radius, q_axis, center_q + std::sin(angle) * radius, i_layout.orientation)); - } - painter.solid_circles(anchors, 4.5, properties.anchor_color); - if (properties.partition_count == 0 || points.empty()) return; - std::vector mapped; - mapped.reserve(points.size()); - for (const auto& point : points) mapped.push_back(detail::map_plot_point(i_axis, point.point.x, q_axis, point.point.y, i_layout.orientation)); - for (Plot_Partition_Count partition = 0; partition < properties.partition_count; ++partition) { - const auto first = mapped.size() / properties.partition_count * partition + std::min(partition, mapped.size() % properties.partition_count); - const auto last = mapped.size() / properties.partition_count * (partition + 1) + std::min(partition + 1, mapped.size() % properties.partition_count); - painter.solid_circles(std::span{mapped}.subspan(first, last - first), 2.5, properties.point_color); - } - } - - [[nodiscard]] static Plot_Duration_Milliseconds current_milliseconds() noexcept { - return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); - } - - Numeric_Axis* i_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - Numeric_Axis* q_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - std::deque points{}; /* advance 线程唯一拥有的尚未过期点集。 */ + void advance() override; + void prepare_data(); + void build_graph(Plot_Partition_Count partition_count); + void paint_anchors(); + void paint_partition(Plot_Partition_Count index); + [[nodiscard]] static Rect_F point_region(std::span points, double radius); + [[nodiscard]] static Plot_Duration_Milliseconds current_milliseconds() noexcept; +private: + Numeric_Axis* i_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + Numeric_Axis* q_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + std::deque points{}; + Prepared prepared{}; + std::optional graph_partition_count{}; }; + +inline void Constellation_Diagram::Private::advance() { + auto properties = concurrent(); + auto stream = concurrent(); + auto state = concurrent(); + properties.internal.advance(); + stream.internal.advance(); + for (const auto& point : *stream.internal.use()) points.push_back(point); + const auto& current_properties = static_cast(*properties.internal.use()); + const auto current = current_milliseconds(); + while (!points.empty() && current - points.front().submitted_at_ms > current_properties.point_lifetime_ms) points.pop_front(); + state.internal.use()->point_count = points.size(); + state.internal.use()->anchor_count = static_cast(current_properties.type); + state.internal.advance(); + if (!graph_partition_count || *graph_partition_count != current_properties.partition_count) build_graph(current_properties.partition_count); +} + +inline void Constellation_Diagram::Private::prepare_data() { + const auto& state = static_cast(*concurrent().internal.use()); + const auto& i_layout = static_cast(*aethera::detail::model_private(*i_axis).concurrent().internal.use()); + const auto& q_layout = static_cast(*aethera::detail::model_private(*q_axis).concurrent().internal.use()); + const auto current = current_milliseconds(); + prepared = {}; + prepared.canvas = paint_viewport(); + if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) return; + for (const auto& value : points) + if (current - value.submitted_at_ms <= state.point_lifetime_ms) prepared.points.push_back(detail::map_plot_point(i_axis, value.point.x, q_axis, value.point.y, i_layout.orientation)); + const int count = static_cast(state.type); + const Plot_Coordinate center_i = state.i_range.center(); + const Plot_Coordinate center_q = state.q_range.center(); + const Plot_Coordinate radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; + for (int index = 0; index < count; ++index) { + const Plot_Ratio angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; + prepared.anchors.push_back(detail::map_plot_point(i_axis, center_i + std::cos(angle) * radius, q_axis, center_q + std::sin(angle) * radius, i_layout.orientation)); + } + prepared.valid = true; +} + +inline void Constellation_Diagram::Private::build_graph(Plot_Partition_Count partition_count) { + graph_partition_count = partition_count; + Task_Graph graph{this}; + auto prepare = graph.add("prepare", [this] { prepare_data(); }); + auto anchors = graph.add("paint.anchors", [this] { paint_anchors(); }); + prepare.precede(anchors); + for (Plot_Partition_Count index = 0; index < partition_count; ++index) { + auto partition = graph.add("paint.partition", [this, index] { paint_partition(index); }); + anchors.precede(partition); + } +} + +inline Rect_F Constellation_Diagram::Private::point_region(std::span points_value, double radius) { + if (points_value.empty()) return {}; + double left = points_value.front().x; + double right = left; + double top = points_value.front().y; + double bottom = top; + for (const Point_F point : points_value.subspan(1)) { + left = std::min(left, point.x); + right = std::max(right, point.x); + top = std::min(top, point.y); + bottom = std::max(bottom, point.y); + } + constexpr double antialias_padding{1.0}; + const double padding = radius + antialias_padding; + return {left - padding, top - padding, right - left + padding * 2.0, bottom - top + padding * 2.0}; +} + +inline void Constellation_Diagram::Private::paint_anchors() { + const auto& state = static_cast(*concurrent().internal.use()); + if (!prepared.valid || prepared.anchors.empty()) return; + detail::Painter painter(paint_surface(), prepared.canvas, point_region(prepared.anchors, 4.5)); + painter.solid_circles(prepared.anchors, 4.5, state.anchor_color); +} + +inline void Constellation_Diagram::Private::paint_partition(Plot_Partition_Count index) { + const auto& state = static_cast(*concurrent().internal.use()); + if (!prepared.valid || state.partition_count == 0 || index >= state.partition_count) return; + const std::size_t point_count = prepared.points.size(); + const auto boundary = [point_count, count = state.partition_count](Plot_Partition_Count position) { + return point_count / count * position + std::min(position, point_count % count); + }; + const std::size_t first = boundary(index); + const std::size_t last = boundary(index + 1); + const std::span partition_points{prepared.points.data() + first, last - first}; + if (partition_points.empty()) return; + detail::Painter painter(paint_surface(), prepared.canvas, point_region(partition_points, 2.5)); + painter.solid_circles(partition_points, 2.5, state.point_color); +} + +inline Plot_Duration_Milliseconds Constellation_Diagram::Private::current_milliseconds() noexcept { + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); +} } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/plottable/Frequency_Trace.ipp b/render_2D/render_2D/module/plottable/Frequency_Trace.ipp index 7117231..5cb4f1d 100644 --- a/render_2D/render_2D/module/plottable/Frequency_Trace.ipp +++ b/render_2D/render_2D/module/plottable/Frequency_Trace.ipp @@ -2,56 +2,90 @@ #include "common/Curve_Plot.hpp" #include #include +#include namespace aethera::render_2d { struct Frequency_Trace::Private : Prev_Private { + struct Prepared { + std::vector partitions{}; + std::vector samples{}; + Size canvas{}; + bool valid{}; + }; + Private(Time_Axis& time_axis, Numeric_Axis& value_axis) : time_axis(std::addressof(time_axis)), value_axis(std::addressof(value_axis)) {} - - void advance() override { - auto properties = concurrent(); - auto stream = concurrent(); - auto state = concurrent(); - properties.internal.advance(); - stream.internal.advance(); - for (const auto& sample : *stream.internal.use()) samples.push_back(sample); - const auto& time_properties = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); - const auto limit = static_cast(std::max(2, time_properties.visible_count)); - while (samples.size() > limit) samples.pop_front(); - state.internal.use()->sample_count = samples.size(); - state.internal.advance(); - Prev_Private::advance(); - } - - void paint(detail::Painter& painter, Size) override { - const auto& properties = static_cast(*concurrent().internal.use()); - auto state = concurrent(); - state.internal.use()->rendered_point_count = 0; - if (properties.partition_count == 0 || samples.empty()) { - state.internal.advance(); - return; - } - const auto& time_layout = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); - const auto& value_layout = static_cast(*aethera::detail::model_private(*value_axis).concurrent().internal.use()); - const auto& value_state = static_cast(*aethera::detail::model_private(*value_axis).concurrent().internal.use()); - if (time_layout.orientation == value_layout.orientation) { - state.internal.advance(); - return; - } - std::vector source; - source.reserve(samples.size()); - for (const auto& sample : samples) source.push_back({static_cast(sample.tick), sample.value}); - const Axis_Range domain{source.front().coordinate, source.back().coordinate}; - for (Plot_Partition_Count index = 0; index < properties.partition_count; ++index) { - const auto range = detail::curve_partition_range(source.size(), index, properties.partition_count, domain); - if (range.sample_count == 0) continue; - const auto curve = detail::prepare_curve(std::span{source}.subspan(range.first_sample, range.sample_count), true, time_axis->coordinate_range(), value_state.coordinate_range, time_axis, value_axis, time_layout.orientation, value_layout.orientation); - detail::paint_curve(painter, curve, properties.pen); - state.internal.use()->rendered_point_count += curve.points.size(); - } - state.internal.advance(); - } - - Time_Axis* time_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - Numeric_Axis* value_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - std::deque samples{}; /* advance 线程唯一拥有的已提交历史。 */ + void advance() override; + void build_graph(Plot_Partition_Count partition_count); + void prepare_frame(Plot_Partition_Count partition_count); + void prepare_partition(Plot_Partition_Count partition_index); + void paint_partition(Plot_Partition_Count partition_index); +private: + Time_Axis* time_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + Numeric_Axis* value_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + std::deque samples{}; + Prepared prepared{}; + std::optional graph_partition_count{}; }; + +inline void Frequency_Trace::Private::advance() { + auto properties = concurrent(); + auto stream = concurrent(); + auto state = concurrent(); + properties.internal.advance(); + stream.internal.advance(); + for (const auto& sample : *stream.internal.use()) samples.push_back(sample); + const auto& time_properties = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); + const auto limit = static_cast(std::max(2, time_properties.visible_count)); + while (samples.size() > limit) samples.pop_front(); + auto& diagnostics = *state.internal.use(); + diagnostics.sample_count = samples.size(); + diagnostics.rendered_point_count = 0; + for (const auto& partition : prepared.partitions) diagnostics.rendered_point_count += partition.points.size(); + state.internal.advance(); + const auto partition_count = static_cast(*properties.internal.use()).partition_count; + if (!graph_partition_count || *graph_partition_count != partition_count) build_graph(partition_count); +} + +inline void Frequency_Trace::Private::build_graph(Plot_Partition_Count partition_count) { + graph_partition_count = partition_count; + Task_Graph graph{this}; + auto begin = graph.add("prepare.frame", [this, partition_count] { prepare_frame(partition_count); }); + for (Plot_Partition_Count index = 0; index < partition_count; ++index) { + auto prepare = graph.add("prepare.partition", [this, index] { prepare_partition(index); }); + auto paint = graph.add("paint.partition", [this, index] { paint_partition(index); }); + begin.precede(prepare); + prepare.precede(paint); + } +} + +inline void Frequency_Trace::Private::prepare_frame(Plot_Partition_Count partition_count) { + const auto& time_layout = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); + const auto& value_layout = static_cast(*aethera::detail::model_private(*value_axis).concurrent().internal.use()); + prepared = {}; + prepared.partitions.resize(partition_count); + prepared.canvas = paint_viewport(); + if (prepared.canvas.empty() || time_layout.orientation == value_layout.orientation) return; + prepared.samples.reserve(samples.size()); + for (const auto& sample : samples) prepared.samples.push_back({static_cast(sample.tick), sample.value}); + prepared.valid = !prepared.samples.empty(); +} + +inline void Frequency_Trace::Private::prepare_partition(Plot_Partition_Count partition_index) { + if (!prepared.valid) return; + const auto& time_layout = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); + const auto& value_layout = static_cast(*aethera::detail::model_private(*value_axis).concurrent().internal.use()); + const auto& value_state = static_cast(*aethera::detail::model_private(*value_axis).concurrent().internal.use()); + const Axis_Range domain{prepared.samples.front().coordinate, prepared.samples.back().coordinate}; + const auto range = detail::curve_partition_range(prepared.samples.size(), partition_index, prepared.partitions.size(), domain); + prepared.partitions[partition_index] = detail::prepare_curve(std::span(prepared.samples).subspan(range.first_sample, range.sample_count), true, time_axis->coordinate_range(), value_state.coordinate_range, time_axis, value_axis, time_layout.orientation, value_layout.orientation); +} + +inline void Frequency_Trace::Private::paint_partition(Plot_Partition_Count partition_index) { + if (!prepared.valid || partition_index >= prepared.partitions.size()) return; + const auto& state = static_cast(*concurrent().internal.use()); + const auto& curve = prepared.partitions[partition_index]; + const Rect_F region = detail::curve_paint_region(curve, state.pen.width + 2.0); + if (region.empty()) return; + detail::Painter painter(paint_surface(), prepared.canvas, region); + detail::paint_curve(painter, curve, state.pen); +} } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/plottable/Selection_Rectangle_Overlay.ipp b/render_2D/render_2D/module/plottable/Selection_Rectangle_Overlay.ipp index 9dec473..4e6275e 100644 --- a/render_2D/render_2D/module/plottable/Selection_Rectangle_Overlay.ipp +++ b/render_2D/render_2D/module/plottable/Selection_Rectangle_Overlay.ipp @@ -9,100 +9,113 @@ namespace aethera::render_2d { struct Selection_Rectangle_Overlay::Private : Prev_Private { Private(Abs_Axis& horizontal_axis, Abs_Axis& vertical_axis) : horizontal_axis(std::addressof(horizontal_axis)), vertical_axis(std::addressof(vertical_axis)) {} - - void advance() override { - concurrent().internal.advance(); - auto state = concurrent(); - state.internal.use()->selected_region_count = static_cast(*concurrent().internal.use()).selected_regions.size(); - state.internal.advance(); - Prev_Private::advance(); - } - - [[nodiscard]] std::optional event_routing_distance(const Event& event) const noexcept override { - const auto* pointer = dynamic_cast(std::addressof(event)); - if (!pointer) return std::nullopt; - if (!dragging && !plot_region().contains(pointer->position)) return std::nullopt; - return 0.0; - } - - void dispatch_event(Event& event) override { - const auto* pointer = dynamic_cast(std::addressof(event)); - if (!pointer) return; - const Point_F point = pointer->position; - const Axis_Point axis_point{horizontal_axis->point_to_coordinate(point), vertical_axis->point_to_coordinate(point)}; - if (event.type == Event_Type::pointer_press && pointer->button == Mouse_Button::left) { - const auto modifiers = static_cast>(pointer->modifiers); - const auto control = static_cast>(Keyboard_Modifier::control); - if ((modifiers & control) == 0) concurrent().set(&Prop::selected_regions, std::vector{}); - dragging = true; - drag_origin = axis_point; - drag_current = axis_point; - press_position = point; - event.accept(); - return; - } - if (event.type == Event_Type::pointer_move && dragging) { - drag_current = axis_point; - event.accept(); - return; - } - if (event.type != Event_Type::pointer_release || !dragging) return; - dragging = false; - drag_current = axis_point; - if (std::hypot(point.x - press_position.x, point.y - press_position.y) >= minimum_drag_distance_pixels) { - const Axis_Rectangle selected = axis_rectangle(drag_origin, drag_current); - concurrent().set([selected](Prop& properties) { properties.selected_regions.push_back(selected); }); - } - event.accept(); - } - - void paint(detail::Painter& painter, Size) override { - const auto& properties = static_cast(*concurrent().internal.use()); - const auto region = plot_region(); - auto clip = painter.scoped_clip(region); - const auto paint_region = [&](const Axis_Rectangle& selection) { - const Rect_F rect = detail::map_plot_rect(horizontal_axis, selection.horizontal, vertical_axis, selection.vertical, Axis_Orientation::horizontal); - painter.rect(rect, properties.selection_border_pen, properties.selection_brush); - const Point_F label_position{rect.x + 4.0, rect.y + 3.0}; - painter.text(label_position, "X " + range_label(*horizontal_axis, selection.horizontal), properties.label_font, properties.label_pen); - painter.text({label_position.x, label_position.y + std::max(12.0, properties.label_font.size * 1.35)}, "Y " + range_label(*vertical_axis, selection.vertical), properties.label_font, properties.label_pen); - }; - for (const auto& selection : properties.selected_regions) paint_region(selection); - if (dragging && has_visible_drag()) paint_region(axis_rectangle(drag_origin, drag_current)); - } - - [[nodiscard]] Rect_F plot_region() const { - return detail::map_plot_rect(horizontal_axis, horizontal_axis->coordinate_range(), vertical_axis, vertical_axis->coordinate_range(), Axis_Orientation::horizontal); - } - - [[nodiscard]] bool has_visible_drag() const { - const Point_F current_position{horizontal_axis->coordinate_to_pixel(drag_current.horizontal), vertical_axis->coordinate_to_pixel(drag_current.vertical)}; - return std::hypot(current_position.x - press_position.x, current_position.y - press_position.y) >= minimum_drag_distance_pixels; - } - - [[nodiscard]] static Axis_Rectangle axis_rectangle(Axis_Point first, Axis_Point second) { - const auto [horizontal_low, horizontal_high] = std::minmax(first.horizontal, second.horizontal); - const auto [vertical_low, vertical_high] = std::minmax(first.vertical, second.vertical); - return {{horizontal_low, horizontal_high}, {vertical_low, vertical_high}}; - } - - [[nodiscard]] static std::string range_label(const Abs_Axis& axis, Axis_Range range) { - const auto coordinate_label = [&axis](Axis_Coordinate coordinate) { - auto label = axis.tick_label(coordinate); - if (!label.empty()) return label; - std::ostringstream stream; - stream << std::fixed << std::setprecision(3) << coordinate; - return stream.str(); - }; - return coordinate_label(range.origin) + " .. " + coordinate_label(range.target); - } - + void advance() override; + [[nodiscard]] std::optional event_routing_distance(const Event& event) const noexcept override; + void dispatch_event(Event& event) override; +private: + void paint(); + [[nodiscard]] Rect_F plot_region() const; + [[nodiscard]] bool has_visible_drag() const; + [[nodiscard]] static Axis_Rectangle axis_rectangle(Axis_Point first, Axis_Point second); + [[nodiscard]] static std::string range_label(const Abs_Axis& axis, Axis_Range range); static constexpr double minimum_drag_distance_pixels{3.0}; Abs_Axis* horizontal_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本 overlay。 */ Abs_Axis* vertical_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本 overlay。 */ Axis_Point drag_origin{}; Axis_Point drag_current{}; - Point_F press_position{}; - bool dragging{}; + Point_F press_position{}; + bool dragging{}; }; + +inline void Selection_Rectangle_Overlay::Private::advance() { + auto properties = concurrent(); + auto state = concurrent(); + properties.internal.advance(); + state.internal.use()->selected_region_count = static_cast(*properties.internal.use()).selected_regions.size(); + state.internal.advance(); + Task_Graph graph{this}; + graph.add("paint", [this] { paint(); }); +} + +inline std::optional Selection_Rectangle_Overlay::Private::event_routing_distance(const Event& event) const noexcept { + const auto* pointer = dynamic_cast(std::addressof(event)); + if (!pointer) return std::nullopt; + if (!dragging && !plot_region().contains(pointer->position)) return std::nullopt; + return 0.0; +} + +inline void Selection_Rectangle_Overlay::Private::dispatch_event(Event& event) { + const auto* pointer = dynamic_cast(std::addressof(event)); + if (!pointer) return; + const Point_F point = pointer->position; + const Axis_Point axis_point{horizontal_axis->point_to_coordinate(point), vertical_axis->point_to_coordinate(point)}; + if (event.type == Event_Type::pointer_press && pointer->button == Mouse_Button::left) { + const auto modifiers = static_cast>(pointer->modifiers); + const auto control = static_cast>(Keyboard_Modifier::control); + if ((modifiers & control) == 0) concurrent().set(&Prop::selected_regions, std::vector{}); + dragging = true; + drag_origin = axis_point; + drag_current = axis_point; + press_position = point; + event.accept(); + return; + } + if (event.type == Event_Type::pointer_move && dragging) { + drag_current = axis_point; + event.accept(); + return; + } + if (event.type != Event_Type::pointer_release || !dragging) return; + dragging = false; + drag_current = axis_point; + if (std::hypot(point.x - press_position.x, point.y - press_position.y) >= minimum_drag_distance_pixels) { + const Axis_Rectangle selected = axis_rectangle(drag_origin, drag_current); + concurrent().set([selected](Prop& properties) { properties.selected_regions.push_back(selected); }); + } + event.accept(); +} + +inline void Selection_Rectangle_Overlay::Private::paint() { + const auto& properties = static_cast(*concurrent().internal.use()); + const Size canvas = paint_viewport(); + if (canvas.empty()) return; + detail::Painter painter(paint_surface(), canvas); + const auto region = plot_region(); + auto clip = painter.scoped_clip(region); + const auto paint_region = [&](const Axis_Rectangle& selection) { + const Rect_F rect = detail::map_plot_rect(horizontal_axis, selection.horizontal, vertical_axis, selection.vertical, Axis_Orientation::horizontal); + painter.rect(rect, properties.selection_border_pen, properties.selection_brush); + const Point_F label_position{rect.x + 4.0, rect.y + 3.0}; + painter.text(label_position, "X " + range_label(*horizontal_axis, selection.horizontal), properties.label_font, properties.label_pen); + painter.text({label_position.x, label_position.y + std::max(12.0, properties.label_font.size * 1.35)}, "Y " + range_label(*vertical_axis, selection.vertical), properties.label_font, properties.label_pen); + }; + for (const auto& selection : properties.selected_regions) paint_region(selection); + if (dragging && has_visible_drag()) paint_region(axis_rectangle(drag_origin, drag_current)); +} + +inline Rect_F Selection_Rectangle_Overlay::Private::plot_region() const { + return detail::map_plot_rect(horizontal_axis, horizontal_axis->coordinate_range(), vertical_axis, vertical_axis->coordinate_range(), Axis_Orientation::horizontal); +} + +inline bool Selection_Rectangle_Overlay::Private::has_visible_drag() const { + const Point_F current_position{horizontal_axis->coordinate_to_pixel(drag_current.horizontal), vertical_axis->coordinate_to_pixel(drag_current.vertical)}; + return std::hypot(current_position.x - press_position.x, current_position.y - press_position.y) >= minimum_drag_distance_pixels; +} + +inline Axis_Rectangle Selection_Rectangle_Overlay::Private::axis_rectangle(Axis_Point first, Axis_Point second) { + const auto [horizontal_low, horizontal_high] = std::minmax(first.horizontal, second.horizontal); + const auto [vertical_low, vertical_high] = std::minmax(first.vertical, second.vertical); + return {{horizontal_low, horizontal_high}, {vertical_low, vertical_high}}; +} + +inline std::string Selection_Rectangle_Overlay::Private::range_label(const Abs_Axis& axis, Axis_Range range) { + const auto coordinate_label = [&axis](Axis_Coordinate coordinate) { + auto label = axis.tick_label(coordinate); + if (!label.empty()) return label; + std::ostringstream stream; + stream << std::fixed << std::setprecision(3) << coordinate; + return stream.str(); + }; + return coordinate_label(range.origin) + " .. " + coordinate_label(range.target); +} } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/plottable/Spectrum.ipp b/render_2D/render_2D/module/plottable/Spectrum.ipp index 90d7e22..a0788aa 100644 --- a/render_2D/render_2D/module/plottable/Spectrum.ipp +++ b/render_2D/render_2D/module/plottable/Spectrum.ipp @@ -2,78 +2,224 @@ #include "common/Curve_Plot.hpp" #include #include +#include +#include #include namespace aethera::render_2d { struct Spectrum::Private : Prev_Private { + using Prepared_Curve = detail::Curve_Prepared; + struct Prepared_Partition { + Prepared_Curve current{}; + Prepared_Curve maximum{}; + Prepared_Curve minimum{}; + Rect_F clip{}; + }; + enum struct Marker_Style : std::uint8_t { + middle, + normal, + selected + }; + struct Prepared_Marker { + Point_F first{}; + Point_F second{}; + Marker_Style style{Marker_Style::normal}; + }; + struct Prepared_Extreme { + Point_F point{}; + bool maximum{}; + }; + struct Prepared { + std::vector partitions{}; + std::vector markers{}; + std::array, 2> extremes{}; + Rect_F sweep_region{}; + Size canvas_size{}; + bool valid{}; + }; + Private(Frequency_Axis& frequency_axis, Numeric_Axis& power_axis) : frequency_axis(std::addressof(frequency_axis)), power_axis(std::addressof(power_axis)) {} - - void advance() override { - concurrent().internal.advance(); - concurrent().internal.advance(); - Prev_Private::advance(); - } - - void paint(detail::Painter& painter, Size) override { - const auto& properties = static_cast(*concurrent().internal.use()); - const auto& frame = *concurrent().internal.use(); - auto published = concurrent(); - auto& state = *published.internal.use(); - state.sample_count = frame.samples.size(); - state.rendered_point_count = 0; - state.selectable_marker_count = properties.custom_markers.size(); - if (maxima.size() != frame.samples.size()) { - maxima = frame.samples; - minima = frame.samples; - } - else { - for (std::size_t index = 0; index < frame.samples.size(); ++index) { - maxima[index] = std::max(maxima[index], frame.samples[index]); - minima[index] = std::min(minima[index], frame.samples[index]); - } - } - const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); - const auto& power_layout = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); - const auto& power_state = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); - if (frequency_layout.orientation != power_layout.orientation) { - if (properties.sweep_region_visible) painter.rect(detail::map_plot_rect(frequency_axis, properties.sweep_frequency_range, power_axis, power_state.coordinate_range, frequency_layout.orientation), Pen{.style = Line_Style::none}, properties.sweep_region_brush); - const auto paint_values = [&](std::span values, const Pen& pen, const Brush& brush) { - if (properties.partition_count == 0 || values.empty()) return; - for (Plot_Partition_Count index = 0; index < properties.partition_count; ++index) { - const auto range = detail::curve_partition_range(values.size(), index, properties.partition_count, properties.frequency_range); - if (range.sample_count == 0) continue; - const auto curve = detail::prepare_curve(values.subspan(range.first_sample, range.sample_count), range.domain, properties.interpolation_mode, properties.visible_range_only, frequency_axis->coordinate_range(), power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); - detail::paint_curve(painter, curve, pen, brush); - state.rendered_point_count += curve.points.size(); - } - }; - if (properties.max_hold_visible) paint_values(maxima, properties.max_pen, properties.max_brush); - if (properties.min_hold_visible) paint_values(minima, properties.min_pen, properties.min_brush); - paint_values(frame.samples, properties.current_pen, properties.current_brush); - const auto paint_marker = [&](Spectrum_Frequency frequency, const Pen& pen) { - painter.line(detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation), detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation), pen); - }; - paint_marker(properties.center_frequency, properties.middle_frequency_pen); - for (std::size_t index = 0; index < properties.custom_markers.size(); ++index) paint_marker(properties.custom_markers[index], static_cast(index) == properties.selected_marker ? properties.selected_marker_pen : properties.marker_pen); - if (!frame.samples.empty()) { - const auto paint_extreme = [&](bool maximum) { - if (maximum ? !properties.max_marker_visible : !properties.min_marker_visible) return; - const auto iterator = maximum ? std::max_element(frame.samples.begin(), frame.samples.end()) : std::min_element(frame.samples.begin(), frame.samples.end()); - const auto index = static_cast(std::distance(frame.samples.begin(), iterator)); - const auto denominator = frame.samples.size() > 1 ? static_cast(frame.samples.size() - 1) : 1.0; - const auto frequency = properties.frequency_range.origin + properties.frequency_range.length() * static_cast(index) / denominator; - const Pen& pen = maximum ? properties.max_pen : properties.min_pen; - painter.circle(detail::map_plot_point(frequency_axis, frequency, power_axis, *iterator, frequency_layout.orientation), 3.0, pen, Brush{pen.color, Brush_Style::solid}); - }; - paint_extreme(true); - paint_extreme(false); - } - } - published.internal.advance(); - } - - Frequency_Axis* frequency_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - Numeric_Axis* power_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - std::vector maxima{}; /* paint 线程唯一拥有的逐点最大历史。 */ - std::vector minima{}; /* paint 线程唯一拥有的逐点最小历史。 */ + void advance() override; + void build_graph(Plot_Partition_Count partition_count); + void prepare_frame(std::size_t partition_count); + void update_hold_partition(std::size_t partition_index); + void prepare_extreme(bool maximum); + void prepare_partition(std::size_t partition_index); + void begin_paint(); + void paint_partition(std::size_t partition_index); + void paint_overlays(); +private: + Frequency_Axis* frequency_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + Numeric_Axis* power_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + Prepared prepared{}; + std::vector maxima{}; + std::vector minima{}; + bool hold_history_reset{}; + std::optional graph_partition_count{}; }; + +inline void Spectrum::Private::advance() { + auto properties = concurrent(); + auto frame = concurrent(); + auto state = concurrent(); + properties.internal.advance(); + frame.internal.advance(); + state.internal.advance(); + const auto partition_count = static_cast(*properties.internal.use()).partition_count; + if (!graph_partition_count || *graph_partition_count != partition_count) build_graph(partition_count); +} + +inline void Spectrum::Private::build_graph(Plot_Partition_Count partition_count) { + graph_partition_count = partition_count; + Task_Graph graph{this}; + auto prepare_begin = graph.add("prepare.frame", [this, partition_count] { prepare_frame(partition_count); }); + auto hold_complete = graph.add("prepare.hold.complete", [] {}); + std::vector hold_partitions; + hold_partitions.reserve(partition_count); + for (std::size_t index = 0; index < partition_count; ++index) { + auto partition = graph.add("prepare.hold.partition", [this, index] { update_hold_partition(index); }); + prepare_begin.precede(partition); + partition.precede(hold_complete); + hold_partitions.push_back(std::move(partition)); + } + if (hold_partitions.empty()) prepare_begin.precede(hold_complete); + std::vector prepared_partitions; + prepared_partitions.reserve(partition_count); + for (std::size_t index = 0; index < partition_count; ++index) { + auto partition = graph.add("prepare.partition", [this, index] { prepare_partition(index); }); + hold_complete.precede(partition); + prepared_partitions.push_back(std::move(partition)); + } + auto maximum = graph.add("prepare.extreme.maximum", [this] { prepare_extreme(true); }); + auto minimum = graph.add("prepare.extreme.minimum", [this] { prepare_extreme(false); }); + prepare_begin.precede(maximum); + prepare_begin.precede(minimum); + auto paint_begin = graph.add("paint.begin", [this] { begin_paint(); }); + if (prepared_partitions.empty()) prepare_begin.precede(paint_begin); + for (const auto& partition : prepared_partitions) partition.precede(paint_begin); + auto overlays = graph.add("paint.overlays", [this] { paint_overlays(); }); + for (std::size_t index = 0; index < partition_count; ++index) { + auto partition = graph.add("paint.partition", [this, index] { paint_partition(index); }); + paint_begin.precede(partition); + partition.precede(overlays); + } + maximum.precede(overlays); + minimum.precede(overlays); + if (partition_count == 0) paint_begin.precede(overlays); +} + +inline void Spectrum::Private::prepare_frame(std::size_t partition_count) { + const auto& state = static_cast(*concurrent().internal.use()); + const auto& frame = *concurrent().internal.use(); + const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + const auto& power_layout = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); + const auto& power_state = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); + hold_history_reset = maxima.size() != frame.samples.size(); + if (hold_history_reset) { + maxima = frame.samples; + minima = frame.samples; + } + prepared = {}; + prepared.partitions.resize(partition_count); + if (frequency_layout.orientation == power_layout.orientation) return; + if (paint_viewport().empty()) return; + prepared.canvas_size = paint_viewport(); + if (state.sweep_region_visible) prepared.sweep_region = detail::map_plot_rect(frequency_axis, state.sweep_frequency_range, power_axis, power_state.coordinate_range, frequency_layout.orientation); + prepared.markers.push_back({detail::map_plot_point(frequency_axis, state.center_frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation), detail::map_plot_point(frequency_axis, state.center_frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation), Marker_Style::middle}); + for (std::size_t index = 0; index < state.custom_markers.size(); ++index) { + const Spectrum_Frequency frequency = state.custom_markers[index]; + const Marker_Style style = static_cast(index) == state.selected_marker ? Marker_Style::selected : Marker_Style::normal; + prepared.markers.push_back({detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation), detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation), style}); + } + prepared.valid = true; +} + +inline void Spectrum::Private::update_hold_partition(std::size_t partition_index) { + if (hold_history_reset || *graph_partition_count == 0) return; + const auto& frame = *concurrent().internal.use(); + const std::size_t first = frame.samples.size() * partition_index / *graph_partition_count; + const std::size_t last = frame.samples.size() * (partition_index + 1) / *graph_partition_count; + for (std::size_t index = first; index < last; ++index) { + maxima[index] = std::max(maxima[index], frame.samples[index]); + minima[index] = std::min(minima[index], frame.samples[index]); + } +} + +inline void Spectrum::Private::prepare_extreme(bool maximum) { + const auto& state = static_cast(*concurrent().internal.use()); + const std::size_t slot = maximum ? 0 : 1; + if (!prepared.valid || (maximum ? !state.max_marker_visible : !state.min_marker_visible)) { + prepared.extremes[slot].reset(); + return; + } + const auto& frame = *concurrent().internal.use(); + if (frame.samples.empty()) { + prepared.extremes[slot].reset(); + return; + } + const auto iterator = maximum ? std::max_element(frame.samples.begin(), frame.samples.end()) : std::min_element(frame.samples.begin(), frame.samples.end()); + const auto index = static_cast(std::distance(frame.samples.begin(), iterator)); + const Spectrum_Interpolation_Ratio denominator = frame.samples.size() > 1 ? static_cast(frame.samples.size() - 1) : 1.0; + const Spectrum_Frequency frequency = state.frequency_range.origin + state.frequency_range.length() * static_cast(index) / denominator; + const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + prepared.extremes[slot] = Prepared_Extreme{detail::map_plot_point(frequency_axis, frequency, power_axis, *iterator, frequency_layout.orientation), maximum}; +} + +inline void Spectrum::Private::prepare_partition(std::size_t partition_index) { + if (!prepared.valid || partition_index >= prepared.partitions.size()) return; + const auto& state = static_cast(*concurrent().internal.use()); + const auto& frame = *concurrent().internal.use(); + if (frame.samples.empty()) return; + const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + const auto& power_layout = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); + const auto& frequency_state = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + const auto& power_state = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); + const auto range = detail::curve_partition_range(frame.samples.size(), partition_index, prepared.partitions.size(), state.frequency_range); + auto& partition = prepared.partitions[partition_index]; + partition.clip = detail::map_plot_rect(frequency_axis, range.domain, power_axis, power_state.coordinate_range, frequency_layout.orientation); + partition.current = detail::prepare_curve(std::span(frame.samples).subspan(range.first_sample, range.sample_count), range.domain, state.interpolation_mode, state.visible_range_only, frequency_state.coordinate_range, power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); + if (state.max_hold_visible && maxima.size() == frame.samples.size()) partition.maximum = detail::prepare_curve(std::span(maxima).subspan(range.first_sample, range.sample_count), range.domain, state.interpolation_mode, state.visible_range_only, frequency_state.coordinate_range, power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); + if (state.min_hold_visible && minima.size() == frame.samples.size()) partition.minimum = detail::prepare_curve(std::span(minima).subspan(range.first_sample, range.sample_count), range.domain, state.interpolation_mode, state.visible_range_only, frequency_state.coordinate_range, power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); +} + +inline void Spectrum::Private::begin_paint() { + const auto& state = static_cast(*concurrent().internal.use()); + const auto& frame = *concurrent().internal.use(); + auto& published = *concurrent().internal.use(); + published.sample_count = frame.samples.size(); + published.rendered_point_count = 0; + for (const auto& partition : prepared.partitions) published.rendered_point_count += partition.current.points.size(); + published.selectable_marker_count = state.custom_markers.size(); + if (!prepared.valid || !state.sweep_region_visible || prepared.sweep_region.empty()) return; + detail::Painter painter(paint_surface(), prepared.canvas_size, prepared.sweep_region); + painter.rect(prepared.sweep_region, Pen{.style = Line_Style::none}, state.sweep_region_brush); +} + +inline void Spectrum::Private::paint_partition(std::size_t partition_index) { + if (!prepared.valid || partition_index >= prepared.partitions.size()) return; + const auto& state = static_cast(*concurrent().internal.use()); + const auto& partition = prepared.partitions[partition_index]; + if (partition.clip.empty()) return; + const Rect_F clip = partition.clip.normalized(); + constexpr double overlap{2.0}; + detail::Painter painter(paint_surface(), prepared.canvas_size, {clip.x - overlap, clip.y - overlap, clip.width + overlap * 2.0, clip.height + overlap * 2.0}); + const auto clip_scope = painter.scoped_clip(clip); + detail::paint_curve(painter, partition.maximum, state.max_pen, state.max_brush); + detail::paint_curve(painter, partition.minimum, state.min_pen, state.min_brush); + detail::paint_curve(painter, partition.current, state.current_pen, state.current_brush); +} + +inline void Spectrum::Private::paint_overlays() { + if (!prepared.valid) return; + const auto& state = static_cast(*concurrent().internal.use()); + detail::Painter painter(paint_surface(), prepared.canvas_size); + for (const auto& marker : prepared.markers) { + const Pen& pen = marker.style == Marker_Style::middle ? state.middle_frequency_pen : marker.style == Marker_Style::selected ? state.selected_marker_pen : state.marker_pen; + if (pen.enabled()) painter.line(marker.first, marker.second, pen); + } + for (const auto& extreme : prepared.extremes) { + if (!extreme) continue; + const Pen& pen = extreme->maximum ? state.max_pen : state.min_pen; + painter.circle(extreme->point, 3.0, pen, Brush{pen.color, Brush_Style::solid}); + } +} } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/plottable/Sweep_Spectrum.ipp b/render_2D/render_2D/module/plottable/Sweep_Spectrum.ipp index 0ce52ba..74b16f9 100644 --- a/render_2D/render_2D/module/plottable/Sweep_Spectrum.ipp +++ b/render_2D/render_2D/module/plottable/Sweep_Spectrum.ipp @@ -1,68 +1,145 @@ #pragma once #include "common/Curve_Plot.hpp" #include +#include +#include namespace aethera::render_2d { struct Sweep_Spectrum::Private : Prev_Private { + struct Prepared { + std::vector partitions{}; + std::vector values{}; + Axis_Range domain{}; + Point_F marker_first{}; + Point_F marker_second{}; + Size canvas{}; + bool valid{}; + }; + Private(Frequency_Axis& frequency_axis, Numeric_Axis& power_axis) : frequency_axis(std::addressof(frequency_axis)), power_axis(std::addressof(power_axis)) {} - - void advance() override { - concurrent().internal.advance(); - auto stream = concurrent(); - stream.internal.advance(); - const auto& properties = static_cast(*concurrent().internal.use()); - const auto block_count = std::max(1, properties.block_count); - if (blocks.size() != block_count) { - blocks.assign(block_count, {}); - has_latest_block = false; - } - for (const auto& block : *stream.internal.use()) { - if (!block || block->index >= blocks.size()) continue; - blocks[block->index] = block; - latest_block_index = block->index; - has_latest_block = true; - } - auto state = concurrent(); - auto& current = *state.internal.use(); - current.stored_block_count = static_cast(std::count_if(blocks.begin(), blocks.end(), [](const auto& block) { return static_cast(block); })); - current.stored_point_count = 0; - for (const auto& block : blocks) - if (block) current.stored_point_count += block->values.size(); - state.internal.advance(); - Prev_Private::advance(); - } - - void paint(detail::Painter& painter, Size) override { - const auto& properties = static_cast(*concurrent().internal.use()); - const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); - const auto& power_layout = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); - const auto& power_state = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); - if (properties.partition_count == 0 || frequency_layout.orientation == power_layout.orientation) return; - std::vector values; - std::size_t available_blocks{}; - for (const auto& block : blocks) { - if (!block) continue; - ++available_blocks; - values.insert(values.end(), block->values.begin(), block->values.end()); - } - if (values.empty()) return; - const double progress = available_blocks == blocks.size() ? 1.0 : static_cast(latest_block_index + 1) / static_cast(blocks.size()); - const Axis_Range domain{properties.frequency_range.origin, properties.frequency_range.origin + properties.frequency_range.length() * progress}; - for (Plot_Partition_Count index = 0; index < properties.partition_count; ++index) { - const auto range = detail::curve_partition_range(values.size(), index, properties.partition_count, domain); - if (range.sample_count == 0) continue; - const auto curve = detail::prepare_curve(std::span{values}.subspan(range.first_sample, range.sample_count), range.domain, properties.interpolation_mode, properties.visible_range_only, frequency_axis->coordinate_range(), power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); - detail::paint_curve(painter, curve, properties.pen); - } - if (!has_latest_block) return; - const double marker_progress = static_cast(latest_block_index + 1) / static_cast(blocks.size()); - const auto frequency = properties.frequency_range.origin + properties.frequency_range.length() * marker_progress; - painter.line(detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation), detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation), properties.current_frequency_pen); - } - - Frequency_Axis* frequency_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - Numeric_Axis* power_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - std::vector> blocks{}; /* paint/advance 线程持有的完整扫频槽。 */ - std::size_t latest_block_index{}; - bool has_latest_block{}; + void advance() override; + void build_graph(Plot_Partition_Count partition_count); + void prepare_frame(); + void prepare_partition(Plot_Partition_Count index); + void paint_partition(Plot_Partition_Count index); + void paint_marker(); + [[nodiscard]] std::size_t stored_point_count() const; +private: + Frequency_Axis* frequency_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + Numeric_Axis* power_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + std::vector> sweep_blocks{}; + std::size_t latest_block_index{}; + bool has_latest_block{}; + Prepared prepared{}; + std::optional graph_partition_count{}; }; + +inline void Sweep_Spectrum::Private::advance() { + auto properties = concurrent(); + auto stream = concurrent(); + auto state = concurrent(); + properties.internal.advance(); + stream.internal.advance(); + const auto& current = static_cast(*properties.internal.use()); + const auto block_count = std::max(1, current.block_count); + if (sweep_blocks.size() != block_count) { + sweep_blocks.assign(block_count, {}); + latest_block_index = 0; + has_latest_block = false; + } + for (const auto& block : *stream.internal.use()) { + if (!block || block->index >= block_count) continue; + sweep_blocks[block->index] = block; + latest_block_index = block->index; + has_latest_block = true; + } + state.internal.use()->stored_block_count = static_cast(std::count_if(sweep_blocks.begin(), sweep_blocks.end(), [](const auto& block) { return static_cast(block); })); + state.internal.use()->stored_point_count = stored_point_count(); + state.internal.advance(); + if (!graph_partition_count || *graph_partition_count != current.partition_count) build_graph(current.partition_count); +} + +inline void Sweep_Spectrum::Private::build_graph(Plot_Partition_Count partition_count) { + graph_partition_count = partition_count; + Task_Graph graph{this}; + auto begin = graph.add("prepare.frame", [this] { prepare_frame(); }); + std::vector prepare_tasks; + for (Plot_Partition_Count index = 0; index < partition_count; ++index) { + auto task = graph.add("prepare.partition", [this, index] { prepare_partition(index); }); + begin.precede(task); + prepare_tasks.push_back(std::move(task)); + } + auto marker = graph.add("paint.marker", [this] { paint_marker(); }); + if (partition_count == 0) begin.precede(marker); + for (Plot_Partition_Count index = 0; index < partition_count; ++index) { + auto partition = graph.add("paint.partition", [this, index] { paint_partition(index); }); + if (prepare_tasks.empty()) begin.precede(partition); + else prepare_tasks[index].precede(partition); + partition.precede(marker); + } +} + +inline void Sweep_Spectrum::Private::prepare_frame() { + const auto& state = static_cast(*concurrent().internal.use()); + const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + const auto& power_layout = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); + const auto& power_state = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); + prepared = {}; + prepared.canvas = paint_viewport(); + const std::size_t block_count = std::max(1, state.block_count); + std::size_t available_blocks{}; + for (const auto& block : sweep_blocks) { + if (!block) continue; + ++available_blocks; + prepared.values.insert(prepared.values.end(), block->values.begin(), block->values.end()); + } + if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation || available_blocks == 0) return; + prepared.partitions.resize(*graph_partition_count); + if (prepared.values.empty()) return; + const double domain_progress = available_blocks == block_count ? 1.0 : static_cast(latest_block_index + 1) / static_cast(block_count); + const double marker_progress = has_latest_block ? static_cast(latest_block_index + 1) / static_cast(block_count) : 0.0; + const auto domain_target = state.frequency_range.origin + state.frequency_range.length() * domain_progress; + const auto latest_frequency = state.frequency_range.origin + state.frequency_range.length() * marker_progress; + prepared.domain = {state.frequency_range.origin, domain_target}; + prepared.marker_first = detail::map_plot_point(frequency_axis, latest_frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation); + prepared.marker_second = detail::map_plot_point(frequency_axis, latest_frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation); + prepared.valid = true; +} + +inline void Sweep_Spectrum::Private::prepare_partition(Plot_Partition_Count index) { + if (!prepared.valid) return; + const auto& state = static_cast(*concurrent().internal.use()); + const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + const auto& power_layout = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); + const auto& frequency_state = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + const auto& power_state = static_cast(*aethera::detail::model_private(*power_axis).concurrent().internal.use()); + const auto range = detail::curve_partition_range(prepared.values.size(), index, prepared.partitions.size(), prepared.domain); + prepared.partitions[index] = detail::prepare_curve(std::span(prepared.values).subspan(range.first_sample, range.sample_count), range.domain, state.interpolation_mode, state.visible_range_only, frequency_state.coordinate_range, power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); +} + +inline void Sweep_Spectrum::Private::paint_partition(Plot_Partition_Count index) { + if (!prepared.valid || index >= prepared.partitions.size()) return; + const auto& state = static_cast(*concurrent().internal.use()); + const auto& curve = prepared.partitions[index]; + const Rect_F region = detail::curve_paint_region(curve, state.pen.width + 2.0); + if (region.empty()) return; + detail::Painter painter(paint_surface(), prepared.canvas, region); + detail::paint_curve(painter, curve, state.pen); +} + +inline void Sweep_Spectrum::Private::paint_marker() { + if (!prepared.valid) return; + const auto& state = static_cast(*concurrent().internal.use()); + const double padding = state.current_frequency_pen.width + 2.0; + const auto [left, right] = std::minmax(prepared.marker_first.x, prepared.marker_second.x); + const auto [top, bottom] = std::minmax(prepared.marker_first.y, prepared.marker_second.y); + detail::Painter painter(paint_surface(), prepared.canvas, {left - padding, top - padding, right - left + padding * 2.0, bottom - top + padding * 2.0}); + painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); +} + +inline std::size_t Sweep_Spectrum::Private::stored_point_count() const { + std::size_t count{}; + for (const auto& block : sweep_blocks) + if (block) count += block->values.size(); + return count; +} } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/plottable/Waterfall.ipp b/render_2D/render_2D/module/plottable/Waterfall.ipp index 3774f27..fb3eff2 100644 --- a/render_2D/render_2D/module/plottable/Waterfall.ipp +++ b/render_2D/render_2D/module/plottable/Waterfall.ipp @@ -1,99 +1,188 @@ #pragma once +#include "common/Curve_Plot.hpp" #include "common/Raster_Plot.hpp" #include #include #include #include +#include #include #include namespace aethera::render_2d { struct Waterfall::Private : Prev_Private { + struct Prepared { + struct Tile { + detail::Raster_Partition source{}; + Rect_F target{}; + std::vector pixels{}; + }; + std::vector> rows_by_slot{}; + detail::Raster_Layout layout{}; + std::vector tiles{}; + Rect_F tooltip_box{}; + std::string tooltip_text{}; + Size canvas{}; + int source_first{}; + bool valid{}; + }; + Private(Frequency_Axis& frequency_axis, Time_Axis& time_axis) : frequency_axis(std::addressof(frequency_axis)), time_axis(std::addressof(time_axis)) {} - - void advance() override { - concurrent().internal.advance(); - auto stream = concurrent(); - stream.internal.advance(); - for (const auto& row : *stream.internal.use()) - if (row) rows.push_back(row); - const auto& time_properties = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); - const auto limit = static_cast(std::max(2, time_properties.visible_count)); - while (rows.size() > limit) rows.pop_front(); - auto state = concurrent(); - state.internal.use()->row_count = rows.size(); - state.internal.advance(); - Prev_Private::advance(); - } - - [[nodiscard]] std::optional event_routing_distance(const Event& event) const noexcept override { - if (event.type == Event_Type::leave) return std::nullopt; - const auto* pointer = dynamic_cast(std::addressof(event)); - if (!pointer || !plot_region().contains(pointer->position)) return std::nullopt; - return 0.0; - } - - void dispatch_event(Event& event) override { - (void)detail::update_hover_tooltip(tooltip, event); - } - - void paint(detail::Painter& painter, Size) override { - auto published = concurrent(); - published.internal.use()->visible_row_count = 0; - const auto& properties = static_cast(*concurrent().internal.use()); - const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); - const auto& time_layout = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); - if (!properties.partition_grid.valid() || rows.empty() || frequency_layout.orientation == time_layout.orientation) { - published.internal.advance(); - return; - } - const auto shortest = std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left->values.size() < right->values.size(); }); - const std::size_t available = (*shortest)->values.size(); - const int source_columns = static_cast(properties.frequency_bin_count ? std::min(properties.frequency_bin_count, available) : available); - const auto selection = detail::raster_axis_selection(properties.frequency_range, frequency_axis->coordinate_range(), source_columns, properties.visible_range_only); - if (!selection) { - published.internal.advance(); - return; - } - const auto& time_properties = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); - const int time_slots = std::max(2, time_properties.visible_count); - const Axis_Range time_range = time_axis->coordinate_range(); - const auto layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, time_slots, frequency_layout.orientation, time_layout.orientation); - if (!layout.valid()) { - published.internal.advance(); - return; - } - std::vector pixels(static_cast(layout.width) * layout.height); - const Axis_Coordinate direction = time_range.length() < 0.0 ? -1.0 : 1.0; - const Axis_Coordinate first_center = time_range.origin + direction * 0.5; - for (const auto& row : rows) { - const int slot = static_cast(std::llround((static_cast(row->tick) - first_center) / direction)); - if (slot < 0 || slot >= time_slots) continue; - ++published.internal.use()->visible_row_count; - for (int column = 0; column < selection->count(); ++column) { - const auto source = static_cast(selection->first + column); - if (source >= row->values.size()) continue; - pixels[layout.index(column, slot)] = premultiply(properties.color_map.sample(detail::normalized_plot_value(row->values[source], properties.power_range))); - } - } - detail::paint_raster(painter, layout, pixels, properties.interpolation_mode); - if (properties.tooltip_enabled && tooltip.active && layout.target.contains(tooltip.position)) { - std::ostringstream text; - text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; - const Rect_F box{tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; - painter.rect(box, Pen{properties.tooltip_text_pen.color}, properties.tooltip_background_brush); - painter.text({box.x + 4.0, box.y + 3.0}, text.str(), properties.tooltip_font, properties.tooltip_text_pen); - } - published.internal.advance(); - } - - [[nodiscard]] Rect_F plot_region() const { - const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); - return detail::map_plot_rect(frequency_axis, frequency_axis->coordinate_range(), time_axis, time_axis->coordinate_range(), frequency_layout.orientation); - } - - Frequency_Axis* frequency_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - Time_Axis* time_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ - std::deque> rows{}; /* advance 线程唯一拥有的可见时间历史。 */ - detail::Hover_Tooltip_Runtime tooltip{}; + void advance() override; + [[nodiscard]] std::optional event_routing_distance(const Event& event) const noexcept override; + void dispatch_event(Event& event) override; + void build_graph(Plot_Partition_Grid partition_grid); + void prepare_frame(); + void prepare_partition(Plot_Partition_Count index); + void paint_partition(Plot_Partition_Count index); + void paint_tooltip(); + [[nodiscard]] Rect_F plot_region() const; +private: + Frequency_Axis* frequency_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + Time_Axis* time_axis{}; /* 必填、非拥有借用;调用方保证其生命周期覆盖本图元。 */ + std::deque> rows{}; + Prepared prepared{}; + detail::Hover_Tooltip_Runtime tooltip{}; + std::optional graph_partition_grid{}; }; + +inline void Waterfall::Private::advance() { + auto properties = concurrent(); + auto stream = concurrent(); + auto state = concurrent(); + properties.internal.advance(); + stream.internal.advance(); + for (const auto& row : *stream.internal.use()) + if (row) rows.push_back(row); + const auto& time_properties = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); + const auto limit = static_cast(std::max(2, time_properties.visible_count)); + while (rows.size() > limit) rows.pop_front(); + state.internal.use()->row_count = rows.size(); + state.internal.advance(); + const auto partition_grid = static_cast(*properties.internal.use()).partition_grid; + if (!graph_partition_grid || *graph_partition_grid != partition_grid) build_graph(partition_grid); +} + +inline std::optional Waterfall::Private::event_routing_distance(const Event& event) const noexcept { + if (event.type == Event_Type::leave) return std::nullopt; + const auto* pointer = dynamic_cast(std::addressof(event)); + if (!pointer || !plot_region().contains(pointer->position)) return std::nullopt; + return 0.0; +} + +inline void Waterfall::Private::dispatch_event(Event& event) { + (void)detail::update_hover_tooltip(tooltip, event); +} + +inline void Waterfall::Private::build_graph(Plot_Partition_Grid partition_grid) { + graph_partition_grid = partition_grid; + const Plot_Partition_Count partition_count = detail::raster_partition_count(partition_grid); + Task_Graph graph{this}; + auto begin = graph.add("prepare.frame", [this] { prepare_frame(); }); + auto tooltip_task = graph.add("paint.tooltip", [this] { paint_tooltip(); }); + if (partition_count == 0) begin.precede(tooltip_task); + for (Plot_Partition_Count index = 0; index < partition_count; ++index) { + auto prepare = graph.add("prepare.partition", [this, index] { prepare_partition(index); }); + auto paint = graph.add("paint.partition", [this, index] { paint_partition(index); }); + begin.precede(prepare); + prepare.precede(paint); + paint.precede(tooltip_task); + } +} + +inline void Waterfall::Private::prepare_frame() { + const auto& state = static_cast(*concurrent().internal.use()); + const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + const auto& time_layout = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); + prepared = {}; + prepared.canvas = paint_viewport(); + concurrent().internal.use()->visible_row_count = 0; + if (rows.empty()) return; + const auto shortest = std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left->values.size() < right->values.size(); }); + const std::size_t available = (*shortest)->values.size(); + const int source_columns = static_cast(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); + const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); + if (!selection) return; + const auto& time_properties = static_cast(*aethera::detail::model_private(*time_axis).concurrent().internal.use()); + const int time_slots = std::max(2, time_properties.visible_count); + const Axis_Range time_range = time_axis->coordinate_range(); + prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, time_slots, frequency_layout.orientation, time_layout.orientation); + if (prepared.canvas.empty() || !prepared.layout.valid()) return; + prepared.source_first = selection->first; + prepared.tiles.resize(detail::raster_partition_count(*graph_partition_grid)); + const Axis_Coordinate direction = time_range.length() < 0.0 ? -1.0 : 1.0; + const Axis_Coordinate first_center = time_range.origin + direction * 0.5; + prepared.rows_by_slot.resize(static_cast(time_slots)); + std::size_t visible_rows{}; + for (const auto& source_row : rows) { + const int slot = static_cast(std::llround((static_cast(source_row->tick) - first_center) / direction)); + if (slot >= 0 && slot < time_slots) { + prepared.rows_by_slot[static_cast(slot)] = source_row; + ++visible_rows; + } + } + concurrent().internal.use()->visible_row_count = visible_rows; + if (visible_rows == 0) return; + if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { + std::ostringstream text; + text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; + prepared.tooltip_text = text.str(); + prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; + } + prepared.valid = true; +} + +inline void Waterfall::Private::prepare_partition(Plot_Partition_Count index) { + if (!prepared.valid) return; + const auto& state = static_cast(*concurrent().internal.use()); + const detail::Raster_Partition core = detail::raster_partition(prepared.layout, index, *graph_partition_grid); + if (core.empty() || index >= prepared.tiles.size()) return; + const int halo = state.interpolation_mode == Image_Interpolation_Mode::nearest ? 0 : state.interpolation_mode == Image_Interpolation_Mode::bilinear ? 1 : 2; + auto& tile = prepared.tiles[index]; + tile.source = {std::max(0, core.first_x - halo), std::min(prepared.layout.width, core.last_x + halo), std::max(0, core.first_y - halo), std::min(prepared.layout.height, core.last_y + halo)}; + const int tile_width = tile.source.last_x - tile.source.first_x; + const int tile_height = tile.source.last_y - tile.source.first_y; + tile.pixels.assign(static_cast(tile_width) * tile_height, 0); + const Rect_F target = prepared.layout.target.normalized(); + const auto project_x = [&](int boundary) { return target.x + target.width * boundary / prepared.layout.width; }; + const auto project_y = [&](int boundary) { return target.y + target.height * boundary / prepared.layout.height; }; + const double left = project_x(tile.source.first_x); + const double top = project_y(tile.source.first_y); + tile.target = {left, top, project_x(tile.source.last_x) - left, project_y(tile.source.last_y) - top}; + for (int y = tile.source.first_y; y < tile.source.last_y; ++y) { + for (int x = tile.source.first_x; x < tile.source.last_x; ++x) { + const auto [column, slot] = detail::raster_coordinates(prepared.layout, x, y); + if (slot < 0 || static_cast(slot) >= prepared.rows_by_slot.size()) continue; + const auto& row = prepared.rows_by_slot[static_cast(slot)]; + if (!row) continue; + const std::size_t source = static_cast(prepared.source_first + column); + tile.pixels[static_cast(y - tile.source.first_y) * tile_width + (x - tile.source.first_x)] = premultiply(state.color_map.sample(detail::normalized_plot_value(row->values[source], state.power_range))); + } + } +} + +inline void Waterfall::Private::paint_partition(Plot_Partition_Count index) { + const auto& state = static_cast(*concurrent().internal.use()); + if (!prepared.valid || index >= detail::raster_partition_count(*graph_partition_grid) || index >= prepared.tiles.size()) return; + const Rect_F region = detail::raster_paint_region(prepared.layout, index, *graph_partition_grid); + if (region.empty()) return; + const auto& tile = prepared.tiles[index]; + const int tile_width = tile.source.last_x - tile.source.first_x; + const int tile_height = tile.source.last_y - tile.source.first_y; + if (tile_width <= 0 || tile_height <= 0 || tile.pixels.empty()) return; + detail::Painter painter(paint_surface(), prepared.canvas, region); + painter.heatmap(tile.target, tile_width, tile_height, tile.pixels, state.interpolation_mode); +} + +inline void Waterfall::Private::paint_tooltip() { + if (!prepared.valid || prepared.tooltip_text.empty()) return; + const auto& state = static_cast(*concurrent().internal.use()); + detail::Painter painter(paint_surface(), prepared.canvas, prepared.tooltip_box); + painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); + painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); +} + +inline Rect_F Waterfall::Private::plot_region() const { + const auto& frequency_layout = static_cast(*aethera::detail::model_private(*frequency_axis).concurrent().internal.use()); + return detail::map_plot_rect(frequency_axis, frequency_axis->coordinate_range(), time_axis, time_axis->coordinate_range(), frequency_layout.orientation); +} } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/plottable/axis/Abs_Axis.ipp b/render_2D/render_2D/module/plottable/axis/Abs_Axis.ipp index 3cd8aec..2ce5ead 100644 --- a/render_2D/render_2D/module/plottable/axis/Abs_Axis.ipp +++ b/render_2D/render_2D/module/plottable/axis/Abs_Axis.ipp @@ -1,34 +1,56 @@ #pragma once #include #include -#include #include +#include namespace aethera::render_2d { struct Abs_Axis::Private : Prev_Private { + struct Prepared_Line { + Point_F first{}; + Point_F second{}; + }; + struct Prepared_Label { + Point_F position{}; + std::string text{}; + }; + struct Prepared_Axis { + std::vector lines{}; + std::vector labels{}; + Point_F unit_position{}; + double unit_width{}; + bool valid{}; + }; + ~Private() override; void advance() override; [[nodiscard]] std::optional event_routing_distance(const Event& event) const noexcept override; void dispatch_event(Event& event) override; - void paint(detail::Painter& painter, Size viewport) override; - [[nodiscard]] virtual Axis_Range coordinate_range() const; - [[nodiscard]] virtual Axis_Coordinate tick_step(Axis_Range coordinate_range) const; - [[nodiscard]] virtual std::string tick_label(Axis_Coordinate tick) const; - [[nodiscard]] virtual Axis_Tick_Count sub_tick_count(Axis_Coordinate major_step) const; - [[nodiscard]] Axis_Pixel_Position coordinate_to_pixel(Axis_Coordinate coordinate) const; - [[nodiscard]] Axis_Coordinate pixel_to_coordinate(Axis_Pixel_Position pixel) const; - [[nodiscard]] Axis_Coordinate point_to_coordinate(Point_F point) const; - [[nodiscard]] Axis_Pixel_Sample_Count pixel_sample_count(Axis_Range coordinate_range) const; - [[nodiscard]] static Axis_Coordinate nice_tick_step(Axis_Range coordinate_range); - [[nodiscard]] static std::string localized_number(double value, int precision, Number_Locale locale); - protected: - virtual void handle_event(Event& event); + [[nodiscard]] virtual Axis_Range coordinate_range() const; + [[nodiscard]] virtual Axis_Coordinate tick_step(Axis_Range coordinate_range) const; + [[nodiscard]] virtual std::string tick_label(Axis_Coordinate tick) const; + [[nodiscard]] virtual Axis_Tick_Count sub_tick_count(Axis_Coordinate major_step) const; + [[nodiscard]] Axis_Pixel_Position coordinate_to_pixel(Axis_Coordinate coordinate) const; + [[nodiscard]] Axis_Coordinate pixel_to_coordinate(Axis_Pixel_Position pixel) const; + [[nodiscard]] Axis_Coordinate point_to_coordinate(Point_F point) const; + [[nodiscard]] Axis_Pixel_Sample_Count pixel_sample_count(Axis_Range coordinate_range) const; + [[nodiscard]] static Axis_Coordinate nice_tick_step(Axis_Range coordinate_range); + [[nodiscard]] static std::string localized_number(double value, int precision, Number_Locale locale); +protected: + virtual void handle_event(Event& event); + void prepare_data(); + void paint(); +private: + Prepared_Axis prepared{}; /* 当前属性推导出的绘制输入;prepare 完成后只由其后继 paint 读取。 */ }; inline Abs_Axis::Private::~Private() = default; inline void Abs_Axis::Private::advance() { concurrent().internal.advance(); - Prev_Private::advance(); + Task_Graph graph{this}; + auto prepare = graph.add("prepare.axis", [this] { prepare_data(); }); + auto paint_node = graph.add("paint.axis", [this] { this->paint(); }); + prepare.precede(paint_node); } inline Axis_Pixel_Position Abs_Axis::Private::coordinate_to_pixel(Axis_Coordinate coordinate) const { @@ -52,9 +74,9 @@ inline Axis_Coordinate Abs_Axis::Private::point_to_coordinate(Point_F point) con return pixel_to_coordinate(properties.orientation == Axis_Orientation::horizontal ? point.x : point.y); } -inline Axis_Pixel_Sample_Count Abs_Axis::Private::pixel_sample_count(Axis_Range coordinate_range) const { - const double first_pixel = coordinate_to_pixel(coordinate_range.origin); - const double last_pixel = coordinate_to_pixel(coordinate_range.target); +inline Axis_Pixel_Sample_Count Abs_Axis::Private::pixel_sample_count(Axis_Range coordinate_range_value) const { + const double first_pixel = coordinate_to_pixel(coordinate_range_value.origin); + const double last_pixel = coordinate_to_pixel(coordinate_range_value.target); return std::max(0, static_cast(std::abs(last_pixel - first_pixel)) + 1); } @@ -86,15 +108,16 @@ inline void Abs_Axis::Private::dispatch_event(Event& event) { inline void Abs_Axis::Private::handle_event(Event&) {} -inline void Abs_Axis::Private::paint(detail::Painter& painter, Size) { - const auto& properties = static_cast(*concurrent().internal.use()); - if (properties.pixel_length == 0.0) return; +inline void Abs_Axis::Private::prepare_data() { + const auto& state = static_cast(*concurrent().internal.use()); + prepared = {}; + if (state.pixel_length == 0.0) return; const Axis_Range coordinates = coordinate_range(); const double step = tick_step(coordinates); if (!(step > 0.0) || !std::isfinite(step)) return; - const Point_F first = properties.position; - const Point_F last = properties.orientation == Axis_Orientation::horizontal ? Point_F{first.x + properties.pixel_length, first.y} : Point_F{first.x, first.y + properties.pixel_length}; - painter.line(first, last, properties.axis_pen); + const Point_F first = state.position; + const Point_F last = state.orientation == Axis_Orientation::horizontal ? Point_F{first.x + state.pixel_length, first.y} : Point_F{first.x, first.y + state.pixel_length}; + prepared.lines.push_back({first, last}); const auto [low, high] = std::minmax(coordinates.origin, coordinates.target); const double initial = std::ceil(low / step) * step; for (int tick_index = 0; tick_index < 1000; ++tick_index) { @@ -104,31 +127,42 @@ inline void Abs_Axis::Private::paint(detail::Painter& painter, Size) { Point_F tick_start{}; Point_F tick_end{}; Point_F label{}; - if (properties.orientation == Axis_Orientation::horizontal) { - tick_start = {pixel, properties.position.y}; - tick_end = {pixel, properties.position.y + properties.tick_length}; - label = {pixel + 2.0, properties.position.y + properties.tick_length + 2.0}; + if (state.orientation == Axis_Orientation::horizontal) { + tick_start = {pixel, state.position.y}; + tick_end = {pixel, state.position.y + state.tick_length}; + label = {pixel + 2.0, state.position.y + state.tick_length + 2.0}; } else { - tick_start = {properties.position.x, pixel}; - tick_end = {properties.position.x + properties.tick_length, pixel}; - label = {properties.position.x + properties.tick_length + 2.0, pixel - 7.0}; + tick_start = {state.position.x, pixel}; + tick_end = {state.position.x + state.tick_length, pixel}; + label = {state.position.x + state.tick_length + 2.0, pixel - 7.0}; } - painter.line(tick_start, tick_end, properties.axis_pen); - painter.text(label, tick_label(tick), properties.unit_text_font, properties.unit_text_pen, properties.label_rotation_degrees); + prepared.lines.push_back({tick_start, tick_end}); + prepared.labels.push_back({label, tick_label(tick)}); const int subdivisions = std::max(0, sub_tick_count(step)); for (int sub_index = 1; sub_index <= subdivisions; ++sub_index) { const double sub_tick = tick + step * sub_index / (subdivisions + 1.0); if (sub_tick >= high) break; const double sub_pixel = coordinate_to_pixel(sub_tick); - if (properties.orientation == Axis_Orientation::horizontal) painter.line({sub_pixel, properties.position.y}, {sub_pixel, properties.position.y + properties.sub_tick_length}, properties.axis_pen); - else painter.line({properties.position.x, sub_pixel}, {properties.position.x + properties.sub_tick_length, sub_pixel}, properties.axis_pen); + if (state.orientation == Axis_Orientation::horizontal) prepared.lines.push_back({{sub_pixel, state.position.y}, {sub_pixel, state.position.y + state.sub_tick_length}}); + else prepared.lines.push_back({{state.position.x, sub_pixel}, {state.position.x + state.sub_tick_length, sub_pixel}}); } } - if (properties.unit_text.empty()) return; - const Point_F unit_position{last.x + 4.0, last.y + 4.0}; - const double unit_width = std::max(4.0, properties.unit_text.size() * properties.unit_text_font.size * 0.65); - painter.rect({unit_position.x - 2.0, unit_position.y - 2.0, unit_width + 4.0, properties.unit_text_font.size * 1.5 + 4.0}, Pen{.style = Line_Style::none}, properties.unit_text_background_brush); - painter.text(unit_position, properties.unit_text, properties.unit_text_font, properties.unit_text_pen); + if (!state.unit_text.empty()) { + prepared.unit_position = {last.x + 4.0, last.y + 4.0}; + prepared.unit_width = std::max(4.0, state.unit_text.size() * state.unit_text_font.size * 0.65); + } + prepared.valid = true; +} + +inline void Abs_Axis::Private::paint() { + const auto& state = static_cast(*concurrent().internal.use()); + if (!prepared.valid) return; + detail::Painter painter(paint_surface(), paint_viewport()); + for (const auto& line : prepared.lines) painter.line(line.first, line.second, state.axis_pen); + for (const auto& label : prepared.labels) painter.text(label.position, label.text, state.unit_text_font, state.unit_text_pen, state.label_rotation_degrees); + if (state.unit_text.empty()) return; + painter.rect({prepared.unit_position.x - 2.0, prepared.unit_position.y - 2.0, prepared.unit_width + 4.0, state.unit_text_font.size * 1.5 + 4.0}, Pen{.style = Line_Style::none}, state.unit_text_background_brush); + painter.text(prepared.unit_position, state.unit_text, state.unit_text_font, state.unit_text_pen); } } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/plottable/axis/Numeric_Axis.ipp b/render_2D/render_2D/module/plottable/axis/Numeric_Axis.ipp index aa5287f..218adff 100644 --- a/render_2D/render_2D/module/plottable/axis/Numeric_Axis.ipp +++ b/render_2D/render_2D/module/plottable/axis/Numeric_Axis.ipp @@ -1,24 +1,23 @@ #pragma once #include -#include namespace aethera::render_2d { struct Numeric_Axis::Private : Prev_Private { [[nodiscard]] Axis_Range coordinate_range() const override; [[nodiscard]] Axis_Coordinate tick_step(Axis_Range coordinate_range) const override; [[nodiscard]] std::string tick_label(Axis_Coordinate tick) const override; - protected: - void handle_event(Event& event) override; - private: - bool dragging{}; /* advance 线程正在处理的左键拖动手势。 */ - Point_F last_pointer{}; /* 上一个拖动事件的画布局部坐标。 */ +protected: + void handle_event(Event& event) override; +private: + bool dragging{}; + Point_F last_pointer{}; }; inline Axis_Range Numeric_Axis::Private::coordinate_range() const { return static_cast(*concurrent().internal.use()).coordinate_range; } -inline Axis_Coordinate Numeric_Axis::Private::tick_step(Axis_Range coordinate_range) const { - return nice_tick_step(coordinate_range); +inline Axis_Coordinate Numeric_Axis::Private::tick_step(Axis_Range coordinate_range_value) const { + return nice_tick_step(coordinate_range_value); } inline std::string Numeric_Axis::Private::tick_label(Axis_Coordinate tick) const { @@ -50,10 +49,10 @@ inline void Numeric_Axis::Private::handle_event(Event& event) { return; } if (event.type == Event_Type::pointer_move && dragging) { - const Point_F current = pointer->position; - const double delta = axis_properties.orientation == Axis_Orientation::horizontal ? current.x - last_pointer.x : current.y - last_pointer.y; - const double shift = axis_properties.pixel_length == 0.0 ? 0.0 : -delta * numeric_properties.coordinate_range.length() / axis_properties.pixel_length; - last_pointer = current; + const Point_F current = pointer->position; + const double delta = axis_properties.orientation == Axis_Orientation::horizontal ? current.x - last_pointer.x : current.y - last_pointer.y; + const double shift = axis_properties.pixel_length == 0.0 ? 0.0 : -delta * numeric_properties.coordinate_range.length() / axis_properties.pixel_length; + last_pointer = current; concurrent().set(&Prop::coordinate_range, Axis_Range{numeric_properties.coordinate_range.origin + shift, numeric_properties.coordinate_range.target + shift}); event.accept(); return; diff --git a/render_2D/render_2D/module/plottable/axis/Time_Axis.ipp b/render_2D/render_2D/module/plottable/axis/Time_Axis.ipp index 7d4d99b..af4d3bb 100644 --- a/render_2D/render_2D/module/plottable/axis/Time_Axis.ipp +++ b/render_2D/render_2D/module/plottable/axis/Time_Axis.ipp @@ -13,16 +13,16 @@ struct Time_Axis::Private : Prev_Private { Axis_Time_Tick append_time(Time_Of_Day time); [[nodiscard]] Time_Of_Day tick_to_time(Axis_Time_Tick tick) const noexcept; [[nodiscard]] static std::string formatted_time(Time_Of_Day time, std::string_view format); - private: - std::atomic next_tick{}; /* 多生产者分配单调 tick 的唯一计数器。 */ - std::deque> samples{}; /* advance 线程唯一拥有的已提交时间历史。 */ +private: + std::atomic next_tick{}; /* 多生产者分配 tick 的唯一来源。 */ + std::deque> samples{}; /* advance 线程唯一拥有的已提交历史。 */ }; inline void Time_Axis::Private::advance() { - Prev_Private::advance(); auto stream = concurrent(); stream.internal.advance(); for (const auto& sample : *stream.internal.use()) samples.push_back(sample); + Prev_Private::advance(); const auto& properties = static_cast(*concurrent().internal.use()); const auto limit = static_cast(std::max(2, properties.visible_count)); while (samples.size() > limit) samples.pop_front(); @@ -31,17 +31,17 @@ inline void Time_Axis::Private::advance() { inline Axis_Range Time_Axis::Private::coordinate_range() const { const auto& properties = static_cast(*concurrent().internal.use()); const auto visible = std::max(2, properties.visible_count); - const auto latest = samples.empty() ? Axis_Time_Tick{} : samples.back().first; + const auto latest = std::max(0, next_tick.load(std::memory_order_relaxed) - 1); if (properties.newest_at_start) return {static_cast(latest) + 0.5, static_cast(latest - visible) + 0.5}; return {static_cast(latest - visible) + 0.5, static_cast(latest) + 0.5}; } -inline Axis_Coordinate Time_Axis::Private::tick_step(Axis_Range coordinate_range) const { +inline Axis_Coordinate Time_Axis::Private::tick_step(Axis_Range coordinate_range_value) const { const auto& properties = static_cast(*concurrent().internal.use()); const auto& axis_properties = static_cast(properties); const double label_width = std::max(76.0, properties.estimated_label_width_px); const double label_count = std::max(1.0, std::abs(axis_properties.pixel_length) / (label_width + std::max(0.0, properties.tick_label_spacing_px))); - return std::max(1.0, std::ceil(coordinate_range.size() / label_count)); + return std::max(1.0, std::ceil(coordinate_range_value.size() / label_count)); } inline std::string Time_Axis::Private::tick_label(Axis_Coordinate tick) const { diff --git a/render_2D/render_2D/module/plottable/common/Curve_Plot.cpp b/render_2D/render_2D/module/plottable/common/Curve_Plot.cpp index 33c2239..7e3fb3a 100644 --- a/render_2D/render_2D/module/plottable/common/Curve_Plot.cpp +++ b/render_2D/render_2D/module/plottable/common/Curve_Plot.cpp @@ -3,32 +3,36 @@ #include namespace aethera::render_2d::detail { namespace { -std::size_t partition_boundary(std::size_t work_size, Plot_Partition_Count index, Plot_Partition_Count count) { - return work_size / count * index + std::min(index, work_size % count); +std::size_t partition_boundary(std::size_t work_size, + Plot_Partition_Count index, + Plot_Partition_Count count) { + return work_size / count * index + + std::min(index, work_size % count); } - Plot_Value power_domain_lerp(Plot_Value first, Plot_Value second, Plot_Ratio ratio) { - const Plot_Value first_power = std::pow(10.0, std::clamp(first, -3'000.0, 3'000.0) / 10.0); + const Plot_Value first_power = std::pow(10.0, std::clamp(first, -3'000.0, 3'000.0) / 10.0); const Plot_Value second_power = std::pow(10.0, std::clamp(second, -3'000.0, 3'000.0) / 10.0); return 10.0 * std::log10(std::max(first_power + (second_power - first_power) * ratio, 1e-300)); } - Plot_Value cubic_value(Plot_Value previous, Plot_Value first, Plot_Value second, Plot_Value next, Plot_Ratio ratio) { const Plot_Ratio ratio2 = ratio * ratio; const Plot_Ratio ratio3 = ratio2 * ratio; return 0.5 * ((2.0 * first) + (-previous + second) * ratio + (2.0 * previous - 5.0 * first + 4.0 * second - next) * ratio2 + (-previous + 3.0 * first - 3.0 * second + next) * ratio3); } -} // namespace - +} Curve_Partition_Range curve_partition_range(std::size_t sample_count, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count, Axis_Range domain) { - if (sample_count == 0 || partition_count == 0 || partition_index >= partition_count) return {}; + if (sample_count == 0 || partition_count == 0) return {}; const std::size_t segment_count = sample_count > 1 ? sample_count - 1 : 1; - const std::size_t first_sample = std::min(partition_boundary(segment_count, partition_index, partition_count), sample_count - 1); - const std::size_t last_sample = std::min(partition_boundary(segment_count, partition_index + 1, partition_count), sample_count - 1); - const Plot_Ratio denominator = sample_count > 1 ? static_cast(sample_count - 1) : 1.0; + const std::size_t first_sample = std::min( + partition_boundary(segment_count, partition_index, partition_count), + sample_count - 1); + const std::size_t last_sample = std::min( + partition_boundary(segment_count, partition_index + 1, + partition_count), + sample_count - 1); + const Plot_Ratio denominator = sample_count > 1 ? static_cast(sample_count - 1) : 1.0; return {first_sample, last_sample - first_sample + 1, {domain.origin + domain.length() * static_cast(first_sample) / denominator, domain.origin + domain.length() * static_cast(last_sample) / denominator}}; } - std::vector interpolate_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode) { std::vector result; if (values.empty()) return result; @@ -38,64 +42,30 @@ std::vector interpolate_curve(std::span values, const auto coordinate = [domain, denominator](std::size_t index) { return domain.origin + domain.length() * static_cast(index) / denominator; }; result.push_back({coordinate(0), values.front()}); for (std::size_t index = 0; index + 1 < values.size(); ++index) { - const Plot_Coordinate first_coordinate = coordinate(index); + const Plot_Coordinate first_coordinate = coordinate(index); const Plot_Coordinate second_coordinate = coordinate(index + 1); - const Plot_Value first = values[index]; - const Plot_Value second = values[index + 1]; + const Plot_Value first = values[index]; + const Plot_Value second = values[index + 1]; switch (mode) { - case Line_Interpolation_Mode::nearest_sample: { - const Plot_Coordinate middle = (first_coordinate + second_coordinate) * 0.5; - result.push_back({middle, first}); - result.push_back({middle, second}); - result.push_back({second_coordinate, second}); - break; - } - case Line_Interpolation_Mode::linear_value: - result.push_back({second_coordinate, second}); - break; - case Line_Interpolation_Mode::linear_power_domain: - for (int part = 1; part <= subdivisions; ++part) { - const Plot_Ratio ratio = static_cast(part) / subdivisions; - result.push_back({first_coordinate + (second_coordinate - first_coordinate) * ratio, power_domain_lerp(first, second, ratio)}); - } - break; - case Line_Interpolation_Mode::step_left: - result.push_back({second_coordinate, first}); - result.push_back({second_coordinate, second}); - break; - case Line_Interpolation_Mode::step_right: - result.push_back({first_coordinate, second}); - result.push_back({second_coordinate, second}); - break; - case Line_Interpolation_Mode::cubic_value: { - const Plot_Value previous = values[index == 0 ? 0 : index - 1]; - const Plot_Value next = values[std::min(index + 2, values.size() - 1)]; - for (int part = 1; part <= subdivisions; ++part) { - const Plot_Ratio ratio = static_cast(part) / subdivisions; - result.push_back({first_coordinate + (second_coordinate - first_coordinate) * ratio, cubic_value(previous, first, second, next, ratio)}); - } - break; - } + case Line_Interpolation_Mode::nearest_sample: { const Plot_Coordinate middle = (first_coordinate + second_coordinate) * 0.5; result.push_back({middle, first}); result.push_back({middle, second}); result.push_back({second_coordinate, second}); break; } + case Line_Interpolation_Mode::linear_value: result.push_back({second_coordinate, second}); break; + case Line_Interpolation_Mode::linear_power_domain: for (int part = 1; part <= subdivisions; ++part) { const Plot_Ratio ratio = static_cast(part) / subdivisions; result.push_back({first_coordinate + (second_coordinate - first_coordinate) * ratio, power_domain_lerp(first, second, ratio)}); } break; + case Line_Interpolation_Mode::step_left: result.push_back({second_coordinate, first}); result.push_back({second_coordinate, second}); break; + case Line_Interpolation_Mode::step_right: result.push_back({first_coordinate, second}); result.push_back({second_coordinate, second}); break; + case Line_Interpolation_Mode::cubic_value: { const Plot_Value previous = values[index == 0 ? 0 : index - 1]; const Plot_Value next = values[std::min(index + 2, values.size() - 1)]; for (int part = 1; part <= subdivisions; ++part) { const Plot_Ratio ratio = static_cast(part) / subdivisions; result.push_back({first_coordinate + (second_coordinate - first_coordinate) * ratio, cubic_value(previous, first, second, next, ratio)}); } break; } } } return result; } - std::vector visible_curve_samples(std::vector samples, Axis_Range visible_range) { const auto [low, high] = std::minmax(visible_range.origin, visible_range.target); if (samples.size() < 2) return !samples.empty() && samples.front().coordinate >= low && samples.front().coordinate <= high ? std::move(samples) : std::vector{}; std::size_t first = samples.size(); std::size_t last{}; - for (std::size_t index = 0; index + 1 < samples.size(); ++index) { - const auto [segment_low, segment_high] = std::minmax(samples[index].coordinate, samples[index + 1].coordinate); - if (segment_high < low || segment_low > high) continue; - first = std::min(first, index); - last = std::max(last, index + 1); - } + for (std::size_t index = 0; index + 1 < samples.size(); ++index) { const auto [segment_low, segment_high] = std::minmax(samples[index].coordinate, samples[index + 1].coordinate); if (segment_high < low || segment_low > high) continue; first = std::min(first, index); last = std::max(last, index + 1); } if (first == samples.size()) return {}; return {samples.begin() + static_cast(first), samples.begin() + static_cast(last + 1)}; } - Rect_F curve_paint_region(const Curve_Prepared& curve, double padding) { bool initialized{}; double left{}; @@ -109,21 +79,22 @@ Rect_F curve_paint_region(const Curve_Prepared& curve, double padding) { initialized = true; return; } - left = std::min(left, point.x); - top = std::min(top, point.y); - right = std::max(right, point.x); + left = std::min(left, point.x); + top = std::min(top, point.y); + right = std::max(right, point.x); bottom = std::max(bottom, point.y); }; for (const auto& point : curve.points) include(point); for (const auto& point : curve.fill) include(point); if (!initialized) return {}; padding = std::max(1.0, padding); - return {left - padding, top - padding, right - left + padding * 2.0, bottom - top + padding * 2.0}; + return {left - padding, top - padding, + right - left + padding * 2.0, + bottom - top + padding * 2.0}; } - void paint_curve(Painter& painter, const Curve_Prepared& curve, const Pen& pen, const Brush& brush) { if (curve.points.size() < 2) return; if (brush.enabled()) painter.polygon(curve.fill, Pen{.style = Line_Style::none}, brush); painter.polyline(curve.points, pen); } -} // namespace aethera::render_2d::detail +} diff --git a/render_2D/render_2D/module/plottable/common/Curve_Plot.hpp b/render_2D/render_2D/module/plottable/common/Curve_Plot.hpp index 70f2b46..7d5a3eb 100644 --- a/render_2D/render_2D/module/plottable/common/Curve_Plot.hpp +++ b/render_2D/render_2D/module/plottable/common/Curve_Plot.hpp @@ -1,77 +1,67 @@ #pragma once -#include "../Plot_Types.hpp" #include "../axis/Axis.hpp" #include "../../scene/frame/Blend2D_Cache.hpp" -#include +#include "../Plot_Types.hpp" #include #include +#include namespace aethera::render_2d::detail { struct Curve_Sample { - Plot_Coordinate coordinate{}; - Plot_Value value{}; + Plot_Coordinate coordinate{}; /* 曲线样本的横向业务坐标。 */ + Plot_Value value{}; /* 曲线样本的纵向业务值。 */ }; - struct Curve_Prepared { - std::vector points{}; - std::vector fill{}; + std::vector points{}; /* 映射到画布后的折线点。 */ + std::vector fill{}; /* 含值轴基线闭合点的填充多边形。 */ }; - struct Curve_Partition_Range { - std::size_t first_sample{}; - std::size_t sample_count{}; - Axis_Range domain{}; + std::size_t first_sample{}; /* 分块包含的首个样本下标。 */ + std::size_t sample_count{}; /* 分块包含的样本数;相邻块共享边界样本。 */ + Axis_Range domain{}; /* 分块样本对应的业务坐标范围。 */ }; - [[nodiscard]] Curve_Partition_Range curve_partition_range(std::size_t sample_count, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count, Axis_Range domain); [[nodiscard]] std::vector interpolate_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode); [[nodiscard]] std::vector visible_curve_samples(std::vector samples, Axis_Range visible_range); - template [[nodiscard]] Point_F map_plot_point(const Coordinate_Axis* coordinate_axis, Plot_Coordinate coordinate, const Value_Axis* value_axis, Plot_Value value, Axis_Orientation coordinate_orientation) { const Axis_Pixel_Position coordinate_pixel = coordinate_axis->coordinate_to_pixel(coordinate); - const Axis_Pixel_Position value_pixel = value_axis->coordinate_to_pixel(value); - return coordinate_orientation == Axis_Orientation::horizontal ? Point_F{coordinate_pixel, value_pixel} : Point_F{value_pixel, coordinate_pixel}; + const Axis_Pixel_Position value_pixel = value_axis->coordinate_to_pixel(value); + return coordinate_orientation == Axis_Orientation::horizontal + ? Point_F{coordinate_pixel, value_pixel} + : Point_F{value_pixel, coordinate_pixel}; } - template [[nodiscard]] Rect_F map_plot_rect(const Coordinate_Axis* coordinate_axis, Axis_Range coordinate_range, const Value_Axis* value_axis, Axis_Range value_range, Axis_Orientation coordinate_orientation) { - const Point_F first = map_plot_point(coordinate_axis, coordinate_range.origin, value_axis, value_range.origin, coordinate_orientation); + const Point_F first = map_plot_point(coordinate_axis, coordinate_range.origin, value_axis, value_range.origin, coordinate_orientation); const Point_F second = map_plot_point(coordinate_axis, coordinate_range.target, value_axis, value_range.target, coordinate_orientation); return Rect_F{first.x, first.y, second.x - first.x, second.y - first.y}.normalized(); } - template [[nodiscard]] Curve_Prepared prepare_curve(std::span source, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Coordinate_Axis* coordinate_axis, const Value_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation) { Curve_Prepared result; std::vector samples{source.begin(), source.end()}; if (visible_only) samples = visible_curve_samples(std::move(samples), visible_coordinate_range); for (const auto& sample : samples) - if (std::isfinite(sample.coordinate) && std::isfinite(sample.value)) result.points.push_back(map_plot_point(coordinate_axis, sample.coordinate, value_axis, sample.value, coordinate_orientation)); + if (std::isfinite(sample.coordinate) && std::isfinite(sample.value)) + result.points.push_back(map_plot_point(coordinate_axis, sample.coordinate, value_axis, sample.value, coordinate_orientation)); if (result.points.size() < 2) return result; const Axis_Pixel_Position baseline = value_axis->coordinate_to_pixel(value_range.target); Point_F first = result.points.front(); - Point_F last = result.points.back(); - if (value_orientation == Axis_Orientation::horizontal) { - first.x = baseline; - last.x = baseline; - } - else { - first.y = baseline; - last.y = baseline; - } + Point_F last = result.points.back(); + if (value_orientation == Axis_Orientation::horizontal) { first.x = baseline; last.x = baseline; } + else { first.y = baseline; last.y = baseline; } result.fill.reserve(result.points.size() + 2); result.fill.push_back(first); result.fill.insert(result.fill.end(), result.points.begin(), result.points.end()); result.fill.push_back(last); return result; } - template [[nodiscard]] Curve_Prepared prepare_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Coordinate_Axis* coordinate_axis, const Value_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation) { const auto samples = interpolate_curve(values, domain, mode); return prepare_curve(std::span{samples}, visible_only, visible_coordinate_range, value_range, coordinate_axis, value_axis, coordinate_orientation, value_orientation); } - +/* 返回曲线及填充覆盖的 framebuffer 视图,padding 用于描边与抗锯齿重叠边界。 */ [[nodiscard]] Rect_F curve_paint_region(const Curve_Prepared& curve, double padding); void paint_curve(Painter& painter, const Curve_Prepared& curve, const Pen& pen, const Brush& brush = {}); -} // namespace aethera::render_2d::detail +} diff --git a/render_2D/render_2D/module/plottable/common/Raster_Plot.cpp b/render_2D/render_2D/module/plottable/common/Raster_Plot.cpp index ac7c74a..7fa6b42 100644 --- a/render_2D/render_2D/module/plottable/common/Raster_Plot.cpp +++ b/render_2D/render_2D/module/plottable/common/Raster_Plot.cpp @@ -1,15 +1,23 @@ #include "Raster_Plot.hpp" +#include "Curve_Plot.hpp" #include -#include namespace aethera::render_2d::detail { namespace { -Plot_Partition_Count partition_boundary(Plot_Partition_Count work_size, Plot_Partition_Count index, Plot_Partition_Count count) { - return work_size / count * index + std::min(index, work_size % count); +Plot_Partition_Count partition_boundary(Plot_Partition_Count work_size, + Plot_Partition_Count index, + Plot_Partition_Count count) { + return work_size / count * index + + std::min(index, work_size % count); +} } -} // namespace - bool Raster_Layout::valid() const noexcept { return width > 0 && height > 0 && !target.empty(); } - +int Raster_Axis_Selection::count() const noexcept { return last - first + 1; } +bool Raster_Partition::empty() const noexcept { + return first_x >= last_x || first_y >= last_y; +} +bool Raster_Partition::contains(int x, int y) const noexcept { + return x >= first_x && x < last_x && y >= first_y && y < last_y; +} std::size_t Raster_Layout::index(int first, int second) const noexcept { if (first_reversed) first = (first_horizontal ? width : height) - 1 - first; if (second_reversed) second = (first_horizontal ? height : width) - 1 - second; @@ -17,69 +25,80 @@ std::size_t Raster_Layout::index(int first, int second) const noexcept { const int y = first_horizontal ? second : first; return static_cast(y) * static_cast(width) + static_cast(x); } - -int Raster_Axis_Selection::count() const noexcept { return last - first + 1; } -bool Raster_Partition::empty() const noexcept { return first_x >= last_x || first_y >= last_y; } -bool Raster_Partition::contains(int x, int y) const noexcept { return x >= first_x && x < last_x && y >= first_y && y < last_y; } - Plot_Ratio normalized_plot_value(Plot_Value value, Axis_Range range) { return range.length() == 0.0 ? 0.0 : std::clamp((value - range.origin) / range.length(), 0.0, 1.0); } - std::optional raster_axis_selection(Axis_Range data_range, Axis_Range visible_range, int source_count, bool visible_only) { if (source_count <= 0) return std::nullopt; if (!visible_only || source_count == 1 || data_range.length() == 0.0) return Raster_Axis_Selection{0, source_count - 1, data_range}; - const auto [data_low, data_high] = std::minmax(data_range.origin, data_range.target); + const auto [data_low, data_high] = std::minmax(data_range.origin, data_range.target); const auto [visible_low, visible_high] = std::minmax(visible_range.origin, visible_range.target); - const Axis_Coordinate clipped_low = std::max(data_low, visible_low); - const Axis_Coordinate clipped_high = std::min(data_high, visible_high); + const Axis_Coordinate clipped_low = std::max(data_low, visible_low); + const Axis_Coordinate clipped_high = std::min(data_high, visible_high); if (clipped_low > clipped_high) return std::nullopt; const auto position = [data_range, source_count](Axis_Coordinate coordinate) { return (coordinate - data_range.origin) / data_range.length() * static_cast(source_count - 1); }; const auto [position_low, position_high] = std::minmax(position(clipped_low), position(clipped_high)); int first = std::clamp(static_cast(std::floor(position_low)), 0, source_count - 1); - int last = std::clamp(static_cast(std::ceil(position_high)), first, source_count - 1); - if (first == last) { - if (last + 1 < source_count) ++last; - else if (first > 0) --first; - } + int last = std::clamp(static_cast(std::ceil(position_high)), first, source_count - 1); + if (first == last) { if (last + 1 < source_count) ++last; else if (first > 0) --first; } const auto coordinate = [data_range, source_count](int index) { return data_range.origin + data_range.length() * static_cast(index) / static_cast(source_count - 1); }; return Raster_Axis_Selection{first, last, {coordinate(first), coordinate(last)}}; } - -Plot_Partition_Count raster_partition_count(Plot_Partition_Grid grid) noexcept { return grid.valid() ? grid.columns * grid.rows : 0; } - -Raster_Partition raster_partition(const Raster_Layout& layout, Plot_Partition_Count partition_index, Plot_Partition_Grid grid) { +Plot_Partition_Count raster_partition_count(Plot_Partition_Grid grid) noexcept { + return grid.valid() ? grid.columns * grid.rows : 0; +} +Raster_Partition raster_partition(const Raster_Layout& layout, + Plot_Partition_Count partition_index, + Plot_Partition_Grid grid) { const Plot_Partition_Count count = raster_partition_count(grid); if (!layout.valid() || partition_index >= count) return {}; const auto column = partition_index % grid.columns; - const auto row = partition_index / grid.columns; - return {static_cast(partition_boundary(layout.width, column, grid.columns)), static_cast(partition_boundary(layout.width, column + 1, grid.columns)), static_cast(partition_boundary(layout.height, row, grid.rows)), static_cast(partition_boundary(layout.height, row + 1, grid.rows))}; + const auto row = partition_index / grid.columns; + return { + static_cast(partition_boundary(layout.width, column, + grid.columns)), + static_cast(partition_boundary(layout.width, column + 1, + grid.columns)), + static_cast(partition_boundary(layout.height, row, + grid.rows)), + static_cast(partition_boundary(layout.height, row + 1, + grid.rows))}; } - -std::pair raster_coordinates(const Raster_Layout& layout, int x, int y) noexcept { - int first = layout.first_horizontal ? x : y; +std::pair raster_coordinates(const Raster_Layout& layout, + int x, int y) noexcept { + int first = layout.first_horizontal ? x : y; int second = layout.first_horizontal ? y : x; - if (layout.first_reversed) first = (layout.first_horizontal ? layout.width : layout.height) - 1 - first; - if (layout.second_reversed) second = (layout.first_horizontal ? layout.height : layout.width) - 1 - second; + if (layout.first_reversed) + first = (layout.first_horizontal ? layout.width : layout.height) - 1 - first; + if (layout.second_reversed) + second = (layout.first_horizontal ? layout.height : layout.width) - 1 - second; return {first, second}; } - -Rect_F raster_paint_region(const Raster_Layout& layout, Plot_Partition_Count partition_index, Plot_Partition_Grid grid) { - const Raster_Partition partition = raster_partition(layout, partition_index, grid); +Rect_F raster_paint_region(const Raster_Layout& layout, + Plot_Partition_Count partition_index, + Plot_Partition_Grid grid) { + const Raster_Partition partition = + raster_partition(layout, partition_index, grid); if (partition.empty()) return {}; const Rect_F target = layout.target.normalized(); - const auto pixel_boundary = [](double origin, double extent, int source_boundary, int source_extent, bool last) { - const double projected = origin + extent * source_boundary / static_cast(source_extent); + const auto pixel_boundary = [](double origin, double extent, + int source_boundary, int source_extent, + bool last) { + const double projected = origin + extent * source_boundary / + static_cast(source_extent); return last ? std::ceil(projected) : std::floor(projected); }; - const double left = pixel_boundary(target.x, target.width, partition.first_x, layout.width, false); - const double right = pixel_boundary(target.x, target.width, partition.last_x, layout.width, partition.last_x == layout.width); - const double top = pixel_boundary(target.y, target.height, partition.first_y, layout.height, false); - const double bottom = pixel_boundary(target.y, target.height, partition.last_y, layout.height, partition.last_y == layout.height); + const double left = pixel_boundary(target.x, target.width, + partition.first_x, layout.width, false); + const double right = pixel_boundary(target.x, target.width, + partition.last_x, layout.width, partition.last_x == layout.width); + const double top = pixel_boundary(target.y, target.height, + partition.first_y, layout.height, false); + const double bottom = pixel_boundary(target.y, target.height, + partition.last_y, layout.height, partition.last_y == layout.height); return {left, top, right - left, bottom - top}; } - void paint_raster(Painter& painter, const Raster_Layout& layout, std::span pixels, Image_Interpolation_Mode interpolation) { if (layout.valid() && pixels.size() == static_cast(layout.width) * static_cast(layout.height)) painter.heatmap(layout.target, layout.width, layout.height, pixels, interpolation); } -} // namespace aethera::render_2d::detail +} diff --git a/render_2D/render_2D/module/plottable/common/Raster_Plot.hpp b/render_2D/render_2D/module/plottable/common/Raster_Plot.hpp index 4b30f4c..20bb695 100644 --- a/render_2D/render_2D/module/plottable/common/Raster_Plot.hpp +++ b/render_2D/render_2D/module/plottable/common/Raster_Plot.hpp @@ -1,50 +1,60 @@ #pragma once +#include "../axis/Axis.hpp" +#include "../../scene/frame/Blend2D_Cache.hpp" +#include "../Plot_Types.hpp" #include "Curve_Plot.hpp" -#include #include +#include #include #include namespace aethera::render_2d::detail { struct Raster_Layout { - int width{}; - int height{}; - Rect_F target{}; - bool first_reversed{}; - bool second_reversed{}; - bool first_horizontal{}; + int width{}; /* 像素矩阵宽度。 */ + int height{}; /* 像素矩阵高度。 */ + Rect_F target{}; /* 栅格映射到画布的目标矩形。 */ + bool first_reversed{}; /* 第一坐标轴是否按像素反向。 */ + bool second_reversed{}; /* 第二坐标轴是否按像素反向。 */ + bool first_horizontal{}; /* 第一坐标轴是否映射到矩阵水平方向。 */ [[nodiscard]] bool valid() const noexcept; [[nodiscard]] std::size_t index(int first, int second) const noexcept; }; - struct Raster_Axis_Selection { - int first{}; - int last{}; - Axis_Range range{}; + int first{}; /* 源数据中首个被选中的格点下标。 */ + int last{}; /* 源数据中最后一个被选中的格点下标。 */ + Axis_Range range{}; /* 选中格点覆盖的业务坐标范围。 */ [[nodiscard]] int count() const noexcept; }; - struct Raster_Partition { - int first_x{}; - int last_x{}; - int first_y{}; - int last_y{}; + int first_x{}; /* framebuffer 矩阵中包含的首列。 */ + int last_x{}; /* framebuffer 矩阵中不包含的尾列。 */ + int first_y{}; /* framebuffer 矩阵中包含的首行。 */ + int last_y{}; /* framebuffer 矩阵中不包含的尾行。 */ [[nodiscard]] bool empty() const noexcept; [[nodiscard]] bool contains(int x, int y) const noexcept; }; - [[nodiscard]] Plot_Ratio normalized_plot_value(Plot_Value value, Axis_Range range); [[nodiscard]] std::optional raster_axis_selection(Axis_Range data_range, Axis_Range visible_range, int source_count, bool visible_only); - template [[nodiscard]] Raster_Layout raster_layout(const First_Axis* first_axis, Axis_Range first_range, int first_count, const Second_Axis* second_axis, Axis_Range second_range, int second_count, Axis_Orientation first_orientation, Axis_Orientation second_orientation) { if (first_orientation == second_orientation || first_count <= 0 || second_count <= 0) return {}; const bool horizontal = first_orientation == Axis_Orientation::horizontal; - return {horizontal ? first_count : second_count, horizontal ? second_count : first_count, map_plot_rect(first_axis, first_range, second_axis, second_range, first_orientation), first_axis->coordinate_to_pixel(first_range.origin) > first_axis->coordinate_to_pixel(first_range.target), second_axis->coordinate_to_pixel(second_range.origin) > second_axis->coordinate_to_pixel(second_range.target), horizontal}; + return {horizontal ? first_count : second_count, + horizontal ? second_count : first_count, + map_plot_rect(first_axis, first_range, second_axis, second_range, first_orientation), + first_axis->coordinate_to_pixel(first_range.origin) > first_axis->coordinate_to_pixel(first_range.target), + second_axis->coordinate_to_pixel(second_range.origin) > second_axis->coordinate_to_pixel(second_range.target), + horizontal}; } - -[[nodiscard]] Plot_Partition_Count raster_partition_count(Plot_Partition_Grid grid) noexcept; -[[nodiscard]] Raster_Partition raster_partition(const Raster_Layout& layout, Plot_Partition_Count partition_index, Plot_Partition_Grid grid); -[[nodiscard]] std::pair raster_coordinates(const Raster_Layout& layout, int x, int y) noexcept; -[[nodiscard]] Rect_F raster_paint_region(const Raster_Layout& layout, Plot_Partition_Count partition_index, Plot_Partition_Grid grid); +[[nodiscard]] Plot_Partition_Count raster_partition_count( + Plot_Partition_Grid grid) noexcept; +[[nodiscard]] Raster_Partition raster_partition( + const Raster_Layout& layout, Plot_Partition_Count partition_index, + Plot_Partition_Grid grid); +[[nodiscard]] std::pair raster_coordinates( + const Raster_Layout& layout, int x, int y) noexcept; +/* 将目标矩形切成互斥的整数 framebuffer 视图;每个目标像素只有一个写入者。 */ +[[nodiscard]] Rect_F raster_paint_region(const Raster_Layout& layout, + Plot_Partition_Count partition_index, + Plot_Partition_Grid grid); void paint_raster(Painter& painter, const Raster_Layout& layout, std::span pixels, Image_Interpolation_Mode interpolation); -} // namespace aethera::render_2d::detail +} diff --git a/render_2D/render_2D/module/renderable/Renderable_2D.ipp b/render_2D/render_2D/module/renderable/Renderable_2D.ipp index cf2fa57..43b2452 100644 --- a/render_2D/render_2D/module/renderable/Renderable_2D.ipp +++ b/render_2D/render_2D/module/renderable/Renderable_2D.ipp @@ -1,37 +1,53 @@ #pragma once +#include "Error_handling_specification/Failure_Policy.hpp" +#include +#include namespace aethera::render_2d { struct Renderable_2D::Private : Prev_Private { + struct Task_Node { + mutable proxy value{}; + Private* owner{}; /* 非拥有借用;节点及 owner 都由同一个 Renderable 任务图覆盖。 */ + void precede(Task_Node& target) const { + const auto result = owner->internal_taskflow()->precede(value, target.value); + if (result != Precede_Task_Graph_Result::preceded) Failure_Policy::handle_unknown_failure(std::make_exception_ptr(std::logic_error("2D renderable task dependency is invalid"))); + } + }; + struct Task_Graph { + explicit Task_Graph(Private* owner) : owner(owner) { + owner->internal_taskflow()->clear(); + } + Task_Node add(std::string name, std::function work) { + auto node = owner->internal_taskflow()->add(std::move(name), std::move(work)); + if (!node) Failure_Policy::handle_unknown_failure(std::make_exception_ptr(std::logic_error("2D renderable task is invalid"))); + return {std::move(*node), owner}; + } + Private* owner; /* 非拥有借用;临时建图器不超过当前 advance 调用。 */ + }; ~Private() override; - void render(Size viewport); + void render(Blend2D_Cache& target) noexcept; void advance() override; - virtual void paint(detail::Painter& painter, Size viewport); - void composite(Blend2D_Cache& target); - private: - Blend2D_Cache layer{}; /* 本 Renderable 独占的绘制层;并行 Paint 完成后由 Scene 串行合成。 */ + [[nodiscard]] Blend2D_Cache& paint_surface() const; + [[nodiscard]] Size paint_viewport() const noexcept; +private: + Blend2D_Cache* paint_target{}; /* 非拥有借用;由 Scene 在 advance 前绑定,并存活到整张帧图完成。 */ }; inline Renderable_2D::Private::~Private() = default; -inline void Renderable_2D::Private::render(Size viewport) { - layer.ensure_size(viewport); - layer.clear(); +inline void Renderable_2D::Private::render(Blend2D_Cache& target) noexcept { + paint_target = std::addressof(target); } inline void Renderable_2D::Private::advance() { - auto& graph = internal_taskflow(); - graph->clear(); - auto* private_data = this; /* 非拥有借用;最终 Model Private 比自身任务图存活更久。 */ - (void)graph->add("paint", [private_data] { - const Size viewport = private_data->layer.size(); - if (viewport.empty()) return; - detail::Painter painter(private_data->layer, viewport); - private_data->paint(painter, viewport); - }); + internal_taskflow()->clear(); } -inline void Renderable_2D::Private::paint(detail::Painter&, Size) {} +inline Blend2D_Cache& Renderable_2D::Private::paint_surface() const { + if (!paint_target) Failure_Policy::handle_unknown_failure(std::make_exception_ptr(std::logic_error("2D renderable has no scene paint target"))); + return *paint_target; +} -inline void Renderable_2D::Private::composite(Blend2D_Cache& target) { - target.composite(layer); +inline Size Renderable_2D::Private::paint_viewport() const noexcept { + return paint_target ? paint_target->size() : Size{}; } } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/scene/Render_Scene_2D.cpp b/render_2D/render_2D/module/scene/Render_Scene_2D.cpp index c92ea2a..e9a14c1 100644 --- a/render_2D/render_2D/module/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/module/scene/Render_Scene_2D.cpp @@ -1,6 +1,7 @@ #include "Render_Scene_2D.hpp" #include #include +#include #include namespace aethera::render_2d { proxy scene::make_task_graph(std::string name) { @@ -11,63 +12,47 @@ Render_Scene_2D::Render_Scene_2D() = default; Render_Scene_2D::~Render_Scene_2D() noexcept = default; Render_Scene_2D::Private::Advance_Result Render_Scene_2D::Private::advance(Blend2D_Cache& target, Size viewport, Color background) { - target.ensure_size(viewport); - detail::Painter painter(target, viewport); - painter.clear(); - painter.rect({0.0, 0.0, static_cast(viewport.width), static_cast(viewport.height)}, Pen{Color::transparent(), 0.0, Line_Style::none}, Brush{background, Brush_Style::solid}); - auto next_graph = scene::make_task_graph("render_2d.scene"); - struct Renderable_Nodes { - proxy render; /* Renderable 独占层的并行绘制子图。 */ - proxy composite; /* 向 Frame_2D 权威颜色层串行合成的节点。 */ - }; - std::unordered_map nodes; commit(); - auto& renderables = relation(); - std::optional error; + auto& renderables = relation(); + renderables.for_each_topological([&](Dag_Node_Id, proxy& renderable) { + renderable->render(target); + renderable->advance(); + }); + + auto next_graph = scene::make_task_graph("render_2d.scene"); + auto setup = next_graph->add("scene.background", [&target, viewport, background] { + target.ensure_size(viewport); + detail::Painter painter(target, viewport); + painter.clear(); + painter.rect({0.0, 0.0, static_cast(viewport.width), static_cast(viewport.height)}, Pen{Color::transparent(), 0.0, Line_Style::none}, Brush{background, Brush_Style::solid}); + }); + if (!setup) return std::unexpected(Render_Scene_2D_Error{setup.error()}); + + std::unordered_map> nodes; + std::optional error; renderables.for_each_topological([&](Dag_Node_Id id, proxy& renderable) { if (error) return; - renderable->render(viewport); - renderable->advance(); - auto component = renderable->taskflow(); - auto composed = next_graph->compose("renderable." + std::to_string(id.value) + ".paint", component); - auto* renderable_ptr = std::addressof(renderable); /* 非拥有借用;Renderable DAG 在本帧完成前禁止提交修改。 */ - auto* target_ptr = std::addressof(target); /* 非拥有借用;Frame_2D 在 completion 返回前保持存活。 */ + auto component = renderable->taskflow(); + auto composed = next_graph->compose("renderable." + std::to_string(id.value), component); if (!composed) { error = composed.error(); return; } - auto composite = next_graph->add("renderable." + std::to_string(id.value) + ".composite", [renderable_ptr, target_ptr] { - (*renderable_ptr)->composite(*target_ptr); - }); - if (!composite) { - error = composite.error(); + const auto after_setup = next_graph->precede(*setup, *composed); + if (after_setup != Precede_Task_Graph_Result::preceded) { + error = after_setup; return; } - const auto paint_before_composite = next_graph->precede(*composed, *composite); - if (paint_before_composite != Precede_Task_Graph_Result::preceded) { - error = paint_before_composite; - return; - } - nodes.emplace(id.value, Renderable_Nodes{std::move(*composed), std::move(*composite)}); + nodes.emplace(id.value, std::move(*composed)); }); if (error) return std::unexpected(*error); renderables.for_each_dependency([&](Dag_Node_Id target_id, Dag_Node_Id source_id) { if (error) return; - const auto result = next_graph->precede(nodes.at(source_id.value).render, nodes.at(target_id.value).render); - if (result != Precede_Task_Graph_Result::preceded) error = result; + const auto dependency = next_graph->precede(nodes.at(source_id.value), nodes.at(target_id.value)); + if (dependency != Precede_Task_Graph_Result::preceded) error = dependency; }); if (error) return std::unexpected(*error); - proxy* previous_composite{}; /* 非拥有借用;指向 nodes 中本轮已插入的稳定节点 proxy。 */ - renderables.for_each_topological([&](Dag_Node_Id id, proxy&) { - auto& composite = nodes.at(id.value).composite; - if (previous_composite) { - const auto result = next_graph->precede(*previous_composite, composite); - if (result != Precede_Task_Graph_Result::preceded) error = result; - } - previous_composite = std::addressof(composite); - }); - if (error) return std::unexpected(*error); - graph = std::move(next_graph); + taskflow() = std::move(next_graph); dispatch_collected_events(); return {}; } @@ -88,7 +73,7 @@ Render_Scene_2D_Result Render_Scene_2D::Private::render(Frame_2D* frame, Complet auto& target = detail::Frame_2D_Access::render_target(frame); const auto advanced = advance(target, current.viewport, current.background); if (!advanced) return advanced; - const auto submitted = graph->run([frame, completion = std::move(completion)](std::exception_ptr failure) mutable { + const auto submitted = taskflow()->run([frame, completion = std::move(completion)](std::exception_ptr failure) mutable { completion(frame, std::move(failure)); }); if (submitted != Run_Taskflow_Result::submitted) return std::unexpected(Render_Scene_2D_Error{submitted}); diff --git a/render_2D/render_2D/module/scene/Render_Scene_2D.ipp b/render_2D/render_2D/module/scene/Render_Scene_2D.ipp index 1a500b1..7e500af 100644 --- a/render_2D/render_2D/module/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/module/scene/Render_Scene_2D.ipp @@ -7,6 +7,5 @@ struct Render_Scene_2D::Private : Prev_Private { private: Advance_Result advance(Blend2D_Cache& target, Size viewport, Color background); void dispatch_events(Event_Batch& events) override; - proxy graph{scene::make_task_graph("render_2d.scene")}; /* 唯一拥有当前单帧组合任务图。 */ }; } // namespace aethera::render_2d diff --git a/render_2D/render_2D/module/scene/rely_facade.h b/render_2D/render_2D/module/scene/rely_facade.h index db8c24f..5d13c58 100644 --- a/render_2D/render_2D/module/scene/rely_facade.h +++ b/render_2D/render_2D/module/scene/rely_facade.h @@ -6,8 +6,7 @@ namespace aethera::render_2d::scene { struct Renderable : facade_builder ::add_facade_with_substitution -::add_convention<_render, void(Size)> -::add_convention<_composite, void(Blend2D_Cache &)> +::add_convention<_render, void(Blend2D_Cache &) noexcept> ::support_relocation ::build {}; diff --git a/render_2D/tests/Async_Render_Contract_Test.cpp b/render_2D/tests/Async_Render_Contract_Test.cpp index 751488f..c885668 100644 --- a/render_2D/tests/Async_Render_Contract_Test.cpp +++ b/render_2D/tests/Async_Render_Contract_Test.cpp @@ -5,6 +5,7 @@ #include #include namespace { +/* The process exits only from the asynchronous Scene completion boundary. */ using namespace aethera; using namespace aethera::render_2d; @@ -28,15 +29,22 @@ int main() { .set(&Abs_Axis::Prop::pixel_length, -80.0) .set(&Abs_Axis::Prop::orientation, Axis_Orientation::vertical) .set(&Numeric_Axis::Prop::coordinate_range, Axis_Range{-100.0, 0.0}); - auto* frequency = frequency_builder.build().release(); /* 进程测试独占拥有;completion 退出进程后由 OS 回收。 */ - auto* power = power_builder.build().release(); /* 进程测试独占拥有;completion 退出进程后由 OS 回收。 */ + auto frequency = frequency_builder.build(); + auto power = power_builder.build(); Spectrum::Builder spectrum_builder(*frequency, *power); spectrum_builder .set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .set(&Spectrum::Prop::partition_count, 4U) .set([](Spectrum::Frame& frame) { frame.samples = {-100.0, -50.0, -10.0}; }); auto spectrum = spectrum_builder.build(); + /* 两根轴并行完成后再进入 Spectrum 的四分区子图。 */ Dag_Relation::Builder dag_builder; - dag_builder.add({1}, model_proxy(std::move(spectrum))); + dag_builder + .add({1}, model_proxy(std::move(frequency))) + .add({2}, model_proxy(std::move(power))) + .add({3}, model_proxy(std::move(spectrum))) + .add_dependency({3}, {1}) + .add_dependency({3}, {2}); auto dag = dag_builder.build(); if (!dag) return 2; Render_Scene_2D::Builder scene_builder; diff --git a/render_2D/tests/Model_Adaptation_Test.cpp b/render_2D/tests/Model_Adaptation_Test.cpp index e90d9a7..ad048a3 100644 --- a/render_2D/tests/Model_Adaptation_Test.cpp +++ b/render_2D/tests/Model_Adaptation_Test.cpp @@ -9,8 +9,10 @@ struct Test_Renderable_2D : aethera::Def(*renderable); + aethera::render_2d::Blend2D_Cache target; ASSERT_TRUE(proxy); - proxy->render({32, 24}); + target.ensure_size({32, 24}); + proxy->render(target); proxy->advance(); EXPECT_TRUE(proxy->taskflow()); } diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp index e53a3ec..833dba2 100644 --- a/render_2D/tests/Plottable_Migration_Test.cpp +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -18,23 +18,6 @@ std::unique_ptr make_axis(Axis_Orientation orientation, Axis_Range range) return axis; } -template -Blend2D_Cache paint_plot(Plot& plot) { - auto& private_data = aethera::detail::model_private(plot); - private_data.advance(); - Blend2D_Cache cache; - cache.ensure_size({120, 100}); - aethera::render_2d::detail::Painter painter(cache, {120, 100}); - private_data.paint(painter, {120, 100}); - return cache; -} - -bool contains_color(Image_View image) { - for (const auto value : image.data) - if (value != std::byte{}) return true; - return false; -} - TEST(Plottable_Migration, Frequency_Trace_And_Sweep_Use_Import_Lists) { auto time = Time_Axis::Builder{}.build(); auto value = make_axis(Axis_Orientation::vertical, {0.0, 100.0}); @@ -46,7 +29,7 @@ TEST(Plottable_Migration, Frequency_Trace_And_Sweep_Use_Import_Lists) { Frequency_Trace::Builder trace_builder(*time, *value); auto trace = trace_builder.build(); trace->set([](Frequency_Trace::Samples& samples) { samples.push_back({0, 10.0}); samples.push_back({1, 80.0}); }); - EXPECT_TRUE(contains_color(paint_plot(*trace).view())); + aethera::detail::model_private(*trace).advance(); Frequency_Trace::State trace_state; trace->get([&](const Frequency_Trace::State& state) { trace_state = state; }); EXPECT_EQ(trace_state.sample_count, 2U); @@ -59,7 +42,7 @@ TEST(Plottable_Migration, Frequency_Trace_And_Sweep_Use_Import_Lists) { .set(&Sweep_Spectrum::Prop::block_count, 2U); auto sweep = sweep_builder.build(); sweep->set([](Sweep_Spectrum::Blocks& blocks) { blocks.push_back(std::make_shared(Sweep_Spectrum_Block{0, {-90.0, -40.0}})); blocks.push_back(std::make_shared(Sweep_Spectrum_Block{1, {-30.0, -10.0}})); }); - EXPECT_TRUE(contains_color(paint_plot(*sweep).view())); + aethera::detail::model_private(*sweep).advance(); Sweep_Spectrum::State sweep_state; sweep->get([&](const Sweep_Spectrum::State& state) { sweep_state = state; }); EXPECT_EQ(sweep_state.stored_block_count, 2U); @@ -76,7 +59,10 @@ TEST(Plottable_Migration, Raster_Plots_Keep_One_Private_History) { .set(&Afterglow::Prop::partition_grid, Plot_Partition_Grid{2, 2}); auto glow = glow_builder.build(); glow->set([](Afterglow::Spectra& spectra) { spectra.push_back(std::make_shared>(std::vector{-90.0, -60.0, -30.0, -10.0})); }); - EXPECT_TRUE(contains_color(paint_plot(*glow).view())); + aethera::detail::model_private(*glow).advance(); + Afterglow::State glow_state; + glow->get([&](const Afterglow::State& state) { glow_state = state; }); + EXPECT_EQ(glow_state.spectrum_count, 1U); auto time = Time_Axis::Builder{}.build(); time->set(&Abs_Axis::Prop::position, Point_F{10.0, 90.0}); @@ -92,7 +78,10 @@ TEST(Plottable_Migration, Raster_Plots_Keep_One_Private_History) { .set(&Waterfall::Prop::partition_grid, Plot_Partition_Grid{2, 2}); auto waterfall = waterfall_builder.build(); waterfall->set([first_tick, second_tick](Waterfall::Rows& rows) { rows.push_back(std::make_shared(Waterfall_Row{first_tick, {-90.0, -60.0, -30.0, -10.0}})); rows.push_back(std::make_shared(Waterfall_Row{second_tick, {-80.0, -50.0, -20.0, -5.0}})); }); - EXPECT_TRUE(contains_color(paint_plot(*waterfall).view())); + aethera::detail::model_private(*waterfall).advance(); + Waterfall::State waterfall_state; + waterfall->get([&](const Waterfall::State& state) { waterfall_state = state; }); + EXPECT_EQ(waterfall_state.row_count, 2U); } TEST(Plottable_Migration, Constellation_And_Overlay_Use_Virtual_Private_Runtime) { @@ -105,7 +94,10 @@ TEST(Plottable_Migration, Constellation_And_Overlay_Use_Virtual_Private_Runtime) .set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.0, 1.0}); auto diagram = diagram_builder.build(); diagram->set([now](Constellation_Diagram::Points& points) { points.push_back({{0.25, -0.25}, now}); }); - EXPECT_TRUE(contains_color(paint_plot(*diagram).view())); + aethera::detail::model_private(*diagram).advance(); + Constellation_Diagram::State diagram_state; + diagram->get([&](const Constellation_Diagram::State& state) { diagram_state = state; }); + EXPECT_EQ(diagram_state.point_count, 1U); Selection_Rectangle_Overlay::Builder overlay_builder(*horizontal, *vertical); auto overlay = overlay_builder.build(); diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp index 7a0aca0..bc27707 100644 --- a/render_2D/tests/Spectrum_Test.cpp +++ b/render_2D/tests/Spectrum_Test.cpp @@ -26,7 +26,7 @@ struct Spectrum_Fixture { std::unique_ptr power; }; -TEST(Spectrum, Imports_One_Frame_And_Publishes_Diagnostics) { +TEST(Spectrum, Imports_One_Frame_And_Builds_Partition_Graph) { Spectrum_Fixture fixture; Spectrum::Builder builder(*fixture.frequency, *fixture.power); builder @@ -35,14 +35,10 @@ TEST(Spectrum, Imports_One_Frame_And_Publishes_Diagnostics) { auto spectrum = builder.build(); auto& private_data = aethera::detail::model_private(*spectrum); private_data.advance(); - Blend2D_Cache cache; - cache.ensure_size({120, 100}); - aethera::render_2d::detail::Painter painter(cache, {120, 100}); - private_data.paint(painter, {120, 100}); - Spectrum::State state; - spectrum->get([&](const Spectrum::State& value) { state = value; }); - EXPECT_EQ(state.sample_count, 3U); - EXPECT_GT(state.rendered_point_count, 0U); + std::size_t sample_count{}; + spectrum->get([&](const Spectrum::Frame& frame) { sample_count = frame.samples.size(); }); + EXPECT_EQ(sample_count, 3U); + EXPECT_TRUE(private_data.taskflow()); } TEST(Spectrum, Marker_Collection_Remains_One_Property_Source) {