迁移完一个视图,待审依赖轴,scene的方式。帧策略替代方案,是否使用graph了,失效机制
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
#include "render_common.hpp"
|
||||
namespace aethera {
|
||||
/* Renderable 派生实现声明颜色缓存时使用的业务标签。 */
|
||||
struct Color_Cache {};
|
||||
struct Color_Cache {
|
||||
virtual ~Color_Cache() = default;
|
||||
};
|
||||
/* Renderable 状态标签,用于访问和订阅 Renderable::State。 */
|
||||
struct Renderable_State_Tag {};
|
||||
struct Renderable;
|
||||
|
||||
@@ -15,6 +15,8 @@ struct Renderable::Private : Prev_Private {
|
||||
using State_Get = State* (*)(Root*);
|
||||
using State_Notify = void (*)(Root*);
|
||||
using Event_Run = void (*)(Root*, const Event&);
|
||||
using Color_Cache_Visitor = void (*)(void*, const Color_Cache&);
|
||||
using Color_Cache_Visit = void (*)(Root*, void*, Color_Cache_Visitor);
|
||||
struct Stage_Dispatch {
|
||||
Run_Predicate predicate; /* 判断该阶段本次是否执行。 */
|
||||
Rebuild_Predicate rebuild_predicate; /* 判断已有阶段子图是否重建。 */
|
||||
@@ -32,6 +34,7 @@ struct Renderable::Private : Prev_Private {
|
||||
};
|
||||
const Dispatch* dispatch{}; /* 绑定最终对象类型后指向其静态分派表。 */
|
||||
Event_Run event_run{}; /* 最终 Private 具备事件能力时的无虚函数入口。 */
|
||||
Color_Cache_Visit color_cache_visit{}; /* 最终对象存在 Color_Cache Buffer 时访问本轮写入结果。 */
|
||||
std::unique_ptr<tf::Taskflow> prepare_graph; /* Prepare 子图模式的当前构建产物。 */
|
||||
std::unique_ptr<tf::Taskflow> paint_graph; /* Paint 子图模式的当前构建产物。 */
|
||||
bool prepare_graph_built{}; /* Prepare 子图是否至少成功构建过一次。 */
|
||||
@@ -80,6 +83,20 @@ void Renderable::bind_private_crtp(Object* object) {
|
||||
private_data.handle_event(value, event);
|
||||
};
|
||||
}
|
||||
if constexpr (requires { object->template pending_buffer<Color_Cache>(); }) {
|
||||
using Cache = std::remove_reference_t<decltype(object->template pending_buffer<Color_Cache>())>;
|
||||
if constexpr (std::derived_from<Cache, Color_Cache>) {
|
||||
data.color_cache_visit = [](Root* root, void* context, Private::Color_Cache_Visitor visitor) {
|
||||
auto* value = static_cast<Object*>(root);
|
||||
auto& private_data = static_cast<typename Object::Private&>(*value->d);
|
||||
const auto& render_state = static_cast<const State&>(*private_data.state.current);
|
||||
if (render_state.paint_executed)
|
||||
visitor(context, value->template pending_buffer<Color_Cache>());
|
||||
else
|
||||
visitor(context, value->template current_buffer<Color_Cache>());
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
inline void Renderable::bind_dependency_graph_object(Attached auto* object) {
|
||||
using Object = std::remove_pointer_t<decltype(object)>;
|
||||
|
||||
@@ -150,6 +150,14 @@ struct Time_Of_Day {
|
||||
bool operator==(const Time_Of_Day&) const = default;
|
||||
};
|
||||
enum class Image_Interpolation_Mode : std::uint8_t { nearest, bilinear, bicubic };
|
||||
enum class Line_Interpolation_Mode : std::uint8_t {
|
||||
nearest_sample,
|
||||
linear_value,
|
||||
linear_power_domain,
|
||||
step_left,
|
||||
step_right,
|
||||
cubic_value
|
||||
};
|
||||
enum class Pixel_Format : std::uint8_t { premultiplied_32 };
|
||||
/* 只读图像像素视图。 */
|
||||
struct Image_View {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "Spectrum.hpp"
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "Spectrum.hpp"
|
||||
namespace aethera::render_2d {
|
||||
void Spectrum::update_samples(std::span<const double> values) {
|
||||
static_cast<Private&>(*d).dispatch->update_samples(this, values);
|
||||
}
|
||||
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);
|
||||
}
|
||||
bool Spectrum::power_at(double frequency, double& power) const {
|
||||
return static_cast<const Private&>(*d).dispatch->power_at(this, frequency, power);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
#include "../axis/Axis.hpp"
|
||||
#include "../render/Blend2D_Cache.hpp"
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
namespace aethera::render_2d {
|
||||
struct Spectrum_State_Tag {};
|
||||
struct Spectrum_Frame_Tag {};
|
||||
struct Spectrum_Frame {
|
||||
std::vector<double> samples{}; /* 最近提交的当前频谱功率样本。 */
|
||||
std::vector<double> maxima{}; /* 与 samples 同尺寸的逐点历史最大值。 */
|
||||
std::vector<double> minima{}; /* 与 samples 同尺寸的逐点历史最小值。 */
|
||||
bool operator==(const Spectrum_Frame&) const = default;
|
||||
};
|
||||
/* 使用频率轴和功率轴绘制当前值、最大保持、最小保持及扫频区域。 */
|
||||
struct Spectrum : Def<Spectrum, Renderable, State_Type<Spectrum_State_Tag>,
|
||||
Tagged_Buffer<Color_Cache, Blend2D_Cache>,
|
||||
Tagged_Buffer<Spectrum_Frame_Tag, Spectrum_Frame>> {
|
||||
struct Prop : Prev_Prop {};
|
||||
struct State : Prev_State<Spectrum_State_Tag> {
|
||||
Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */
|
||||
double center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */
|
||||
Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */
|
||||
|
||||
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}; /* 是否裁掉频率轴当前范围以外的线段。 */
|
||||
|
||||
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 middle_frequency_pen{Color::red_color()}; /* 中心频率垂线样式。 */
|
||||
|
||||
Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::solid}; /* 扫频区域背景样式。 */
|
||||
bool operator==(const State&) const = default;
|
||||
};
|
||||
/* 完整声明、曲线准备缓存和 CRTP 能力见 Spectrum.ipp。 */
|
||||
struct Private;
|
||||
template <typename Object>
|
||||
struct Builder : Prev_Builder<Object> {
|
||||
using Base = Prev_Builder<Object>;
|
||||
template <Axis_Object Frequency_Axis_Object, Axis_Object Power_Axis_Object>
|
||||
Builder(Frequency_Axis_Object* frequency_axis, Power_Axis_Object* power_axis)
|
||||
: Base(), configure([frequency_axis, power_axis](Object* object) {
|
||||
object->bind_axes(frequency_axis, power_axis);
|
||||
}) {}
|
||||
[[nodiscard]] auto build() {
|
||||
auto result = Base::build();
|
||||
if (result) configure(result->get());
|
||||
return result;
|
||||
}
|
||||
private:
|
||||
std::function<void(Object*)> configure; /* build 后把轴引用写入最终 Spectrum::Private。 */
|
||||
};
|
||||
/* 提交新一帧样本并更新逐点最大/最小保持。 */
|
||||
void update_samples(std::span<const double> values);
|
||||
/* 返回最近一次双缓冲交换后发布的样本数。 */
|
||||
[[nodiscard]] std::size_t sample_count() const;
|
||||
/* 返回最近一次 Prepare 得到的当前曲线点数。 */
|
||||
[[nodiscard]] std::size_t rendered_point_count() const;
|
||||
/* 在已发布帧的线性样本位置上查询功率;频率越界或没有样本时返回 false。 */
|
||||
[[nodiscard]] bool power_at(double frequency, double& power) const;
|
||||
protected:
|
||||
/* Builder 挂接最终 Private 后安装 Renderable 与 Spectrum 业务分派。 */
|
||||
template <Attached Object>
|
||||
void bind_private_crtp(Object* object);
|
||||
/* Spectrum::Builder 在最终 Private 创建后绑定两根轴;轴必须比 Spectrum 生命周期更长。 */
|
||||
template <Axis_Object Frequency_Axis_Object, Axis_Object Power_Axis_Object>
|
||||
void bind_axes(Frequency_Axis_Object* frequency_axis, Power_Axis_Object* power_axis);
|
||||
};
|
||||
}
|
||||
#include "Spectrum.ipp"
|
||||
@@ -0,0 +1,365 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
namespace aethera::render_2d {
|
||||
struct Spectrum::Private : Prev_Private {
|
||||
struct Axis_Binding {
|
||||
Root* object{}; /* 不拥有的轴对象;Builder 要求其生命周期覆盖 Spectrum。 */
|
||||
Axis_Range (*coordinate_range)(const Root*){}; /* 读取轴当前有向坐标范围。 */
|
||||
double (*coordinate_to_pixel)(const Root*, double){}; /* 将坐标映射到轴向画布像素。 */
|
||||
Axis_Orientation (*orientation)(const Root*){}; /* 查询轴当前方向,不保存方向镜像。 */
|
||||
Size (*canvas_size)(const Root*){}; /* 查询轴当前画布尺寸,不保存尺寸镜像。 */
|
||||
template <Axis_Object Axis>
|
||||
[[nodiscard]] static Axis_Binding make(Axis* axis) {
|
||||
return {
|
||||
axis,
|
||||
[](const Root* root) { return static_cast<const Axis*>(root)->coordinate_range(); },
|
||||
[](const Root* root, double coordinate) {
|
||||
return static_cast<const Axis*>(root)->coordinate_to_pixel(coordinate);
|
||||
},
|
||||
[](const Root* root) {
|
||||
Axis_Orientation result{};
|
||||
static_cast<const Axis*>(root)->template access_state<Abs_Axis_State_Tag>(
|
||||
[&](const Abs_Axis::State& state) { result = state.orientation; });
|
||||
return result;
|
||||
},
|
||||
[](const Root* root) {
|
||||
Size result{};
|
||||
static_cast<const Axis*>(root)->template access_state<Abs_Axis_State_Tag>(
|
||||
[&](const Abs_Axis::State& state) { result = state.canvas_size; });
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
[[nodiscard]] Axis_Range range() const { return coordinate_range(object); }
|
||||
[[nodiscard]] double pixel(double coordinate) const { return coordinate_to_pixel(object, coordinate); }
|
||||
[[nodiscard]] explicit operator bool() const noexcept {
|
||||
return object && coordinate_range && coordinate_to_pixel && orientation && canvas_size;
|
||||
}
|
||||
};
|
||||
struct Sample {
|
||||
double coordinate{}; /* 插值样本对应的频率,单位为 Hz。 */
|
||||
double value{}; /* 插值后的功率值。 */
|
||||
};
|
||||
struct Prepared_Curve {
|
||||
std::vector<Point_F> points{}; /* 映射到画布后的折线点。 */
|
||||
std::vector<Point_F> fill{}; /* 含功率轴基线闭合点的填充多边形。 */
|
||||
};
|
||||
struct Prepared {
|
||||
Prepared_Curve current{}; /* 当前频谱曲线。 */
|
||||
Prepared_Curve maximum{}; /* 最大保持曲线。 */
|
||||
Prepared_Curve minimum{}; /* 最小保持曲线。 */
|
||||
Rect_F sweep_region{}; /* 扫频背景在画布中的矩形。 */
|
||||
Point_F center_first{}; /* 中心频率线的功率轴起点。 */
|
||||
Point_F center_second{}; /* 中心频率线的功率轴终点。 */
|
||||
Point_F maximum_point{}; /* 当前样本最大值标记位置。 */
|
||||
Point_F minimum_point{}; /* 当前样本最小值标记位置。 */
|
||||
Size canvas_size{}; /* 两根轴当前状态共同确定的颜色层尺寸。 */
|
||||
|
||||
bool maximum_valid{}; /* maximum_point 是否有效。 */
|
||||
bool minimum_valid{}; /* minimum_point 是否有效。 */
|
||||
bool valid{}; /* 两根轴与画布是否足以生成绘制数据。 */
|
||||
};
|
||||
using Update_Run = void (*)(Root*, std::span<const double>);
|
||||
using Count_Run = std::size_t (*)(const Root*);
|
||||
using Power_Run = bool (*)(const Root*, double, double&);
|
||||
struct Dispatch {
|
||||
Update_Run update_samples; /* 向最终对象写入 Spectrum_Frame_Tag。 */
|
||||
Count_Run sample_count; /* 查询最终对象最近发布的样本数。 */
|
||||
Count_Run rendered_point_count; /* 查询本轮准备出的当前曲线点数。 */
|
||||
Power_Run power_at; /* 查询最终对象最近发布帧的插值功率。 */
|
||||
};
|
||||
Axis_Binding frequency_axis{}; /* 频率到横向像素的唯一映射来源。 */
|
||||
Axis_Binding power_axis{}; /* 功率到纵向像素的唯一映射来源。 */
|
||||
Prepared prepared{}; /* 由当前 State、Frame 和轴状态推导的 Paint 输入。 */
|
||||
const Dispatch* dispatch{}; /* Builder 绑定最终 Spectrum 类型后的静态分派表。 */
|
||||
template <Attached Object>
|
||||
[[nodiscard]] static const Dispatch& dispatch_for();
|
||||
template <Attached Object>
|
||||
void update_samples(Object* object, std::span<const double> values);
|
||||
template <Attached Object>
|
||||
void prepare_data(Object* object);
|
||||
template <Attached Object>
|
||||
void paint(Object* object);
|
||||
[[nodiscard]] static bool power_at(const State& state,
|
||||
const Spectrum_Frame& frame,
|
||||
double frequency,
|
||||
double& power);
|
||||
[[nodiscard]] static double power_domain_lerp(double first, double second, double ratio);
|
||||
[[nodiscard]] static double cubic_value(double previous, double first, double second,
|
||||
double next, double ratio) noexcept;
|
||||
[[nodiscard]] static std::vector<Sample> interpolate(std::span<const double> values,
|
||||
Axis_Range domain,
|
||||
Line_Interpolation_Mode mode);
|
||||
[[nodiscard]] std::vector<Sample> visible_samples(std::vector<Sample> samples) const;
|
||||
[[nodiscard]] Prepared_Curve prepare_curve(std::span<const double> values,
|
||||
Axis_Range domain,
|
||||
Line_Interpolation_Mode mode,
|
||||
bool visible_only) const;
|
||||
[[nodiscard]] Point_F map_point(double frequency, double power) const;
|
||||
static void paint_curve(detail::Painter& painter,
|
||||
const Prepared_Curve& curve,
|
||||
const Pen& pen,
|
||||
const Brush& brush);
|
||||
};
|
||||
inline bool Spectrum::Private::power_at(const State& state, const Spectrum_Frame& frame,
|
||||
double frequency, double& power) {
|
||||
if (frame.samples.empty() || !state.frequency_range.contains(frequency) ||
|
||||
state.frequency_range.length() == 0.0) return false;
|
||||
const double normalized = (frequency - state.frequency_range.origin) / state.frequency_range.length();
|
||||
const double position = std::clamp(normalized, 0.0, 1.0) * (frame.samples.size() - 1);
|
||||
const auto lower = static_cast<std::size_t>(std::floor(position));
|
||||
const auto upper = std::min(lower + 1, frame.samples.size() - 1);
|
||||
const double fraction = position - lower;
|
||||
power = frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction;
|
||||
return true;
|
||||
}
|
||||
inline double Spectrum::Private::power_domain_lerp(double first, double second, double ratio) {
|
||||
const double first_power = std::pow(10.0, std::clamp(first, -3'000.0, 3'000.0) / 10.0);
|
||||
const double 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));
|
||||
}
|
||||
inline double Spectrum::Private::cubic_value(double previous, double first, double second,
|
||||
double next, double ratio) noexcept {
|
||||
const double ratio2 = ratio * ratio;
|
||||
const double 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);
|
||||
}
|
||||
inline std::vector<Spectrum::Private::Sample> Spectrum::Private::interpolate(
|
||||
std::span<const double> values, Axis_Range domain, Line_Interpolation_Mode mode) {
|
||||
std::vector<Sample> result;
|
||||
if (values.empty()) return result;
|
||||
if (values.size() == 1) return {{domain.origin, values.front()}};
|
||||
constexpr int subdivisions = 4;
|
||||
const double denominator = static_cast<double>(values.size() - 1);
|
||||
const auto coordinate = [domain, denominator](std::size_t index) {
|
||||
return domain.origin + domain.length() * index / denominator;
|
||||
};
|
||||
result.push_back({coordinate(0), values.front()});
|
||||
for (std::size_t index = 0; index + 1 < values.size(); ++index) {
|
||||
const double first_coordinate = coordinate(index);
|
||||
const double second_coordinate = coordinate(index + 1);
|
||||
const double first = values[index];
|
||||
const double second = values[index + 1];
|
||||
switch (mode) {
|
||||
case Line_Interpolation_Mode::nearest_sample: {
|
||||
const double 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 double ratio = static_cast<double>(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 double previous = values[index == 0 ? 0 : index - 1];
|
||||
const double next = values[std::min(index + 2, values.size() - 1)];
|
||||
for (int part = 1; part <= subdivisions; ++part) {
|
||||
const double ratio = static_cast<double>(part) / subdivisions;
|
||||
result.push_back({first_coordinate + (second_coordinate - first_coordinate) * ratio,
|
||||
cubic_value(previous, first, second, next, ratio)});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
inline std::vector<Spectrum::Private::Sample> Spectrum::Private::visible_samples(
|
||||
std::vector<Sample> samples) const {
|
||||
const auto [low, high] = std::minmax(frequency_axis.range().origin, frequency_axis.range().target);
|
||||
if (samples.size() < 2)
|
||||
return !samples.empty() && samples.front().coordinate >= low && samples.front().coordinate <= high
|
||||
? std::move(samples) : std::vector<Sample>{};
|
||||
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);
|
||||
}
|
||||
if (first == samples.size()) return {};
|
||||
return {samples.begin() + static_cast<std::ptrdiff_t>(first),
|
||||
samples.begin() + static_cast<std::ptrdiff_t>(last + 1)};
|
||||
}
|
||||
inline Spectrum::Private::Prepared_Curve Spectrum::Private::prepare_curve(
|
||||
std::span<const double> values, Axis_Range domain, Line_Interpolation_Mode mode,
|
||||
bool visible_only) const {
|
||||
Prepared_Curve result;
|
||||
auto samples = interpolate(values, domain, mode);
|
||||
if (visible_only) samples = visible_samples(std::move(samples));
|
||||
for (const auto& sample : samples) {
|
||||
if (std::isfinite(sample.coordinate) && std::isfinite(sample.value))
|
||||
result.points.push_back(map_point(sample.coordinate, sample.value));
|
||||
}
|
||||
if (result.points.size() < 2) return result;
|
||||
result.fill.reserve(result.points.size() + 2);
|
||||
const double baseline = power_axis.pixel(power_axis.range().target);
|
||||
Point_F first_baseline = result.points.front();
|
||||
Point_F last_baseline = result.points.back();
|
||||
if (power_axis.orientation(power_axis.object) == Axis_Orientation::horizontal) {
|
||||
first_baseline.x = baseline;
|
||||
last_baseline.x = baseline;
|
||||
} else {
|
||||
first_baseline.y = baseline;
|
||||
last_baseline.y = baseline;
|
||||
}
|
||||
result.fill.push_back(first_baseline);
|
||||
result.fill.insert(result.fill.end(), result.points.begin(), result.points.end());
|
||||
result.fill.push_back(last_baseline);
|
||||
return result;
|
||||
}
|
||||
inline Point_F Spectrum::Private::map_point(double frequency, double power) const {
|
||||
const double frequency_pixel = frequency_axis.pixel(frequency);
|
||||
const double power_pixel = power_axis.pixel(power);
|
||||
return frequency_axis.orientation(frequency_axis.object) == Axis_Orientation::horizontal
|
||||
? Point_F{frequency_pixel, power_pixel} : Point_F{power_pixel, frequency_pixel};
|
||||
}
|
||||
inline void Spectrum::Private::paint_curve(detail::Painter& painter, const Prepared_Curve& 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);
|
||||
}
|
||||
template <Attached Object>
|
||||
void Spectrum::Private::update_samples(Object* object, std::span<const double> 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::prepare_data(Object* object) {
|
||||
auto& private_data = static_cast<typename Object::Private&>(*this);
|
||||
const auto& state = static_cast<const State&>(*private_data.state.current);
|
||||
const auto& frame = object->template current_buffer<Spectrum_Frame_Tag>();
|
||||
prepared = {};
|
||||
if (!frequency_axis || !power_axis) return;
|
||||
const Axis_Orientation frequency_orientation = frequency_axis.orientation(frequency_axis.object);
|
||||
const Axis_Orientation power_orientation = power_axis.orientation(power_axis.object);
|
||||
if (frequency_orientation == power_orientation) return;
|
||||
const Size frequency_canvas = frequency_axis.canvas_size(frequency_axis.object);
|
||||
const Size power_canvas = power_axis.canvas_size(power_axis.object);
|
||||
if (frequency_canvas.empty() || frequency_canvas != power_canvas) return;
|
||||
prepared.canvas_size = frequency_canvas;
|
||||
prepared.current = prepare_curve(frame.samples, state.frequency_range,
|
||||
state.interpolation_mode, state.visible_range_only);
|
||||
if (state.max_hold_visible)
|
||||
prepared.maximum = prepare_curve(frame.maxima, state.frequency_range,
|
||||
state.interpolation_mode, state.visible_range_only);
|
||||
if (state.min_hold_visible)
|
||||
prepared.minimum = prepare_curve(frame.minima, state.frequency_range,
|
||||
state.interpolation_mode, state.visible_range_only);
|
||||
const Axis_Range power_range = power_axis.range();
|
||||
prepared.center_first = map_point(state.center_frequency, power_range.origin);
|
||||
prepared.center_second = map_point(state.center_frequency, power_range.target);
|
||||
if (state.sweep_region_visible) {
|
||||
const Point_F first = map_point(state.sweep_frequency_range.origin, power_range.origin);
|
||||
const Point_F second = map_point(state.sweep_frequency_range.target, power_range.target);
|
||||
prepared.sweep_region = Rect_F{first.x, first.y, second.x - first.x, second.y - first.y}.normalized();
|
||||
}
|
||||
if (!frame.samples.empty()) {
|
||||
const double denominator = frame.samples.size() > 1 ? frame.samples.size() - 1.0 : 1.0;
|
||||
const auto point_at = [&](auto iterator) {
|
||||
const std::size_t index = static_cast<std::size_t>(std::distance(frame.samples.begin(), iterator));
|
||||
const double frequency = state.frequency_range.origin + state.frequency_range.length() * index / denominator;
|
||||
return map_point(frequency, *iterator);
|
||||
};
|
||||
if (state.max_marker_visible) {
|
||||
prepared.maximum_point = point_at(std::max_element(frame.samples.begin(), frame.samples.end()));
|
||||
prepared.maximum_valid = true;
|
||||
}
|
||||
if (state.min_marker_visible) {
|
||||
prepared.minimum_point = point_at(std::min_element(frame.samples.begin(), frame.samples.end()));
|
||||
prepared.minimum_valid = true;
|
||||
}
|
||||
}
|
||||
prepared.valid = true;
|
||||
}
|
||||
template <Attached Object>
|
||||
void Spectrum::Private::paint(Object* object) {
|
||||
auto& private_data = static_cast<typename Object::Private&>(*this);
|
||||
const auto& state = static_cast<const State&>(*private_data.state.current);
|
||||
auto& cache = object->template pending_buffer<Color_Cache>();
|
||||
cache.ensure_size(prepared.canvas_size);
|
||||
cache.clear();
|
||||
if (!prepared.valid) return;
|
||||
detail::Painter painter(cache, prepared.canvas_size);
|
||||
if (state.sweep_region_visible)
|
||||
painter.rect(prepared.sweep_region, Pen{.style = Line_Style::none}, state.sweep_region_brush);
|
||||
paint_curve(painter, prepared.maximum, state.max_pen, state.max_brush);
|
||||
paint_curve(painter, prepared.minimum, state.min_pen, state.min_brush);
|
||||
paint_curve(painter, prepared.current, state.current_pen, state.current_brush);
|
||||
painter.line(prepared.center_first, prepared.center_second, state.middle_frequency_pen);
|
||||
if (prepared.maximum_valid)
|
||||
painter.circle(prepared.maximum_point, 3.0, state.max_pen, Brush{state.max_pen.color, Brush_Style::solid});
|
||||
if (prepared.minimum_valid)
|
||||
painter.circle(prepared.minimum_point, 3.0, state.min_pen, Brush{state.min_pen.color, Brush_Style::solid});
|
||||
}
|
||||
template <Attached Object>
|
||||
const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() {
|
||||
static const Dispatch value{
|
||||
[](Root* root, std::span<const double> values) {
|
||||
auto* object = static_cast<Object*>(root);
|
||||
static_cast<typename Object::Private&>(*object->d).update_samples(object, values);
|
||||
},
|
||||
[](const Root* root) {
|
||||
const auto* object = static_cast<const Object*>(root);
|
||||
const auto& data = static_cast<const typename Object::Private&>(*object->d);
|
||||
return object->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);
|
||||
return data.prepared.current.points.size();
|
||||
},
|
||||
[](const Root* root, double frequency, double& power) {
|
||||
const auto* object = static_cast<const Object*>(root);
|
||||
const auto& data = static_cast<const typename Object::Private&>(*object->d);
|
||||
const auto& state = static_cast<const State&>(*data.state.current);
|
||||
return Private::power_at(state, object->template current_buffer<Spectrum_Frame_Tag>(), frequency, power);
|
||||
}
|
||||
};
|
||||
return value;
|
||||
}
|
||||
template <Attached Object>
|
||||
void Spectrum::bind_private_crtp(Object* object) {
|
||||
Renderable::bind_private_crtp(object);
|
||||
static_cast<Private&>(*object->d).dispatch = &Private::dispatch_for<Object>();
|
||||
}
|
||||
template <Axis_Object Frequency_Axis_Object, Axis_Object Power_Axis_Object>
|
||||
void Spectrum::bind_axes(Frequency_Axis_Object* frequency_axis, Power_Axis_Object* power_axis) {
|
||||
auto& private_data = static_cast<Private&>(*d);
|
||||
private_data.frequency_axis = Private::Axis_Binding::make(frequency_axis);
|
||||
private_data.power_axis = Private::Axis_Binding::make(power_axis);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "../base/Types.hpp"
|
||||
#include <renderable.hpp>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
@@ -8,10 +9,10 @@ namespace detail {
|
||||
struct Painter;
|
||||
}
|
||||
/* 可作为双缓冲值深拷贝的 Blend2D 像素缓存。 */
|
||||
struct Blend2D_Cache {
|
||||
struct Blend2D_Cache : Color_Cache {
|
||||
struct Private;
|
||||
Blend2D_Cache();
|
||||
~Blend2D_Cache();
|
||||
~Blend2D_Cache() override;
|
||||
Blend2D_Cache(const Blend2D_Cache& other);
|
||||
Blend2D_Cache& operator=(const Blend2D_Cache& other);
|
||||
Blend2D_Cache(Blend2D_Cache&& other) noexcept;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#include "Render_Scene_2D.hpp"
|
||||
namespace aethera::render_2d {
|
||||
Render_Frame_Status Render_Scene_2D::render_frame() {
|
||||
return static_cast<Private&>(*d).dispatch->render_frame(this);
|
||||
}
|
||||
void Render_Scene_2D::dispatch_event(const Event& event) {
|
||||
static_cast<Private&>(*d).dispatch->dispatch_event(this, event);
|
||||
}
|
||||
void Render_Scene_2D::activate_view() {
|
||||
static_cast<Private&>(*d).dispatch->set_active(this, true);
|
||||
}
|
||||
void Render_Scene_2D::deactivate_view() {
|
||||
static_cast<Private&>(*d).dispatch->set_active(this, false);
|
||||
}
|
||||
Image_View Render_Scene_2D::frame_view() const {
|
||||
return static_cast<const Private&>(*d).dispatch->frame_view(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include "../base/Types.hpp"
|
||||
#include "../render/Blend2D_Cache.hpp"
|
||||
#include <scene.hpp>
|
||||
namespace aethera::render_2d {
|
||||
struct Render_Scene_2D_State_Tag {};
|
||||
struct Scene_Color_Cache_Tag {};
|
||||
enum class Render_Frame_Status : std::uint8_t {
|
||||
rendered,
|
||||
view_inactive,
|
||||
empty_viewport
|
||||
};
|
||||
/* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */
|
||||
struct Render_Scene_2D : Def<Render_Scene_2D, Scene, State_Type<Render_Scene_2D_State_Tag>,
|
||||
Tagged_Buffer<Scene_Color_Cache_Tag, Blend2D_Cache>> {
|
||||
struct Prop : Prev_Prop {};
|
||||
struct State : Prev_State<Render_Scene_2D_State_Tag> {
|
||||
Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */
|
||||
Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */
|
||||
bool view_active{}; /* 视图是否接受 render_frame 请求。 */
|
||||
bool operator==(const State&) const = default;
|
||||
};
|
||||
/* 完整声明、合成顺序和 CRTP 分派见 Render_Scene_2D.ipp。 */
|
||||
struct Private;
|
||||
/* 执行当前依赖图并合成一帧。 */
|
||||
[[nodiscard]] Render_Frame_Status render_frame();
|
||||
/* 按 Paint 图逆序派发事件,首个接受者终止传播。 */
|
||||
void dispatch_event(const Event& event);
|
||||
/* 激活后 render_frame 才会执行。 */
|
||||
void activate_view();
|
||||
/* 停止后续 render_frame 请求,不清除最后一帧。 */
|
||||
void deactivate_view();
|
||||
/* 返回最后一次成功合成的只读像素视图;下次渲染后失效。 */
|
||||
[[nodiscard]] Image_View frame_view() const;
|
||||
protected:
|
||||
/* Builder 挂接最终 Private 后安装二维 Scene 的无虚函数业务分派。 */
|
||||
template <Attached Object>
|
||||
void bind_private_crtp(Object* object);
|
||||
};
|
||||
}
|
||||
#include "Render_Scene_2D.ipp"
|
||||
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
namespace aethera::render_2d {
|
||||
struct Render_Scene_2D::Private : Prev_Private {
|
||||
using Render_Run = Render_Frame_Status (*)(Root*);
|
||||
using Event_Run = void (*)(Root*, const Event&);
|
||||
using Active_Run = void (*)(Root*, bool);
|
||||
using Frame_View_Run = Image_View (*)(const Root*);
|
||||
struct Dispatch {
|
||||
Render_Run render_frame; /* 执行最终 Scene 并合成颜色层。 */
|
||||
Event_Run dispatch_event; /* 向最终 Scene 当前 Paint 图派发事件。 */
|
||||
Active_Run set_active; /* 修改最终 Scene 的视图活动状态。 */
|
||||
Frame_View_Run frame_view; /* 访问最终 Scene 已合成的写侧帧。 */
|
||||
};
|
||||
const Dispatch* dispatch{}; /* Builder 绑定最终 Scene 类型后的静态分派表。 */
|
||||
/* Impl CRTP 实现:在对象锁内执行 Kernel Scene,再按 Paint 图拓扑顺序合成颜色层。 */
|
||||
template <Attached Object, typename Callback>
|
||||
void process(Object* object, Callback&& callback)
|
||||
requires std::invocable<Callback, const Render_Frame_Status&>;
|
||||
/* CRTP 业务实现:按 Paint 图拓扑逆序派发事件。 */
|
||||
template <Attached Object>
|
||||
void dispatch_event(Object* object, const Event& event);
|
||||
template <Attached Object>
|
||||
[[nodiscard]] static const Dispatch& dispatch_for();
|
||||
};
|
||||
template <Attached Object, typename Callback>
|
||||
void Render_Scene_2D::Private::process(Object* object, Callback&& callback)
|
||||
requires std::invocable<Callback, const Render_Frame_Status&> {
|
||||
const auto& state = static_cast<const State&>(*static_cast<typename Object::Private&>(*this).state.current);
|
||||
if (!state.view_active) {
|
||||
const Render_Frame_Status status = Render_Frame_Status::view_inactive;
|
||||
std::invoke(std::forward<Callback>(callback), status);
|
||||
return;
|
||||
}
|
||||
if (state.viewport.empty()) {
|
||||
const Render_Frame_Status status = Render_Frame_Status::empty_viewport;
|
||||
std::invoke(std::forward<Callback>(callback), status);
|
||||
return;
|
||||
}
|
||||
object->template current_dependency_graph<Paint_Tag>().for_each(
|
||||
[](const Dependency_Graph::Node& node) { node.object->template mark_dirty<Paint_Tag>(); });
|
||||
Prev_Private::process(object, [&](const Scene::Private::Result&) {
|
||||
auto& frame = object->template pending_buffer<Scene_Color_Cache_Tag>();
|
||||
frame.ensure_size(state.viewport);
|
||||
frame.clear();
|
||||
{
|
||||
detail::Painter painter(frame, state.viewport);
|
||||
painter.rect({0.0, 0.0, static_cast<double>(state.viewport.width),
|
||||
static_cast<double>(state.viewport.height)},
|
||||
Pen{.style = Line_Style::none}, Brush{state.background, Brush_Style::solid});
|
||||
}
|
||||
const auto graph = object->template current_dependency_graph<Paint_Tag>();
|
||||
const auto result = graph.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) {
|
||||
auto* private_data = view.private_data(node);
|
||||
if (!private_data || !private_data->color_cache_visit) return;
|
||||
private_data->color_cache_visit(node.object, &frame, [](void* context, const Color_Cache& cache) {
|
||||
auto& destination = *static_cast<Blend2D_Cache*>(context);
|
||||
if (const auto* layer = dynamic_cast<const Blend2D_Cache*>(&cache)) destination.composite(*layer);
|
||||
});
|
||||
});
|
||||
if (!result) throw std::logic_error("render scene paint graph became invalid during composition");
|
||||
});
|
||||
const Render_Frame_Status status = Render_Frame_Status::rendered;
|
||||
std::invoke(std::forward<Callback>(callback), status);
|
||||
}
|
||||
template <Attached Object>
|
||||
void Render_Scene_2D::Private::dispatch_event(Object* object, const Event& event) {
|
||||
std::vector<Renderable*> order;
|
||||
const auto graph = object->template current_dependency_graph<Paint_Tag>();
|
||||
const auto result = graph.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) {
|
||||
if (auto* renderable = view.object(node)) order.push_back(renderable);
|
||||
});
|
||||
if (!result) throw std::logic_error("render scene paint graph became invalid during event dispatch");
|
||||
for (auto current = order.rbegin(); current != order.rend() && !event.is_accepted(); ++current)
|
||||
(*current)->dispatch_event(event);
|
||||
}
|
||||
template <Attached Object>
|
||||
const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for() {
|
||||
static const Dispatch value{
|
||||
[](Root* root) {
|
||||
auto* object = static_cast<Object*>(root);
|
||||
Render_Frame_Status result{};
|
||||
object->process([&](const Render_Frame_Status& status) { result = status; });
|
||||
return result;
|
||||
},
|
||||
[](Root* root, const Event& event) {
|
||||
auto* object = static_cast<Object*>(root);
|
||||
static_cast<typename Object::Private&>(*object->d).dispatch_event(object, event);
|
||||
},
|
||||
[](Root* root, bool active) {
|
||||
static_cast<Object*>(root)->template update_state<&State::view_active>(active);
|
||||
},
|
||||
[](const Root* root) {
|
||||
const auto* object = static_cast<const Object*>(root);
|
||||
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
|
||||
return private_data.buffer_storage.template get<Scene_Color_Cache_Tag>().pending->view();
|
||||
}
|
||||
};
|
||||
return value;
|
||||
}
|
||||
template <Attached Object>
|
||||
void Render_Scene_2D::bind_private_crtp(Object* object) {
|
||||
static_cast<Private&>(*object->d).dispatch = &Private::dispatch_for<Object>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "Render_Scene_2D.hpp"
|
||||
@@ -0,0 +1,87 @@
|
||||
#include <render_2D/plottable/Spectrum.hpp>
|
||||
#include <render_2D/scene/Render_Scene_2D.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
namespace {
|
||||
using namespace aethera;
|
||||
using namespace aethera::render_2d;
|
||||
template <typename Object, typename... Args>
|
||||
std::unique_ptr<Object> build_object(Args&&... args) {
|
||||
typename Object::Builder builder(std::forward<Args>(args)...);
|
||||
auto result = builder.build();
|
||||
if (!result) std::terminate();
|
||||
return std::move(result).value();
|
||||
}
|
||||
bool contains_color(Image_View view) {
|
||||
for (int y = 0; y < view.height; ++y) {
|
||||
const auto* row = reinterpret_cast<const Pixel*>(view.data + static_cast<std::ptrdiff_t>(y) * view.stride);
|
||||
for (int x = 0; x < view.width; ++x)
|
||||
if (row[x] != 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
TEST(spectrum_data, publishes_samples_and_interpolates_power) {
|
||||
using Frequency = Impl<Frequency_Axis>;
|
||||
using Power = Impl<Numeric_Axis>;
|
||||
using Object = Impl<Spectrum>;
|
||||
auto frequency = build_object<Frequency>();
|
||||
auto power = build_object<Power>();
|
||||
auto spectrum = build_object<Object>(frequency.get(), power.get());
|
||||
spectrum->update_state<&Spectrum::State::frequency_range>(Axis_Range{0.0, 100.0});
|
||||
const double samples[]{-100.0, -50.0, 0.0};
|
||||
spectrum->update_samples(samples);
|
||||
spectrum->advance();
|
||||
EXPECT_EQ(spectrum->sample_count(), 3u);
|
||||
double value{};
|
||||
EXPECT_TRUE(spectrum->power_at(25.0, value));
|
||||
EXPECT_DOUBLE_EQ(value, -75.0);
|
||||
EXPECT_FALSE(spectrum->power_at(101.0, value));
|
||||
}
|
||||
TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
|
||||
using Frequency = Impl<Frequency_Axis>;
|
||||
using Power = Impl<Numeric_Axis>;
|
||||
using Spectrum_Object = Impl<Spectrum>;
|
||||
using Scene_Object = Impl<Render_Scene_2D>;
|
||||
initialize_runtime(2);
|
||||
auto frequency = build_object<Frequency>();
|
||||
auto power = build_object<Power>();
|
||||
auto spectrum = build_object<Spectrum_Object>(frequency.get(), power.get());
|
||||
auto scene = build_object<Scene_Object>();
|
||||
const Size canvas{160, 120};
|
||||
frequency->update_state<&Abs_Axis::State::position>(Point_F{20.0, 100.0});
|
||||
frequency->update_state<&Abs_Axis::State::canvas_size>(canvas);
|
||||
frequency->update_state<&Abs_Axis::State::pixel_length>(120.0);
|
||||
frequency->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 100.0});
|
||||
power->update_state<&Abs_Axis::State::position>(Point_F{20.0, 100.0});
|
||||
power->update_state<&Abs_Axis::State::canvas_size>(canvas);
|
||||
power->update_state<&Abs_Axis::State::pixel_length>(-80.0);
|
||||
power->update_state<&Abs_Axis::State::orientation>(Axis_Orientation::vertical);
|
||||
power->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-100.0, 0.0});
|
||||
spectrum->update_state<&Spectrum::State::frequency_range>(Axis_Range{0.0, 100.0});
|
||||
spectrum->update_state<&Spectrum::State::sweep_region_visible>(true);
|
||||
spectrum->update_state<&Spectrum::State::max_hold_visible>(true);
|
||||
const double samples[]{-90.0, -65.0, -20.0, -45.0, -75.0};
|
||||
spectrum->update_samples(samples);
|
||||
scene->update_state<&Render_Scene_2D::State::viewport>(canvas);
|
||||
scene->update_state<&Render_Scene_2D::State::background>(Color::transparent());
|
||||
scene->activate_view();
|
||||
ASSERT_TRUE((scene->edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) {
|
||||
prepare.add(frequency.get());
|
||||
prepare.add(power.get());
|
||||
prepare.add(spectrum.get());
|
||||
prepare.template add_dependency<&Numeric_Axis::State::coordinate_range>(spectrum.get(), frequency.get());
|
||||
prepare.template add_dependency<&Numeric_Axis::State::coordinate_range>(spectrum.get(), power.get());
|
||||
paint.add(frequency.get());
|
||||
paint.add(power.get());
|
||||
paint.add(spectrum.get());
|
||||
}).has_value()));
|
||||
EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered);
|
||||
EXPECT_GT(spectrum->rendered_point_count(), 0u);
|
||||
const Image_View frame = scene->frame_view();
|
||||
ASSERT_FALSE(frame.empty());
|
||||
EXPECT_TRUE(contains_color(frame));
|
||||
EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered);
|
||||
EXPECT_TRUE(contains_color(scene->frame_view()));
|
||||
EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered);
|
||||
EXPECT_TRUE(contains_color(scene->frame_view()));
|
||||
}
|
||||
Reference in New Issue
Block a user