Skip to content

Commit d1bc7e4

Browse files
authored
Merge pull request #45 from mavlink/feat/pre-release-ux
Pre-release UX improvements
2 parents d986c2d + 2b1531e commit d1bc7e4

File tree

8 files changed

+267
-57
lines changed

8 files changed

+267
-57
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,14 @@ Position data in ULog files is typically logged at 5-10 Hz. Dead-reckoning inter
130130
| `H` | Toggle HUD visibility |
131131
| `T` | Cycle trail mode (off / directional trail / speed ribbon) |
132132
| `G` | Toggle ground track projection |
133-
| `M` | Cycle vehicle model |
133+
| `F` | Toggle terrain texture |
134+
| `M` | Cycle vehicle model (`Shift+M`: all models) |
135+
| `K` | Toggle classic/modern arm colors |
134136
| `Ctrl+D` | Toggle debug performance overlay |
135137
| `O` | Toggle orthographic side panel (Top / Front / Right) |
136138
| `Alt+1` | Return to perspective camera |
137139
| `Alt+2`-`7` | Fullscreen ortho view (Top / Front / Left / Right / Bottom / Back) |
140+
| `Alt+Scroll` | Zoom ortho view span |
138141
| `TAB` | Cycle to next vehicle |
139142
| `[` / `]` | Previous / next vehicle |
140143
| `1`-`9` | Select vehicle directly |

src/data_source.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
#include <stdint.h>
66
#include "mavlink_receiver.h" // hil_state_t, home_position_t
77

8+
// Flight mode change event for timeline markers
9+
typedef struct {
10+
float time_s; // seconds from log start
11+
uint8_t nav_state; // PX4 nav_state value
12+
} playback_mode_change_t;
13+
814
// Playback state (only meaningful for replay sources)
915
typedef struct {
1016
bool paused;
@@ -14,6 +20,9 @@ typedef struct {
1420
float progress; // 0.0 to 1.0
1521
float duration_s; // total log duration in seconds
1622
float position_s; // current position in seconds
23+
uint8_t current_nav_state; // current flight mode (0xFF = unknown)
24+
const playback_mode_change_t *mode_changes; // array of mode transitions (NULL for MAVLink)
25+
int mode_change_count;
1726
} playback_state_t;
1827

1928
typedef struct data_source data_source_t;

src/data_source_ulog.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ static void ulog_poll(data_source_t *ds, float dt) {
1818
ds->state = ctx->state;
1919
ds->home = ctx->home;
2020
ds->mav_type = ctx->vehicle_type;
21+
ds->playback.current_nav_state = ctx->current_nav_state;
22+
ds->playback.mode_changes = (const playback_mode_change_t *)ctx->mode_changes;
23+
ds->playback.mode_change_count = ctx->mode_change_count;
2124

2225
// Update playback progress
2326
uint64_t range = ctx->parser.end_timestamp - ctx->parser.start_timestamp;

src/hud.c

Lines changed: 146 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "hud.h"
22
#include "asset_path.h"
3+
#include "ulog_replay.h"
34
#include "raylib.h"
45
#include "raymath.h"
56

@@ -497,6 +498,37 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
497498
float dot_x = prog_x + prog_w * pb->progress;
498499
DrawCircle((int)dot_x, (int)(prog_y + prog_h / 2.0f), 3 * s, accent);
499500

501+
// Flight mode markers on timeline
502+
if (pb->mode_changes && pb->mode_change_count > 0 && pb->duration_s > 0.0f) {
503+
float fs_marker = 9 * s;
504+
float last_label_x = -100.0f; // track last drawn label to avoid overlap
505+
for (int i = 0; i < pb->mode_change_count; i++) {
506+
float t = pb->mode_changes[i].time_s / pb->duration_s;
507+
if (t < 0.0f || t > 1.0f) continue;
508+
float mx = prog_x + prog_w * t;
509+
510+
// Tick mark: white and on top once past, dim accent when ahead
511+
bool past = (t <= pb->progress);
512+
Color tick_col = past
513+
? (Color){255, 255, 255, 220}
514+
: (Color){accent.r, accent.g, accent.b, 80};
515+
DrawCircle((int)mx, (int)(prog_y + prog_h / 2.0f), 2 * s, tick_col);
516+
517+
// Label (skip if too close to previous label)
518+
if (mx - last_label_x > 40 * s) {
519+
const char *name = ulog_nav_state_name(pb->mode_changes[i].nav_state);
520+
Vector2 tw = MeasureTextEx(h->font_label, name, fs_marker, 0.5f);
521+
float lx = mx - tw.x / 2.0f;
522+
if (lx < prog_x) lx = prog_x;
523+
if (lx + tw.x > prog_x + prog_w) lx = prog_x + prog_w - tw.x;
524+
DrawTextEx(h->font_label, name,
525+
(Vector2){lx, prog_y - tw.y - 2 * s},
526+
fs_marker, 0.5f, tick_col);
527+
last_label_x = mx;
528+
}
529+
}
530+
}
531+
500532
cx = prog_x + prog_w + 8 * s;
501533

502534
// Duration
@@ -570,22 +602,18 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
570602
float sep1_x = adi_cx + inst_radius + sep_margin; // instruments | NAV
571603
float sep3_x = timer_x - sep_margin; // ENERGY | timer
572604

573-
// Place sep2 to split the zone proportionally (3 NAV : 4 ENERGY)
605+
// Distribute all 7 telemetry items with equal step across the zone
574606
float tel_zone_w = sep3_x - sep1_x;
575-
float sep2_x = sep1_x + tel_zone_w * 3.0f / 7.0f;
607+
float item_step = tel_zone_w / 7.0f;
608+
float item_x0 = sep1_x + sep_margin;
576609

577-
// Evenly distribute NAV items (3) between sep1 and sep2
578-
// space-evenly: gap = zone / (N+1), item[i] at gap*(i+1)
579-
float nav_zone = sep2_x - sep1_x;
580-
float nav_gap = nav_zone / 4.0f; // 3 items + 1 = 4 gaps
581-
float nav_start = sep1_x + nav_gap;
582-
float nav_step = nav_gap;
610+
float nav_start = item_x0;
611+
float nav_step = item_step;
612+
float energy_start = item_x0 + 3 * item_step;
613+
float energy_step = item_step;
583614

584-
// Evenly distribute ENERGY items (4) between sep2 and sep3
585-
float energy_zone = sep3_x - sep2_x;
586-
float energy_gap = energy_zone / 5.0f; // 4 items + 1 = 5 gaps
587-
float energy_start = sep2_x + energy_gap;
588-
float energy_step = energy_gap;
615+
// Separator between NAV and ENERGY groups
616+
float sep2_x = sep1_x + 3 * item_step;
589617

590618
float nav_group_x = nav_start; // used by secondary rows
591619

@@ -607,7 +635,10 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
607635
snprintf(b, sizeof(b), "%03d", ((int)v->heading_deg % 360 + 360) % 360);
608636
DrawTextEx(h->font_value, b, (Vector2){x, (float)value_y}, fs_value, 0.5f, value_color);
609637
Vector2 vw = MeasureTextEx(h->font_value, b, fs_value, 0.5f);
610-
DrawTextEx(h->font_label, "deg", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
638+
Vector2 uw = MeasureTextEx(h->font_label, "deg", fs_unit, 0.5f);
639+
float boundary = item_x0 + 1 * item_step;
640+
if (x + vw.x + 3 + uw.x < boundary - 4 * s)
641+
DrawTextEx(h->font_label, "deg", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
611642
}
612643

613644
// ROLL
@@ -637,7 +668,10 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
637668
snprintf(b, sizeof(b), "%.1f", v->altitude_rel);
638669
DrawTextEx(h->font_value, b, (Vector2){x, (float)value_y}, fs_value, 0.5f, value_color);
639670
Vector2 vw = MeasureTextEx(h->font_value, b, fs_value, 0.5f);
640-
DrawTextEx(h->font_label, "m", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
671+
Vector2 uw = MeasureTextEx(h->font_label, "m", fs_unit, 0.5f);
672+
float boundary = item_x0 + 4 * item_step;
673+
if (x + vw.x + 3 + uw.x < boundary - 4 * s)
674+
DrawTextEx(h->font_label, "m", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
641675
}
642676

643677
// GS
@@ -648,7 +682,10 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
648682
snprintf(b, sizeof(b), "%.1f", v->ground_speed);
649683
DrawTextEx(h->font_value, b, (Vector2){x, (float)value_y}, fs_value, 0.5f, value_color);
650684
Vector2 vw = MeasureTextEx(h->font_value, b, fs_value, 0.5f);
651-
DrawTextEx(h->font_label, "m/s", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
685+
Vector2 uw = MeasureTextEx(h->font_label, "m/s", fs_unit, 0.5f);
686+
float boundary = item_x0 + 5 * item_step;
687+
if (x + vw.x + 3 + uw.x < boundary - 4 * s)
688+
DrawTextEx(h->font_label, "m/s", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
652689
}
653690

654691
// AS
@@ -660,7 +697,10 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
660697
snprintf(b, sizeof(b), "%.1f", v->airspeed);
661698
DrawTextEx(h->font_value, b, (Vector2){x, (float)value_y}, fs_value, 0.5f, value_color);
662699
Vector2 vw = MeasureTextEx(h->font_value, b, fs_value, 0.5f);
663-
DrawTextEx(h->font_label, "m/s", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
700+
Vector2 uw = MeasureTextEx(h->font_label, "m/s", fs_unit, 0.5f);
701+
float boundary = item_x0 + 6 * item_step;
702+
if (x + vw.x + 3 + uw.x < boundary - 4 * s)
703+
DrawTextEx(h->font_label, "m/s", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
664704
} else {
665705
DrawTextEx(h->font_value, "--", (Vector2){x, (float)value_y}, fs_value, 0.5f, dim_color);
666706
}
@@ -678,7 +718,9 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
678718
snprintf(b, sizeof(b), "%.1f%s", v->vertical_speed, arrow);
679719
DrawTextEx(h->font_value, b, (Vector2){x, (float)value_y}, fs_value, 0.5f, vs_color);
680720
Vector2 vw = MeasureTextEx(h->font_value, b, fs_value, 0.5f);
681-
DrawTextEx(h->font_label, "m/s", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
721+
Vector2 uw = MeasureTextEx(h->font_label, "m/s", fs_unit, 0.5f);
722+
if (x + vw.x + 3 + uw.x < sep3_x - 4 * s)
723+
DrawTextEx(h->font_label, "m/s", (Vector2){x + vw.x + 3, (float)(value_y + unit_y_off)}, fs_unit, 0.5f, dim_color);
682724
}
683725

684726
// Timer (sim time from HIL_STATE_QUATERNION)
@@ -772,36 +814,78 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
772814
float line_h = 24 * s;
773815
float col_gap = 24 * s;
774816

817+
// Grouped shortcut entries: NULL key = section header
775818
typedef struct { const char *key; const char *action; } shortcut_entry_t;
776-
shortcut_entry_t entries[] = {
777-
{"C", "Toggle camera mode (Chase / FPV)"},
778-
{"V", "Cycle view mode (Grid / Rez / Snow)"},
779-
{"TAB", "Cycle to next vehicle"},
780-
{"[ / ]", "Previous / next vehicle"},
781-
{"1-9", "Select vehicle directly"},
782-
{"Shift+1-9", "Toggle pin/unpin vehicle to HUD"},
783-
{"H", "Toggle HUD visibility"},
784-
{"M", "Cycle vehicle model"},
785-
{"Left-drag", "Orbit camera (chase mode)"},
786-
{"Scroll", "Zoom FOV"},
819+
820+
// Left column: VIEW + VEHICLE
821+
shortcut_entry_t left_col[] = {
822+
{NULL, "VIEW"},
823+
{"C", "Camera mode (Chase / FPV)"},
824+
{"V", "View mode (Grid / Rez / Snow)"},
825+
{"F", "Terrain texture"},
826+
{"K", "Arm colors (classic / modern)"},
827+
{"O", "Orthographic side panel"},
828+
{"Ctrl+D", "Debug overlay"},
829+
{"Alt+1-7", "Ortho views (1=perspective)"},
830+
{NULL, "VEHICLE MODEL"},
831+
{"M", "Switch variant (Shift: all)"},
832+
{NULL, "MULTI-VEHICLE"},
833+
{"TAB", "Next vehicle"},
834+
{"[ / ]", "Prev / next vehicle"},
835+
{"1-9", "Select vehicle"},
836+
{"Sh+1-9", "Pin / unpin to HUD"},
837+
};
838+
839+
// Right column: HUD + CAMERA + REPLAY
840+
shortcut_entry_t right_col[] = {
841+
{NULL, "HUD"},
842+
{"H", "Toggle HUD"},
843+
{"T", "Cycle trail mode"},
844+
{"G", "Ground track projection"},
787845
{"?", "Toggle this help"},
788-
{"Space", "Pause/resume replay"},
789-
{"+/-", "Replay speed"},
846+
{NULL, "CAMERA"},
847+
{"Drag", "Orbit (chase mode)"},
848+
{"Scroll", "Zoom FOV"},
849+
{"Alt+Scrl", "Zoom ortho span"},
850+
{NULL, "REPLAY"},
851+
{"Space", "Pause / resume"},
852+
{"+/-", "Playback speed"},
790853
{"<-/->", "Seek 5s (Shift: 30s)"},
791-
{"L", "Toggle replay loop"},
792-
{"R", "Restart replay"},
854+
{"L", "Toggle loop"},
855+
{"I", "Interpolation"},
856+
{"R", "Restart"},
793857
};
794-
int entry_count = sizeof(entries) / sizeof(entries[0]);
795858

796-
// Measure widths for alignment
859+
int left_count = sizeof(left_col) / sizeof(left_col[0]);
860+
int right_count = sizeof(right_col) / sizeof(right_col[0]);
861+
int max_rows = left_count > right_count ? left_count : right_count;
862+
863+
float help_fs_group = 14 * s;
864+
float group_top_pad = 6 * s;
865+
866+
// Measure max key width across both columns
797867
float max_key_w = 0;
798-
for (int i = 0; i < entry_count; i++) {
799-
Vector2 kw = MeasureTextEx(h->font_value, entries[i].key, help_fs, 0.5f);
868+
for (int i = 0; i < left_count; i++) {
869+
if (!left_col[i].key) continue;
870+
Vector2 kw = MeasureTextEx(h->font_value, left_col[i].key, help_fs, 0.5f);
871+
if (kw.x > max_key_w) max_key_w = kw.x;
872+
}
873+
for (int i = 0; i < right_count; i++) {
874+
if (!right_col[i].key) continue;
875+
Vector2 kw = MeasureTextEx(h->font_value, right_col[i].key, help_fs, 0.5f);
800876
if (kw.x > max_key_w) max_key_w = kw.x;
801877
}
802878

803-
float panel_w = max_key_w + col_gap + 320 * s;
804-
float panel_h = 40 * s + entry_count * line_h + 20 * s;
879+
// Count group headers for extra padding
880+
int left_headers = 0, right_headers = 0;
881+
for (int i = 0; i < left_count; i++) if (!left_col[i].key) left_headers++;
882+
for (int i = 0; i < right_count; i++) if (!right_col[i].key) right_headers++;
883+
int max_headers = left_headers > right_headers ? left_headers : right_headers;
884+
885+
float col_w = max_key_w + col_gap + 220 * s;
886+
float mid_gap = 32 * s;
887+
float panel_w = col_w * 2 + mid_gap + 40 * s;
888+
float panel_h = 40 * s + max_rows * line_h + max_headers * group_top_pad + 20 * s;
805889
float panel_x = (screen_w - panel_w) / 2.0f;
806890
float panel_y = (screen_h - panel_h) / 2.0f;
807891

@@ -820,16 +904,29 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
820904
(Vector2){panel_x + (panel_w - tw.x) / 2, panel_y + 12 * s},
821905
help_fs_title, 0.5f, accent);
822906

823-
// Entries
824-
float ey = panel_y + 40 * s;
825-
float key_x = panel_x + 20 * s;
826-
float action_x = key_x + max_key_w + col_gap;
827-
for (int i = 0; i < entry_count; i++) {
828-
DrawTextEx(h->font_value, entries[i].key,
829-
(Vector2){key_x, ey}, help_fs, 0.5f, accent);
830-
DrawTextEx(h->font_label, entries[i].action,
831-
(Vector2){action_x, ey}, help_fs, 0.5f, value_color);
832-
ey += line_h;
907+
// Draw a column of grouped entries
908+
float ey_start = panel_y + 40 * s;
909+
shortcut_entry_t *cols[] = { left_col, right_col };
910+
int counts[] = { left_count, right_count };
911+
912+
for (int c = 0; c < 2; c++) {
913+
float key_x = panel_x + 20 * s + c * (col_w + mid_gap);
914+
float action_x = key_x + max_key_w + col_gap;
915+
float ey = ey_start;
916+
for (int i = 0; i < counts[c]; i++) {
917+
if (!cols[c][i].key) {
918+
// Section header
919+
if (i > 0) ey += group_top_pad;
920+
DrawTextEx(h->font_label, cols[c][i].action,
921+
(Vector2){key_x, ey}, help_fs_group, 0.5f, dim_color);
922+
} else {
923+
DrawTextEx(h->font_value, cols[c][i].key,
924+
(Vector2){key_x, ey}, help_fs, 0.5f, accent);
925+
DrawTextEx(h->font_label, cols[c][i].action,
926+
(Vector2){action_x, ey}, help_fs, 0.5f, value_color);
927+
}
928+
ey += line_h;
929+
}
833930
}
834931
}
835932
}

src/scene.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,13 +490,23 @@ void scene_handle_input(scene_t *s) {
490490
}
491491
}
492492

493-
// Scroll wheel FOV zoom (only in perspective mode, not when Alt held)
493+
// Scroll wheel zoom (only in perspective mode, not when Alt held)
494494
if (!IsKeyDown(KEY_LEFT_ALT) && !IsKeyDown(KEY_RIGHT_ALT) && s->ortho_mode == ORTHO_NONE) {
495495
float wheel = GetMouseWheelMove();
496496
if (wheel != 0.0f) {
497-
s->camera.fovy -= wheel * 5.0f;
498-
if (s->camera.fovy < 10.0f) s->camera.fovy = 10.0f;
499-
if (s->camera.fovy > 120.0f) s->camera.fovy = 120.0f;
497+
if (s->cam_mode == CAM_MODE_CHASE) {
498+
// Chase: combined distance + FOV for natural zoom feel
499+
s->chase_distance -= wheel * 0.5f;
500+
if (s->chase_distance < 1.5f) s->chase_distance = 1.5f;
501+
if (s->chase_distance > 30.0f) s->chase_distance = 30.0f;
502+
// FOV widens as distance increases, narrows as it decreases
503+
float t = (s->chase_distance - 1.5f) / (30.0f - 1.5f);
504+
s->camera.fovy = 40.0f + t * 40.0f; // 40° close, 80° far
505+
} else {
506+
s->camera.fovy -= wheel * 5.0f;
507+
if (s->camera.fovy < 10.0f) s->camera.fovy = 10.0f;
508+
if (s->camera.fovy > 120.0f) s->camera.fovy = 120.0f;
509+
}
500510
}
501511
}
502512
// Scroll wheel ortho span zoom (fullscreen ortho, no Alt needed)

0 commit comments

Comments
 (0)