fix(track-core): align RSMT switches to line end

Allow repeated speed-mileage time-segmented schemes to use either the original start-boundary timestamp form or the segment-end timestamp form emitted by older clients.

For RSMT schemes whose first time segment starts at zero, keep switching against the next segment boundary. For schemes whose first time is non-zero, treat each timestamp as the current segment's end time so payloads such as 120/240/360 are accepted and scheduled correctly.

Defer eligible time-segment changes until the track mileage reaches the configured line-length boundary after movement, matching the legacy app-track behavior that avoided mid-line speed pattern changes.

Add runtime tests covering line-end switching and non-zero first time segment acceptance.

Verification: cmake --build build && ctest --test-dir build --output-on-failure
This commit is contained in:
2026-05-27 13:12:17 +08:00
parent 7b0f6a523d
commit 5ed07253d8
2 changed files with 103 additions and 22 deletions
+67 -18
View File
@@ -160,6 +160,55 @@ bool strictly_sorted_by_time(const std::vector<T> &segments) {
return true; return true;
} }
[[nodiscard]]
bool rsmt_uses_segment_end_times(const std::vector<RepeatedSMSegment> &segments) {
return !segments.empty() && segments.front().time_since_start_s != 0;
}
[[nodiscard]]
std::uint16_t rsmt_next_switch_time_s(
const std::vector<RepeatedSMSegment> &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]] [[nodiscard]]
expected<DecodedScheme, TrackError> canonicalize_scheme(DecodedScheme scheme) { expected<DecodedScheme, TrackError> canonicalize_scheme(DecodedScheme scheme) {
if (!valid_acceleration_profile(scheme.acceleration_profile)) { if (!valid_acceleration_profile(scheme.acceleration_profile)) {
@@ -224,7 +273,7 @@ expected<DecodedScheme, TrackError> canonicalize_scheme(DecodedScheme scheme) {
return unexpected<TrackError>{TrackError::invalid_arg}; return unexpected<TrackError>{TrackError::invalid_arg};
} }
std::ranges::sort(*segments, {}, &RepeatedSMSegment::time_since_start_s); 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>{TrackError::invalid_arg}; return unexpected<TrackError>{TrackError::invalid_arg};
} }
for (auto &time_segment : *segments) { for (auto &time_segment : *segments) {
@@ -421,23 +470,12 @@ void tick_repeated_speed_mileage_time(const TrackConfig &config, SchemeTrackRunt
const auto &segments = std::get<std::vector<RepeatedSMSegment>>(runtime.scheme.segments); const auto &segments = std::get<std::vector<RepeatedSMSegment>>(runtime.scheme.segments);
state.elapsed_s += delta_s; state.elapsed_s += delta_s;
if (state.primary_segment_index + 1 < segments.size()) { const bool has_next_time_segment = state.primary_segment_index + 1 < segments.size();
const auto &next_time_segment = segments[state.primary_segment_index + 1]; const bool should_switch_time_segment =
if (state.elapsed_s >= static_cast<float>(next_time_segment.time_since_start_s)) { has_next_time_segment &&
const float line_length_m = config.line_length_m; state.elapsed_s >= static_cast<float>(rsmt_next_switch_time_s(segments, state.primary_segment_index));
const float err = line_length_m > 0.0F
? std::fmod(state.mileage_m, line_length_m) if (!has_next_time_segment && state.elapsed_s >= static_cast<float>(segments.back().time_since_start_s)) {
: 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<float>(segments.back().time_since_start_s)) {
state.is_running = false; state.is_running = false;
return; return;
} }
@@ -455,10 +493,21 @@ void tick_repeated_speed_mileage_time(const TrackConfig &config, SchemeTrackRunt
state.speed_m_s = current.speed_m_s; 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; const float distance_traveled = state.speed_m_s * delta_s;
state.mileage_m += distance_traveled; state.mileage_m += distance_traveled;
state.loop_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); const float loop_length = current_rsmt_loop_length(runtime);
if (state.loop_mileage_m >= loop_length && loop_length > 0.0F) { if (state.loop_mileage_m >= loop_length && loop_length > 0.0F) {
state.loop_mileage_m -= loop_length; state.loop_mileage_m -= loop_length;
+36 -4
View File
@@ -524,11 +524,42 @@ void test_repeated_speed_mileage_time_runtime_ticks() {
require(runtime.has_value(), "RSMT runtime builds"); require(runtime.has_value(), "RSMT runtime builds");
auto track = track_core::start_scheme_track(std::move(*runtime)); 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), 3.0F);
track = track_core::tick_scheme_track(runtime_config(), std::move(track), 2.0F); require_eq_u32(static_cast<std::uint32_t>(track.state.primary_segment_index), 0, "RSMT waits before line end");
require_eq_u32(static_cast<std::uint32_t>(track.state.primary_segment_index), 0, "RSMT waits for line alignment");
track = track_core::tick_scheme_track(runtime_config(), std::move(track), 1.0F); track = track_core::tick_scheme_track(runtime_config(), std::move(track), 2.0F);
require_eq_u32(static_cast<std::uint32_t>(track.state.primary_segment_index), 1, "RSMT switches on alignment"); require_eq_u32(static_cast<std::uint32_t>(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<std::uint32_t>(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<std::uint32_t>(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"); 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_mileage_time_runtime_ticks();
test_speed_time_runtime_ticks(); test_speed_time_runtime_ticks();
test_repeated_speed_mileage_time_runtime_ticks(); test_repeated_speed_mileage_time_runtime_ticks();
test_repeated_speed_mileage_time_accepts_segment_end_times();
test_scheme_training_runtime_renders(); test_scheme_training_runtime_renders();
std::cout << "track-core tests passed\n"; std::cout << "track-core tests passed\n";
return 0; return 0;