Files
crosstyan 7b0f6a523d 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.
2026-05-18 17:23:00 +08:00

753 lines
28 KiB
Python

from __future__ import annotations
import math
import time
from pathlib import Path
from . import (
Color,
SchemeTrainingRuntime,
TrackConfig,
TrackDrawKind,
TrackInfo,
TrackReport,
TrackState,
render_pixels,
)
from .emulator_scheme import (
ACCELERATION_PROFILES,
SCHEME_KINDS,
clone_draft,
default_draft,
draft_to_scheme,
load_draft,
normalize_draft,
save_draft,
)
DRAWLIST_TAG = "track_core_drawlist"
EDITOR_TAG = "scheme_editor"
STATUS_TAG = "schema_status"
FILE_PATH_TAG = "schema_file_path"
LOAD_DIALOG_TAG = "schema_load_dialog"
SAVE_DIALOG_TAG = "schema_save_dialog"
def _load_dearpygui():
try:
import dearpygui.dearpygui as dpg
except ImportError as exc:
raise SystemExit(
"DearPyGui is not installed. Install the emulator extra with "
'`python -m pip install -e ".[emulator]"`.'
) from exc
return dpg
def _rgba(color: Color) -> tuple[int, int, int, int]:
return (color.r, color.g, color.b, 255)
def _color_from_draft(draft: dict) -> Color:
rgb = draft.get("color", [0, 255, 0])
return Color(int(rgb[0]), int(rgb[1]), int(rgb[2]))
def _config_from_ui(dpg) -> TrackConfig:
line_length = float(dpg.get_value("line_length"))
active_length = min(float(dpg.get_value("active_length")), max(0.1, line_length - 0.001))
config = TrackConfig.default_config()
config.draw_kind = (
TrackDrawKind.circular
if dpg.get_value("draw_kind") == "circular"
else TrackDrawKind.linear
)
config.line_length_m = line_length
config.active_line_length_m = active_length
config.head_offset_m = float(dpg.get_value("head_offset"))
config.line_leds_num = int(dpg.get_value("led_count"))
return config
def _toggle_draw_kind(dpg) -> None:
next_kind = "linear" if dpg.get_value("draw_kind") == "circular" else "circular"
dpg.set_value("draw_kind", next_kind)
def _manual_state_to_core(dpg, draft: dict | None = None) -> tuple[TrackConfig, TrackInfo, TrackReport]:
config = _config_from_ui(dpg)
color = _color_from_draft(draft or default_draft())
info = TrackInfo()
info.color = color
info.id = int((draft or {}).get("id", 1))
info.is_running = dpg.get_value("manual_running")
info.num_segments = 1
report = TrackReport()
report.id = info.id
report.state = TrackState.run if info.is_running else TrackState.stop
report.mileage_m = max(0.0, float(dpg.get_value("manual_mileage")))
report.speed_m_s = 0.0
report.time_elapsed_ms = 0
return config, info, report
def _draw_linear(dpg, pixels: list[Color]) -> None:
width = 560
x0 = 24
y0 = 174
gap = 1 if len(pixels) <= 240 else 0
cell_w = max(1, int((width - gap * max(0, len(pixels) - 1)) / max(1, len(pixels))))
cell_h = 34
pitch = cell_w + gap
dpg.draw_rectangle(
(x0, y0),
(x0 + width, y0 + cell_h),
color=(55, 58, 64, 255),
fill=(16, 18, 22, 255),
parent=DRAWLIST_TAG,
)
index = 0
while index < len(pixels):
color = pixels[index]
if color == Color.black():
index += 1
continue
start = index
while index < len(pixels) and pixels[index] == color:
index += 1
count = index - start
x = x0 + start * pitch
span_width = count * cell_w + max(0, count - 1) * gap
dpg.draw_rectangle(
(x, y0),
(x + span_width, y0 + cell_h),
color=_rgba(color),
fill=_rgba(color),
parent=DRAWLIST_TAG,
)
for index in range(len(pixels) + 1):
x = x0 + index * pitch - (gap if index == len(pixels) and gap else 0)
dpg.draw_line((x, y0), (x, y0 + cell_h), color=(70, 74, 84, 255), parent=DRAWLIST_TAG)
dpg.draw_rectangle(
(x0 - 1, y0 - 1),
(x0 + width + 1, y0 + cell_h + 1),
color=(90, 96, 108, 255),
parent=DRAWLIST_TAG,
)
dpg.draw_text((x0, y0 + 52), f"{len(pixels)} LEDs", color=(190, 195, 205, 255), parent=DRAWLIST_TAG)
def _draw_circular(dpg, pixels: list[Color]) -> None:
center = (304, 158)
radius = 122
led_radius = 5 if len(pixels) <= 160 else 3
dpg.draw_circle(center, radius, color=(80, 86, 98, 255), thickness=1, parent=DRAWLIST_TAG)
for index, color in enumerate(pixels):
angle = (2.0 * math.pi * index / max(1, len(pixels))) - (math.pi / 2.0)
pos = (
center[0] + math.cos(angle) * radius,
center[1] + math.sin(angle) * radius,
)
dpg.draw_circle(
pos,
led_radius,
color=(42, 45, 52, 255),
fill=_rgba(color),
parent=DRAWLIST_TAG,
)
dpg.draw_text((24, 300), f"{len(pixels)} LEDs", color=(190, 195, 205, 255), parent=DRAWLIST_TAG)
class TrackRuntimeApp:
def __init__(self) -> None:
self.runtime = SchemeTrainingRuntime()
self.draft = default_draft("ST")
self.file_path: Path | None = None
self.last_frame_s = time.perf_counter()
self.last_valid_draft = clone_draft(self.draft)
self.status = "Ready"
self.has_unapplied_edits = False
def load_scheme(self, dpg, run: bool | None = None) -> bool:
if run is None:
run = True if not self.runtime.has_program() else not self.runtime.all_stopped()
try:
normalized = normalize_draft(self.draft)
scheme = draft_to_scheme(normalized)
except Exception as exc:
self.set_status(dpg, f"Invalid scheme: {exc}", error=True)
return False
self.draft = normalized
self.runtime.clear()
self.runtime.add_scheme(scheme)
if run:
self.runtime.start()
else:
self.runtime.stop()
self.last_frame_s = time.perf_counter()
self.last_valid_draft = clone_draft(normalized)
self.has_unapplied_edits = False
self.set_status(dpg, "Loaded into runtime" if run else "Loaded into runtime (stopped)")
return True
def start(self) -> None:
self.runtime.start()
self.last_frame_s = time.perf_counter()
def stop(self) -> None:
self.runtime.stop()
def set_status(self, dpg, message: str, error: bool = False) -> None:
self.status = message
if dpg.does_item_exist(STATUS_TAG):
dpg.set_value(STATUS_TAG, message)
dpg.configure_item(STATUS_TAG, color=(255, 110, 90) if error else (170, 220, 170))
def refresh_file_path(self, dpg) -> None:
if dpg.does_item_exist(FILE_PATH_TAG):
dpg.set_value(FILE_PATH_TAG, str(self.file_path) if self.file_path else "")
def mark_dirty(self, dpg) -> None:
self.has_unapplied_edits = True
self.set_status(dpg, "Edited; click Apply to validate and reload")
def apply_editor_values(self, dpg) -> None:
self.draft["id"] = int(dpg.get_value("schema_id"))
self.draft["kind"] = dpg.get_value("schema_kind")
self.draft["acceleration_profile"] = dpg.get_value("schema_accel")
color = dpg.get_value("schema_color")
self.draft["color"] = [int(color[0]), int(color[1]), int(color[2])]
def apply_schema(self, dpg) -> bool:
self.apply_editor_values(dpg)
return self.load_scheme(dpg)
def reset_template(self, dpg, kind: str | None = None) -> None:
self.draft = default_draft(kind or self.draft.get("kind", "ST"))
self.file_path = None
self.refresh_file_path(dpg)
rebuild_editor(dpg, self)
self.mark_dirty(dpg)
def save_current(self, dpg) -> None:
if self.file_path is None:
dpg.show_item(SAVE_DIALOG_TAG)
return
try:
self.draft = save_draft(self.file_path, self.draft)
self.set_status(dpg, f"Saved {self.file_path}")
rebuild_editor(dpg, self)
except Exception as exc:
self.set_status(dpg, f"Save failed: {exc}", error=True)
def save_as(self, dpg, path: Path) -> None:
if path.suffix == "":
path = path.with_suffix(".json")
try:
self.draft = save_draft(path, self.draft)
self.file_path = path
self.refresh_file_path(dpg)
self.set_status(dpg, f"Saved {path}")
rebuild_editor(dpg, self)
except Exception as exc:
self.set_status(dpg, f"Save failed: {exc}", error=True)
def load_from_file(self, dpg, path: Path) -> None:
try:
draft = load_draft(path)
draft_to_scheme(draft)
except Exception as exc:
self.set_status(dpg, f"Load failed: {exc}", error=True)
return
self.draft = draft
self.file_path = path
self.refresh_file_path(dpg)
rebuild_editor(dpg, self)
self.mark_dirty(dpg)
self.set_status(dpg, f"Loaded {path}; click Apply to validate and reload")
def _draw_status(self, dpg) -> None:
if dpg.get_value("source_mode") == "manual":
_, info, report = _manual_state_to_core(dpg, self.last_valid_draft)
state = "run" if info.is_running else "stop"
dpg.draw_text(
(24, 18),
f"manual {state} {report.mileage_m:6.2f}m {dpg.get_value('draw_kind')} model",
color=(220, 225, 235, 255),
parent=DRAWLIST_TAG,
)
return
states = self.runtime.state_collection()
if states:
state = states[0]
dpg.draw_text(
(24, 18),
f"id {state.id} {state.time_elapsed_ms / 1000.0:5.1f}s "
f"{state.mileage_m:6.2f}m {state.speed_m_s:4.2f}m/s",
color=(220, 225, 235, 255),
parent=DRAWLIST_TAG,
)
def _runtime_pixels(self, dpg, config: TrackConfig) -> list[Color]:
now_s = time.perf_counter()
delta_s = min(0.1, now_s - self.last_frame_s)
self.last_frame_s = now_s
if dpg.get_value("source_mode") == "runtime":
self.runtime.tick(config, delta_s * float(dpg.get_value("time_scale")))
return self.runtime.render_pixels(config)
manual_config, info, report = _manual_state_to_core(dpg, self.last_valid_draft)
return render_pixels(manual_config, info, report)
def render(self, dpg) -> None:
dpg.delete_item(DRAWLIST_TAG, children_only=True)
config = _config_from_ui(dpg)
pixels = self._runtime_pixels(dpg, config)
if config.draw_kind == TrackDrawKind.circular:
_draw_circular(dpg, pixels)
else:
_draw_linear(dpg, pixels)
self._draw_status(dpg)
def _segment_input_float(dpg, tag: str, value: float, callback) -> None:
dpg.add_input_float(
tag=tag,
default_value=float(value),
width=92,
min_value=0.0,
min_clamped=True,
step=0.1,
format="%.2f",
callback=callback,
)
def _segment_input_int(dpg, tag: str, value: int, callback) -> None:
dpg.add_input_int(
tag=tag,
default_value=int(value),
width=82,
min_value=0,
min_clamped=True,
step=1,
callback=callback,
)
def _editor_changed(dpg, app: TrackRuntimeApp) -> None:
app.apply_editor_values(dpg)
app.mark_dirty(dpg)
def _set_simple_segment_value(dpg, app: TrackRuntimeApp, index: int, field: str, value) -> None:
app.draft["segments"][index][field] = value
_editor_changed(dpg, app)
def _set_rsmt_time(dpg, app: TrackRuntimeApp, index: int, value: int) -> None:
app.draft["segments"][index]["time_s"] = value
_editor_changed(dpg, app)
def _set_rsmt_sub_value(
dpg,
app: TrackRuntimeApp,
segment_index: int,
sub_index: int,
field: str,
value,
) -> None:
app.draft["segments"][segment_index]["sub_segments"][sub_index][field] = value
_editor_changed(dpg, app)
def _add_simple_segment(dpg, app: TrackRuntimeApp) -> None:
app.apply_editor_values(dpg)
kind = app.draft["kind"]
segments = app.draft["segments"]
if kind == "SM":
last = segments[-1] if segments else {"mileage_m": 0, "speed_m_s": 1.0}
segments.append({"mileage_m": int(last["mileage_m"]) + 5, "speed_m_s": float(last["speed_m_s"])})
elif kind == "MT":
last = segments[-1] if segments else {"time_s": 0, "mileage_m": 5}
segments.append({"time_s": int(last["time_s"]) + 5, "mileage_m": max(1, int(last["mileage_m"]))})
else:
last = segments[-1] if segments else {"time_s": 0, "speed_m_s": 1.0}
segments.append({"time_s": int(last["time_s"]) + 5, "speed_m_s": float(last["speed_m_s"])})
rebuild_editor(dpg, app)
app.mark_dirty(dpg)
def _remove_simple_segment(dpg, app: TrackRuntimeApp, index: int) -> None:
app.apply_editor_values(dpg)
if len(app.draft["segments"]) > 1:
app.draft["segments"].pop(index)
rebuild_editor(dpg, app)
app.mark_dirty(dpg)
def _duplicate_simple_segment(dpg, app: TrackRuntimeApp, index: int) -> None:
app.apply_editor_values(dpg)
source = clone_draft(app.draft["segments"][index])
if app.draft["kind"] == "SM":
source["mileage_m"] = int(source["mileage_m"]) + 5
else:
source["time_s"] = int(source["time_s"]) + 5
app.draft["segments"].insert(index + 1, source)
rebuild_editor(dpg, app)
app.mark_dirty(dpg)
def _add_rsmt_segment(dpg, app: TrackRuntimeApp) -> None:
app.apply_editor_values(dpg)
segments = app.draft["segments"]
last = segments[-1] if segments else {"time_s": 0}
segments.append(
{
"time_s": int(last["time_s"]) + 10,
"sub_segments": [
{"mileage_m": 0, "speed_m_s": 1.0},
{"mileage_m": 8, "speed_m_s": 1.0},
],
}
)
rebuild_editor(dpg, app)
app.mark_dirty(dpg)
def _remove_rsmt_segment(dpg, app: TrackRuntimeApp, index: int) -> None:
app.apply_editor_values(dpg)
if len(app.draft["segments"]) > 1:
app.draft["segments"].pop(index)
rebuild_editor(dpg, app)
app.mark_dirty(dpg)
def _add_rsmt_sub_segment(dpg, app: TrackRuntimeApp, segment_index: int) -> None:
app.apply_editor_values(dpg)
sub_segments = app.draft["segments"][segment_index]["sub_segments"]
last = sub_segments[-1] if sub_segments else {"mileage_m": 0, "speed_m_s": 1.0}
sub_segments.append({"mileage_m": int(last["mileage_m"]) + 5, "speed_m_s": float(last["speed_m_s"])})
rebuild_editor(dpg, app)
app.mark_dirty(dpg)
def _remove_rsmt_sub_segment(dpg, app: TrackRuntimeApp, segment_index: int, sub_index: int) -> None:
app.apply_editor_values(dpg)
sub_segments = app.draft["segments"][segment_index]["sub_segments"]
if len(sub_segments) > 1:
sub_segments.pop(sub_index)
rebuild_editor(dpg, app)
app.mark_dirty(dpg)
def rebuild_editor(dpg, app: TrackRuntimeApp) -> None:
dpg.delete_item(EDITOR_TAG, children_only=True)
draft = app.draft
with dpg.group(parent=EDITOR_TAG):
dpg.add_text("Training Scheme")
dpg.add_input_text(
label="file",
tag=FILE_PATH_TAG,
default_value=str(app.file_path) if app.file_path else "",
readonly=True,
width=410,
)
with dpg.group(horizontal=True):
dpg.add_button(label="Load JSON", callback=lambda *_: dpg.show_item(LOAD_DIALOG_TAG))
dpg.add_button(label="Apply", callback=lambda *_: app.apply_schema(dpg))
dpg.add_button(label="Save", callback=lambda *_: app.save_current(dpg))
dpg.add_button(label="Save As", callback=lambda *_: dpg.show_item(SAVE_DIALOG_TAG))
dpg.add_button(label="Reset", callback=lambda *_: app.reset_template(dpg))
dpg.add_separator()
with dpg.group(horizontal=True):
dpg.add_input_int(
label="id",
tag="schema_id",
default_value=int(draft["id"]),
width=90,
min_value=0,
max_value=255,
min_clamped=True,
max_clamped=True,
callback=lambda *_: _editor_changed(dpg, app),
)
dpg.add_combo(
SCHEME_KINDS,
label="kind",
tag="schema_kind",
default_value=draft["kind"],
width=96,
callback=lambda *_: app.reset_template(dpg, dpg.get_value("schema_kind")),
)
dpg.add_combo(
ACCELERATION_PROFILES,
label="accel",
tag="schema_accel",
default_value=draft["acceleration_profile"],
width=110,
callback=lambda *_: _editor_changed(dpg, app),
)
dpg.add_color_edit(
default_value=(*draft["color"], 255),
label="color",
tag="schema_color",
no_alpha=True,
width=260,
callback=lambda *_: _editor_changed(dpg, app),
)
dpg.add_separator()
if draft["kind"] == "RSMT":
_build_rsmt_editor(dpg, app)
else:
_build_simple_editor(dpg, app)
dpg.add_separator()
dpg.add_text(app.status, tag=STATUS_TAG, color=(170, 220, 170))
def _build_simple_editor(dpg, app: TrackRuntimeApp) -> None:
kind = app.draft["kind"]
dpg.add_button(label="Add Segment", callback=lambda *_: _add_simple_segment(dpg, app))
with dpg.table(header_row=True, borders_innerH=True, borders_outerH=True, borders_innerV=True):
dpg.add_table_column(label="#", width_fixed=True)
if kind == "SM":
dpg.add_table_column(label="mileage m")
dpg.add_table_column(label="speed m/s")
elif kind == "MT":
dpg.add_table_column(label="time s")
dpg.add_table_column(label="mileage m")
else:
dpg.add_table_column(label="time s")
dpg.add_table_column(label="speed m/s")
dpg.add_table_column(label="actions")
for index, segment in enumerate(app.draft["segments"]):
with dpg.table_row():
dpg.add_text(str(index))
if kind == "SM":
_segment_input_int(
dpg,
f"segment_{index}_mileage",
int(segment["mileage_m"]),
lambda sender, value, user_data: _set_simple_segment_value(
dpg, app, user_data, "mileage_m", int(value)
),
)
dpg.set_item_user_data(f"segment_{index}_mileage", index)
_segment_input_float(
dpg,
f"segment_{index}_speed",
float(segment["speed_m_s"]),
lambda sender, value, user_data: _set_simple_segment_value(
dpg, app, user_data, "speed_m_s", float(value)
),
)
dpg.set_item_user_data(f"segment_{index}_speed", index)
elif kind == "MT":
_segment_input_int(
dpg,
f"segment_{index}_time",
int(segment["time_s"]),
lambda sender, value, user_data: _set_simple_segment_value(
dpg, app, user_data, "time_s", int(value)
),
)
dpg.set_item_user_data(f"segment_{index}_time", index)
_segment_input_int(
dpg,
f"segment_{index}_mileage",
int(segment["mileage_m"]),
lambda sender, value, user_data: _set_simple_segment_value(
dpg, app, user_data, "mileage_m", int(value)
),
)
dpg.set_item_user_data(f"segment_{index}_mileage", index)
else:
_segment_input_int(
dpg,
f"segment_{index}_time",
int(segment["time_s"]),
lambda sender, value, user_data: _set_simple_segment_value(
dpg, app, user_data, "time_s", int(value)
),
)
dpg.set_item_user_data(f"segment_{index}_time", index)
_segment_input_float(
dpg,
f"segment_{index}_speed",
float(segment["speed_m_s"]),
lambda sender, value, user_data: _set_simple_segment_value(
dpg, app, user_data, "speed_m_s", float(value)
),
)
dpg.set_item_user_data(f"segment_{index}_speed", index)
with dpg.group(horizontal=True):
dpg.add_button(
label="copy",
callback=lambda _, __, ___=None, i=index: _duplicate_simple_segment(dpg, app, i),
)
dpg.add_button(
label="del",
callback=lambda _, __, ___=None, i=index: _remove_simple_segment(dpg, app, i),
)
def _build_rsmt_editor(dpg, app: TrackRuntimeApp) -> None:
dpg.add_button(label="Add Time Segment", callback=lambda *_: _add_rsmt_segment(dpg, app))
for segment_index, segment in enumerate(app.draft["segments"]):
with dpg.collapsing_header(label=f"time segment {segment_index}", default_open=True):
with dpg.group(horizontal=True):
dpg.add_text("time s")
_segment_input_int(
dpg,
f"rsmt_{segment_index}_time",
int(segment["time_s"]),
lambda sender, value, user_data: _set_rsmt_time(dpg, app, user_data, int(value)),
)
dpg.set_item_user_data(f"rsmt_{segment_index}_time", segment_index)
dpg.add_button(
label="Add Sub-Segment",
callback=lambda _, __, ___=None, i=segment_index: _add_rsmt_sub_segment(dpg, app, i),
)
dpg.add_button(
label="Delete Time Segment",
callback=lambda _, __, ___=None, i=segment_index: _remove_rsmt_segment(dpg, app, i),
)
with dpg.table(header_row=True, borders_innerH=True, borders_outerH=True, borders_innerV=True):
dpg.add_table_column(label="#")
dpg.add_table_column(label="mileage m")
dpg.add_table_column(label="speed m/s")
dpg.add_table_column(label="actions")
for sub_index, sub_segment in enumerate(segment["sub_segments"]):
with dpg.table_row():
dpg.add_text(str(sub_index))
_segment_input_int(
dpg,
f"rsmt_{segment_index}_{sub_index}_mileage",
int(sub_segment["mileage_m"]),
lambda sender, value, user_data: _set_rsmt_sub_value(
dpg, app, user_data[0], user_data[1], "mileage_m", int(value)
),
)
dpg.set_item_user_data(f"rsmt_{segment_index}_{sub_index}_mileage", (segment_index, sub_index))
_segment_input_float(
dpg,
f"rsmt_{segment_index}_{sub_index}_speed",
float(sub_segment["speed_m_s"]),
lambda sender, value, user_data: _set_rsmt_sub_value(
dpg, app, user_data[0], user_data[1], "speed_m_s", float(value)
),
)
dpg.set_item_user_data(f"rsmt_{segment_index}_{sub_index}_speed", (segment_index, sub_index))
dpg.add_button(
label="del",
callback=lambda _, __, ___=None, i=segment_index, j=sub_index: _remove_rsmt_sub_segment(
dpg, app, i, j
),
)
def _dialog_path(app_data) -> Path:
if "file_path_name" in app_data:
return Path(app_data["file_path_name"])
return Path(app_data["current_path"]) / app_data["file_name"]
def _build_file_dialogs(dpg, app: TrackRuntimeApp) -> None:
with dpg.file_dialog(
directory_selector=False,
show=False,
callback=lambda _, app_data, ___=None: app.load_from_file(dpg, _dialog_path(app_data)),
tag=LOAD_DIALOG_TAG,
width=700,
height=420,
):
dpg.add_file_extension(".json", color=(120, 190, 255, 255))
with dpg.file_dialog(
directory_selector=False,
show=False,
callback=lambda _, app_data, ___=None: app.save_as(dpg, _dialog_path(app_data)),
tag=SAVE_DIALOG_TAG,
width=700,
height=420,
):
dpg.add_file_extension(".json", color=(120, 190, 255, 255))
def main() -> None:
dpg = _load_dearpygui()
app = TrackRuntimeApp()
dpg.create_context()
dpg.create_viewport(title="track-core emulator", width=1160, height=690)
with dpg.window(label="track-core emulator", tag="main_window", width=1140, height=660):
with dpg.group(horizontal=True):
with dpg.child_window(width=500, height=630, border=True):
dpg.add_child_window(tag=EDITOR_TAG, width=480, height=610, border=False)
with dpg.child_window(width=610, height=630, border=True):
dpg.add_text("Runtime Preview")
with dpg.group(horizontal=True):
dpg.add_combo(("runtime", "manual"), default_value="runtime", label="source", tag="source_mode")
dpg.add_combo(("circular", "linear"), default_value="circular", label="track model", tag="draw_kind")
dpg.add_button(label="toggle model", callback=lambda *_: _toggle_draw_kind(dpg))
with dpg.group(horizontal=True):
dpg.add_button(label="reload schema", callback=lambda *_: app.load_scheme(dpg))
dpg.add_button(label="start", callback=lambda *_: app.start())
dpg.add_button(label="stop", callback=lambda *_: app.stop())
dpg.add_slider_float(label="time scale", tag="time_scale", default_value=1.0, min_value=0.0, max_value=8.0)
dpg.add_slider_float(label="line length m", tag="line_length", default_value=40.0, min_value=5.0, max_value=80.0)
dpg.add_slider_float(label="active length m", tag="active_length", default_value=10.0, min_value=0.1, max_value=40.0)
dpg.add_slider_float(label="head offset m", tag="head_offset", default_value=0.0, min_value=-40.0, max_value=40.0)
dpg.add_slider_int(label="LEDs", tag="led_count", default_value=160, min_value=8, max_value=400)
dpg.add_checkbox(label="manual running", default_value=True, tag="manual_running")
dpg.add_slider_float(label="manual position m", tag="manual_mileage", default_value=8.5, min_value=0.0, max_value=120.0)
dpg.add_drawlist(width=590, height=330, tag=DRAWLIST_TAG)
_build_file_dialogs(dpg, app)
rebuild_editor(dpg, app)
app.load_scheme(dpg)
dpg.setup_dearpygui()
dpg.set_primary_window("main_window", True)
dpg.show_viewport()
while dpg.is_dearpygui_running():
app.render(dpg)
dpg.render_dearpygui_frame()
dpg.destroy_context()
if __name__ == "__main__":
main()