This commit is contained in:
2026-08-24 09:37:04 +08:00
parent 1d3847d454
commit 4f21169ade
42 changed files with 1371 additions and 833 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ concept Axis_Object = Renderable_Object<T> && std::derived_from<T, Abs_Axis> &&
{ private_data.sub_tick_count(object, tick) } -> std::same_as<Axis_Tick_Count>;
};
/* 所有二维坐标轴共享的定义层;最终通过 Impl<Derived_Axis> 创建运行时对象。 */
struct Abs_Axis : Def<Abs_Axis, Renderable_2D<>> {
struct Abs_Axis : Def<Abs_Axis, Renderable_2D> {
struct Prop : Prev_Prop {
Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */
Axis_Pixel_Length pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */
+8 -23
View File
@@ -4,36 +4,21 @@
#include <expected>
#include <functional>
namespace aethera::render_2d {
/* 二维 Renderable 是否拥有可跨帧复用的完整颜色缓存。 */
enum class Renderable_2D_Cache {
disabled,
enabled
};
/*
* 二维绘制能力层。业务类型在该模板位置选择是否拥有颜色缓存;默认不分配完整缓存
* 二维绘制能力层。每个对象都具备惰性颜色缓存,是否使用由当前 Prop 决定
* Paint 实现只向 Scene 本轮指定的绘制目标输出,不直接选择或清理双缓冲角色。
*/
template <Renderable_2D_Cache Cache = Renderable_2D_Cache::disabled>
struct Renderable_2D;
template <>
struct Renderable_2D<Renderable_2D_Cache::disabled> : Def<Renderable_2D<Renderable_2D_Cache::disabled>, Renderable> {
struct Prop : Prev_Prop {};
struct Renderable_2D
: Def<Renderable_2D, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Prop : Prev_Prop {
bool cache_enabled{}; /* 是否跨帧复用本对象及其缓存组的完整颜色结果。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
bool operator==(const State&) const;
};
struct Private;
};
template <>
struct Renderable_2D<Renderable_2D_Cache::enabled>
: Def<Renderable_2D<Renderable_2D_Cache::enabled>,
Renderable_2D<Renderable_2D_Cache::disabled>,
Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Prop : Prev_Prop {};
struct State : Prev_State {
bool operator==(const State&) const;
};
struct Private;
};
using Renderable_2D_Base = Renderable_2D<Renderable_2D_Cache::disabled>;
using Renderable_2D_Base = Renderable_2D;
}
#include "Renderable_2D.ipp"
+20 -15
View File
@@ -1,11 +1,13 @@
#pragma once
#include <stdexcept>
namespace aethera::render_2d {
struct Renderable_2D<Renderable_2D_Cache::disabled>::Private : Prev_Private {
struct Renderable_2D::Private : Prev_Private {
using Scene_Attach = std::function<std::expected<void, Dependency_Graph_Error>(Root*)>;
using Cache_Access = Blend2D_Cache* (*)(Root*);
using Cache_Enabled = bool (*)(const Root*);
using Event_Region_Run = Rect_F (*)(const Root*, Size);
Cache_Access pending_cache{}; /* 非空时返回最终对象本轮可写的缓存物理对象。 */
Cache_Enabled cache_enabled{}; /* 读取最终对象当前发布的运行时缓存策略。 */
Event_Region_Run event_region_run{}; /* 计算最终对象当前事件区域的无虚函数入口。 */
Blend2D_Cache* paint_target{}; /* 仅在 Scene Paint 阶段有效的非拥有绘制目标。 */
Blend2D_Cache* valid_cache{}; /* 缓存根最近一次完整重绘产生的权威物理缓存。 */
@@ -14,24 +16,27 @@ struct Renderable_2D<Renderable_2D_Cache::disabled>::Private : Prev_Private {
[[nodiscard]] Blend2D_Cache& paint_surface();
/* CRTP 可覆盖:返回最终对象可接收指针与滚轮事件的画布区域;默认覆盖整个 viewport。 */
[[nodiscard]] Rect_F event_region(const Attached auto* object, Size viewport) const;
/* CRTP 覆盖:识别最终类型是否选择了二维颜色缓存。 */
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void after_prop_set(Object* object, Member Owner::* member,
Prop_Access<Prop_Type> pending_props);
template <Attached Object> void bind_private_crtp(Object* object);
};
struct Renderable_2D<Renderable_2D_Cache::enabled>::Private : Prev_Private {
/* CRTP 覆盖:继续绑定二维绘制能力;缓存机制由本定义层静态加入。 */
template <Attached Object> void bind_private_crtp(Object* object);
};
inline bool Renderable_2D<Renderable_2D_Cache::disabled>::State::operator==(const State&) const = default;
inline bool Renderable_2D<Renderable_2D_Cache::enabled>::State::operator==(const State&) const = default;
inline Blend2D_Cache& Renderable_2D<Renderable_2D_Cache::disabled>::Private::paint_surface() {
inline bool Renderable_2D::Prop::operator==(const Prop&) const = default;
inline bool Renderable_2D::State::operator==(const State&) const = default;
inline Blend2D_Cache& Renderable_2D::Private::paint_surface() {
if (!paint_target) throw std::logic_error("2D renderable painted without a scene paint target");
return *paint_target;
}
inline Rect_F Renderable_2D<Renderable_2D_Cache::disabled>::Private::event_region(const Attached auto*, Size viewport) const {
inline Rect_F Renderable_2D::Private::event_region(const Attached auto*, Size viewport) const {
return {0.0, 0.0, static_cast<double>(viewport.width), static_cast<double>(viewport.height)};
}
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Renderable_2D::Private::after_prop_set(Object* object, Member Owner::*,
Prop_Access<Prop_Type>) {
if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Paint_Tag>();
}
template <Attached Object>
void Renderable_2D<Renderable_2D_Cache::disabled>::Private::bind_private_crtp(Object* object) {
void Renderable_2D::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
object->template mark_dirty<Paint_Tag>();
this->event_region_run = [](const Root* root, Size viewport) {
@@ -39,12 +44,12 @@ void Renderable_2D<Renderable_2D_Cache::disabled>::Private::bind_private_crtp(Ob
const auto& private_data = static_cast<const typename Object::Private&>(*value->d);
return private_data.event_region(value, viewport);
};
}
template <Attached Object>
void Renderable_2D<Renderable_2D_Cache::enabled>::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
this->pending_cache = [](Root* root) {
return &static_cast<Object*>(root)->template pending_buffer<Color_Cache>();
};
this->cache_enabled = [](const Root* root) {
return static_cast<const Object*>(root)
->template read_prop<Renderable_2D::Base_Tag>().cache_enabled;
};
}
}
+2 -1
View File
@@ -10,11 +10,12 @@
#include <vector>
namespace aethera::render_2d {
struct Afterglow_Stream_Tag {};
struct Afterglow : Def<Afterglow, Renderable_2D<Renderable_2D_Cache::enabled>,
struct Afterglow : Def<Afterglow, Renderable_2D,
Mpmc_Triple_Buffer<Afterglow_Stream_Tag,
std::shared_ptr<const std::vector<Plot_Value>>>> {
using Scene_Object = Impl<Render_Scene_2D>; using Frequency_Object = Impl<Frequency_Axis>; using Power_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {
Prop() { cache_enabled = true; }
std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */
std::size_t power_point_size{}; /* 栅格功率行数;零值使用默认尺寸。 */
Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */
@@ -15,11 +15,12 @@ struct Constellation_Point {
};
struct Constellation_Stream_Tag {};
struct Constellation_Diagram : Def<Constellation_Diagram,
Renderable_2D<Renderable_2D_Cache::enabled>,
Renderable_2D,
Mpmc_Triple_Buffer<Constellation_Stream_Tag, Constellation_Point>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Axis_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {
Prop() { cache_enabled = true; }
Plot_Duration_Milliseconds point_lifetime_ms{1000}; /* 接收点保留时间,单位为毫秒。 */
Constellation_Diagram_Type type{Constellation_Diagram_Type::psk8}; /* 理想 PSK 锚点数量。 */
Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */
@@ -13,12 +13,13 @@ struct Frequency_Trace_Sample {
bool operator==(const Frequency_Trace_Sample&) const;
};
struct Frequency_Trace_Stream_Tag {};
struct Frequency_Trace : Def<Frequency_Trace, Renderable_2D<Renderable_2D_Cache::enabled>,
struct Frequency_Trace : Def<Frequency_Trace, Renderable_2D,
Mpmc_Triple_Buffer<Frequency_Trace_Stream_Tag, Frequency_Trace_Sample>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Time_Object = Impl<Time_Axis>;
using Value_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {
Prop() { cache_enabled = true; }
Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */
Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */
Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */
@@ -20,6 +20,4 @@ std::string Selection_Rectangle_Overlay::Private::range_label(const Abs_Axis* ax
};
return coordinate_label(range.origin) + " .. " + coordinate_label(range.target);
}
std::vector<Axis_Rectangle> Selection_Rectangle_Overlay::selected_regions() const { return static_cast<const Private&>(*d).dispatch->selected_regions(this); }
void Selection_Rectangle_Overlay::clear_selected_regions() { static_cast<Private&>(*d).dispatch->clear_selected_regions(this); }
}
@@ -8,7 +8,7 @@
#include <memory>
#include <vector>
namespace aethera::render_2d {
struct Selection_Rectangle_Overlay : Def<Selection_Rectangle_Overlay, Renderable_2D<>> {
struct Selection_Rectangle_Overlay : Def<Selection_Rectangle_Overlay, Renderable_2D> {
using Scene_Object = Impl<Render_Scene_2D>;
struct Prop : Prev_Prop {
Font label_font{}; /* 选择范围标签使用的字体。 */
@@ -32,8 +32,6 @@ struct Selection_Rectangle_Overlay : Def<Selection_Rectangle_Overlay, Renderable
private:
std::function<void(Object*)> initialize{}; /* build 成功后绑定两根最终轴及其 Scene 拓扑。 */
};
[[nodiscard]] std::vector<Axis_Rectangle> selected_regions() const;
void clear_selected_regions();
};
}
#include "Selection_Rectangle_Overlay.ipp"
@@ -6,12 +6,6 @@
namespace aethera::render_2d {
struct Selection_Rectangle_Overlay::Private : Prev_Private {
using No_Prepare = void;
using Regions_Get = std::vector<Axis_Rectangle> (*)(const Root*);
using Clear_Run = void (*)(Root*);
struct Dispatch {
Regions_Get selected_regions; /* 查询最终对象已发布选择区域。 */
Clear_Run clear_selected_regions; /* 清空最终对象选择区域。 */
};
Scene_Object* scene{}; /* 不拥有的所属 Scene。 */
Abs_Axis* horizontal_axis{}; /* 不拥有的水平坐标轴。 */
Abs_Axis* vertical_axis{}; /* 不拥有的垂直坐标轴。 */
@@ -19,12 +13,8 @@ struct Selection_Rectangle_Overlay::Private : Prev_Private {
Axis_Point drag_current{}; /* 当前拖动终点的两轴数据坐标。 */
Point_F press_position{}; /* 仅用于区分点击与拖动的按下像素位置。 */
bool dragging{}; /* 是否正在构造尚未提交的选择矩形。 */
const Dispatch* dispatch{}; /* 最终类型公开薄壳分派表。 */
static constexpr double minimum_drag_distance_pixels{3.0}; /* 允许提交和绘制选择框的最小像素距离。 */
/* CRTP 覆盖:绑定 Paint-only、事件能力和最终 Overlay 分派表。 */
template <Attached Object> void bind_private_crtp(Object* object);
void bind_sources(Abs_Axis* horizontal_axis_value, Abs_Axis* vertical_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
[[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);
[[nodiscard]] bool has_visible_drag() const;
@@ -119,15 +109,5 @@ template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Selection_Rectangle_Overlay::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Paint_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type>
void Selection_Rectangle_Overlay::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { pending_states.template get<Selection_Rectangle_Overlay::Base_Tag>().selected_region_count = static_cast<const Prop&>(*current_prop).selected_regions.size(); }
template <Attached Object>
const Selection_Rectangle_Overlay::Private::Dispatch& Selection_Rectangle_Overlay::Private::dispatch_for() {
static const Dispatch value{
[](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Selection_Rectangle_Overlay::Base_Tag>().selected_regions; },
[](Root* root) { static_cast<Object*>(root)->template set<&Prop::selected_regions>(std::vector<Axis_Rectangle>{}); }
};
return value;
}
template <Attached Object>
void Selection_Rectangle_Overlay::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
inline void Selection_Rectangle_Overlay::Private::bind_sources(Abs_Axis* horizontal_axis_value, Abs_Axis* vertical_axis_value) { horizontal_axis = horizontal_axis_value; vertical_axis = vertical_axis_value; }
}
@@ -3,18 +3,6 @@ namespace aethera::render_2d {
bool Spectrum_Frame::operator==(const Spectrum_Frame&) const = default;
bool Spectrum::Prop::operator==(const Prop&) const = default;
bool Spectrum::State::operator==(const State&) const = default;
void Spectrum::update_samples(std::span<const Spectrum_Power> values) {
static_cast<Private&>(*d).dispatch->update_samples(this, values);
}
void Spectrum::update_samples(std::pmr::vector<Spectrum_Power>&& values) {
update_samples(std::span<const Spectrum_Power>(values.data(), values.size()));
}
std::size_t Spectrum::sample_count() const {
return static_cast<const Private&>(*d).dispatch->sample_count(this);
}
std::size_t Spectrum::rendered_point_count() const {
return static_cast<const Private&>(*d).dispatch->rendered_point_count(this);
}
std::expected<Spectrum_Power, Spectrum::Power_At_Result> Spectrum::power_at(Spectrum_Frequency frequency) const {
return static_cast<const Private&>(*d).dispatch->power_at(this, frequency);
}
+30 -43
View File
@@ -5,8 +5,6 @@
#include "Plot_Types.hpp"
#include <expected>
#include <memory>
#include <memory_resource>
#include <span>
#include <utility>
#include <vector>
namespace aethera::render_2d {
@@ -17,47 +15,47 @@ using Spectrum_Marker_Index = Plot_Index;
using Spectrum_Partition_Mode = Plot_Partition_Mode;
struct Spectrum_Frame_Tag {};
struct Spectrum_Frame {
std::vector<Spectrum_Power> samples{}; /* 最近提交的当前频谱功率样本。 */
std::vector<Spectrum_Power> maxima{}; /* 与 samples 同尺寸的逐点历史最大值。 */
std::vector<Spectrum_Power> minima{}; /* 与 samples 同尺寸的逐点历史最小值。 */
std::vector<Spectrum_Power> samples{}; /* 最近提交的当前频谱功率样本。 */
bool operator==(const Spectrum_Frame&) const;
};
/* 使用频率轴和功率轴分块准备、绘制当前值、保持曲线及频率标记。 */
struct Spectrum : Def<Spectrum, Renderable_2D<Renderable_2D_Cache::enabled>, Tagged_Buffer<Spectrum_Frame_Tag, Spectrum_Frame>> {
struct Spectrum : Def<Spectrum, Renderable_2D,
Tagged_Buffer<Spectrum_Frame_Tag, Spectrum_Frame>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Frequency_Object = Impl<Frequency_Axis>;
using Power_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {
Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */
std::size_t partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */
bool max_hold_visible{}; /* 是否绘制逐点历史最大值曲线。 */
bool min_hold_visible{}; /* 是否绘制逐点历史最值曲线。 */
bool max_marker_visible{}; /* 是否标记当前样本的最大值位置。 */
bool min_marker_visible{}; /* 是否标记当前样本的最值位置。 */
bool sweep_region_visible{}; /* 是否绘制扫频范围背景。 */
bool visible_range_only{true}; /* 是否裁掉频率轴当前范围外的线段。 */
Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */
Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */
Prop() { cache_enabled = true; }
Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */
std::size_t partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */
bool max_hold_visible{}; /* 是否绘制逐点历史最值曲线。 */
bool min_hold_visible{}; /* 是否绘制逐点历史最小值曲线。 */
bool max_marker_visible{}; /* 是否标记当前样本的最值位置。 */
bool min_marker_visible{}; /* 是否标记当前样本的最小值位置。 */
bool sweep_region_visible{}; /* 是否绘制扫频范围背景。 */
bool visible_range_only{true}; /* 是否裁掉频率轴当前范围外的线段。 */
Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */
Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */
Spectrum_Partition_Mode partition_mode{Spectrum_Partition_Mode::automatic}; /* Prepare 子图的分块策略。 */
Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻样本间的插值规则。 */
Brush max_brush{}; /* 最大保持曲线下方的填充样式。 */
Brush current_brush{}; /* 当前频谱曲线下方的填充样式。 */
Brush min_brush{}; /* 最小保持曲线下方的填充样式。 */
Pen max_pen{Color::red_color()}; /* 最大保持曲线及最大值标记样式。 */
Pen current_pen{Color::green_color()}; /* 当前频谱曲线样式。 */
Pen min_pen{Color::white()}; /* 最小保持曲线及最小值标记样式。 */
Pen selected_marker_pen{Color{0, 0, 139, 255}, 2.0}; /* 当前选中自定义标记的线条样式。 */
Pen marker_pen{Color::red_color()}; /* 未选中自定义标记的线条样式。 */
Pen middle_frequency_pen{Color::red_color()}; /* 中心频率垂线样式。 */
Brush max_brush{}; /* 最大保持曲线下方的填充样式。 */
Brush current_brush{}; /* 当前频谱曲线下方的填充样式。 */
Brush min_brush{}; /* 最小保持曲线下方的填充样式。 */
Pen max_pen{Color::red_color()}; /* 最大保持曲线及最大值标记样式。 */
Pen current_pen{Color::green_color()}; /* 当前频谱曲线样式。 */
Pen min_pen{Color::white()}; /* 最小保持曲线及最小值标记样式。 */
Pen selected_marker_pen{Color{0, 0, 139, 255}, 2.0}; /* 当前选中自定义标记的线条样式。 */
Pen marker_pen{Color::red_color()}; /* 未选中自定义标记的线条样式。 */
Pen middle_frequency_pen{Color::red_color()}; /* 中心频率垂线样式。 */
Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::solid}; /* 扫频区域背景样式。 */
std::vector<Spectrum_Frequency> custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */
Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */
std::vector<Spectrum_Frequency> custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */
Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
std::size_t sample_count{}; /* 最近一次发布帧的样本数。 */
std::size_t rendered_point_count{}; /* 最近一次 Prepare 生成的曲线点数。 */
std::size_t selectable_marker_count{}; /* 当前可选择的自定义标记数。 */
std::size_t sample_count{}; /* 最近一次发布帧的样本数。 */
std::size_t rendered_point_count{}; /* 最近一次 Prepare 生成的曲线点数。 */
std::size_t selectable_marker_count{}; /* 当前可选择的自定义标记数。 */
bool operator==(const State&) const;
};
/* 完整声明、分块子图、内部状态操作和 CRTP 分派见 Spectrum.ipp。 */
@@ -68,20 +66,9 @@ struct Spectrum : Def<Spectrum, Renderable_2D<Renderable_2D_Cache::enabled>, Tag
Builder(Frequency_Object* frequency_axis, Power_Object* power_axis);
[[nodiscard]] std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> build();
private:
Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Spectrum。 */
Power_Object* power_axis{}; /* 不拥有的功率轴;生命周期必须覆盖 Spectrum。 */
Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Spectrum。 */
Power_Object* power_axis{}; /* 不拥有的功率轴;生命周期必须覆盖 Spectrum。 */
};
/* 提交新一帧样本并更新逐点最大、最小保持。 */
void update_samples(std::span<const Spectrum_Power> values);
/* 接收 PMR 连续样本容器;提交完成后调用方仍拥有容器。 */
void update_samples(std::pmr::vector<Spectrum_Power>&& values);
/* 接收具有 data() 和 size() 的连续样本容器。 */
template <typename Values>
void update_samples(const Values& values);
/* 返回最近一次双缓冲交换后发布的样本数。 */
[[nodiscard]] std::size_t sample_count() const;
/* 返回最近一次 Prepare 子图得到的当前曲线点数。 */
[[nodiscard]] std::size_t rendered_point_count() const;
enum class Power_At_Result {
no_samples,
invalid_frequency_range,
+14 -29
View File
@@ -30,9 +30,8 @@ struct Spectrum::Private : Prev_Private {
Size canvas_size{}; /* 所属 Scene viewport 决定的颜色层尺寸。 */
bool valid{}; /* 两根轴与画布是否足以生成绘制数据。 */
};
using Update_Run = void (*)(Root*, std::span<const Spectrum_Power>);
using Count_Run = std::size_t (*)(const Root*);
using Power_Run = std::expected<Spectrum_Power, Power_At_Result> (*)(const Root*, Spectrum_Frequency);
using Count_Run = std::size_t (*)(const Root*);
using Frequency_Run = void (*)(Root*, Spectrum_Frequency);
using Void_Run = void (*)(Root*);
using Index_Get_Run = Spectrum_Marker_Index (*)(const Root*);
@@ -41,9 +40,6 @@ struct Spectrum::Private : Prev_Private {
using Set_Marker_Frequency_Run = Set_Marker_Frequency_Result (*)(Root*, Spectrum_Marker_Index, Spectrum_Frequency);
using Set_Current_Marker_Frequency_Run = Set_Current_Marker_Frequency_Result (*)(Root*, Spectrum_Frequency);
struct Dispatch {
Update_Run update_samples; /* 向最终对象写入 Spectrum_Frame_Tag。 */
Count_Run sample_count; /* 查询最终对象已发布样本数量。 */
Count_Run rendered_point_count; /* 查询 Prepare 子图生成的曲线点数。 */
Power_Run power_at; /* 查询最终对象已发布帧的插值功率。 */
Frequency_Run add_marker; /* 添加自定义标记。 */
Frequency_Run remove_marker; /* 删除最接近指定频率的标记。 */
@@ -62,6 +58,8 @@ struct Spectrum::Private : Prev_Private {
Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;Prepare 直接读取其当前状态。 */
Power_Object* power_axis{}; /* 不拥有的功率轴;Prepare 直接读取其当前状态。 */
Prepared prepared{}; /* 当前权威 State、Frame 和轴状态推导出的 Paint 输入。 */
std::vector<Spectrum_Power> maxima{}; /* 当前样本历史逐点最大值;只由 Prepare 更新。 */
std::vector<Spectrum_Power> minima{}; /* 当前样本历史逐点最小值;只由 Prepare 更新。 */
std::size_t prepare_graph_partition_count{}; /* 当前 Prepare 子图实际固化的任务分块数。 */
const Dispatch* dispatch{}; /* Builder 绑定最终 Spectrum 类型后的静态分派表。 */
/* CRTP 覆盖:绑定 Renderable 机制和 Spectrum 公开薄壳分派;派生 Private 必须先调用此实现。 */
@@ -69,7 +67,6 @@ struct Spectrum::Private : Prev_Private {
/* Builder 内部绑定 Scene 与两根轴;三个来源都必须比 Spectrum 生命周期更长。 */
void bind_render_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
template <Attached Object> void update_samples(Object* object, std::span<const Spectrum_Power> values);
template <Attached Object> void add_marker(Object* object, Spectrum_Frequency frequency);
template <Attached Object> void remove_marker(Object* object, Spectrum_Frequency frequency);
template <Attached Object> void remove_selected_marker(Object* object);
@@ -128,10 +125,6 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Spectrum::Builder
};
return spectrum;
}
template <typename Values>
void Spectrum::update_samples(const Values& values) {
update_samples(std::span<const Spectrum_Power>(std::data(values), std::size(values)));
}
inline std::expected<Spectrum_Power, Spectrum::Power_At_Result> Spectrum::Private::power_at(const Prop& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency) {
if (frame.samples.empty()) return std::unexpected(Power_At_Result::no_samples);
if (state.frequency_range.length() == 0.0) return std::unexpected(Power_At_Result::invalid_frequency_range);
@@ -174,6 +167,15 @@ void Spectrum::Private::prepare_frame(Object* object, std::size_t partition_coun
auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const Prop&>(*private_data.current);
const auto& frame = object->template current_buffer<Spectrum_Frame_Tag>();
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& scene_state = scene->template read_prop<Render_Scene_2D::Base_Tag>();
const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>();
const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>();
@@ -218,8 +220,8 @@ void Spectrum::Private::prepare_partition(Object* object, std::size_t partition_
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<const Spectrum_Power>(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 && frame.maxima.size() == frame.samples.size()) partition.maximum = detail::prepare_curve(std::span<const Spectrum_Power>(frame.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 && frame.minima.size() == frame.samples.size()) partition.minimum = detail::prepare_curve(std::span<const Spectrum_Power>(frame.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);
if (state.max_hold_visible && maxima.size() == frame.samples.size()) partition.maximum = detail::prepare_curve(std::span<const Spectrum_Power>(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<const Spectrum_Power>(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);
}
template <Attached Object>
void Spectrum::Private::paint_frame(Object* object) {
@@ -246,20 +248,6 @@ void Spectrum::Private::paint_frame(Object* object) {
}
}
template <Attached Object>
void Spectrum::Private::update_samples(Object* object, std::span<const Spectrum_Power> values) {
const Spectrum_Frame& previous = object->template current_buffer<Spectrum_Frame_Tag>();
Spectrum_Frame next;
next.samples.assign(values.begin(), values.end());
next.maxima.resize(values.size());
next.minima.resize(values.size());
for (std::size_t index = 0; index < values.size(); ++index) {
next.maxima[index] = previous.maxima.size() == values.size() ? std::max(previous.maxima[index], values[index]) : values[index];
next.minima[index] = previous.minima.size() == values.size() ? std::min(previous.minima[index], values[index]) : values[index];
}
object->template pending_buffer<Spectrum_Frame_Tag>() = std::move(next);
object->template mark_dirty<Prepare_Data_Tag>();
}
template <Attached Object>
void Spectrum::Private::add_marker(Object* object, Spectrum_Frequency frequency) {
object->template update_prop<&Prop::custom_markers>([frequency](Prop_Access<typename Object::Prop> props) { props.template get<Spectrum::Base_Tag>().custom_markers.push_back(frequency); });
object->template mark_dirty<Prepare_Data_Tag>();
@@ -336,9 +324,6 @@ void Spectrum::Private::before_advance(Object*, Prop_Type*, State_Access<State_T
template <Attached Object>
const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() {
static const Dispatch value{
[](Root* root, std::span<const Spectrum_Power> values) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).update_samples(object, values); },
[](const Root* root) { return static_cast<const Object*>(root)->template current_buffer<Spectrum_Frame_Tag>().samples.size(); },
[](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t count{}; for (const auto& partition : data.prepared.partitions) count += partition.current.points.size(); return count; },
[](const Root* root, Spectrum_Frequency frequency) { const auto* object = static_cast<const Object*>(root); const auto& data = static_cast<const typename Object::Private&>(*object->d); return Private::power_at(static_cast<const Prop&>(*data.current), object->template current_buffer<Spectrum_Frame_Tag>(), frequency); },
[](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).add_marker(object, frequency); },
[](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).remove_marker(object, frequency); },
@@ -10,13 +10,14 @@
#include <vector>
namespace aethera::render_2d {
struct Sweep_Spectrum_Stream_Tag {};
struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable_2D<Renderable_2D_Cache::enabled>,
struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable_2D,
Mpmc_Triple_Buffer<Sweep_Spectrum_Stream_Tag,
std::shared_ptr<const std::vector<Plot_Value>>>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Frequency_Object = Impl<Frequency_Axis>;
using Power_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {
Prop() { cache_enabled = true; }
std::size_t bins_per_block{}; /* 每个扫频块期望的功率点数;零值接受首块尺寸。 */
std::size_t block_count{1}; /* 一个完整扫频周期包含的块数;零值按 1 处理。 */
Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */
+2 -1
View File
@@ -16,12 +16,13 @@ struct Waterfall_Row {
bool operator==(const Waterfall_Row&) const;
};
struct Waterfall_Stream_Tag {};
struct Waterfall : Def<Waterfall, Renderable_2D<Renderable_2D_Cache::enabled>,
struct Waterfall : Def<Waterfall, Renderable_2D,
Mpmc_Triple_Buffer<Waterfall_Stream_Tag, std::shared_ptr<const Waterfall_Row>>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Frequency_Object = Impl<Frequency_Axis>;
using Time_Object = Impl<Time_Axis>;
struct Prop : Prev_Prop {
Prop() { cache_enabled = true; }
bool tooltip_enabled{true}; /* 是否响应指针位置显示提示。 */
Font tooltip_font{}; /* 提示文字字体。 */
Pen tooltip_text_pen{Color::white()}; /* 提示文字样式。 */
@@ -86,6 +86,13 @@ template <Attached Object>
void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access<State>, const Prop*, State_Access<State>) {
bool rebuild = !paint_taskflow;
object->template access_pending_dependency_graph<Paint_Tag>([&](auto& paint_state) { rebuild = rebuild || paint_state.dirty(); });
for (auto& node : paint_order) {
const bool enabled = node.private_data->cache_enabled &&
node.private_data->cache_enabled(node.object);
if (enabled == (node.cache_owner == node.object)) continue;
node.private_data->valid_cache = nullptr;
rebuild = true;
}
if (!rebuild) return;
if (!paint_taskflow) paint_taskflow = std::make_unique<tf::Taskflow>();
auto& taskflow = *paint_taskflow;
@@ -100,7 +107,7 @@ void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access
auto* data = view.private_data(node);
if (!data) return;
Root* cache_owner{};
if (data->pending_cache) cache_owner = node.object;
if (data->cache_enabled && data->cache_enabled(node.object)) cache_owner = node.object;
else {
bool conflict{};
for (const auto* dependency : node.dependencies) {
+7 -7
View File
@@ -207,11 +207,11 @@ TEST(selection_overlay, control_extends_selection_and_plain_click_clears_it) {
render_once(scene.get());
};
drag({30.0, 90.0}, {60.0, 60.0}, Keyboard_Modifier::none);
EXPECT_EQ(overlay->selected_regions().size(), 1u);
EXPECT_EQ(overlay->read_prop<Selection_Rectangle_Overlay::Base_Tag>().selected_regions.size(), 1u);
drag({70.0, 90.0}, {100.0, 60.0}, Keyboard_Modifier::control);
EXPECT_EQ(overlay->selected_regions().size(), 2u);
EXPECT_EQ(overlay->read_prop<Selection_Rectangle_Overlay::Base_Tag>().selected_regions.size(), 2u);
drag({120.0, 40.0}, {120.0, 40.0}, Keyboard_Modifier::none);
EXPECT_TRUE(overlay->selected_regions().empty());
EXPECT_TRUE(overlay->read_prop<Selection_Rectangle_Overlay::Base_Tag>().selected_regions.empty());
}
TEST(selection_overlay, time_axis_selection_keeps_axis_coordinates_while_window_moves) {
@@ -247,12 +247,12 @@ TEST(selection_overlay, time_axis_selection_keeps_axis_coordinates_while_window_
release->button = Mouse_Button::left;
scene->submit_stream<Scene_Event_Stream_Tag>(release);
render_once(scene.get());
ASSERT_EQ(overlay->selected_regions().size(), 1u);
const Axis_Rectangle selected = overlay->selected_regions().front();
ASSERT_EQ(overlay->read_prop<Selection_Rectangle_Overlay::Base_Tag>().selected_regions.size(), 1u);
const Axis_Rectangle selected = overlay->read_prop<Selection_Rectangle_Overlay::Base_Tag>().selected_regions.front();
const auto pixel_before = time->coordinate_to_pixel(selected.horizontal.center());
time->append_time({4'000});
render_once(scene.get());
ASSERT_EQ(overlay->selected_regions().size(), 1u);
EXPECT_EQ(overlay->selected_regions().front(), selected);
ASSERT_EQ(overlay->read_prop<Selection_Rectangle_Overlay::Base_Tag>().selected_regions.size(), 1u);
EXPECT_EQ(overlay->read_prop<Selection_Rectangle_Overlay::Base_Tag>().selected_regions.front(), selected);
EXPECT_NE(time->coordinate_to_pixel(selected.horizontal.center()), pixel_before);
}
+11 -5
View File
@@ -49,9 +49,11 @@ TEST(spectrum_data, publishes_samples_and_interpolates_power) {
EXPECT_EQ(missing.error(), Spectrum::Power_At_Result::no_samples);
spectrum->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0});
const double samples[]{-100.0, -50.0, 0.0};
spectrum->update_samples(samples);
spectrum->pending_buffer<Spectrum_Frame_Tag>() =
Spectrum_Frame{std::vector<Spectrum_Power>(std::begin(samples), std::end(samples))};
spectrum->mark_dirty<Prepare_Data_Tag>();
spectrum->advance();
EXPECT_EQ(spectrum->sample_count(), 3u);
EXPECT_EQ(spectrum->current_buffer<Spectrum_Frame_Tag>().samples.size(), 3u);
const auto value = spectrum->power_at(25.0);
ASSERT_TRUE(value.has_value());
EXPECT_DOUBLE_EQ(*value, -75.0);
@@ -111,7 +113,9 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
spectrum->set<&Spectrum::Prop::sweep_region_visible>(true);
spectrum->set<&Spectrum::Prop::max_hold_visible>(true);
const double samples[]{-90.0, -65.0, -20.0, -45.0, -75.0};
spectrum->update_samples(samples);
spectrum->pending_buffer<Spectrum_Frame_Tag>() =
Spectrum_Frame{std::vector<Spectrum_Power>(std::begin(samples), std::end(samples))};
spectrum->mark_dirty<Prepare_Data_Tag>();
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->set<&Render_Scene_2D::Prop::background>(Color::transparent());
scene->activate_view();
@@ -127,7 +131,7 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
EXPECT_TRUE(cache_graph.depends_on(spectrum.get(), frequency.get()));
EXPECT_TRUE(cache_graph.depends_on(spectrum.get(), power.get()));
auto output_frame = render_frame(scene.get());
EXPECT_GT(spectrum->rendered_point_count(), 0u);
EXPECT_GT(spectrum->read_state<Spectrum::Base_Tag>().rendered_point_count, 0u);
const auto& render_state = spectrum->read_state<Renderable::Base_Tag>();
EXPECT_EQ(render_state.prepare_task_count, 4u);
EXPECT_EQ(render_state.paint_task_count, 1u);
@@ -153,7 +157,9 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
EXPECT_FALSE(spectrum->dirty<Paint_Cache_Tag>());
EXPECT_TRUE(spectrum->read_state<Renderable::Base_Tag>().prepare_executed);
spectrum->set<&Spectrum::Prop::partition_count>(2u);
spectrum->update_samples(samples);
spectrum->pending_buffer<Spectrum_Frame_Tag>() =
Spectrum_Frame{std::vector<Spectrum_Power>(std::begin(samples), std::end(samples))};
spectrum->mark_dirty<Prepare_Data_Tag>();
output_frame = render_frame(scene.get());
const auto& rebuilt_state = spectrum->read_state<Renderable::Base_Tag>();
EXPECT_TRUE(rebuilt_state.prepare_graph_rebuilt);