功能比较完善的一版

This commit is contained in:
2026-08-21 17:06:38 +08:00
parent b280e2f5ee
commit ade34e18f2
77 changed files with 30093 additions and 624 deletions
+23
View File
@@ -154,6 +154,29 @@ template <Axis_Object Object>
void Abs_Axis::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
dispatch = &Private::dispatch_for<Object>();
this->event_routing_distance_run = [](const Root* root, const Event& event) -> std::optional<double> {
if (event.type != Event_Type::wheel && event.type != Event_Type::pointer_press
&& event.type != Event_Type::pointer_move && event.type != Event_Type::pointer_release) return std::nullopt;
const auto* pointer = dynamic_cast<const Pointer_Event_Capability*>(&event);
if (!pointer) return std::nullopt;
const auto* axis = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*axis->d);
const auto& layout = static_cast<const Prop&>(*private_data.current);
const Point_F first = layout.position;
const Point_F second = layout.orientation == Axis_Orientation::horizontal
? Point_F{first.x + layout.pixel_length, first.y}
: Point_F{first.x, first.y + layout.pixel_length};
const double segment_x = second.x - first.x;
const double segment_y = second.y - first.y;
const double length_squared = segment_x * segment_x + segment_y * segment_y;
const double projection = length_squared > 0.0
? std::clamp(((pointer->position_x() - first.x) * segment_x
+ (pointer->position_y() - first.y) * segment_y) / length_squared, 0.0, 1.0)
: 0.0;
const double nearest_x = first.x + segment_x * projection;
const double nearest_y = first.y + segment_y * projection;
return std::hypot(pointer->position_x() - nearest_x, pointer->position_y() - nearest_y);
};
}
inline void Abs_Axis::Private::prepare_data(Attached auto* object) {
using Object = std::remove_pointer_t<decltype(object)>;
@@ -0,0 +1,20 @@
#pragma once
#include <algorithm>
#include <optional>
#include <vector>
namespace aethera::render_2d::detail {
/* 保留无评分接收者的绘制层级顺序,再将有评分的候选者按距离从近到远排列。 */
struct Nearest_Event_Target_Rule {
template <typename Target, typename Score>
static void apply(std::vector<Target>& targets, Score&& score) {
std::stable_sort(targets.begin(), targets.end(), [&](const Target& left, const Target& right) {
const std::optional<double> left_score = score(left);
const std::optional<double> right_score = score(right);
if (!left_score) return right_score.has_value();
if (!right_score) return false;
return *left_score < *right_score;
});
}
};
}
@@ -1,5 +1,6 @@
#pragma once
#include "common/Curve_Plot.hpp"
#include <cmath>
#include <sstream>
namespace aethera::render_2d {
struct Selection_Rectangle_Overlay::Private : Prev_Private {
@@ -62,10 +63,18 @@ void Selection_Rectangle_Overlay::Private::handle_event(Object* object, const Ev
const auto* pointer = dynamic_cast<const Pointer_Event_Capability*>(&event);
if (!pointer) return;
const Point_F point{pointer->position_x(), pointer->position_y()};
if (event.type == Event_Type::pointer_press && pointer->pointer_button() == Mouse_Button::left) { dragging = true; drag_origin = point; drag_current = point; object->template mark_dirty<Paint_Tag>(); event.accept(); return; }
if (event.type == Event_Type::pointer_press && pointer->pointer_button() == Mouse_Button::left) {
const auto modifiers = static_cast<std::underlying_type_t<Keyboard_Modifier>>(pointer->keyboard_modifiers());
const auto control = static_cast<std::underlying_type_t<Keyboard_Modifier>>(Keyboard_Modifier::control);
if ((modifiers & control) == 0) object->template set<&Prop::selected_regions>(std::vector<Rect_F>{});
dragging = true; drag_origin = point; drag_current = point; object->template mark_dirty<Paint_Tag>(); event.accept(); return;
}
if (event.type == Event_Type::pointer_move && dragging) { drag_current = point; object->template mark_dirty<Paint_Tag>(); event.accept(); return; }
if (event.type != Event_Type::pointer_release || !dragging) return;
dragging = false; drag_current = point;
if (std::hypot(drag_current.x - drag_origin.x, drag_current.y - drag_origin.y) < 3.0) {
object->template mark_dirty<Paint_Tag>(); event.accept(); return;
}
const Axis_Coordinate first_x = horizontal_axis->point_to_coordinate(drag_origin);
const Axis_Coordinate second_x = horizontal_axis->point_to_coordinate(drag_current);
const Axis_Coordinate first_y = vertical_axis->point_to_coordinate(drag_origin);
@@ -1,11 +1,13 @@
#pragma once
#include "common/Curve_Plot.hpp"
#include <algorithm>
#include <iterator>
namespace aethera::render_2d {
struct Sweep_Spectrum::Private : Prev_Private {
struct Prepared {
std::vector<detail::Curve_Prepared> partitions{}; /* Prepare 子图各分块的曲线输出。 */
std::vector<Plot_Value> values{}; /* 已提交扫描块拼接后的连续功率值。 */
Axis_Range domain{}; /* 当前已到达数据对应的频率区间。 */
Point_F marker_first{}; /* 当前扫描位置线的首端点。 */
Point_F marker_second{}; /* 当前扫描位置线的末端点。 */
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
@@ -58,11 +60,19 @@ template <Attached Object>
void Sweep_Spectrum::Private::prepare_frame(Object* object) {
const auto& state = object->template read_prop<Sweep_Spectrum::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>(); const auto& power_state = power_axis->template read_prop<Numeric_Axis::Base_Tag>(); prepared = {}; prepared.partitions.resize(graph_partition_count); prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation) return; for (const auto& block : state.blocks) prepared.values.insert(prepared.values.end(), block.begin(), block.end()); if (prepared.values.empty()) return;
prepared.marker_first = detail::map_plot_point(frequency_axis, state.frequency_range.target, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation); prepared.marker_second = detail::map_plot_point(frequency_axis, state.frequency_range.target, power_axis, power_state.coordinate_range.target, frequency_layout.orientation); prepared.valid = true;
const std::size_t bins_per_block = state.bins_per_block > 0
? state.bins_per_block : state.blocks.front().size();
const std::size_t expected_points = std::max<std::size_t>(1, bins_per_block * std::max<std::size_t>(1, state.block_count));
const double progress = expected_points > 1
? std::clamp(static_cast<double>(prepared.values.size() - 1) / static_cast<double>(expected_points - 1), 0.0, 1.0)
: 1.0;
const auto latest_frequency = state.frequency_range.origin + state.frequency_range.length() * progress;
prepared.domain = {state.frequency_range.origin, latest_frequency};
prepared.marker_first = detail::map_plot_point(frequency_axis, latest_frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation); prepared.marker_second = detail::map_plot_point(frequency_axis, latest_frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation); prepared.valid = true;
}
template <Attached Object>
void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_Count index) {
if (!prepared.valid) return; const auto& state = object->template read_prop<Sweep_Spectrum::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>(); const auto& frequency_state = frequency_axis->template read_prop<Numeric_Axis::Base_Tag>(); const auto& power_state = power_axis->template read_prop<Numeric_Axis::Base_Tag>(); const auto range = detail::curve_partition_range(prepared.values.size(), index, prepared.partitions.size(), state.frequency_range);
if (!prepared.valid) return; const auto& state = object->template read_prop<Sweep_Spectrum::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>(); const auto& frequency_state = frequency_axis->template read_prop<Numeric_Axis::Base_Tag>(); const auto& power_state = power_axis->template read_prop<Numeric_Axis::Base_Tag>(); const auto range = detail::curve_partition_range(prepared.values.size(), index, prepared.partitions.size(), prepared.domain);
prepared.partitions[index] = detail::prepare_curve(std::span<const Plot_Value>(prepared.values).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>
@@ -1,4 +1,5 @@
#pragma once
#include "../event/Event_Routing_Rules.hpp"
#include <stdexcept>
#include <unordered_map>
#include <unordered_set>
@@ -277,8 +278,13 @@ void Render_Scene_2D::Private::dispatch_event(Object* object, const Event& event
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);
std::vector<Renderable*> delivery_order(order.rbegin(), order.rend());
detail::Nearest_Event_Target_Rule::apply(delivery_order,
[&](const Renderable* target) { return target->event_routing_distance(event); });
for (auto* target : delivery_order) {
if (event.is_accepted()) break;
target->dispatch_event(event);
}
}
template <Attached Object>
const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for() {
+50
View File
@@ -1,5 +1,6 @@
#include <render_2D/axis/Axis.hpp>
#include <render_2D/event/Event.hpp>
#include <render_2D/event/Event_Routing_Rules.hpp>
#include <render_2D/scene/Render_Scene_2D.hpp>
#include <gtest/gtest.h>
namespace {
@@ -12,6 +13,15 @@ std::unique_ptr<Object> build_axis() {
return std::move(result).value();
}
}
TEST(event_routing_rule, keeps_layered_targets_before_nearest_scored_targets) {
std::vector<int> targets{1, 2, 3, 4};
aethera::render_2d::detail::Nearest_Event_Target_Rule::apply(targets, [](const int target) -> std::optional<double> {
if (target == 2) return 5.0;
if (target == 4) return 1.0;
return std::nullopt;
});
EXPECT_EQ(targets, (std::vector<int>{1, 3, 4, 2}));
}
TEST(axis_dispatch, numeric_axis_public_shell_reads_final_private_state) {
using Object = Impl<Numeric_Axis>;
auto axis = build_axis<Object>();
@@ -104,6 +114,46 @@ TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) {
axis->advance();
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-0.4, 8.6}));
}
TEST(axis_event, scene_routes_pointer_interaction_to_the_nearest_axis_segment) {
using Axis_Object = Impl<Numeric_Axis>;
using Scene_Object = Impl<Render_Scene_2D>;
initialize_runtime(2);
auto horizontal = build_axis<Axis_Object>();
auto vertical = build_axis<Axis_Object>();
auto scene = build_axis<Scene_Object>();
horizontal->set<&Abs_Axis::Prop::position>(Point_F{20.0, 180.0});
horizontal->set<&Abs_Axis::Prop::canvas_size>(Size{200, 200});
horizontal->set<&Abs_Axis::Prop::pixel_length>(160.0);
horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
vertical->set<&Abs_Axis::Prop::position>(Point_F{20.0, 180.0});
vertical->set<&Abs_Axis::Prop::canvas_size>(Size{200, 200});
vertical->set<&Abs_Axis::Prop::pixel_length>(-160.0);
vertical->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical);
vertical->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
scene->set<&Render_Scene_2D::Prop::viewport>(Size{200, 200});
ASSERT_TRUE((scene->edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) {
prepare.add(horizontal.get()); prepare.add(vertical.get());
paint.add(horizontal.get()); paint.add(vertical.get());
}).has_value()));
scene->activate_view();
scene->render();
Wheel_Event near_vertical;
near_vertical.position = {22.0, 80.0};
near_vertical.angle_delta_y = 120.0;
scene->dispatch_event(near_vertical);
scene->render();
EXPECT_DOUBLE_EQ(horizontal->coordinate_range().size(), 10.0);
EXPECT_DOUBLE_EQ(vertical->coordinate_range().size(), 9.0);
Wheel_Event near_horizontal;
near_horizontal.position = {120.0, 178.0};
near_horizontal.angle_delta_y = 120.0;
scene->dispatch_event(near_horizontal);
scene->render();
EXPECT_DOUBLE_EQ(horizontal->coordinate_range().size(), 9.0);
EXPECT_DOUBLE_EQ(vertical->coordinate_range().size(), 9.0);
}
TEST(axis_state, invalid_numeric_range_is_rejected_without_poisoning_pending_state) {
using Object = Impl<Numeric_Axis>;
auto axis = build_axis<Object>();
+61 -9
View File
@@ -15,6 +15,15 @@ std::unique_ptr<Object> build_object(Args&&... args) {
return std::move(result).value();
}
template <typename Scene, typename... Renderables>
std::unique_ptr<Scene> build_scene(Renderables*... renderables) {
typename Scene::Builder builder;
(builder.add_renderable(renderables), ...);
auto result = builder.build();
if (!result) std::terminate();
return std::move(result).value();
}
template <typename Axis>
void configure_axis(Axis* axis, Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Size canvas) {
axis->template set<&Abs_Axis::Prop::orientation>(orientation);
@@ -33,7 +42,6 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) {
using Sweep = Impl<Sweep_Spectrum>;
initialize_runtime(2);
const Size canvas{160, 120};
auto scene = build_object<Scene>();
auto frequency = build_object<Frequency>();
auto power = build_object<Numeric>();
auto time = build_object<Time>();
@@ -45,8 +53,8 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) {
frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0});
value->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
auto trace = build_object<Trace>(scene.get(), time.get(), value.get());
auto sweep = build_object<Sweep>(scene.get(), frequency.get(), power.get());
auto trace = build_object<Trace>(time.get(), value.get());
auto sweep = build_object<Sweep>(frequency.get(), power.get());
trace->set<&Frequency_Trace::Prop::partition_mode>(Plot_Partition_Mode::fixed);
trace->set<&Frequency_Trace::Prop::partition_count>(2u);
trace->append_sample(0, 10.0);
@@ -57,6 +65,7 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) {
sweep->set<&Sweep_Spectrum::Prop::partition_count>(2u);
const std::array<Plot_Value, 4> block{-90.0, -60.0, -30.0, -10.0};
sweep->append_block(block);
auto scene = build_scene<Scene>(trace.get(), sweep.get());
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view();
scene->render();
@@ -75,7 +84,6 @@ TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) {
using Fall = Impl<Waterfall>;
initialize_runtime(2);
const Size canvas{160, 120};
auto scene = build_object<Scene>();
auto frequency = build_object<Frequency>();
auto power = build_object<Numeric>();
auto time = build_object<Time>();
@@ -84,8 +92,8 @@ TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) {
configure_axis(time.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas);
frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0});
auto glow = build_object<Glow>(scene.get(), frequency.get(), power.get());
auto waterfall = build_object<Fall>(scene.get(), frequency.get(), time.get());
auto glow = build_object<Glow>(frequency.get(), power.get());
auto waterfall = build_object<Fall>(frequency.get(), time.get());
glow->set<&Afterglow::Prop::frequency_range>(Axis_Range{0.0, 100.0});
glow->set<&Afterglow::Prop::power_range>(Axis_Range{-100.0, 0.0});
glow->set<&Afterglow::Prop::power_point_size>(16u);
@@ -95,6 +103,7 @@ TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) {
glow->append_spectrum(row);
waterfall->append_row(0, row);
waterfall->append_row(1, row);
auto scene = build_scene<Scene>(glow.get(), waterfall.get());
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view();
scene->render();
@@ -111,18 +120,18 @@ TEST(plottable_migration, direct_overlay_and_constellation_build_and_render) {
using Overlay = Impl<Selection_Rectangle_Overlay>;
initialize_runtime(2);
const Size canvas{160, 120};
auto scene = build_object<Scene>();
auto horizontal = build_object<Numeric>();
auto vertical = build_object<Numeric>();
configure_axis(horizontal.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas);
configure_axis(vertical.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas);
horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.0, 1.0});
vertical->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.0, 1.0});
auto diagram = build_object<Diagram>(scene.get(), horizontal.get(), vertical.get());
auto overlay = build_object<Overlay>(scene.get(), horizontal.get(), vertical.get());
auto diagram = build_object<Diagram>(horizontal.get(), vertical.get());
auto overlay = build_object<Overlay>(horizontal.get(), vertical.get());
diagram->set<&Constellation_Diagram::Prop::i_range>(Axis_Range{-1.0, 1.0});
diagram->set<&Constellation_Diagram::Prop::q_range>(Axis_Range{-1.0, 1.0});
diagram->append_point({0.25, -0.25});
auto scene = build_scene<Scene>(diagram.get(), overlay.get());
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view();
scene->render();
@@ -130,3 +139,46 @@ TEST(plottable_migration, direct_overlay_and_constellation_build_and_render) {
EXPECT_EQ(overlay->read_state<Renderable::Base_Tag>().prepare_task_count, 0u);
EXPECT_EQ(overlay->read_state<Renderable::Base_Tag>().paint_task_count, 1u);
}
TEST(selection_overlay, control_extends_selection_and_plain_click_clears_it) {
using Scene = Impl<Render_Scene_2D>;
using Numeric = Impl<Numeric_Axis>;
using Overlay = Impl<Selection_Rectangle_Overlay>;
initialize_runtime(2);
const Size canvas{160, 120};
auto horizontal = build_object<Numeric>();
auto vertical = build_object<Numeric>();
configure_axis(horizontal.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas);
configure_axis(vertical.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas);
horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
vertical->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
auto overlay = build_object<Overlay>(horizontal.get(), vertical.get());
auto scene = build_scene<Scene>(overlay.get());
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view();
scene->render();
const auto drag = [&](Point_F first, Point_F second, Keyboard_Modifier modifiers) {
Pointer_Event press(Event_Type::pointer_press);
press.position = first;
press.button = Mouse_Button::left;
press.modifiers = modifiers;
overlay->dispatch_event(press);
Pointer_Event move(Event_Type::pointer_move);
move.position = second;
move.modifiers = modifiers;
overlay->dispatch_event(move);
Pointer_Event release(Event_Type::pointer_release);
release.position = second;
release.button = Mouse_Button::left;
release.modifiers = modifiers;
overlay->dispatch_event(release);
scene->render();
};
drag({30.0, 90.0}, {60.0, 60.0}, Keyboard_Modifier::none);
EXPECT_EQ(overlay->selected_regions().size(), 1u);
drag({70.0, 90.0}, {100.0, 60.0}, Keyboard_Modifier::control);
EXPECT_EQ(overlay->selected_regions().size(), 2u);
drag({120.0, 40.0}, {120.0, 40.0}, Keyboard_Modifier::none);
EXPECT_TRUE(overlay->selected_regions().empty());
}