You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1411 lines
57 KiB
1411 lines
57 KiB
#include "ui_renderer.h" |
|
#include "spacecraft.h" |
|
#include "maneuver.h" |
|
#include <cstdio> |
|
#include <cstring> |
|
#include <cmath> |
|
|
|
// raygui implementation (header-only library) |
|
#define RAYGUI_IMPLEMENTATION |
|
#include "raygui.h" |
|
#include "amber/style_amber.h" |
|
|
|
// UI Panel constants |
|
#define INFO_PANEL_WIDTH 300 |
|
#define INFO_PANEL_HEIGHT 350 |
|
#define BODY_LIST_WIDTH 200 |
|
#define BODY_LIST_HEIGHT 400 |
|
#define BODY_INFO_WIDTH 250 |
|
#define BODY_INFO_HEIGHT 300 |
|
#define MANEUVER_LIST_WIDTH 300 |
|
#define MANEUVER_LIST_HEIGHT 450 |
|
#define MANEUVER_LIST_Y_OFFSET 320 |
|
|
|
void gui_init() { |
|
GuiLoadStyleAmber(); |
|
} |
|
|
|
// Render simulation information overlay |
|
void render_info(SimulationState* sim) { |
|
int panel_width = INFO_PANEL_WIDTH; |
|
int panel_height = INFO_PANEL_HEIGHT; |
|
int panel_x = 10; |
|
int panel_y = GetScreenHeight() - panel_height - 10; |
|
|
|
Rectangle panel_bounds = { (float)panel_x, (float)panel_y, (float)panel_width, (float)panel_height }; |
|
|
|
// Draw window |
|
if (GuiWindowBox(panel_bounds, "Info")) { |
|
// Window box close button doesn't close this panel |
|
} |
|
|
|
// Prepare info text |
|
char info_text[1024]; |
|
char temp_buffer[512]; |
|
|
|
snprintf(info_text, sizeof(info_text), "Orbital Mechanics Simulation\n\n"); |
|
|
|
// Config name |
|
if (sim->config_name[0] != '\0') { |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Config: %s\n\n", sim->config_name); |
|
strcat(info_text, temp_buffer); |
|
} |
|
|
|
// Simulation time (in days) |
|
double days = sim->time / 86400.0; // seconds to days |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Time: %.2f days\n", days); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Body count |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Bodies: %d\n", sim->body_count); |
|
strcat(info_text, temp_buffer); |
|
|
|
// FPS |
|
snprintf(temp_buffer, sizeof(temp_buffer), "FPS: %d\n\n", GetFPS()); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Controls |
|
strcat(info_text, "Controls:\n"); |
|
strcat(info_text, " Arrows: Rotate/Zoom\n"); |
|
strcat(info_text, " Space: Pause/Resume\n"); |
|
strcat(info_text, " +/-: Speed control\n"); |
|
strcat(info_text, " ESC: Quit\n"); |
|
|
|
// Draw info text (inside window, leave space for title bar) |
|
Rectangle text_bounds = { |
|
(float)panel_x + 8, |
|
(float)panel_y + 25, |
|
(float)panel_width - 16, |
|
(float)panel_height - 29 |
|
}; |
|
|
|
GuiLabel(text_bounds, info_text); |
|
} |
|
|
|
// Render body list UI panel |
|
void render_body_list_ui(SimulationState* sim, RenderState* render_state, UIState* ui_state) { |
|
int panel_width = BODY_LIST_WIDTH; |
|
int panel_height = BODY_LIST_HEIGHT; |
|
int panel_x = 10; |
|
int panel_y = 10; |
|
|
|
Rectangle panel_bounds = { (float)panel_x, (float)panel_y, (float)panel_width, (float)panel_height }; |
|
|
|
// Create list string for raygui (NULL-terminated) |
|
// Include both bodies and spacecraft |
|
int total_items = sim->body_count + sim->craft_count; |
|
int buffer_size = total_items * (64 + 3 + 1) + 1; // Extra space for "🚀 " prefix |
|
char* list_text = (char*)malloc(buffer_size); |
|
if (!list_text) return; |
|
|
|
list_text[0] = '\0'; |
|
int offset = 0; |
|
int item_index = 0; |
|
|
|
// Add bodies to list |
|
for (int i = 0; i < sim->body_count; i++) { |
|
if (item_index > 0) { |
|
offset += snprintf(list_text + offset, buffer_size - offset, ";"); |
|
} |
|
offset += snprintf(list_text + offset, buffer_size - offset, "%s", sim->bodies[i].name); |
|
item_index++; |
|
} |
|
|
|
// Add spacecraft to list with "🚀 " prefix |
|
for (int i = 0; i < sim->craft_count; i++) { |
|
offset += snprintf(list_text + offset, buffer_size - offset, ";"); |
|
offset += snprintf(list_text + offset, buffer_size - offset, "🚀 %s", sim->spacecraft[i].name); |
|
item_index++; |
|
} |
|
|
|
// Draw window (no close button - panels always shown) |
|
GuiWindowBox(panel_bounds, "Objects"); |
|
|
|
// Draw list view (inside window, leave space for title bar) |
|
Rectangle list_bounds = { |
|
(float)panel_x + 4, |
|
(float)panel_y + 25, |
|
(float)panel_width - 8, |
|
(float)panel_height - 29 |
|
}; |
|
|
|
int previous_active = ui_state->body_list_active; |
|
GuiListView(list_bounds, list_text, &ui_state->body_list_scroll, &ui_state->body_list_active); |
|
|
|
// Check if an item was selected |
|
if (ui_state->body_list_active >= 0 && ui_state->body_list_active != previous_active) { |
|
if (ui_state->body_list_active < sim->body_count) { |
|
render_state->selected_body_index = ui_state->body_list_active; |
|
ui_state->selected_craft_index = -1; |
|
render_state->camera_target_enabled = true; |
|
printf("Camera follow enabled for body: %s\n", sim->bodies[render_state->selected_body_index].name); |
|
} else { |
|
int craft_index = ui_state->body_list_active - sim->body_count; |
|
if (craft_index >= 0 && craft_index < sim->craft_count) { |
|
Spacecraft* craft = &sim->spacecraft[craft_index]; |
|
render_state->selected_body_index = craft->parent_index; |
|
ui_state->selected_craft_index = craft_index; |
|
render_state->camera_target_enabled = true; |
|
printf("Camera follow enabled for spacecraft: %s\n", craft->name); |
|
} |
|
} |
|
} |
|
|
|
free(list_text); |
|
} |
|
|
|
// Render body information UI panel |
|
void render_body_info_ui(SimulationState* sim, RenderState* render_state, UIState* ui_state) { |
|
int panel_width = BODY_INFO_WIDTH; |
|
int panel_height = BODY_INFO_HEIGHT; |
|
int panel_x = GetScreenWidth() - panel_width - 10; |
|
int panel_y = 10; |
|
|
|
if ((render_state->selected_body_index < 0 || render_state->selected_body_index >= sim->body_count) && |
|
(ui_state->selected_craft_index < 0 || ui_state->selected_craft_index >= sim->craft_count)) { |
|
// No body or spacecraft selected - render empty panel |
|
Rectangle panel_bounds = { (float)panel_x, (float)panel_y, (float)panel_width, (float)panel_height }; |
|
GuiWindowBox(panel_bounds, "Info"); |
|
return; |
|
} |
|
|
|
Rectangle panel_bounds = { (float)panel_x, (float)panel_y, (float)panel_width, (float)panel_height }; |
|
|
|
// Check if a body is selected |
|
if (render_state->selected_body_index >= 0 && render_state->selected_body_index < sim->body_count) { |
|
CelestialBody* body = &sim->bodies[render_state->selected_body_index]; |
|
|
|
// Draw window (no close button - panels always shown) |
|
GuiWindowBox(panel_bounds, body->name); |
|
|
|
// Prepare info text |
|
char info_text[1024]; |
|
char temp_buffer[256]; |
|
|
|
snprintf(info_text, sizeof(info_text), "Name: %s", body->name); |
|
|
|
snprintf(temp_buffer, sizeof(temp_buffer), "Mass: %.2e kg", body->mass); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
snprintf(temp_buffer, sizeof(temp_buffer), "Radius: %.2e m", body->radius); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
if (body->parent_index <= 0) { |
|
// Position |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Position: (%.2e, %.2e, %.2e)", |
|
body->global_position.x, body->global_position.y, body->global_position.z); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Velocity |
|
double vel_mag = vec3_magnitude(body->global_velocity); |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Velocity: %.2f m/s", vel_mag); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
} else { |
|
// Position (Local) |
|
snprintf(temp_buffer, sizeof(temp_buffer), "local_position: (%.2e, %.2e, %.2e)", |
|
body->local_position.x, body->local_position.y, body->local_position.z); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Velocity (Local) |
|
double parent_vel_mag = vec3_magnitude(sim->bodies[body->parent_index].global_velocity); |
|
double vel_mag = vec3_magnitude(body->global_velocity) - parent_vel_mag; |
|
snprintf(temp_buffer, sizeof(temp_buffer), "local_velocity: %.2f m/s", vel_mag); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
} |
|
|
|
// Orbital elements |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Eccentricity: %.3f", body->orbit.eccentricity); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
snprintf(temp_buffer, sizeof(temp_buffer), "Semi-major axis: %.2e m", body->orbit.semi_major_axis); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Parent body |
|
if (body->parent_index >= 0 && body->parent_index < sim->body_count) { |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Parent: %s", sim->bodies[body->parent_index].name); |
|
} else { |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Parent: None"); |
|
} |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// SOI |
|
snprintf(temp_buffer, sizeof(temp_buffer), "SOI radius: %.2e m", body->soi_radius); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Draw info text (inside window, leave space for title bar) |
|
Rectangle text_bounds = { |
|
(float)panel_x + 8, |
|
(float)panel_y + 25, |
|
(float)panel_width - 16, |
|
(float)panel_height - 29 |
|
}; |
|
|
|
GuiLabel(text_bounds, info_text); |
|
return; |
|
} |
|
|
|
// Check if a spacecraft is selected |
|
if (ui_state->selected_craft_index >= 0 && ui_state->selected_craft_index < sim->craft_count) { |
|
Spacecraft* craft = &sim->spacecraft[ui_state->selected_craft_index]; |
|
|
|
// Draw window (no close button - panels always shown) |
|
GuiWindowBox(panel_bounds, craft->name); |
|
|
|
// Prepare info text |
|
char info_text[1024]; |
|
char temp_buffer[256]; |
|
|
|
snprintf(info_text, sizeof(info_text), "Name: %s", craft->name); |
|
|
|
snprintf(temp_buffer, sizeof(temp_buffer), "Mass: %.2e kg", craft->mass); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Position (Local) |
|
snprintf(temp_buffer, sizeof(temp_buffer), "local_position: (%.2e, %.2e, %.2e)", |
|
craft->local_position.x, craft->local_position.y, craft->local_position.z); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Velocity (Local) |
|
double vel_mag = vec3_magnitude(craft->local_velocity); |
|
snprintf(temp_buffer, sizeof(temp_buffer), "local_velocity: %.2f m/s", vel_mag); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Parent body |
|
if (craft->parent_index >= 0 && craft->parent_index < sim->body_count) { |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Parent: %s", sim->bodies[craft->parent_index].name); |
|
} else { |
|
snprintf(temp_buffer, sizeof(temp_buffer), "Parent: None"); |
|
} |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Maneuver info |
|
int pending_maneuvers = 0; |
|
int executed_maneuvers = 0; |
|
for (int i = 0; i < sim->maneuver_count; i++) { |
|
if (sim->maneuvers[i].craft_index == ui_state->selected_craft_index) { |
|
if (sim->maneuvers[i].executed) { |
|
executed_maneuvers++; |
|
} else { |
|
pending_maneuvers++; |
|
} |
|
} |
|
} |
|
|
|
snprintf(temp_buffer, sizeof(temp_buffer), "Pending maneuvers: %d", pending_maneuvers); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
snprintf(temp_buffer, sizeof(temp_buffer), "Executed maneuvers: %d", executed_maneuvers); |
|
strcat(info_text, "\n"); |
|
strcat(info_text, temp_buffer); |
|
|
|
// Draw info text (inside window, leave space for title bar) |
|
Rectangle text_bounds = { |
|
(float)panel_x + 8, |
|
(float)panel_y + 25, |
|
(float)panel_width - 16, |
|
(float)panel_height - 29 |
|
}; |
|
|
|
GuiLabel(text_bounds, info_text); |
|
return; |
|
} |
|
} |
|
|
|
void render_maneuver_list_ui(SimulationState* sim, RenderState* render_state, UIState* ui_state) { |
|
if (ui_state->selected_craft_index < 0 || ui_state->selected_craft_index >= sim->craft_count) { |
|
return; |
|
} |
|
|
|
int panel_width = MANEUVER_LIST_WIDTH; |
|
int panel_height = MANEUVER_LIST_HEIGHT; |
|
int panel_x = GetScreenWidth() - panel_width - 10; |
|
int panel_y = MANEUVER_LIST_Y_OFFSET; |
|
|
|
Rectangle panel_bounds = {(float)panel_x, (float)panel_y, (float)panel_width, (float)panel_height}; |
|
|
|
Spacecraft* craft = &sim->spacecraft[ui_state->selected_craft_index]; |
|
|
|
char title[128]; |
|
snprintf(title, sizeof(title), "Maneuvers: %s", craft->name); |
|
|
|
GuiWindowBox(panel_bounds, title); |
|
|
|
int craft_maneuver_count = 0; |
|
for (int i = 0; i < sim->maneuver_count; i++) { |
|
if (sim->maneuvers[i].craft_index == ui_state->selected_craft_index) { |
|
craft_maneuver_count++; |
|
} |
|
} |
|
|
|
if (craft_maneuver_count == 0) { |
|
Rectangle text_bounds = { |
|
(float)panel_x + 8, |
|
(float)panel_y + 25, |
|
(float)panel_width - 16, |
|
(float)panel_height - 29 |
|
}; |
|
GuiLabel(text_bounds, "No maneuvers defined"); |
|
|
|
Rectangle create_btn = {(float)panel_x + 8, (float)panel_y + (float)panel_height - 28, (float)panel_width - 16, 20}; |
|
if (GuiButton(create_btn, "[+] Create Maneuver")) { |
|
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; |
|
ui_state->maneuver_dialog.direction_active = 0; |
|
ui_state->maneuver_dialog.delta_v = 1000.0; |
|
ui_state->maneuver_dialog.trigger_type_active = 0; |
|
ui_state->maneuver_dialog.trigger_value = 0.0; |
|
ui_state->maneuver_dialog.name[0] = '\0'; |
|
ui_state->maneuver_dialog.show_preview = false; |
|
ui_state->maneuver_dialog.show_error = false; |
|
ui_state->maneuver_dialog.error_message[0] = '\0'; |
|
ui_state->maneuver_dialog.show_delete_confirm = false; |
|
ui_state->selected_maneuver_index = -1; |
|
} |
|
return; |
|
} |
|
|
|
int buffer_size = craft_maneuver_count * (64 + 10 + 1) + 1; |
|
char* maneuver_list_text = (char*)malloc(buffer_size); |
|
if (!maneuver_list_text) return; |
|
|
|
maneuver_list_text[0] = '\0'; |
|
int offset = 0; |
|
int maneuver_indices[craft_maneuver_count]; |
|
int maneuver_index_count = 0; |
|
|
|
for (int i = 0; i < sim->maneuver_count; i++) { |
|
if (sim->maneuvers[i].craft_index == ui_state->selected_craft_index) { |
|
maneuver_indices[maneuver_index_count++] = i; |
|
if (offset > 0) { |
|
offset += snprintf(maneuver_list_text + offset, buffer_size - offset, ";"); |
|
} |
|
const char* status_char = sim->maneuvers[i].executed ? "[DONE]" : "[PENDING]"; |
|
offset += snprintf(maneuver_list_text + offset, buffer_size - offset, "%s %s", |
|
status_char, sim->maneuvers[i].name); |
|
} |
|
} |
|
|
|
Rectangle list_bounds = { |
|
(float)panel_x + 4, |
|
(float)panel_y + 25, |
|
(float)panel_width - 8, |
|
(float)panel_height - 280 |
|
}; |
|
|
|
int previous_active = ui_state->maneuver_list_active; |
|
GuiListView(list_bounds, maneuver_list_text, &ui_state->maneuver_list_scroll, &ui_state->maneuver_list_active); |
|
|
|
if (ui_state->maneuver_list_active >= 0 && ui_state->maneuver_list_active != previous_active) { |
|
if (ui_state->maneuver_list_active >= 0 && ui_state->maneuver_list_active < maneuver_index_count) { |
|
ui_state->selected_maneuver_index = maneuver_indices[ui_state->maneuver_list_active]; |
|
} |
|
} |
|
|
|
int preview_y = panel_y + panel_height - 250; |
|
|
|
if (ui_state->selected_maneuver_index >= 0 && ui_state->selected_maneuver_index < sim->maneuver_count) { |
|
Maneuver* selected = &sim->maneuvers[ui_state->selected_maneuver_index]; |
|
|
|
if (selected->craft_index == ui_state->selected_craft_index) { |
|
char selected_text[512]; |
|
char status_str[16]; |
|
snprintf(status_str, sizeof(status_str), selected->executed ? "Executed" : "Pending"); |
|
|
|
snprintf(selected_text, sizeof(selected_text), |
|
"Selected: %s\n\nStatus: %s\nDirection: %s\nΔv: %.1f m/s\n", |
|
selected->name, status_str, get_burn_direction_name(selected->direction), selected->delta_v); |
|
|
|
if (selected->trigger_type == TRIGGER_TIME) { |
|
snprintf(selected_text + strlen(selected_text), sizeof(selected_text) - strlen(selected_text), |
|
"Trigger: Time @ %.1f s", selected->trigger_value); |
|
} else { |
|
snprintf(selected_text + strlen(selected_text), sizeof(selected_text) - strlen(selected_text), |
|
"Trigger: True Anomaly @ %.2f rad", selected->trigger_value); |
|
} |
|
|
|
Rectangle preview_text_bounds = { |
|
(float)panel_x + 8, |
|
(float)preview_y, |
|
(float)panel_width - 16, |
|
80 |
|
}; |
|
GuiLabel(preview_text_bounds, selected_text); |
|
|
|
int orbital_preview_y = preview_y + 85; |
|
|
|
ui_state->maneuver_dialog.craft_index = ui_state->selected_craft_index; |
|
ui_state->maneuver_dialog.direction_active = (int)selected->direction; |
|
ui_state->maneuver_dialog.delta_v = selected->delta_v; |
|
|
|
update_orbital_preview(sim, ui_state); |
|
|
|
if (ui_state->maneuver_dialog.show_preview) { |
|
Rectangle orbital_bounds = { |
|
(float)panel_x + 8, |
|
(float)orbital_preview_y, |
|
(float)panel_width - 16, |
|
110 |
|
}; |
|
render_orbital_preview(sim, ui_state, orbital_bounds); |
|
} |
|
} |
|
} |
|
|
|
int button_row_y = panel_y + panel_height - 50; |
|
|
|
int btn_width = (panel_width - 20) / 4; |
|
|
|
Rectangle create_btn = {(float)panel_x + 10, (float)button_row_y, (float)btn_width, 30}; |
|
if (GuiButton(create_btn, "Create")) { |
|
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; |
|
ui_state->maneuver_dialog.direction_active = 0; |
|
ui_state->maneuver_dialog.delta_v = 1000.0; |
|
ui_state->maneuver_dialog.trigger_type_active = 0; |
|
ui_state->maneuver_dialog.trigger_value = 0.0; |
|
ui_state->maneuver_dialog.name[0] = '\0'; |
|
ui_state->maneuver_dialog.show_preview = false; |
|
ui_state->maneuver_dialog.show_error = false; |
|
ui_state->maneuver_dialog.error_message[0] = '\0'; |
|
ui_state->maneuver_dialog.show_delete_confirm = false; |
|
ui_state->selected_maneuver_index = -1; |
|
} |
|
|
|
Rectangle hohmann_btn = {(float)panel_x + 10 + btn_width, (float)button_row_y, (float)btn_width, 30}; |
|
if (GuiButton(hohmann_btn, "Hohmann...")) { |
|
ui_state->maneuver_dialog.open = true; |
|
ui_state->maneuver_dialog.active_tab = MD_TAB_HOHMANN; |
|
ui_state->maneuver_dialog.craft_index = ui_state->selected_craft_index; |
|
ui_state->maneuver_dialog.edit_maneuver_index = -1; |
|
ui_state->maneuver_dialog.name[0] = '\0'; |
|
ui_state->maneuver_dialog.show_preview = false; |
|
ui_state->maneuver_dialog.show_error = false; |
|
ui_state->maneuver_dialog.error_message[0] = '\0'; |
|
ui_state->maneuver_dialog.show_delete_confirm = false; |
|
} |
|
|
|
Rectangle edit_btn = {(float)panel_x + 10 + btn_width * 2, (float)button_row_y, (float)btn_width, 30}; |
|
if (GuiButton(edit_btn, "Edit")) { |
|
if (ui_state->selected_maneuver_index >= 0 && ui_state->selected_maneuver_index < sim->maneuver_count) { |
|
Maneuver* selected = &sim->maneuvers[ui_state->selected_maneuver_index]; |
|
if (selected->craft_index == ui_state->selected_craft_index) { |
|
load_maneuver_into_dialog(sim, ui_state, ui_state->selected_maneuver_index); |
|
ui_state->maneuver_dialog.open = true; |
|
ui_state->maneuver_dialog.active_tab = MD_TAB_EDIT; |
|
ui_state->maneuver_dialog.show_delete_confirm = false; |
|
} |
|
} |
|
} |
|
|
|
Rectangle delete_btn = {(float)panel_x + 10 + btn_width * 3, (float)button_row_y, (float)btn_width, 30}; |
|
if (GuiButton(delete_btn, "Delete")) { |
|
if (ui_state->selected_maneuver_index >= 0 && ui_state->selected_maneuver_index < sim->maneuver_count) { |
|
Maneuver* selected = &sim->maneuvers[ui_state->selected_maneuver_index]; |
|
if (selected->craft_index == ui_state->selected_craft_index) { |
|
remove_maneuver_by_index(sim, ui_state->selected_maneuver_index); |
|
ui_state->selected_maneuver_index = -1; |
|
ui_state->maneuver_list_active = -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:"); |
|
|
|
if (ui_state->maneuver_dialog.craft_index >= 0 && ui_state->maneuver_dialog.craft_index < sim->craft_count) { |
|
static bool edit_mode_01 = false; |
|
Rectangle dir_combo = {(float)x + 85, (float)current_y, (float)(width - 95), 25}; |
|
const char* directions = "Prograde;Retrograde;Normal;Antinormal;Radial In;Radial Out;Custom"; |
|
if (GuiDropdownBox(dir_combo, directions, &ui_state->maneuver_dialog.direction_active, edit_mode_01)) { |
|
edit_mode_01 = !edit_mode_01; |
|
} |
|
if (edit_mode_01) return; |
|
} |
|
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:"); |
|
|
|
if (ui_state->maneuver_dialog.craft_index >= 0 && ui_state->maneuver_dialog.craft_index < sim->craft_count) { |
|
static bool edit_mode_trigger_create = false; |
|
Rectangle trigger_combo = {(float)x + 85, (float)current_y, (float)(width - 95), 25}; |
|
const char* trigger_types = "Time;True Anomaly"; |
|
if (GuiDropdownBox(trigger_combo, trigger_types, &ui_state->maneuver_dialog.trigger_type_active, edit_mode_trigger_create)) { |
|
edit_mode_trigger_create = !edit_mode_trigger_create; |
|
} |
|
if (edit_mode_trigger_create) return; |
|
} |
|
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_hohmann_tab(SimulationState* sim, UIState* ui_state, int x, int y, int width, int height) { |
|
int current_y = y; |
|
|
|
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) { |
|
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) { |
|
static bool edit_mode_spacecraft = false; |
|
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); |
|
} |
|
|
|
if (GuiDropdownBox({(float)x + 120, (float)(current_y - 35), (float)(width - 130), 25}, |
|
craft_list, &ui_state->maneuver_dialog.craft_index, edit_mode_spacecraft)) { |
|
edit_mode_spacecraft = !edit_mode_spacecraft; |
|
} |
|
free(craft_list); |
|
if (edit_mode_spacecraft) return; |
|
} |
|
} |
|
current_y += 35; |
|
|
|
GuiLabel({(float)x, (float)current_y, 120, 25}, "Transfer Parent:"); |
|
current_y += 35; |
|
|
|
if (sim->body_count > 0) { |
|
static bool edit_mode_transfer_parent = false; |
|
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); |
|
} |
|
|
|
if (GuiDropdownBox({(float)x + 120, (float)(current_y - 35), (float)(width - 130), 25}, |
|
body_list, &ui_state->maneuver_dialog.transfer_body_index, edit_mode_transfer_parent)) { |
|
edit_mode_transfer_parent = !edit_mode_transfer_parent; |
|
} |
|
free(body_list); |
|
if (edit_mode_transfer_parent) return; |
|
} |
|
} |
|
current_y += 35; |
|
|
|
GuiLabel({(float)x, (float)current_y, 120, 25}, "Current Body:"); |
|
current_y += 35; |
|
|
|
if (sim->body_count > 0) { |
|
static bool edit_mode_current_body = false; |
|
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); |
|
} |
|
|
|
if (GuiDropdownBox({(float)x + 120, (float)(current_y - 35), (float)(width - 130), 25}, |
|
body_list, &ui_state->maneuver_dialog.current_body_index, edit_mode_current_body)) { |
|
edit_mode_current_body = !edit_mode_current_body; |
|
} |
|
free(body_list); |
|
if (edit_mode_current_body) return; |
|
} |
|
} |
|
current_y += 35; |
|
|
|
GuiLabel({(float)x, (float)current_y, 120, 25}, "Target Body:"); |
|
current_y += 35; |
|
|
|
if (sim->body_count > 0) { |
|
static bool edit_mode_target_body = false; |
|
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); |
|
} |
|
|
|
if (GuiDropdownBox({(float)x + 120, (float)(current_y - 35), (float)(width - 130), 25}, |
|
body_list, &ui_state->maneuver_dialog.target_body_index, edit_mode_target_body)) { |
|
edit_mode_target_body = !edit_mode_target_body; |
|
} |
|
free(body_list); |
|
if (edit_mode_target_body) return; |
|
} |
|
} |
|
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_error) { |
|
Rectangle error_bounds = {(float)x + 8, (float)current_y, (float)width - 16, 25}; |
|
GuiLabel(error_bounds, TextFormat("Error: %s", ui_state->maneuver_dialog.error_message)); |
|
current_y += 30; |
|
} |
|
|
|
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; |
|
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; |
|
} |
|
|
|
if (ui_state->maneuver_dialog.transfer_body_index < 0 || |
|
ui_state->maneuver_dialog.transfer_body_index >= sim->body_count) { |
|
snprintf(ui_state->maneuver_dialog.error_message, sizeof(ui_state->maneuver_dialog.error_message), |
|
"Invalid transfer parent body selected"); |
|
ui_state->maneuver_dialog.show_error = true; |
|
return; |
|
} |
|
|
|
if (ui_state->maneuver_dialog.current_body_index < 0 || |
|
ui_state->maneuver_dialog.current_body_index >= sim->body_count) { |
|
snprintf(ui_state->maneuver_dialog.error_message, sizeof(ui_state->maneuver_dialog.error_message), |
|
"Invalid current body selected"); |
|
ui_state->maneuver_dialog.show_error = true; |
|
return; |
|
} |
|
|
|
if (ui_state->maneuver_dialog.target_body_index < 0 || |
|
ui_state->maneuver_dialog.target_body_index >= sim->body_count) { |
|
snprintf(ui_state->maneuver_dialog.error_message, sizeof(ui_state->maneuver_dialog.error_message), |
|
"Invalid target body selected"); |
|
ui_state->maneuver_dialog.show_error = true; |
|
return; |
|
} |
|
|
|
if (ui_state->maneuver_dialog.transfer_body_index == ui_state->maneuver_dialog.target_body_index) { |
|
snprintf(ui_state->maneuver_dialog.error_message, sizeof(ui_state->maneuver_dialog.error_message), |
|
"Current and target bodies must be different"); |
|
ui_state->maneuver_dialog.show_error = true; |
|
return; |
|
} |
|
|
|
Spacecraft* craft = &sim->spacecraft[ui_state->maneuver_dialog.craft_index]; |
|
if (craft->parent_index != ui_state->maneuver_dialog.current_body_index) { |
|
snprintf(ui_state->maneuver_dialog.error_message, sizeof(ui_state->maneuver_dialog.error_message), |
|
"Spacecraft is not orbiting the selected current body"); |
|
ui_state->maneuver_dialog.show_error = true; |
|
return; |
|
} |
|
|
|
CelestialBody* transfer_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) { |
|
snprintf(ui_state->maneuver_dialog.error_message, sizeof(ui_state->maneuver_dialog.error_message), |
|
"Current body has invalid orbit (semi-major axis <= 0)"); |
|
ui_state->maneuver_dialog.show_error = true; |
|
return; |
|
} |
|
|
|
if (r2 <= 0) { |
|
snprintf(ui_state->maneuver_dialog.error_message, sizeof(ui_state->maneuver_dialog.error_message), |
|
"Target body has invalid orbit (semi-major axis <= 0)"); |
|
ui_state->maneuver_dialog.show_error = true; |
|
return; |
|
} |
|
|
|
if (transfer_body->mass <= 0) { |
|
snprintf(ui_state->maneuver_dialog.error_message, sizeof(ui_state->maneuver_dialog.error_message), |
|
"Transfer parent has invalid mass"); |
|
ui_state->maneuver_dialog.show_error = true; |
|
return; |
|
} |
|
|
|
ui_state->maneuver_dialog.last_calc = calculate_hohmann_transfer(r1, r2, transfer_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; |
|
} |
|
|
|
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) { |
|
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) { |
|
if (ui_state->maneuver_dialog.show_delete_confirm) { |
|
Rectangle confirm_rect = {(float)x + width / 4, (float)y + height / 3, (float)width / 2, 120}; |
|
|
|
GuiWindowBox(confirm_rect, "Confirm Delete"); |
|
|
|
Rectangle confirm_label = {(float)x + width / 4 + 10, (float)y + height / 3 + 25, (float)width / 2 - 20, 40}; |
|
GuiLabel(confirm_label, "Are you sure you want to delete this maneuver?"); |
|
|
|
Rectangle cancel_confirm_btn = {(float)x + width / 4 + 10, (float)y + height / 3 + 80, 100, 30}; |
|
if (GuiButton(cancel_confirm_btn, "Cancel")) { |
|
ui_state->maneuver_dialog.show_delete_confirm = false; |
|
} |
|
|
|
Rectangle confirm_btn = {(float)x + width - 10 - 110, (float)y + height / 3 + 80, 100, 30}; |
|
if (GuiButton(confirm_btn, "Delete")) { |
|
delete_maneuver_from_dialog(sim, ui_state); |
|
} |
|
} else { |
|
render_maneuver_edit_tab(sim, ui_state, x + 8, y + 60, width - 16, height - 100); |
|
} |
|
} |
|
} |
|
|
|
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; |
|
} |
|
|
|
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 generate_maneuver_name(SimulationState* sim, UIState* ui_state) { |
|
if (strlen(ui_state->maneuver_dialog.name) > 0) { |
|
return; |
|
} |
|
|
|
int burn_num = 0; |
|
for (int i = 0; i < sim->maneuver_count; i++) { |
|
if (sim->maneuvers[i].craft_index == ui_state->maneuver_dialog.craft_index) { |
|
burn_num++; |
|
} |
|
} |
|
|
|
const char* direction_name = get_burn_direction_name((BurnDirection)ui_state->maneuver_dialog.direction_active); |
|
|
|
if (ui_state->maneuver_dialog.trigger_type_active == 0) { |
|
snprintf(ui_state->maneuver_dialog.name, 64, "%s #%d", direction_name, burn_num); |
|
} else { |
|
snprintf(ui_state->maneuver_dialog.name, 64, "%s @ ν=%.2f #%d", |
|
direction_name, ui_state->maneuver_dialog.trigger_value, burn_num); |
|
} |
|
} |
|
|
|
void create_maneuver_from_dialog(SimulationState* sim, UIState* ui_state) { |
|
if (!validate_maneuver_inputs(sim, ui_state)) { |
|
return; |
|
} |
|
|
|
generate_maneuver_name(sim, ui_state); |
|
|
|
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; |
|
} |
|
|
|
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; |
|
} |
|
|
|
void load_maneuver_into_dialog(SimulationState* sim, UIState* ui_state, int maneuver_index) { |
|
if (maneuver_index < 0 || maneuver_index >= sim->maneuver_count) { |
|
return; |
|
} |
|
|
|
Maneuver* maneuver = &sim->maneuvers[maneuver_index]; |
|
|
|
ui_state->maneuver_dialog.edit_maneuver_index = maneuver_index; |
|
ui_state->maneuver_dialog.craft_index = maneuver->craft_index; |
|
strncpy(ui_state->maneuver_dialog.name, maneuver->name, 64); |
|
ui_state->maneuver_dialog.direction_active = (int)maneuver->direction; |
|
ui_state->maneuver_dialog.delta_v = maneuver->delta_v; |
|
ui_state->maneuver_dialog.trigger_type_active = (int)maneuver->trigger_type; |
|
ui_state->maneuver_dialog.trigger_value = maneuver->trigger_value; |
|
ui_state->maneuver_dialog.show_delete_confirm = false; |
|
} |
|
|
|
void render_maneuver_edit_tab(SimulationState* sim, UIState* ui_state, int x, int y, int width, int height) { |
|
int current_y = y; |
|
|
|
if (ui_state->maneuver_dialog.edit_maneuver_index < 0 || |
|
ui_state->maneuver_dialog.edit_maneuver_index >= sim->maneuver_count) { |
|
GuiLabel({(float)x, (float)current_y, (float)width, 25}, "No maneuver selected for editing"); |
|
return; |
|
} |
|
|
|
Maneuver* maneuver = &sim->maneuvers[ui_state->maneuver_dialog.edit_maneuver_index]; |
|
|
|
char title[128]; |
|
snprintf(title, sizeof(title), "Edit: %s", maneuver->name); |
|
GuiLabel({(float)x, (float)current_y, (float)width, 25}, title); |
|
current_y += 30; |
|
|
|
GuiLabel({(float)x, (float)current_y, 80, 25}, "Spacecraft:"); |
|
|
|
if (maneuver->craft_index >= 0 && maneuver->craft_index < sim->craft_count) { |
|
char craft_name[64]; |
|
snprintf(craft_name, sizeof(craft_name), "%s (locked)", sim->spacecraft[maneuver->craft_index].name); |
|
Rectangle craft_label = {(float)x + 85, (float)current_y, (float)(width - 95), 25}; |
|
GuiLabel(craft_label, craft_name); |
|
} |
|
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, true); |
|
current_y += 35; |
|
|
|
Rectangle dir_label = {(float)x, (float)current_y, 80, 25}; |
|
GuiLabel(dir_label, "Direction:"); |
|
|
|
if (ui_state->maneuver_dialog.craft_index >= 0 && ui_state->maneuver_dialog.craft_index < sim->craft_count) { |
|
static bool edit_mode_direction_edit = false; |
|
Rectangle dir_combo = {(float)x + 85, (float)current_y, (float)(width - 95), 25}; |
|
const char* directions = "Prograde;Retrograde;Normal;Antinormal;Radial In;Radial Out;Custom"; |
|
if (GuiDropdownBox(dir_combo, directions, &ui_state->maneuver_dialog.direction_active, edit_mode_direction_edit)) { |
|
edit_mode_direction_edit = !edit_mode_direction_edit; |
|
} |
|
if (edit_mode_direction_edit) return; |
|
} |
|
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:"); |
|
|
|
if (ui_state->maneuver_dialog.craft_index >= 0 && ui_state->maneuver_dialog.craft_index < sim->craft_count) { |
|
static bool edit_mode_trigger_edit = false; |
|
Rectangle trigger_combo = {(float)x + 85, (float)current_y, (float)(width - 95), 25}; |
|
const char* trigger_types = "Time;True Anomaly"; |
|
if (GuiDropdownBox(trigger_combo, trigger_types, &ui_state->maneuver_dialog.trigger_type_active, edit_mode_trigger_edit)) { |
|
edit_mode_trigger_edit = !edit_mode_trigger_edit; |
|
} |
|
if (edit_mode_trigger_edit) return; |
|
} |
|
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); |
|
} |
|
|
|
current_y += 130; |
|
|
|
Rectangle delete_btn = {(float)x, (float)current_y, (float)(width - 10) / 2, 30}; |
|
if (GuiButton(delete_btn, "Delete Maneuver")) { |
|
ui_state->maneuver_dialog.show_delete_confirm = true; |
|
} |
|
} |
|
|
|
void delete_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) { |
|
ui_state->maneuver_dialog.show_delete_confirm = false; |
|
return; |
|
} |
|
|
|
bool success = remove_maneuver_by_index(sim, edit_index); |
|
if (success) { |
|
ui_state->maneuver_dialog.show_delete_confirm = false; |
|
ui_state->maneuver_dialog.open = false; |
|
} |
|
}
|
|
|