From 9c55c7ca02a9f8f84b41f2d137909ce963ac640e Mon Sep 17 00:00:00 2001 From: cinnaboot Date: Wed, 4 Feb 2026 20:25:40 -0500 Subject: [PATCH] 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. --- src/main.cpp | 1 + src/ui_renderer.cpp | 374 ++++++++++++++++++++++++++++++++++++++++++++ src/ui_renderer.h | 50 +++++- 3 files changed, 422 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 2df477e..f1a1850 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -109,6 +109,7 @@ void run_gui_simulation(SimulationState* sim) { render_body_list_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_dialog(sim, &render_state, &ui_state); end_frame(); } diff --git a/src/ui_renderer.cpp b/src/ui_renderer.cpp index 593ed48..e03acb0 100644 --- a/src/ui_renderer.cpp +++ b/src/ui_renderer.cpp @@ -365,6 +365,20 @@ void render_maneuver_list_ui(SimulationState* sim, RenderState* render_state, UI (float)panel_height - 29 }; 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; } @@ -429,5 +443,365 @@ void render_maneuver_list_ui(SimulationState* sim, RenderState* render_state, UI 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); } + +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; +} diff --git a/src/ui_renderer.h b/src/ui_renderer.h index 472c5e9..674d0f3 100644 --- a/src/ui_renderer.h +++ b/src/ui_renderer.h @@ -4,12 +4,49 @@ #include "simulation.h" #include "renderer.h" #include "raylib.h" +#include "maneuver.h" +#include + +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 struct UIState { - int body_list_scroll; // Scroll position for body list - int body_list_active; // Active item index in body list - int selected_craft_index; // Selected spacecraft index (-1 = no selection) + int body_list_scroll; + int body_list_active; + int selected_craft_index; + + ManeuverDialogState maneuver_dialog; + int maneuver_list_active; + int maneuver_list_scroll; }; 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_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_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