283 lines
16 KiB
C++
283 lines
16 KiB
C++
#pragma once
|
|
#include "common/Curve_Plot.hpp"
|
|
#include "common/Raster_Plot.hpp"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <iomanip>
|
|
#include <iterator>
|
|
#include <sstream>
|
|
namespace aethera::render_2d {
|
|
struct Waterfall::Private : Prev_Private {
|
|
struct Prepared_Row {
|
|
std::size_t source{}; /* 本帧冻结 Waterfall_Stream 中可见行的下标。 */
|
|
int slot{}; /* 该 tick 在当前固定时间窗口中的离散槽位。 */
|
|
};
|
|
struct Prepared {
|
|
std::vector<std::shared_ptr<const Waterfall_Row>> source_rows{}; /* Rendering-role row ownership retained through parallel Prepare tasks. */
|
|
detail::Raster_Layout layout{}; /* 可视频段与完整时间窗口组成的色块布局。 */
|
|
std::vector<Pixel> pixels{}; /* 固定时间窗口的像素矩阵;无数据槽保持透明。 */
|
|
std::vector<Prepared_Row> rows{}; /* 可见源行到固定时间槽位的映射。 */
|
|
Rect_F tooltip_box{}; /* 当前 hover 提示框的画布矩形。 */
|
|
std::string tooltip_text{}; /* 当前 hover 频率文本;空值表示不绘制。 */
|
|
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
|
|
int source_first{}; /* 可视频段在源频谱行中的首列。 */
|
|
bool valid{}; /* 轴布局和行数据是否足以生成色块。 */
|
|
};
|
|
using Append_Run = void (*)(Root*, Waterfall_Row);
|
|
using Count_Run = std::size_t (*)(const Root*);
|
|
struct Dispatch {
|
|
Append_Run append; /* 按 tick 向最终对象提交一行。 */
|
|
Count_Run row_count; /* 查询权威行数。 */
|
|
Count_Run point_count; /* 查询权威样本总数。 */
|
|
Count_Run rendered_count; /* 查询已准备的色块数。 */
|
|
};
|
|
Scene_Object* scene{}; /* 不拥有的所属 Scene。 */
|
|
Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */
|
|
Time_Object* time_axis{}; /* 不拥有的时间轴,同时权威决定保留行数。 */
|
|
Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */
|
|
detail::Hover_Tooltip_Runtime tooltip{}; /* 事件侧当前 hover 位置。 */
|
|
Plot_Partition_Count graph_partition_count{}; /* Prepare 子图当前固化的分块数。 */
|
|
const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */
|
|
/* CRTP 覆盖:绑定 Renderable、事件能力和最终 Waterfall 分派表。 */
|
|
template <Attached Object>
|
|
void bind_private_crtp(Object* object);
|
|
void bind_sources(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value);
|
|
template <Attached Object>
|
|
[[nodiscard]] static const Dispatch& dispatch_for();
|
|
/* CRTP 覆盖:按当前色块工作量构建分块 Prepare 子图。 */
|
|
template <Attached Object>
|
|
[[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
|
|
/* CRTP 覆盖:构建消费色块矩阵和提示信息的 Paint 子图。 */
|
|
template <Attached Object>
|
|
[[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
|
|
/* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */
|
|
template <Attached Object>
|
|
[[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
|
|
template <Attached Object>
|
|
void prepare_frame(Object* object);
|
|
template <Attached Object>
|
|
void prepare_partition(Object* object, Plot_Partition_Count index);
|
|
template <Attached Object>
|
|
void paint_frame(Object* object);
|
|
/* CRTP 覆盖:更新 hover 位置并请求重绘。 */
|
|
template <Attached Object>
|
|
void handle_event(Object* object, const Event& event);
|
|
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
|
|
template <typename Object, typename Owner, typename Member, typename Prop_Type>
|
|
void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
|
|
template <typename Object, typename Prop_Type, typename State_Type>
|
|
void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
|
|
};
|
|
template <typename Object>
|
|
Waterfall::Builder<Object>::Builder(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) : Base(), frequency_axis(frequency_axis_value), time_axis(time_axis_value) {}
|
|
template <typename Object>
|
|
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Waterfall::Builder<Object>::build() {
|
|
auto result = Base::build();
|
|
if (!result) return std::unexpected(result.error());
|
|
auto plot = std::move(result).value();
|
|
auto& private_data = static_cast<typename Object::Private&>(*plot->d);
|
|
private_data.bind_sources(frequency_axis, time_axis);
|
|
private_data.scene_attach = [object = plot.get()](Root* root) -> std::expected<void, Dependency_Graph_Error> {
|
|
auto* scene = static_cast<Scene_Object*>(root);
|
|
auto& data = static_cast<typename Object::Private&>(*object->d);
|
|
data.scene = scene;
|
|
auto* frequency_axis = data.frequency_axis;
|
|
auto* time_axis = data.time_axis;
|
|
return scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](auto& prepare, auto& paint, auto& cache) {
|
|
prepare.add_dependency(object, scene);
|
|
prepare.add_dependency(object, frequency_axis);
|
|
prepare.add_dependency(object, time_axis);
|
|
paint.add_dependency(frequency_axis, object);
|
|
paint.add_dependency(time_axis, object);
|
|
cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene);
|
|
cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, frequency_axis);
|
|
cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, frequency_axis);
|
|
cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, frequency_axis);
|
|
cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, frequency_axis);
|
|
cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, time_axis);
|
|
cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, time_axis);
|
|
cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, time_axis);
|
|
cache.template add_prop_dependency<&Time_Axis::Prop::visible_count>(object, time_axis);
|
|
cache.template add_prop_dependency<&Time_Axis::Prop::newest_at_start>(object, time_axis);
|
|
cache.template add_dependency<&Time_Axis::State::next_tick>(object, time_axis);
|
|
});
|
|
};
|
|
return plot;
|
|
}
|
|
template <typename Values>
|
|
void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) {
|
|
append_row(tick, std::span<const Plot_Value>(std::data(values), std::size(values)));
|
|
}
|
|
template <Attached Object>
|
|
bool Waterfall::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) {
|
|
const std::size_t cells = object->template access_query_stream<Waterfall_Stream_Tag>([&](std::span<const std::shared_ptr<const Waterfall_Row>> rows) {
|
|
return rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : rows.empty() ? 1 : rows.back()->values.size());
|
|
});
|
|
return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells);
|
|
}
|
|
template <Attached Object>
|
|
tf::Taskflow Waterfall::Private::build_prepare_graph(Object* object, const Prop& state) {
|
|
const std::size_t cells = object->template access_query_stream<Waterfall_Stream_Tag>([&](std::span<const std::shared_ptr<const Waterfall_Row>> rows) {
|
|
return rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : rows.empty() ? 1 : rows.back()->values.size());
|
|
});
|
|
graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, cells);
|
|
tf::Taskflow graph;
|
|
auto begin = graph.emplace([this, object] {
|
|
prepare_frame(object);
|
|
}).name("waterfall.prepare.frame");
|
|
for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) {
|
|
auto task = graph.emplace([this, object, index] {
|
|
prepare_partition(object, index);
|
|
}).name("waterfall.prepare.partition");
|
|
begin.precede(task);
|
|
}
|
|
return graph;
|
|
}
|
|
template <Attached Object>
|
|
tf::Taskflow Waterfall::Private::build_paint_graph(Object* object, const Prop&) {
|
|
tf::Taskflow graph;
|
|
graph.emplace([this, object] {
|
|
paint_frame(object);
|
|
}).name("waterfall.paint.frame");
|
|
return graph;
|
|
}
|
|
template <Attached Object>
|
|
void Waterfall::Private::prepare_frame(Object* object) {
|
|
const auto& state = object->template read_prop<Waterfall::Base_Tag>();
|
|
object->template exchange_stream<Waterfall_Stream_Tag>();
|
|
prepared = {};
|
|
object->template access_rendering_stream<Waterfall_Stream_Tag>([&](std::span<std::shared_ptr<const Waterfall_Row>> rows) {
|
|
prepared.source_rows.assign(rows.begin(), rows.end());
|
|
});
|
|
const auto& rows = prepared.source_rows;
|
|
const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>();
|
|
const auto& time_layout = time_axis->template read_prop<Abs_Axis::Base_Tag>();
|
|
prepared = {};
|
|
prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
|
|
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<int>(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 int time_slots = std::max<Axis_Visible_Count>(2, time_axis->template read_prop<Time_Axis::Base_Tag>().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.pixels.assign(static_cast<std::size_t>(prepared.layout.width) * prepared.layout.height, 0);
|
|
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 (std::size_t source = 0; source < rows.size(); ++source) {
|
|
const int slot = static_cast<int>(std::llround((static_cast<Axis_Coordinate>(rows[source]->tick) - first_center) / direction));
|
|
if (slot >= 0 && slot < time_slots) prepared.rows.push_back({source, slot});
|
|
}
|
|
if (prepared.rows.empty()) {
|
|
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;
|
|
}
|
|
template <Attached Object>
|
|
void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) {
|
|
if (!prepared.valid) return;
|
|
const auto& state = object->template read_prop<Waterfall::Base_Tag>();
|
|
const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height;
|
|
const std::size_t cells = static_cast<std::size_t>(columns) * prepared.rows.size();
|
|
const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count);
|
|
for (std::size_t cell = first; cell < last; ++cell) {
|
|
const auto& row = prepared.rows[cell / static_cast<std::size_t>(columns)];
|
|
const int column = static_cast<int>(cell % static_cast<std::size_t>(columns));
|
|
const auto& values = prepared.source_rows[row.source]->values;
|
|
const std::size_t source = static_cast<std::size_t>(prepared.source_first + column);
|
|
prepared.pixels[prepared.layout.index(column, row.slot)] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range)));
|
|
}
|
|
}
|
|
template <Attached Object>
|
|
void Waterfall::Private::paint_frame(Object* object) {
|
|
const auto& state = object->template read_prop<Waterfall::Base_Tag>();
|
|
auto& cache = this->paint_surface();
|
|
if (!prepared.valid) {
|
|
return;
|
|
}
|
|
detail::Painter painter(cache, prepared.canvas);
|
|
detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode);
|
|
if (!prepared.tooltip_text.empty()) {
|
|
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);
|
|
}
|
|
}
|
|
template <Attached Object>
|
|
void Waterfall::Private::handle_event(Object* object, const Event& event) {
|
|
if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty<Paint_Tag>();
|
|
}
|
|
template <typename Object, typename Owner, typename Member, typename Prop_Type>
|
|
void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) {
|
|
if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>();
|
|
}
|
|
template <typename Object, typename Prop_Type, typename State_Type>
|
|
void Waterfall::Private::before_advance(Object* object, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type*, State_Access<const State_Type>) {
|
|
auto& state = pending_states.template get<Waterfall::Base_Tag>();
|
|
object->template access_query_stream<Waterfall_Stream_Tag>([&](std::span<const std::shared_ptr<const Waterfall_Row>> rows) {
|
|
state.row_count = rows.size();
|
|
state.stored_point_count = 0;
|
|
for (const auto& row : rows) state.stored_point_count += row->values.size();
|
|
state.rendered_cell_count = state.stored_point_count;
|
|
});
|
|
}
|
|
template <Attached Object>
|
|
const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() {
|
|
static const Dispatch value{
|
|
[](Root* root, Waterfall_Row row) {
|
|
auto* object = static_cast<Object*>(root);
|
|
object->template submit_stream<Waterfall_Stream_Tag>(
|
|
std::make_shared<const Waterfall_Row>(std::move(row)));
|
|
object->template mark_dirty<Prepare_Data_Tag>();
|
|
},
|
|
[](const Root* root) {
|
|
return static_cast<const Object*>(root)->template access_query_stream<Waterfall_Stream_Tag>(
|
|
[](std::span<const std::shared_ptr<const Waterfall_Row>> rows) { return rows.size(); });
|
|
},
|
|
[](const Root* root) {
|
|
return static_cast<const Object*>(root)->template access_query_stream<Waterfall_Stream_Tag>(
|
|
[](std::span<const std::shared_ptr<const Waterfall_Row>> rows) {
|
|
std::size_t count{};
|
|
for (const auto& row : rows) count += row->values.size();
|
|
return count;
|
|
});
|
|
},
|
|
[](const Root* root) {
|
|
return static_cast<const Object*>(root)->template access_query_stream<Waterfall_Stream_Tag>(
|
|
[](std::span<const std::shared_ptr<const Waterfall_Row>> rows) {
|
|
std::size_t count{};
|
|
for (const auto& row : rows) count += row->values.size();
|
|
return count;
|
|
});
|
|
}
|
|
};
|
|
return value;
|
|
}
|
|
template <Attached Object>
|
|
void Waterfall::Private::bind_private_crtp(Object* object) {
|
|
Prev_Private::bind_private_crtp(object);
|
|
dispatch = &dispatch_for<Object>();
|
|
}
|
|
inline void Waterfall::Private::bind_sources(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) {
|
|
frequency_axis = frequency_axis_value;
|
|
time_axis = time_axis_value;
|
|
}
|
|
}
|