Browse Source

feat: Implement Hohmann Transfer tab (Phase 3)

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.
main
cinnaboot 5 months ago
parent
commit
94a7dcb6f7
  1. 264
      src/ui_renderer.cpp
  2. 4
      src/ui_renderer.h

264
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}; Rectangle dir_label = {(float)x, (float)current_y, 80, 25};
GuiLabel(dir_label, "Direction:"); 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) { 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}; 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); 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); 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) { void render_maneuver_dialog(SimulationState* sim, RenderState* render_state, UIState* ui_state) {
if (!ui_state->maneuver_dialog.open) { if (!ui_state->maneuver_dialog.open) {
return; 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) { 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); 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_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) { } 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; 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;
}

4
src/ui_renderer.h

@ -26,6 +26,7 @@ struct ManeuverDialogState {
int edit_maneuver_index; int edit_maneuver_index;
int target_body_index; int target_body_index;
int current_body_index;
int transfer_body_index; int transfer_body_index;
bool show_hohmann_preview; bool show_hohmann_preview;
HohmannTransfer last_calc; 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 generate_maneuver_name(SimulationState* sim, UIState* ui_state);
void create_maneuver_from_dialog(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 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 #endif

Loading…
Cancel
Save