diff --git a/src/scheme_runtime.cpp b/src/scheme_runtime.cpp index 6885c59..dc3e6b3 100644 --- a/src/scheme_runtime.cpp +++ b/src/scheme_runtime.cpp @@ -160,6 +160,55 @@ bool strictly_sorted_by_time(const std::vector &segments) { return true; } +[[nodiscard]] +bool rsmt_uses_segment_end_times(const std::vector &segments) { + return !segments.empty() && segments.front().time_since_start_s != 0; +} + +[[nodiscard]] +std::uint16_t rsmt_next_switch_time_s( + const std::vector &segments, + std::size_t current_index) { + if (rsmt_uses_segment_end_times(segments)) { + return segments[current_index].time_since_start_s; + } + return segments[current_index + 1].time_since_start_s; +} + +[[nodiscard]] +bool at_linear_track_end(float mileage_m, float line_length_m) { + if (!std::isfinite(line_length_m) || line_length_m <= 0.0F) { + return true; + } + if (!std::isfinite(mileage_m)) { + return false; + } + + const float epsilon = line_length_m * 0.01F; + if (mileage_m + epsilon < line_length_m) { + return false; + } + + const float phase_raw_m = std::fmod(mileage_m, line_length_m); + const float phase_m = phase_raw_m < 0.0F ? phase_raw_m + line_length_m : phase_raw_m; + return phase_m <= epsilon; +} + +[[nodiscard]] +bool crossed_linear_track_end(float previous_mileage_m, float mileage_m, float line_length_m) { + if (!std::isfinite(line_length_m) || line_length_m <= 0.0F) { + return true; + } + if (!std::isfinite(previous_mileage_m) || !std::isfinite(mileage_m) || + mileage_m < previous_mileage_m) { + return false; + } + + return at_linear_track_end(previous_mileage_m, line_length_m) || + std::floor(mileage_m / line_length_m) > + std::floor(previous_mileage_m / line_length_m); +} + [[nodiscard]] expected canonicalize_scheme(DecodedScheme scheme) { if (!valid_acceleration_profile(scheme.acceleration_profile)) { @@ -224,7 +273,7 @@ expected canonicalize_scheme(DecodedScheme scheme) { return unexpected{TrackError::invalid_arg}; } std::ranges::sort(*segments, {}, &RepeatedSMSegment::time_since_start_s); - if (segments->front().time_since_start_s != 0) { + if (!strictly_sorted_by_time(*segments)) { return unexpected{TrackError::invalid_arg}; } for (auto &time_segment : *segments) { @@ -421,23 +470,12 @@ void tick_repeated_speed_mileage_time(const TrackConfig &config, SchemeTrackRunt const auto &segments = std::get>(runtime.scheme.segments); state.elapsed_s += delta_s; - if (state.primary_segment_index + 1 < segments.size()) { - const auto &next_time_segment = segments[state.primary_segment_index + 1]; - if (state.elapsed_s >= static_cast(next_time_segment.time_since_start_s)) { - const float line_length_m = config.line_length_m; - const float err = line_length_m > 0.0F - ? std::fmod(state.mileage_m, line_length_m) - : 0.0F; - const float epsilon = line_length_m * 0.01F; - if (std::abs(err) <= epsilon) { - ++state.primary_segment_index; - state.sub_segment_index = 0; - state.loop_mileage_m = 0.0F; - state.speed_m_s = - segments[state.primary_segment_index].speed_mileage_segments[0].speed_m_s; - } - } - } else if (state.elapsed_s >= static_cast(segments.back().time_since_start_s)) { + const bool has_next_time_segment = state.primary_segment_index + 1 < segments.size(); + const bool should_switch_time_segment = + has_next_time_segment && + state.elapsed_s >= static_cast(rsmt_next_switch_time_s(segments, state.primary_segment_index)); + + if (!has_next_time_segment && state.elapsed_s >= static_cast(segments.back().time_since_start_s)) { state.is_running = false; return; } @@ -455,10 +493,21 @@ void tick_repeated_speed_mileage_time(const TrackConfig &config, SchemeTrackRunt state.speed_m_s = current.speed_m_s; } + const float previous_mileage_m = state.mileage_m; const float distance_traveled = state.speed_m_s * delta_s; state.mileage_m += distance_traveled; state.loop_mileage_m += distance_traveled; + if (should_switch_time_segment && + crossed_linear_track_end(previous_mileage_m, state.mileage_m, config.line_length_m)) { + ++state.primary_segment_index; + state.sub_segment_index = 0; + state.loop_mileage_m = 0.0F; + state.speed_m_s = + segments[state.primary_segment_index].speed_mileage_segments[0].speed_m_s; + return; + } + const float loop_length = current_rsmt_loop_length(runtime); if (state.loop_mileage_m >= loop_length && loop_length > 0.0F) { state.loop_mileage_m -= loop_length; diff --git a/tests/track_core_tests.cpp b/tests/track_core_tests.cpp index f4ed912..4057fb2 100644 --- a/tests/track_core_tests.cpp +++ b/tests/track_core_tests.cpp @@ -524,11 +524,42 @@ void test_repeated_speed_mileage_time_runtime_ticks() { require(runtime.has_value(), "RSMT runtime builds"); auto track = track_core::start_scheme_track(std::move(*runtime)); track = track_core::tick_scheme_track(runtime_config(), std::move(track), 3.0F); - track = track_core::tick_scheme_track(runtime_config(), std::move(track), 2.0F); - require_eq_u32(static_cast(track.state.primary_segment_index), 0, "RSMT waits for line alignment"); + require_eq_u32(static_cast(track.state.primary_segment_index), 0, "RSMT waits before line end"); - track = track_core::tick_scheme_track(runtime_config(), std::move(track), 1.0F); - require_eq_u32(static_cast(track.state.primary_segment_index), 1, "RSMT switches on alignment"); + track = track_core::tick_scheme_track(runtime_config(), std::move(track), 2.0F); + require_eq_u32(static_cast(track.state.primary_segment_index), 1, "RSMT switches at line end"); + require_near(track.state.speed_m_s, 3.0F, 0.001F, "RSMT switched speed"); +} + +void test_repeated_speed_mileage_time_accepts_segment_end_times() { + auto runtime = track_core::make_scheme_track_runtime(track_core::make_repeated_speed_mileage_time_scheme( + 16, + track_core::Color::white(), + track_core::AccelerationProfile::instant, + { + track_core::RepeatedSMSegment{ + .speed_mileage_segments = { + track_core::SMSegment{.speed_m_s = 1.0F, .mileage_from_start_m = 0}, + track_core::SMSegment{.speed_m_s = 2.0F, .mileage_from_start_m = 5}, + }, + .time_since_start_s = 4, + }, + track_core::RepeatedSMSegment{ + .speed_mileage_segments = { + track_core::SMSegment{.speed_m_s = 3.0F, .mileage_from_start_m = 0}, + track_core::SMSegment{.speed_m_s = 1.0F, .mileage_from_start_m = 5}, + }, + .time_since_start_s = 20, + }, + })); + + require(runtime.has_value(), "RSMT segment-end-time runtime builds"); + auto track = track_core::start_scheme_track(std::move(*runtime)); + track = track_core::tick_scheme_track(runtime_config(), std::move(track), 3.0F); + require_eq_u32(static_cast(track.state.primary_segment_index), 0, "RSMT segment-end-time waits before boundary"); + + track = track_core::tick_scheme_track(runtime_config(), std::move(track), 2.0F); + require_eq_u32(static_cast(track.state.primary_segment_index), 1, "RSMT segment-end-time switches at line end"); require_near(track.state.speed_m_s, 3.0F, 0.001F, "RSMT switched speed"); } @@ -581,6 +612,7 @@ int main() { test_mileage_time_runtime_ticks(); test_speed_time_runtime_ticks(); test_repeated_speed_mileage_time_runtime_ticks(); + test_repeated_speed_mileage_time_accepts_segment_end_times(); test_scheme_training_runtime_renders(); std::cout << "track-core tests passed\n"; return 0;