feat(emulator): add editable training schema panel

Redesign the DearPyGui emulator into a two-column layout with an interactive training schema editor and runtime preview.

Add JSON load/save support for ST, SM, MT, and RSMT schemes, including draft normalization, conversion to core schemes, and explicit Apply validation so intermediate edits do not restart or block the preview.

Update manual mode to clamp negative mileage, remove speed from manual positioning, preserve stopped runtime state when reloading schemas, and keep linear rendering visible with span-based drawing.

Fix the core linear pingpong render plan so reverse travel remains visible near endpoints and prefix/postfix colors stay tied to physical sides instead of reversing with heading.

Add C++ and Python regressions for schema conversion, emulator edit behavior, manual mode, linear endpoint visibility, and fixed prefix/postfix colors.
This commit is contained in:
2026-05-18 17:23:00 +08:00
parent 1005e50be0
commit 7b0f6a523d
5 changed files with 1178 additions and 158 deletions
+77 -12
View File
@@ -117,18 +117,81 @@ void test_linear_render_reverse() {
const auto plan = track_core::make_track_render_plan(config, running_info(track_core::Color::red()), report);
require_eq_u32(static_cast<std::uint32_t>(plan.span_count), 4, "reverse span count");
require_eq_u32(plan.spans[0].start_led, 5, "reverse span 0 start");
require_eq_u32(plan.spans[0].led_count, 2, "reverse span 0 count");
require(plan.spans[0].color == track_core::Color::blue(), "reverse span 0 color");
require_eq_u32(plan.spans[1].start_led, 7, "reverse span 1 start");
require_eq_u32(plan.spans[1].led_count, 3, "reverse span 1 count");
require(plan.spans[1].color == track_core::Color::red(), "reverse span 1 color");
require_eq_u32(plan.spans[2].start_led, 10, "reverse span 2 start");
require_eq_u32(plan.spans[2].led_count, 3, "reverse span 2 count");
require(plan.spans[2].color == track_core::Color::red(), "reverse span 2 color");
require_eq_u32(plan.spans[3].start_led, 13, "reverse span 3 start");
require_eq_u32(plan.spans[3].led_count, 2, "reverse span 3 count");
require(plan.spans[3].color == track_core::Color::cyan(), "reverse span 3 color");
require_eq_u32(plan.spans[0].start_led, 10, "reverse span 0 start");
require_eq_u32(plan.spans[0].led_count, 3, "reverse span 0 count");
require(plan.spans[0].color == track_core::Color::red(), "reverse span 0 color");
require_eq_u32(plan.spans[1].start_led, 13, "reverse span 1 start");
require_eq_u32(plan.spans[1].led_count, 2, "reverse span 1 count");
require(plan.spans[1].color == track_core::Color::blue(), "reverse span 1 color");
require_eq_u32(plan.spans[2].start_led, 5, "reverse span 2 start");
require_eq_u32(plan.spans[2].led_count, 2, "reverse span 2 count");
require(plan.spans[2].color == track_core::Color::cyan(), "reverse span 2 color");
require_eq_u32(plan.spans[3].start_led, 7, "reverse span 3 start");
require_eq_u32(plan.spans[3].led_count, 3, "reverse span 3 count");
require(plan.spans[3].color == track_core::Color::red(), "reverse span 3 color");
}
void test_linear_render_prefix_postfix_colors_do_not_reverse() {
track_core::TrackConfig config{
.draw_kind = track_core::TrackDrawKind::linear,
.line_length_m = 10.0F,
.active_line_length_m = 5.0F,
.head_offset_m = 0.0F,
.line_leds_num = 20,
};
const track_core::TrackReport forward_report{
.id = 3,
.state = track_core::TrackState::run,
.mileage_m = 5.0F,
.speed_m_s = 2.0F,
};
const track_core::TrackReport reverse_report{
.id = 3,
.state = track_core::TrackState::run,
.mileage_m = 15.0F,
.speed_m_s = 2.0F,
};
const auto forward_plan = track_core::make_track_render_plan(config, running_info(track_core::Color::red()), forward_report);
const auto reverse_plan = track_core::make_track_render_plan(config, running_info(track_core::Color::red()), reverse_report);
require_eq_u32(static_cast<std::uint32_t>(forward_plan.span_count), static_cast<std::uint32_t>(reverse_plan.span_count), "linear same-position span count");
for (std::size_t i = 0; i < forward_plan.span_count; ++i) {
require_eq_u32(forward_plan.spans[i].start_led, reverse_plan.spans[i].start_led, "linear same-position span start");
require_eq_u32(forward_plan.spans[i].led_count, reverse_plan.spans[i].led_count, "linear same-position span count");
require(forward_plan.spans[i].color == reverse_plan.spans[i].color, "linear same-position span color");
}
}
void test_linear_render_reverse_near_ends_stays_visible() {
track_core::TrackConfig config{
.draw_kind = track_core::TrackDrawKind::linear,
.line_length_m = 10.0F,
.active_line_length_m = 4.0F,
.head_offset_m = 0.0F,
.line_leds_num = 20,
};
const auto info = running_info(track_core::Color::red());
const auto require_visible = [&](float mileage_m, const char *message) {
const track_core::TrackReport report{
.id = 4,
.state = track_core::TrackState::run,
.mileage_m = mileage_m,
.speed_m_s = 2.0F,
};
const auto plan = track_core::make_track_render_plan(config, info, report);
std::uint32_t total_leds = 0;
for (std::size_t i = 0; i < plan.span_count; ++i) {
total_leds += plan.spans[i].led_count;
}
require(total_leds > 0, message);
};
require_visible(10.1F, "reverse render visible just after right-end bounce");
require_visible(19.9F, "reverse render visible just before left-end return");
}
void test_linear_render_boundaries_apply() {
@@ -505,6 +568,8 @@ int main() {
test_circular_render_wraps();
test_linear_render_forward_and_memory_strip();
test_linear_render_reverse();
test_linear_render_prefix_postfix_colors_do_not_reverse();
test_linear_render_reverse_near_ends_stays_visible();
test_linear_render_boundaries_apply();
test_memory_strip_bounds();
test_scheme_decoder();