#include #include #include #include namespace { using namespace aethera; using namespace aethera::render_2d; template std::unique_ptr build_axis() { auto result = typename Object::Builder{}.build(); if (!result) std::terminate(); return std::move(result).value(); } } TEST(axis_dispatch, numeric_axis_public_shell_reads_final_private_state) { using Object = Impl; auto axis = build_axis(); 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); } TEST(axis_dispatch, point_conversion_uses_current_orientation) { using Object = Impl; auto axis = build_axis(); 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(view.data + static_cast(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; using Scene_Object = Impl; initialize_runtime(2); auto axis = build_axis(); auto scene = build_axis(); axis->set<&Abs_Axis::Prop::position>(Point_F{8.0, 8.0}); axis->set<&Abs_Axis::Prop::canvas_size>(Size{160, 64}); 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([&](auto& prepare, auto& paint) { prepare.add(axis.get()); paint.add(axis.get()); }).has_value())); scene->render(); const Image_View view = scene->frame_view(); ASSERT_FALSE(view.empty()); bool contains_color{}; for (int y = 0; y < view.height && !contains_color; ++y) { const auto* row = reinterpret_cast(view.data + static_cast(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; auto axis = build_axis(); 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}); axis->advance(); Wheel_Event wheel; wheel.position = {50.0, 0.0}; wheel.angle_delta_y = 120.0; axis->dispatch_event(wheel); EXPECT_TRUE(wheel.is_accepted()); axis->advance(); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.5, 9.5})); Pointer_Event press(Event_Type::pointer_press); press.position = {50.0, 0.0}; press.button = Mouse_Button::left; axis->dispatch_event(press); Pointer_Event move(Event_Type::pointer_move); move.position = {60.0, 0.0}; axis->dispatch_event(move); EXPECT_TRUE(move.is_accepted()); axis->advance(); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-0.4, 8.6})); } TEST(axis_state, invalid_numeric_range_is_rejected_without_poisoning_pending_state) { using Object = Impl; auto axis = build_axis(); 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; auto axis = build_axis(); 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; auto axis = build_axis(); EXPECT_EQ(axis->append_time({3'723'004}), 0); EXPECT_EQ(axis->time_point_count(), 0u); axis->advance(); EXPECT_EQ(axis->time_point_count(), 1u); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 1.0})); 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()); }