diff --git a/kernel/src/kernel/renderable.hpp b/kernel/src/kernel/renderable.hpp index 7b2a7b0..687015c 100644 --- a/kernel/src/kernel/renderable.hpp +++ b/kernel/src/kernel/renderable.hpp @@ -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; diff --git a/kernel/src/kernel/renderable.ipp b/kernel/src/kernel/renderable.ipp index 86a499b..fb48f46 100644 --- a/kernel/src/kernel/renderable.ipp +++ b/kernel/src/kernel/renderable.ipp @@ -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 prepare_graph; /* Prepare 子图模式的当前构建产物。 */ std::unique_ptr 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(); }) { + using Cache = std::remove_reference_ttemplate pending_buffer())>; + if constexpr (std::derived_from) { + data.color_cache_visit = [](Root* root, void* context, Private::Color_Cache_Visitor visitor) { + auto* value = static_cast(root); + auto& private_data = static_cast(*value->d); + const auto& render_state = static_cast(*private_data.state.current); + if (render_state.paint_executed) + visitor(context, value->template pending_buffer()); + else + visitor(context, value->template current_buffer()); + }; + } + } } inline void Renderable::bind_dependency_graph_object(Attached auto* object) { using Object = std::remove_pointer_t; diff --git a/render_2D/render_2D/base/Types.hpp b/render_2D/render_2D/base/Types.hpp index 08f2be1..7d8a0c9 100644 --- a/render_2D/render_2D/base/Types.hpp +++ b/render_2D/render_2D/base/Types.hpp @@ -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 { diff --git a/render_2D/render_2D/plottable/Plottables.hpp b/render_2D/render_2D/plottable/Plottables.hpp new file mode 100644 index 0000000..dc697c6 --- /dev/null +++ b/render_2D/render_2D/plottable/Plottables.hpp @@ -0,0 +1,2 @@ +#pragma once +#include "Spectrum.hpp" diff --git a/render_2D/render_2D/plottable/Spectrum.cpp b/render_2D/render_2D/plottable/Spectrum.cpp new file mode 100644 index 0000000..ee7d712 --- /dev/null +++ b/render_2D/render_2D/plottable/Spectrum.cpp @@ -0,0 +1,15 @@ +#include "Spectrum.hpp" +namespace aethera::render_2d { +void Spectrum::update_samples(std::span values) { + static_cast(*d).dispatch->update_samples(this, values); +} +std::size_t Spectrum::sample_count() const { + return static_cast(*d).dispatch->sample_count(this); +} +std::size_t Spectrum::rendered_point_count() const { + return static_cast(*d).dispatch->rendered_point_count(this); +} +bool Spectrum::power_at(double frequency, double& power) const { + return static_cast(*d).dispatch->power_at(this, frequency, power); +} +} diff --git a/render_2D/render_2D/plottable/Spectrum.hpp b/render_2D/render_2D/plottable/Spectrum.hpp new file mode 100644 index 0000000..3f3b046 --- /dev/null +++ b/render_2D/render_2D/plottable/Spectrum.hpp @@ -0,0 +1,82 @@ +#pragma once +#include "../axis/Axis.hpp" +#include "../render/Blend2D_Cache.hpp" +#include +#include +#include +namespace aethera::render_2d { +struct Spectrum_State_Tag {}; +struct Spectrum_Frame_Tag {}; +struct Spectrum_Frame { + std::vector samples{}; /* 最近提交的当前频谱功率样本。 */ + std::vector maxima{}; /* 与 samples 同尺寸的逐点历史最大值。 */ + std::vector minima{}; /* 与 samples 同尺寸的逐点历史最小值。 */ + bool operator==(const Spectrum_Frame&) const = default; +}; +/* 使用频率轴和功率轴绘制当前值、最大保持、最小保持及扫频区域。 */ +struct Spectrum : Def, + Tagged_Buffer, + Tagged_Buffer> { + struct Prop : Prev_Prop {}; + struct State : Prev_State { + 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 + struct Builder : Prev_Builder { + using Base = Prev_Builder; + template + 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 configure; /* build 后把轴引用写入最终 Spectrum::Private。 */ + }; + /* 提交新一帧样本并更新逐点最大/最小保持。 */ + void update_samples(std::span 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 + void bind_private_crtp(Object* object); + /* Spectrum::Builder 在最终 Private 创建后绑定两根轴;轴必须比 Spectrum 生命周期更长。 */ + template + void bind_axes(Frequency_Axis_Object* frequency_axis, Power_Axis_Object* power_axis); +}; +} +#include "Spectrum.ipp" diff --git a/render_2D/render_2D/plottable/Spectrum.ipp b/render_2D/render_2D/plottable/Spectrum.ipp new file mode 100644 index 0000000..57a60c6 --- /dev/null +++ b/render_2D/render_2D/plottable/Spectrum.ipp @@ -0,0 +1,365 @@ +#pragma once +#include +#include +#include +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 + [[nodiscard]] static Axis_Binding make(Axis* axis) { + return { + axis, + [](const Root* root) { return static_cast(root)->coordinate_range(); }, + [](const Root* root, double coordinate) { + return static_cast(root)->coordinate_to_pixel(coordinate); + }, + [](const Root* root) { + Axis_Orientation result{}; + static_cast(root)->template access_state( + [&](const Abs_Axis::State& state) { result = state.orientation; }); + return result; + }, + [](const Root* root) { + Size result{}; + static_cast(root)->template access_state( + [&](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 points{}; /* 映射到画布后的折线点。 */ + std::vector 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); + 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 + [[nodiscard]] static const Dispatch& dispatch_for(); + template + void update_samples(Object* object, std::span values); + template + void prepare_data(Object* object); + template + 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 interpolate(std::span values, + Axis_Range domain, + Line_Interpolation_Mode mode); + [[nodiscard]] std::vector visible_samples(std::vector samples) const; + [[nodiscard]] Prepared_Curve prepare_curve(std::span 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::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::interpolate( + std::span values, Axis_Range domain, Line_Interpolation_Mode mode) { + std::vector result; + if (values.empty()) return result; + if (values.size() == 1) return {{domain.origin, values.front()}}; + constexpr int subdivisions = 4; + const double denominator = static_cast(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(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(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::visible_samples( + std::vector 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{}; + 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(first), + samples.begin() + static_cast(last + 1)}; +} +inline Spectrum::Private::Prepared_Curve Spectrum::Private::prepare_curve( + std::span 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 +void Spectrum::Private::update_samples(Object* object, std::span values) { + const Spectrum_Frame& previous = object->template current_buffer(); + 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() = std::move(next); + object->template mark_dirty(); +} +template +void Spectrum::Private::prepare_data(Object* object) { + auto& private_data = static_cast(*this); + const auto& state = static_cast(*private_data.state.current); + const auto& frame = object->template current_buffer(); + 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::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 +void Spectrum::Private::paint(Object* object) { + auto& private_data = static_cast(*this); + const auto& state = static_cast(*private_data.state.current); + auto& cache = object->template pending_buffer(); + 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 +const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() { + static const Dispatch value{ + [](Root* root, std::span values) { + auto* object = static_cast(root); + static_cast(*object->d).update_samples(object, values); + }, + [](const Root* root) { + const auto* object = static_cast(root); + const auto& data = static_cast(*object->d); + return object->template current_buffer().samples.size(); + }, + [](const Root* root) { + const auto& data = static_cast(*static_cast(root)->d); + return data.prepared.current.points.size(); + }, + [](const Root* root, double frequency, double& power) { + const auto* object = static_cast(root); + const auto& data = static_cast(*object->d); + const auto& state = static_cast(*data.state.current); + return Private::power_at(state, object->template current_buffer(), frequency, power); + } + }; + return value; +} +template +void Spectrum::bind_private_crtp(Object* object) { + Renderable::bind_private_crtp(object); + static_cast(*object->d).dispatch = &Private::dispatch_for(); +} +template +void Spectrum::bind_axes(Frequency_Axis_Object* frequency_axis, Power_Axis_Object* power_axis) { + auto& private_data = static_cast(*d); + private_data.frequency_axis = Private::Axis_Binding::make(frequency_axis); + private_data.power_axis = Private::Axis_Binding::make(power_axis); +} +} diff --git a/render_2D/render_2D/render/Blend2D_Cache.hpp b/render_2D/render_2D/render/Blend2D_Cache.hpp index a43eae5..62de0be 100644 --- a/render_2D/render_2D/render/Blend2D_Cache.hpp +++ b/render_2D/render_2D/render/Blend2D_Cache.hpp @@ -1,5 +1,6 @@ #pragma once #include "../base/Types.hpp" +#include #include #include #include @@ -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; diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp new file mode 100644 index 0000000..686907c --- /dev/null +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -0,0 +1,18 @@ +#include "Render_Scene_2D.hpp" +namespace aethera::render_2d { +Render_Frame_Status Render_Scene_2D::render_frame() { + return static_cast(*d).dispatch->render_frame(this); +} +void Render_Scene_2D::dispatch_event(const Event& event) { + static_cast(*d).dispatch->dispatch_event(this, event); +} +void Render_Scene_2D::activate_view() { + static_cast(*d).dispatch->set_active(this, true); +} +void Render_Scene_2D::deactivate_view() { + static_cast(*d).dispatch->set_active(this, false); +} +Image_View Render_Scene_2D::frame_view() const { + return static_cast(*d).dispatch->frame_view(this); +} +} diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp new file mode 100644 index 0000000..25c3b41 --- /dev/null +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "../base/Types.hpp" +#include "../render/Blend2D_Cache.hpp" +#include +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, + Tagged_Buffer> { + struct Prop : Prev_Prop {}; + struct State : Prev_State { + 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 + void bind_private_crtp(Object* object); +}; +} +#include "Render_Scene_2D.ipp" diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp new file mode 100644 index 0000000..bea4d9a --- /dev/null +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -0,0 +1,106 @@ +#pragma once +#include +#include +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 + void process(Object* object, Callback&& callback) + requires std::invocable; + /* CRTP 业务实现:按 Paint 图拓扑逆序派发事件。 */ + template + void dispatch_event(Object* object, const Event& event); + template + [[nodiscard]] static const Dispatch& dispatch_for(); +}; +template +void Render_Scene_2D::Private::process(Object* object, Callback&& callback) + requires std::invocable { + const auto& state = static_cast(*static_cast(*this).state.current); + if (!state.view_active) { + const Render_Frame_Status status = Render_Frame_Status::view_inactive; + std::invoke(std::forward(callback), status); + return; + } + if (state.viewport.empty()) { + const Render_Frame_Status status = Render_Frame_Status::empty_viewport; + std::invoke(std::forward(callback), status); + return; + } + object->template current_dependency_graph().for_each( + [](const Dependency_Graph::Node& node) { node.object->template mark_dirty(); }); + Prev_Private::process(object, [&](const Scene::Private::Result&) { + auto& frame = object->template pending_buffer(); + frame.ensure_size(state.viewport); + frame.clear(); + { + detail::Painter painter(frame, state.viewport); + painter.rect({0.0, 0.0, static_cast(state.viewport.width), + static_cast(state.viewport.height)}, + Pen{.style = Line_Style::none}, Brush{state.background, Brush_Style::solid}); + } + const auto graph = object->template current_dependency_graph(); + 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(context); + if (const auto* layer = dynamic_cast(&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), status); +} +template +void Render_Scene_2D::Private::dispatch_event(Object* object, const Event& event) { + std::vector order; + const auto graph = object->template current_dependency_graph(); + 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 +const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for() { + static const Dispatch value{ + [](Root* root) { + auto* object = static_cast(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(root); + static_cast(*object->d).dispatch_event(object, event); + }, + [](Root* root, bool active) { + static_cast(root)->template update_state<&State::view_active>(active); + }, + [](const Root* root) { + const auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + return private_data.buffer_storage.template get().pending->view(); + } + }; + return value; +} +template +void Render_Scene_2D::bind_private_crtp(Object* object) { + static_cast(*object->d).dispatch = &Private::dispatch_for(); +} +} diff --git a/render_2D/render_2D/scene/Scene.hpp b/render_2D/render_2D/scene/Scene.hpp new file mode 100644 index 0000000..6272345 --- /dev/null +++ b/render_2D/render_2D/scene/Scene.hpp @@ -0,0 +1,2 @@ +#pragma once +#include "Render_Scene_2D.hpp" diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp new file mode 100644 index 0000000..1337a7f --- /dev/null +++ b/render_2D/tests/Spectrum_Test.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +namespace { +using namespace aethera; +using namespace aethera::render_2d; +template +std::unique_ptr build_object(Args&&... args) { + typename Object::Builder builder(std::forward(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(view.data + static_cast(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; + using Power = Impl; + using Object = Impl; + auto frequency = build_object(); + auto power = build_object(); + auto spectrum = build_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; + using Power = Impl; + using Spectrum_Object = Impl; + using Scene_Object = Impl; + initialize_runtime(2); + auto frequency = build_object(); + auto power = build_object(); + auto spectrum = build_object(frequency.get(), power.get()); + auto scene = build_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([&](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())); +}