#include #include #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(); } template std::unique_ptr render_frame(Scene* scene) { static std::uint64_t sequence{1}; auto frame = std::make_unique(Frame_Identity{sequence++, 0}); scene->set_frame_callback([](Frame_2D*) {}); EXPECT_TRUE(scene->render(frame.get()).has_value()); return frame; } template void render_once(Scene* scene) { static_cast(render_frame(scene)); } } TEST(event_routing_rule, keeps_layered_targets_before_nearest_scored_targets) { std::vector targets{1, 2, 3, 4}; aethera::render_2d::detail::Nearest_Event_Target_Rule::apply(targets, [](const int target) -> std::optional { if (target == 2) return 5.0; if (target == 4) return 1.0; return std::nullopt; }); EXPECT_EQ(targets, (std::vector{1, 3, 4, 2})); } 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); 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; 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::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())); 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(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; using Scene_Object = Impl; initialize_runtime(2); auto axis = build_axis(); auto scene = 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}); scene->set<&Render_Scene_2D::Prop::viewport>(Size{120, 64}); ASSERT_TRUE((scene->edit_dependency_graph([&](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->position = {50.0, 0.0}; wheel->angle_delta_y = 120.0; scene->submit_stream(wheel); render_once(scene.get()); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.5, 9.5})); auto press = scene->make_event(Event_Type::pointer_press); press->position = {50.0, 0.0}; press->button = Mouse_Button::left; scene->submit_stream(press); auto move = scene->make_event(Event_Type::pointer_move); move->position = {60.0, 0.0}; scene->submit_stream(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; using Scene_Object = Impl; initialize_runtime(2); auto horizontal = build_axis(); auto vertical = build_axis(); auto scene = build_axis(); 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([&](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(); near_vertical->position = {22.0, 80.0}; near_vertical->angle_delta_y = 120.0; scene->submit_stream(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(); near_horizontal->position = {120.0, 178.0}; near_horizontal->angle_delta_y = 120.0; scene->submit_stream(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; 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->exchange_stream(); axis->accumulate_stream(100); axis->exchange_stream(); axis->accumulate_stream(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; auto axis = build_axis(); axis->advance(); axis->template take_dirty(); EXPECT_FALSE(axis->template dirty()); EXPECT_EQ(axis->append_time({1'000}), 0); EXPECT_TRUE(axis->template dirty()); axis->advance(); axis->template take_dirty(); EXPECT_EQ(axis->append_time({2'000}), 1); EXPECT_TRUE(axis->template dirty()); axis->advance(); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-98.5, 1.5})); }