Files
Aethera/render_2D/render_2D/plottable/Waterfall.ipp
T
2026-08-24 08:12:20 +08:00

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;
}
}