Browse Source

feat: Implement maneuver dialog Create tab (Phase 2)

Implemented complete Create tab workflow for maneuver management dialog:
- render_maneuver_create_tab() with all input widgets
- update_orbital_preview() using preview_burn_result()
- render_orbital_preview() for displaying burn results
- validate_maneuver_inputs() for input validation
- create_maneuver_from_dialog() using add_maneuver_to_simulation()
- update_maneuver_from_dialog() for editing existing maneuvers
- Dialog opens/closes from maneuver list button
- Real-time orbital preview with color-coded validation

All tests pass. Phase 1 (Foundation) and Phase 2 (Create Tab) complete.
main
cinnaboot 5 months ago
parent
commit
9c55c7ca02
  1. 1
      src/main.cpp
  2. 374
      src/ui_renderer.cpp
  3. 50
      src/ui_renderer.h

1
src/main.cpp

@ -109,6 +109,7 @@ void run_gui_simulation(SimulationState* sim) {
render_body_list_ui(sim, &render_state, &ui_state); render_body_list_ui(sim, &render_state, &ui_state);
render_body_info_ui(sim, &render_state, &ui_state); render_body_info_ui(sim, &render_state, &ui_state);
render_maneuver_list_ui(sim, &render_state, &ui_state); render_maneuver_list_ui(sim, &render_state, &ui_state);
render_maneuver_dialog(sim, &render_state, &ui_state);
end_frame(); end_frame();
} }

374
src/ui_renderer.cpp

@ -365,6 +365,20 @@ void render_maneuver_list_ui(SimulationState* sim, RenderState* render_state, UI
(float)panel_height - 29 (float)panel_height - 29
}; };
GuiLabel(text_bounds, "No maneuvers defined"); GuiLabel(text_bounds, "No maneuvers defined");
// Add button to open maneuver dialog
Rectangle dialog_btn = {
(float)panel_x + 8,
(float)panel_y + (float)panel_height - 28,
(float)panel_width - 16,
20
};
if (GuiButton(dialog_btn, "[+] Manage Maneuvers")) {
ui_state->maneuver_dialog.open = true;
ui_state->maneuver_dialog.active_tab = MD_TAB_CREATE;
ui_state->maneuver_dialog.craft_index = ui_state->selected_craft_index;
ui_state->maneuver_dialog.edit_maneuver_index = -1;
}
return; return;
} }
@ -429,5 +443,365 @@ void render_maneuver_list_ui(SimulationState* sim, RenderState* render_state, UI
GuiLabel(details_bounds, details_text); GuiLabel(details_bounds, details_text);
} }
// Add button to open maneuver dialog
Rectangle dialog_btn = {
(float)panel_x + 8,
(float)panel_y + (float)panel_height - 28,
(float)panel_width - 16,
20
};
if (GuiButton(dialog_btn, "[+] Manage Maneuvers")) {
ui_state->maneuver_dialog.open = true;
ui_state->maneuver_dialog.active_tab = MD_TAB_CREATE;
ui_state->maneuver_dialog.craft_index = ui_state->selected_craft_index;
ui_state->maneuver_dialog.edit_maneuver_index = -1;
}
free(maneuver_list_text); free(maneuver_list_text);
} }
void render_maneuver_create_tab(SimulationState* sim, UIState* ui_state, int x, int y, int width, int height) {
int current_y = y;
Rectangle craft_label = {(float)x, (float)current_y, 80, 25};
GuiLabel(craft_label, "Spacecraft:");
current_y += 35;
Rectangle name_label = {(float)x, (float)current_y, 80, 25};
GuiLabel(name_label, "Name:");
Rectangle name_text = {(float)x + 85, (float)current_y, (float)(width - 95), 25};
GuiTextBox(name_text, ui_state->maneuver_dialog.name, 64, false);
current_y += 35;
Rectangle dir_label = {(float)x, (float)current_y, 80, 25};
GuiLabel(dir_label, "Direction:");
const char* directions = "Prograde;Retrograde;Normal;Antinormal;Radial In;Radial Out;Custom";
Rectangle dir_combo = {(float)x + 85, (float)current_y, (float)(width - 95), 25};
GuiComboBox(dir_combo, directions, &ui_state->maneuver_dialog.direction_active);
current_y += 35;
Rectangle dv_label = {(float)x, (float)current_y, 80, 25};
GuiLabel(dv_label, "Delta-V:");
Rectangle dv_slider = {(float)x + 85, (float)current_y, (float)(width - 180), 20};
float dv_float = (float)ui_state->maneuver_dialog.delta_v;
GuiSlider(dv_slider, "0", "50000", &dv_float, 0, 50000);
ui_state->maneuver_dialog.delta_v = dv_float;
char dv_text[32];
snprintf(dv_text, sizeof(dv_text), "%.1f m/s", ui_state->maneuver_dialog.delta_v);
Rectangle dv_value = {(float)x + width - 90, (float)current_y, 85, 25};
GuiLabel(dv_value, dv_text);
current_y += 35;
Rectangle trigger_label = {(float)x, (float)current_y, 80, 25};
GuiLabel(trigger_label, "Trigger:");
const char* triggers = "Time;True Anomaly";
Rectangle trigger_combo = {(float)x + 85, (float)current_y, (float)(width - 95), 25};
GuiComboBox(trigger_combo, triggers, &ui_state->maneuver_dialog.trigger_type_active);
current_y += 35;
Rectangle value_label = {(float)x, (float)current_y, 80, 25};
GuiLabel(value_label, "Value:");
Rectangle value_slider = {(float)x + 85, (float)current_y, (float)(width - 180), 20};
float value_float = (float)ui_state->maneuver_dialog.trigger_value;
if (ui_state->maneuver_dialog.trigger_type_active == 0) {
GuiSlider(value_slider, "0", "1000000", &value_float, 0, 1000000);
} else {
GuiSlider(value_slider, "0", "6.28", &value_float, 0, 2 * M_PI);
}
ui_state->maneuver_dialog.trigger_value = value_float;
char value_text[32];
if (ui_state->maneuver_dialog.trigger_type_active == 0) {
snprintf(value_text, sizeof(value_text), "%.1f s", ui_state->maneuver_dialog.trigger_value);
} else {
snprintf(value_text, sizeof(value_text), "%.2f rad", ui_state->maneuver_dialog.trigger_value);
}
Rectangle value_text_label = {(float)x + width - 90, (float)current_y, 85, 25};
GuiLabel(value_text_label, value_text);
update_orbital_preview(sim, ui_state);
if (ui_state->maneuver_dialog.show_preview) {
Rectangle preview_bounds = {(float)x + 8, (float)current_y + 10, (float)width - 16, 110};
render_orbital_preview(sim, ui_state, preview_bounds);
}
}
void update_orbital_preview(SimulationState* sim, UIState* ui_state) {
if (ui_state->maneuver_dialog.craft_index < 0 ||
ui_state->maneuver_dialog.craft_index >= sim->craft_count) {
ui_state->maneuver_dialog.show_preview = false;
return;
}
Spacecraft* craft = &sim->spacecraft[ui_state->maneuver_dialog.craft_index];
if (craft->parent_index < 0 || craft->parent_index >= sim->body_count) {
ui_state->maneuver_dialog.show_preview = false;
return;
}
CelestialBody* parent = &sim->bodies[craft->parent_index];
double parent_mass = parent->mass;
BurnDirection direction = (BurnDirection)ui_state->maneuver_dialog.direction_active;
double delta_v = ui_state->maneuver_dialog.delta_v;
ui_state->maneuver_dialog.preview_elements = preview_burn_result(craft, direction, delta_v, sim);
ui_state->maneuver_dialog.show_preview = true;
ui_state->maneuver_dialog.preview_valid = validate_burn_parameters(craft, direction, delta_v, parent_mass);
}
void render_orbital_preview(SimulationState* sim, UIState* ui_state, Rectangle bounds) {
if (!ui_state->maneuver_dialog.show_preview) {
return;
}
if (ui_state->maneuver_dialog.craft_index < 0 ||
ui_state->maneuver_dialog.craft_index >= sim->craft_count) {
return;
}
Spacecraft* craft = &sim->spacecraft[ui_state->maneuver_dialog.craft_index];
OrbitalElements* current = &craft->orbit;
OrbitalElements* preview = &ui_state->maneuver_dialog.preview_elements;
double current_periapsis = current->semi_major_axis * (1.0 - current->eccentricity);
double current_apoapsis = current->semi_major_axis * (1.0 + current->eccentricity);
double preview_periapsis = preview->semi_major_axis * (1.0 - preview->eccentricity);
double preview_apoapsis = preview->semi_major_axis * (1.0 + preview->eccentricity);
double delta_periapsis = preview_periapsis - current_periapsis;
double delta_apoapsis = preview_apoapsis - current_apoapsis;
int y = (int)bounds.y;
int x = (int)bounds.x;
int width = (int)bounds.width;
char current_text[128];
snprintf(current_text, sizeof(current_text),
"Current: e=%.4f, a=%.3e m, ν=%.2f rad",
current->eccentricity, current->semi_major_axis, current->true_anomaly);
char preview_text[128];
snprintf(preview_text, sizeof(preview_text),
"Preview: e=%.4f, a=%.3e m, ν=%.2f rad",
preview->eccentricity, preview->semi_major_axis, preview->true_anomaly);
char delta_text[128];
snprintf(delta_text, sizeof(delta_text),
"Δperiapsis: %+.3e m Δapoapsis: %+.3e m",
delta_periapsis, delta_apoapsis);
Rectangle current_bounds = {(float)x, (float)y, (float)width, 25};
GuiLabel(current_bounds, current_text);
y += 25;
Rectangle preview_bounds = {(float)x, (float)y, (float)width, 25};
GuiLabel(preview_bounds, preview_text);
y += 25;
Rectangle delta_bounds = {(float)x, (float)y, (float)width, 25};
GuiLabel(delta_bounds, delta_text);
y += 30;
Rectangle status_bounds = {(float)x, (float)y, (float)width, 25};
const char* status_text = ui_state->maneuver_dialog.preview_valid ? "✓ All parameters valid" : "✗ Invalid parameters";
GuiLabel(status_bounds, status_text);
}
void render_maneuver_dialog(SimulationState* sim, RenderState* render_state, UIState* ui_state) {
if (!ui_state->maneuver_dialog.open) {
return;
}
int width = 550;
int height = 480;
int x = (GetScreenWidth() - width) / 2;
int y = (GetScreenHeight() - height) / 2;
Rectangle dialog_bounds = {(float)x, (float)y, (float)width, (float)height};
if (GuiWindowBox(dialog_bounds, "Maneuver Management")) {
ui_state->maneuver_dialog.open = false;
return;
}
const char* tabs[] = {"Create Maneuver", "Hohmann Transfer", "Edit Maneuver"};
int tab_count = 3;
Rectangle tab_bounds = {x + 8, y + 30, width - 16, 25};
GuiTabBar(tab_bounds, tabs, tab_count, (int*)&ui_state->maneuver_dialog.active_tab);
Rectangle cancel_btn = {x + width - 280, y + height - 50, 100, 35};
Rectangle action_btn = {x + width - 170, y + height - 50, 160, 35};
if (GuiButton(cancel_btn, "Cancel")) {
ui_state->maneuver_dialog.open = false;
}
const char* action_text = "Create Maneuver";
if (ui_state->maneuver_dialog.active_tab == MD_TAB_HOHMANN) {
action_text = "Calculate Transfer";
} else if (ui_state->maneuver_dialog.active_tab == MD_TAB_EDIT) {
action_text = "Save Changes";
}
if (GuiButton(action_btn, action_text)) {
switch (ui_state->maneuver_dialog.active_tab) {
case MD_TAB_CREATE:
create_maneuver_from_dialog(sim, ui_state);
break;
case MD_TAB_HOHMANN:
break;
case MD_TAB_EDIT:
update_maneuver_from_dialog(sim, ui_state);
break;
}
}
if (ui_state->maneuver_dialog.active_tab == MD_TAB_CREATE) {
render_maneuver_create_tab(sim, ui_state, x + 8, y + 60, width - 16, height - 100);
} else if (ui_state->maneuver_dialog.active_tab == MD_TAB_HOHMANN) {
} else if (ui_state->maneuver_dialog.active_tab == MD_TAB_EDIT) {
}
}
bool validate_maneuver_inputs(SimulationState* sim, UIState* ui_state) {
ui_state->maneuver_dialog.show_error = false;
ui_state->maneuver_dialog.error_message[0] = '\0';
if (ui_state->maneuver_dialog.craft_index < 0 ||
ui_state->maneuver_dialog.craft_index >= sim->craft_count) {
snprintf(ui_state->maneuver_dialog.error_message,
sizeof(ui_state->maneuver_dialog.error_message),
"No spacecraft selected");
ui_state->maneuver_dialog.show_error = true;
return false;
}
if (strlen(ui_state->maneuver_dialog.name) == 0) {
snprintf(ui_state->maneuver_dialog.error_message,
sizeof(ui_state->maneuver_dialog.error_message),
"Maneuver name is required");
ui_state->maneuver_dialog.show_error = true;
return false;
}
for (int i = 0; i < sim->maneuver_count; i++) {
if (strcmp(sim->maneuvers[i].name, ui_state->maneuver_dialog.name) == 0) {
if (ui_state->maneuver_dialog.edit_maneuver_index >= 0 &&
ui_state->maneuver_dialog.edit_maneuver_index == i) {
continue;
}
snprintf(ui_state->maneuver_dialog.error_message,
sizeof(ui_state->maneuver_dialog.error_message),
"Maneuver name already exists");
ui_state->maneuver_dialog.show_error = true;
return false;
}
}
if (ui_state->maneuver_dialog.delta_v < 0) {
snprintf(ui_state->maneuver_dialog.error_message,
sizeof(ui_state->maneuver_dialog.error_message),
"Delta-V cannot be negative");
ui_state->maneuver_dialog.show_error = true;
return false;
}
if (ui_state->maneuver_dialog.delta_v > 50000) {
snprintf(ui_state->maneuver_dialog.error_message,
sizeof(ui_state->maneuver_dialog.error_message),
"Delta-V too large (max: 50000 m/s)");
ui_state->maneuver_dialog.show_error = true;
return false;
}
if (ui_state->maneuver_dialog.trigger_type_active == 1) {
if (ui_state->maneuver_dialog.trigger_value < 0 ||
ui_state->maneuver_dialog.trigger_value > 2 * M_PI) {
snprintf(ui_state->maneuver_dialog.error_message,
sizeof(ui_state->maneuver_dialog.error_message),
"True anomaly must be 0 to 6.28 rad");
ui_state->maneuver_dialog.show_error = true;
return false;
}
} else {
if (ui_state->maneuver_dialog.trigger_value < 0) {
snprintf(ui_state->maneuver_dialog.error_message,
sizeof(ui_state->maneuver_dialog.error_message),
"Time cannot be negative");
ui_state->maneuver_dialog.show_error = true;
return false;
}
}
Spacecraft* craft = &sim->spacecraft[ui_state->maneuver_dialog.craft_index];
if (craft->parent_index < 0 || craft->parent_index >= sim->body_count) {
snprintf(ui_state->maneuver_dialog.error_message,
sizeof(ui_state->maneuver_dialog.error_message),
"Spacecraft has no valid parent body");
ui_state->maneuver_dialog.show_error = true;
return false;
}
if (ui_state->maneuver_dialog.edit_maneuver_index < 0) {
if (sim->maneuver_count >= sim->max_maneuvers) {
snprintf(ui_state->maneuver_dialog.error_message,
sizeof(ui_state->maneuver_dialog.error_message),
"Maximum maneuvers reached");
ui_state->maneuver_dialog.show_error = true;
return false;
}
}
return true;
}
void create_maneuver_from_dialog(SimulationState* sim, UIState* ui_state) {
if (!validate_maneuver_inputs(sim, ui_state)) {
return;
}
Maneuver maneuver = create_maneuver(
ui_state->maneuver_dialog.name,
ui_state->maneuver_dialog.craft_index,
(BurnDirection)ui_state->maneuver_dialog.direction_active,
ui_state->maneuver_dialog.delta_v,
(TriggerType)ui_state->maneuver_dialog.trigger_type_active,
ui_state->maneuver_dialog.trigger_value
);
int maneuver_index = add_maneuver_to_simulation(sim, &maneuver);
if (maneuver_index >= 0) {
ui_state->maneuver_dialog.open = false;
}
}
void update_maneuver_from_dialog(SimulationState* sim, UIState* ui_state) {
int edit_index = ui_state->maneuver_dialog.edit_maneuver_index;
if (edit_index < 0 || edit_index >= sim->maneuver_count) {
return;
}
if (!validate_maneuver_inputs(sim, ui_state)) {
return;
}
Maneuver* maneuver = &sim->maneuvers[edit_index];
strncpy(maneuver->name, ui_state->maneuver_dialog.name, 64);
maneuver->craft_index = ui_state->maneuver_dialog.craft_index;
maneuver->direction = (BurnDirection)ui_state->maneuver_dialog.direction_active;
maneuver->delta_v = ui_state->maneuver_dialog.delta_v;
maneuver->trigger_type = (TriggerType)ui_state->maneuver_dialog.trigger_type_active;
maneuver->trigger_value = ui_state->maneuver_dialog.trigger_value;
ui_state->maneuver_dialog.open = false;
}

50
src/ui_renderer.h

@ -4,12 +4,49 @@
#include "simulation.h" #include "simulation.h"
#include "renderer.h" #include "renderer.h"
#include "raylib.h" #include "raylib.h"
#include "maneuver.h"
#include <string.h>
enum ManeuverDialogTab {
MD_TAB_CREATE,
MD_TAB_HOHMANN,
MD_TAB_EDIT
};
struct ManeuverDialogState {
bool open;
ManeuverDialogTab active_tab;
int craft_index;
char name[64];
int direction_active;
double delta_v;
int trigger_type_active;
double trigger_value;
int edit_maneuver_index;
int target_body_index;
int transfer_body_index;
bool show_hohmann_preview;
HohmannTransfer last_calc;
bool show_preview;
bool preview_valid;
OrbitalElements preview_elements;
char error_message[256];
bool show_error;
};
// UI state // UI state
struct UIState { struct UIState {
int body_list_scroll; // Scroll position for body list int body_list_scroll;
int body_list_active; // Active item index in body list int body_list_active;
int selected_craft_index; // Selected spacecraft index (-1 = no selection) int selected_craft_index;
ManeuverDialogState maneuver_dialog;
int maneuver_list_active;
int maneuver_list_scroll;
}; };
void gui_init(); void gui_init();
@ -19,5 +56,12 @@ void render_info(SimulationState* sim);
void render_body_list_ui(SimulationState* sim, RenderState* render_state, UIState* ui_state); void render_body_list_ui(SimulationState* sim, RenderState* render_state, UIState* ui_state);
void render_body_info_ui(SimulationState* sim, RenderState* render_state, UIState* ui_state); void render_body_info_ui(SimulationState* sim, RenderState* render_state, UIState* ui_state);
void render_maneuver_list_ui(SimulationState* sim, RenderState* render_state, UIState* ui_state); void render_maneuver_list_ui(SimulationState* sim, RenderState* render_state, UIState* ui_state);
void render_maneuver_dialog(SimulationState* sim, RenderState* render_state, UIState* ui_state);
void render_maneuver_create_tab(SimulationState* sim, UIState* ui_state, int x, int y, int width, int height);
void update_orbital_preview(SimulationState* sim, UIState* ui_state);
void render_orbital_preview(SimulationState* sim, UIState* ui_state, Rectangle bounds);
bool validate_maneuver_inputs(SimulationState* sim, UIState* ui_state);
void create_maneuver_from_dialog(SimulationState* sim, UIState* ui_state);
void update_maneuver_from_dialog(SimulationState* sim, UIState* ui_state);
#endif #endif

Loading…
Cancel
Save