From 94a7dcb6f7b0991d1120d3c71d6b2517ff502233 Mon Sep 17 00:00:00 2001 From: cinnaboot Date: Thu, 5 Feb 2026 12:09:19 -0500 Subject: [PATCH] feat: Implement Hohmann Transfer tab (Phase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added complete Hohmann transfer calculation and UI: - Spacecraft, Transfer Parent, Current Body, Target Body selectors - Calculate Transfer button with real-time calculation - Results panel showing dv1, dv2, transfer time, semi-major axis - Create Both Burns / Create Burn 1 Only buttons - Auto-generated maneuver names with format 'Hohmann Burn N (Source→Target)' - Proper field initialization and validation Fixed body selector logic with separate current_body_index field. --- src/ui_renderer.cpp | 264 ++++++++++++++++++++++++++++++++++++++++++++ src/ui_renderer.h | 4 + 2 files changed, 268 insertions(+) diff --git a/src/ui_renderer.cpp b/src/ui_renderer.cpp index aaa3cb5..60aadad 100644 --- a/src/ui_renderer.cpp +++ b/src/ui_renderer.cpp @@ -494,6 +494,7 @@ void render_maneuver_create_tab(SimulationState* sim, UIState* ui_state, int x, Rectangle dir_label = {(float)x, (float)current_y, 80, 25}; GuiLabel(dir_label, "Direction:"); + // FIXME: we need to handle state differently here: see raygui controls_test_suite.c example if (ui_state->maneuver_dialog.craft_index >= 0 && ui_state->maneuver_dialog.craft_index < sim->craft_count) { Rectangle dir_combo = {(float)x + 85, (float)current_y, (float)(width - 95), 25}; GuiDropdownBox(dir_combo, "Prograde;Retrograde;Normal;Antinormal;Radial In;Radial Out;Custom", &ui_state->maneuver_dialog.direction_active, false); @@ -634,6 +635,208 @@ void render_orbital_preview(SimulationState* sim, UIState* ui_state, Rectangle b GuiLabel(status_bounds, status_text); } +void render_maneuver_hohmann_tab(SimulationState* sim, UIState* ui_state, int x, int y, int width, int height) { + int current_y = y; + + if (ui_state->maneuver_dialog.craft_index >= 0 && + ui_state->maneuver_dialog.craft_index < sim->craft_count) { + 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.current_body_index = craft->parent_index; + } + } + + GuiLabel({(float)x, (float)current_y, (float)width, 25}, "Calculate optimal Hohmann transfer between orbits"); + current_y += 30; + + GuiLabel({(float)x, (float)current_y, 120, 25}, "Spacecraft:"); + current_y += 35; + + if (sim->craft_count > 0) { + int craft_buffer_size = sim->craft_count * 64 + 1; + char* craft_list = (char*)malloc(craft_buffer_size); + if (craft_list) { + craft_list[0] = '\0'; + int offset = 0; + for (int i = 0; i < sim->craft_count; i++) { + if (i > 0) { + offset += snprintf(craft_list + offset, craft_buffer_size - offset, ";"); + } + offset += snprintf(craft_list + offset, craft_buffer_size - offset, "%s", sim->spacecraft[i].name); + } + + GuiDropdownBox({(float)x + 120, (float)(current_y - 35), (float)(width - 130), 25}, + craft_list, &ui_state->maneuver_dialog.craft_index, false); + free(craft_list); + } + } + current_y += 35; + + GuiLabel({(float)x, (float)current_y, 120, 25}, "Transfer Parent:"); + current_y += 35; + + if (sim->body_count > 0) { + int body_buffer_size = sim->body_count * 64 + 1; + char* body_list = (char*)malloc(body_buffer_size); + if (body_list) { + body_list[0] = '\0'; + int offset = 0; + for (int i = 0; i < sim->body_count; i++) { + if (i > 0) { + offset += snprintf(body_list + offset, body_buffer_size - offset, ";"); + } + offset += snprintf(body_list + offset, body_buffer_size - offset, "%s", sim->bodies[i].name); + } + + GuiDropdownBox({(float)x + 120, (float)(current_y - 35), (float)(width - 130), 25}, + body_list, &ui_state->maneuver_dialog.transfer_body_index, false); + free(body_list); + } + } + current_y += 35; + + GuiLabel({(float)x, (float)current_y, 120, 25}, "Current Body:"); + current_y += 35; + + if (sim->body_count > 0) { + int body_buffer_size = sim->body_count * 64 + 1; + char* body_list = (char*)malloc(body_buffer_size); + if (body_list) { + body_list[0] = '\0'; + int offset = 0; + for (int i = 0; i < sim->body_count; i++) { + if (i > 0) { + offset += snprintf(body_list + offset, body_buffer_size - offset, ";"); + } + offset += snprintf(body_list + offset, body_buffer_size - offset, "%s", sim->bodies[i].name); + } + + GuiDropdownBox({(float)x + 120, (float)(current_y - 35), (float)(width - 130), 25}, + body_list, &ui_state->maneuver_dialog.current_body_index, false); + free(body_list); + } + } + current_y += 35; + + GuiLabel({(float)x, (float)current_y, 120, 25}, "Target Body:"); + current_y += 35; + + if (sim->body_count > 0) { + int body_buffer_size = sim->body_count * 64 + 1; + char* body_list = (char*)malloc(body_buffer_size); + if (body_list) { + body_list[0] = '\0'; + int offset = 0; + for (int i = 0; i < sim->body_count; i++) { + if (i > 0) { + offset += snprintf(body_list + offset, body_buffer_size - offset, ";"); + } + offset += snprintf(body_list + offset, body_buffer_size - offset, "%s", sim->bodies[i].name); + } + + GuiDropdownBox({(float)x + 120, (float)(current_y - 35), (float)(width - 130), 25}, + body_list, &ui_state->maneuver_dialog.target_body_index, false); + free(body_list); + } + } + current_y += 35; + + Rectangle calculate_btn = {(float)x, (float)current_y, (float)width, 30}; + if (GuiButton(calculate_btn, "Calculate Transfer")) { + calculate_hohmann_for_dialog(sim, ui_state); + } + current_y += 40; + + if (ui_state->maneuver_dialog.show_hohmann_preview) { + HohmannTransfer* ht = &ui_state->maneuver_dialog.last_calc; + + char transfer_info[256]; + double current_r = (ui_state->maneuver_dialog.current_body_index >= 0 && + ui_state->maneuver_dialog.current_body_index < sim->body_count) ? + sim->bodies[ui_state->maneuver_dialog.current_body_index].orbit.semi_major_axis : 0.0; + double target_r = (ui_state->maneuver_dialog.target_body_index >= 0 && + ui_state->maneuver_dialog.target_body_index < sim->body_count) ? + sim->bodies[ui_state->maneuver_dialog.target_body_index].orbit.semi_major_axis : 0.0; + + snprintf(transfer_info, sizeof(transfer_info), + "Transfer semi-major axis: %.3e m", + (current_r + target_r) / 2.0); + + Rectangle info_bounds = {(float)x + 8, (float)current_y, (float)width - 16, 25}; + GuiLabel(info_bounds, transfer_info); + current_y += 25; + + char time_info[128]; + double days = ht->transfer_time / 86400.0; + snprintf(time_info, sizeof(time_info), "Transfer time: %.1f days", days); + Rectangle time_bounds = {(float)x + 8, (float)current_y, (float)width - 16, 25}; + GuiLabel(time_bounds, time_info); + current_y += 30; + + char burn1_info[256]; + snprintf(burn1_info, sizeof(burn1_info), + "Burn 1 (at periapsis): Δv1 = %.2f km/s, Prograde, ν = 0.00 rad", + ht->dv1 / 1000.0); + Rectangle burn1_bounds = {(float)x + 8, (float)current_y, (float)width - 16, 25}; + GuiLabel(burn1_bounds, burn1_info); + current_y += 25; + + char burn2_info[256]; + snprintf(burn2_info, sizeof(burn2_info), + "Burn 2 (at apoapsis): Δv2 = %.2f km/s, Prograde, ν = %.2f rad", + ht->dv2 / 1000.0, ht->true_anomaly_2); + Rectangle burn2_bounds = {(float)x + 8, (float)current_y, (float)width - 16, 25}; + GuiLabel(burn2_bounds, burn2_info); + current_y += 30; + + char total_info[128]; + snprintf(total_info, sizeof(total_info), "Total Δv: %.2f km/s", (ht->dv1 + ht->dv2) / 1000.0); + Rectangle total_bounds = {(float)x + 8, (float)current_y, (float)width - 16, 25}; + GuiLabel(total_bounds, total_info); + current_y += 30; + + Rectangle both_btn = {(float)x, (float)current_y, (float)(width / 2 - 5), 30}; + Rectangle burn1_btn = {(float)x + (float)(width / 2 + 5), (float)current_y, (float)(width / 2 - 5), 30}; + + if (GuiButton(both_btn, "Create Both Burns")) { + create_hohmann_burns(sim, ui_state, true); + } + + if (GuiButton(burn1_btn, "Create Burn 1 Only")) { + create_hohmann_burns(sim, ui_state, false); + } + } +} + +void calculate_hohmann_for_dialog(SimulationState* sim, UIState* ui_state) { + ui_state->maneuver_dialog.show_hohmann_preview = false; + + if (ui_state->maneuver_dialog.transfer_body_index < 0 || + ui_state->maneuver_dialog.transfer_body_index >= sim->body_count || + ui_state->maneuver_dialog.target_body_index < 0 || + ui_state->maneuver_dialog.target_body_index >= sim->body_count) { + return; + } + + if (ui_state->maneuver_dialog.transfer_body_index == ui_state->maneuver_dialog.target_body_index) { + return; + } + + CelestialBody* central_body = &sim->bodies[ui_state->maneuver_dialog.transfer_body_index]; + CelestialBody* current_body = &sim->bodies[ui_state->maneuver_dialog.current_body_index]; + CelestialBody* target_body = &sim->bodies[ui_state->maneuver_dialog.target_body_index]; + + double r1 = current_body->orbit.semi_major_axis; + double r2 = target_body->orbit.semi_major_axis; + + if (r1 <= 0 || r2 <= 0) { + return; + } + + ui_state->maneuver_dialog.last_calc = calculate_hohmann_transfer(r1, r2, central_body->mass); + ui_state->maneuver_dialog.show_hohmann_preview = true; +} + void render_maneuver_dialog(SimulationState* sim, RenderState* render_state, UIState* ui_state) { if (!ui_state->maneuver_dialog.open) { return; @@ -686,6 +889,7 @@ void render_maneuver_dialog(SimulationState* sim, RenderState* render_state, UIS 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) { + render_maneuver_hohmann_tab(sim, ui_state, x + 8, y + 60, width - 16, height - 100); } else if (ui_state->maneuver_dialog.active_tab == MD_TAB_EDIT) { } } @@ -839,3 +1043,63 @@ void update_maneuver_from_dialog(SimulationState* sim, UIState* ui_state) { ui_state->maneuver_dialog.open = false; } + +void create_hohmann_burns(SimulationState* sim, UIState* ui_state, bool create_both_burns) { + if (ui_state->maneuver_dialog.craft_index < 0 || + ui_state->maneuver_dialog.craft_index >= sim->craft_count) { + return; + } + + if (!ui_state->maneuver_dialog.show_hohmann_preview) { + return; + } + + HohmannTransfer* ht = &ui_state->maneuver_dialog.last_calc; + + const char* current_name = (ui_state->maneuver_dialog.transfer_body_index >= 0 && + ui_state->maneuver_dialog.transfer_body_index < sim->body_count) ? + sim->bodies[ui_state->maneuver_dialog.transfer_body_index].name : "Unknown"; + const char* target_name = (ui_state->maneuver_dialog.target_body_index >= 0 && + ui_state->maneuver_dialog.target_body_index < sim->body_count) ? + sim->bodies[ui_state->maneuver_dialog.target_body_index].name : "Unknown"; + + char burn1_name[128]; + snprintf(burn1_name, sizeof(burn1_name), "Hohmann Burn 1 (%s→%s)", current_name, target_name); + + Maneuver burn1 = create_maneuver( + burn1_name, + ui_state->maneuver_dialog.craft_index, + BURN_PROGRADE, + ht->dv1, + TRIGGER_TRUE_ANOMALY, + 0.0 + ); + + int burn1_index = add_maneuver_to_simulation(sim, &burn1); + + if (burn1_index < 0) { + return; + } + + if (create_both_burns) { + char burn2_name[128]; + snprintf(burn2_name, sizeof(burn2_name), "Hohmann Burn 2 (%s→%s)", current_name, target_name); + + Maneuver burn2 = create_maneuver( + burn2_name, + ui_state->maneuver_dialog.craft_index, + BURN_PROGRADE, + ht->dv2, + TRIGGER_TRUE_ANOMALY, + ht->true_anomaly_2 + ); + + int burn2_index = add_maneuver_to_simulation(sim, &burn2); + + if (burn2_index < 0) { + return; + } + } + + ui_state->maneuver_dialog.open = false; +} diff --git a/src/ui_renderer.h b/src/ui_renderer.h index 6ab0948..9594b68 100644 --- a/src/ui_renderer.h +++ b/src/ui_renderer.h @@ -26,6 +26,7 @@ struct ManeuverDialogState { int edit_maneuver_index; int target_body_index; + int current_body_index; int transfer_body_index; bool show_hohmann_preview; HohmannTransfer last_calc; @@ -64,5 +65,8 @@ bool validate_maneuver_inputs(SimulationState* sim, UIState* ui_state); void generate_maneuver_name(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); +void render_maneuver_hohmann_tab(SimulationState* sim, UIState* ui_state, int x, int y, int width, int height); +void calculate_hohmann_for_dialog(SimulationState* sim, UIState* ui_state); +void create_hohmann_burns(SimulationState* sim, UIState* ui_state, bool create_both_burns); #endif