Files
Aethera/render_2D/tests/Axis_Test.cpp
T
2026-08-24 13:12:57 +08:00

231 lines
10 KiB
C++

#include <render_2D/axis/Axis.hpp>
#include <render_2D/base/Frame_2D.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 {
using namespace aethera;
using namespace aethera::render_2d;
template <typename Object>
std::unique_ptr<Object> build_axis() {
auto result = typename Object::Builder{}.build();
if (!result) std::terminate();
return std::move(result).value();
}
template <typename Scene>
std::unique_ptr<Frame_2D> render_frame(Scene* scene) {
static std::uint64_t sequence{1};
auto frame = std::make_unique<Frame_2D>(Frame_Identity{sequence++, 0});
scene->set_frame_callback([](Frame_2D*) {});
EXPECT_EQ(scene->render(frame.get()), Render_Scene_2D::Render_Result::completed);
return frame;
}
template <typename Scene>
void render_once(Scene* scene) { static_cast<void>(render_frame(scene)); }
}
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>();
axis->set<&Abs_Axis::Prop::position>(Point_F{10.0, 0.0});
axis->set<&Abs_Axis::Prop::pixel_length>(200.0);
axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 20.0});
axis->advance();
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 20.0}));
EXPECT_DOUBLE_EQ(axis->coordinate_to_pixel(5.0), 60.0);
EXPECT_DOUBLE_EQ(axis->pixel_to_coordinate(60.0), 5.0);
EXPECT_EQ(axis->pixel_sample_count({0.0, 20.0}), 201);
EXPECT_DOUBLE_EQ(axis->tick_step({0.0, 20.0}), 5.0);
EXPECT_EQ(axis->tick_label(2.5), "2.5");
EXPECT_EQ(axis->sub_tick_count(5.0), 4);
axis->set<&Abs_Axis::Prop::pixel_length>(-200.0);
axis->advance();
EXPECT_DOUBLE_EQ(axis->coordinate_to_pixel(5.0), -40.0);
EXPECT_DOUBLE_EQ(axis->pixel_to_coordinate(-40.0), 5.0);
}
TEST(axis_dispatch, point_conversion_uses_current_orientation) {
using Object = Impl<Numeric_Axis>;
auto axis = build_axis<Object>();
axis->set<&Abs_Axis::Prop::position>(Point_F{0.0, 20.0});
axis->set<&Abs_Axis::Prop::pixel_length>(100.0);
axis->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical);
axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
axis->advance();
EXPECT_DOUBLE_EQ(axis->point_to_coordinate({90.0, 70.0}), 5.0);
}
TEST(render_cache, copy_owns_independent_pixels) {
Blend2D_Cache original;
{
aethera::render_2d::detail::Painter painter(original, {32, 16});
painter.line({1.0, 1.0}, {30.0, 1.0}, Pen{Color::red_color(), 2.0});
}
Blend2D_Cache copy = original;
original.clear();
const Image_View view = copy.view();
ASSERT_FALSE(view.empty());
bool contains_color{};
for (int y = 0; y < view.height && !contains_color; ++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) contains_color = contains_color || row[x] != 0;
}
EXPECT_TRUE(contains_color);
}
TEST(axis_render, scene_prepares_and_paints_uncached_axis_into_frame) {
using Object = Impl<Numeric_Axis>;
using Scene_Object = Impl<Render_Scene_2D>;
initialize_runtime(2);
auto axis = build_axis<Object>();
auto scene = build_axis<Scene_Object>();
axis->set<&Abs_Axis::Prop::position>(Point_F{8.0, 8.0});
axis->set<&Abs_Axis::Prop::pixel_length>(120.0);
scene->set<&Render_Scene_2D::Prop::viewport>(Size{160, 64});
scene->activate_view();
ASSERT_TRUE((scene->edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) {
prepare.add(axis.get());
paint.add(axis.get());
}).has_value()));
int paint_extension_calls{};
int completion_calls{};
axis->paint_taskflow().emplace(
[&] { ++paint_extension_calls; }).name("test.axis.paint.extension");
scene->completion_taskflow().emplace([&] {
EXPECT_EQ(paint_extension_calls, 1);
++completion_calls;
}).name("test.scene_2d.completion");
const auto frame = render_frame(scene.get());
EXPECT_EQ(paint_extension_calls, 1);
EXPECT_EQ(completion_calls, 1);
const Image_View view = frame->image();
ASSERT_FALSE(view.empty());
bool contains_color{};
for (int y = 0; y < view.height && !contains_color; ++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) contains_color = contains_color || row[x] != 0;
}
EXPECT_TRUE(contains_color);
}
TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) {
using Object = Impl<Numeric_Axis>;
using Scene_Object = Impl<Render_Scene_2D>;
initialize_runtime(2);
auto axis = build_axis<Object>();
auto scene = build_axis<Scene_Object>();
axis->set<&Abs_Axis::Prop::position>(Point_F{0.0, 0.0});
axis->set<&Abs_Axis::Prop::pixel_length>(100.0);
axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
scene->set<&Render_Scene_2D::Prop::viewport>(Size{120, 64});
ASSERT_TRUE((scene->edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) {
prepare.add(axis.get()); paint.add(axis.get());
}).has_value()));
scene->activate_view();
render_once(scene.get());
auto wheel = scene->make_event<Wheel_Event>();
wheel->position = {50.0, 0.0};
wheel->angle_delta_y = 120.0;
scene->submit_stream<Scene_Event_Stream_Tag>(wheel);
render_once(scene.get());
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.5, 9.5}));
auto press = scene->make_event<Pointer_Event>(Event_Type::pointer_press);
press->position = {50.0, 0.0};
press->button = Mouse_Button::left;
scene->submit_stream<Scene_Event_Stream_Tag>(press);
auto move = scene->make_event<Pointer_Event>(Event_Type::pointer_move);
move->position = {60.0, 0.0};
scene->submit_stream<Scene_Event_Stream_Tag>(move);
render_once(scene.get());
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::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::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();
render_once(scene.get());
auto near_vertical = scene->make_event<Wheel_Event>();
near_vertical->position = {22.0, 80.0};
near_vertical->angle_delta_y = 120.0;
scene->submit_stream<Scene_Event_Stream_Tag>(near_vertical);
render_once(scene.get());
EXPECT_DOUBLE_EQ(horizontal->coordinate_range().size(), 10.0);
EXPECT_DOUBLE_EQ(vertical->coordinate_range().size(), 9.0);
auto near_horizontal = scene->make_event<Wheel_Event>();
near_horizontal->position = {120.0, 178.0};
near_horizontal->angle_delta_y = 120.0;
scene->submit_stream<Scene_Event_Stream_Tag>(near_horizontal);
render_once(scene.get());
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>();
EXPECT_THROW((axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{1.0, 1.0})), std::invalid_argument);
axis->advance();
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 20.0}));
}
TEST(axis_label, frequency_axis_selects_engineering_unit) {
using Object = Impl<Frequency_Axis>;
auto axis = build_axis<Object>();
axis->advance();
EXPECT_EQ(axis->tick_label(250.0), "250 Hz");
EXPECT_EQ(axis->tick_label(2'500.0), "2.5 kHz");
EXPECT_EQ(axis->tick_label(2'500'000.0), "2.5 MHz");
}
TEST(axis_time, sample_state_drives_range_lookup_and_label_without_snapshot) {
using Object = Impl<Time_Axis>;
auto axis = build_axis<Object>();
EXPECT_EQ(axis->append_time({3'723'004}), 0);
EXPECT_EQ(axis->time_point_count(), 0u);
axis->exchange_stream<Time_Axis_Stream_Tag>();
axis->accumulate_stream<Time_Axis_Stream_Tag>(100);
axis->exchange_stream<Time_Axis_Stream_Tag>();
axis->accumulate_stream<Time_Axis_Stream_Tag>(100);
axis->advance();
EXPECT_EQ(axis->time_point_count(), 1u);
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-99.5, 0.5}));
EXPECT_EQ(axis->tick_to_time(0), (Time_Of_Day{3'723'004}));
EXPECT_EQ(axis->tick_label(0.0), "02:03.004");
EXPECT_FALSE(axis->tick_to_time(99).valid());
}
TEST(axis_time, appending_each_time_sample_invalidates_tick_preparation) {
using Object = Impl<Time_Axis>;
auto axis = build_axis<Object>();
axis->advance();
axis->template take_dirty<Prepare_Data_Tag>();
EXPECT_FALSE(axis->template dirty<Prepare_Data_Tag>());
EXPECT_EQ(axis->append_time({1'000}), 0);
EXPECT_TRUE(axis->template dirty<Prepare_Data_Tag>());
axis->advance();
axis->template take_dirty<Prepare_Data_Tag>();
EXPECT_EQ(axis->append_time({2'000}), 1);
EXPECT_TRUE(axis->template dirty<Prepare_Data_Tag>());
axis->advance();
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-98.5, 1.5}));
}