diff --git a/code/lab/dialogs/lab_ui.cpp b/code/lab/dialogs/lab_ui.cpp index 2fe96bc72ad..c3eccc7f873 100644 --- a/code/lab/dialogs/lab_ui.cpp +++ b/code/lab/dialogs/lab_ui.cpp @@ -301,11 +301,15 @@ void LabUi::build_options_menu() { with_Menu("Options") { + bool show_widget_menu = getLabManager()->Renderer->getShowOrientationWidget(); MenuItem("Render options", nullptr, &show_render_options_dialog); MenuItem("Object selector", nullptr, &show_object_selection_dialog); MenuItem("Background selector", nullptr, &show_background_selection_dialog); MenuItem("Object options", nullptr, &show_object_options_dialog); MenuItem("Controls reference", nullptr, &show_controls_reference_dialog); + if (MenuItem("Show orientation cube widget", nullptr, show_widget_menu)) { + getLabManager()->Renderer->setShowOrientationWidget(!show_widget_menu); + } MenuItem("Reset View", nullptr, &reset_view); MenuItem("Close lab", "ESC", &close_lab); } @@ -521,6 +525,7 @@ void LabUi::show_render_options() float emissive_factor = ltp::lab_get_emissive(); float exposure_level = ltp::current_exposure(); auto ppcv = ltp::lab_get_ppc(); + show_orientation_widget = getLabManager()->Renderer->getShowOrientationWidget(); bool skip_setting_light_options_this_frame = false; @@ -570,6 +575,7 @@ void LabUi::show_render_options() Checkbox("Hide particles", &no_particles); Checkbox("Render as wireframe", &use_wireframe_rendering); Checkbox("Orthographic projection", &use_orthographic_projection); + Checkbox("Show orientation cube widget", &show_orientation_widget); Checkbox("Render without light", &no_lighting); Checkbox("Render with emissive lighting", &show_emissive_lighting); SliderFloat("Light brightness", &light_factor, 0.0f, 10.0f); @@ -690,6 +696,7 @@ void LabUi::show_render_options() getLabManager()->Renderer->setRenderFlag(LabRenderFlag::MoveSubsystems, animate_subsystems); getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoParticles, no_particles); getLabManager()->Renderer->setRenderFlag(LabRenderFlag::UseOrthographicProjection, use_orthographic_projection); + getLabManager()->Renderer->setShowOrientationWidget(show_orientation_widget); getLabManager()->Renderer->setEmissiveFactor(emissive_factor); getLabManager()->Renderer->setAmbientFactor(ambient_factor); getLabManager()->Renderer->setLightFactor(light_factor); diff --git a/code/lab/dialogs/lab_ui.h b/code/lab/dialogs/lab_ui.h index 9d6bac80c9d..3a29f939a71 100644 --- a/code/lab/dialogs/lab_ui.h +++ b/code/lab/dialogs/lab_ui.h @@ -122,6 +122,7 @@ class LabUi { bool show_emissive_lighting = false; bool show_particles = true; bool use_orthographic_projection = false; + bool show_orientation_widget = true; std::optional volumetrics_pos_backup = std::nullopt; }; \ No newline at end of file diff --git a/code/lab/manager/lab_manager.cpp b/code/lab/manager/lab_manager.cpp index bf0d546caf7..ed585aa813c 100644 --- a/code/lab/manager/lab_manager.cpp +++ b/code/lab/manager/lab_manager.cpp @@ -126,13 +126,28 @@ void LabManager::onFrame(float frametime) { int dx, dy, dz; mouse_get_delta(&dx, &dy); mouse_get_wheel_delta(nullptr, &dz); + int mouse_x = 0; + int mouse_y = 0; + mouse_get_pos(&mouse_x, &mouse_y); + + const bool lmb_down = mouse_down(MOUSE_LEFT_BUTTON) != 0; + bool lmb_pressed = lmb_down && !LastLmbDown; + LastLmbDown = lmb_down; + + if (lmb_pressed && ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { + lmb_pressed = false; + } + if (dz != 0 && ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { dz = 0; } - Renderer->getCurrentCamera()->handleInput(dx, dy, dz, mouse_down(MOUSE_LEFT_BUTTON) != 0, mouse_down(MOUSE_RIGHT_BUTTON) != 0, key_get_shift_status()); + auto& current_camera = Renderer->getCurrentCamera(); + current_camera->handleInput( + dx, dy, dz, lmb_down, lmb_pressed, mouse_down(MOUSE_RIGHT_BUTTON) != 0, key_get_shift_status(), mouse_x, mouse_y); - if (!Renderer->getCurrentCamera()->handlesObjectPlacement()) { - if (mouse_down(MOUSE_LEFT_BUTTON)) { + if (!current_camera->handlesObjectPlacement()) { + const bool over_camera_overlay = Renderer->getShowOrientationWidget() && current_camera->isOverlayHit(mouse_x, mouse_y); + if (lmb_down && !over_camera_overlay) { angles rot_angle; vm_extract_angles_matrix_alternate(&rot_angle, &CurrentOrientation); diff --git a/code/lab/manager/lab_manager.h b/code/lab/manager/lab_manager.h index 7dfaf36b232..dbfc44e44c6 100644 --- a/code/lab/manager/lab_manager.h +++ b/code/lab/manager/lab_manager.h @@ -157,6 +157,7 @@ class LabManager { //float Lab_thrust_len = 0.0f; // Unused bool Weapons_loaded = false; bool CloseThis = false; + bool LastLmbDown = false; LabUi labUi; void changeShipInternal(); diff --git a/code/lab/renderer/lab_cameras.cpp b/code/lab/renderer/lab_cameras.cpp index 519a3fe0574..82c1023d303 100644 --- a/code/lab/renderer/lab_cameras.cpp +++ b/code/lab/renderer/lab_cameras.cpp @@ -1,4 +1,5 @@ #include "globalincs/pstypes.h" +#include "graphics/2d.h" #include "io/key.h" #include "io/mouse.h" #include "lab/renderer/lab_cameras.h" @@ -9,7 +10,293 @@ LabCamera::~LabCamera() { cam_delete(FS_camera); } -void OrbitCamera::handleInput(int dx, int dy, int dz, bool, bool rmbDown, int modifierKeys) { +namespace { +bool point_in_rect(int x, int y, int rect_x, int rect_y, int rect_w, int rect_h) +{ + return x >= rect_x && x < rect_x + rect_w && y >= rect_y && y < rect_y + rect_h; +} + +struct WidgetFaceProjection { + OrbitCamera::SnapDirection direction; + const char* label; + vec3d center_world; + vec3d normal_world; + vec3d corners_world[4]; + int corners_screen_x[4] = {0, 0, 0, 0}; + int corners_screen_y[4] = {0, 0, 0, 0}; + int center_screen_x = 0; + int center_screen_y = 0; + float center_depth = 0.0f; + bool visible = false; +}; + +const char* snap_direction_label(OrbitCamera::SnapDirection direction) +{ + switch (direction) { + case OrbitCamera::SnapDirection::Front: + return "Front"; + case OrbitCamera::SnapDirection::Back: + return "Back"; + case OrbitCamera::SnapDirection::Top: + return "Top"; + case OrbitCamera::SnapDirection::Bottom: + return "Bottom"; + case OrbitCamera::SnapDirection::Left: + return "Left"; + case OrbitCamera::SnapDirection::Right: + return "Right"; + default: + return ""; + } +} + +vec3d snap_direction_normal(OrbitCamera::SnapDirection direction) +{ + switch (direction) { + case OrbitCamera::SnapDirection::Front: + return vm_vec_new(0.0f, 0.0f, 1.0f); + case OrbitCamera::SnapDirection::Back: + return vm_vec_new(0.0f, 0.0f, -1.0f); + case OrbitCamera::SnapDirection::Top: + return vm_vec_new(0.0f, 1.0f, 0.0f); + case OrbitCamera::SnapDirection::Bottom: + return vm_vec_new(0.0f, -1.0f, 0.0f); + case OrbitCamera::SnapDirection::Left: + return vm_vec_new(-1.0f, 0.0f, 0.0f); + case OrbitCamera::SnapDirection::Right: + return vm_vec_new(1.0f, 0.0f, 0.0f); + default: + return vmd_zero_vector; + } +} + +bool point_in_convex_quad(int x, int y, const int quad_x[4], const int quad_y[4]) +{ + int sign = 0; + + for (int i = 0; i < 4; ++i) { + const int next = (i + 1) % 4; + const int edge_x = quad_x[next] - quad_x[i]; + const int edge_y = quad_y[next] - quad_y[i]; + const int to_point_x = x - quad_x[i]; + const int to_point_y = y - quad_y[i]; + const int cross = edge_x * to_point_y - edge_y * to_point_x; + + if (cross == 0) { + continue; + } + + const int current_sign = (cross > 0) ? 1 : -1; + if (sign == 0) { + sign = current_sign; + } else if (sign != current_sign) { + return false; + } + } + + return true; +} + +void get_orbit_view_basis(float phi, float theta, vec3d& forward, vec3d& right, vec3d& up) +{ + vec3d camera_offset; + camera_offset.xyz.x = sinf(phi) * cosf(theta); + camera_offset.xyz.y = cosf(phi); + camera_offset.xyz.z = sinf(phi) * sinf(theta); + vm_vec_normalize_safe(&camera_offset); + + vm_vec_copy_scale(&forward, &camera_offset, -1.0f); + + vec3d world_up = vmd_y_vector; + vm_vec_cross(&right, &world_up, &forward); + if (vm_vec_mag_squared(&right) <= 1e-6f) { + world_up = vmd_x_vector; + vm_vec_cross(&right, &world_up, &forward); + } + vm_vec_normalize_safe(&right); + + vm_vec_cross(&up, &forward, &right); + vm_vec_normalize_safe(&up); +} + +SCP_vector build_widget_faces(const vec3d& forward, const vec3d& right, const vec3d& up, int center_x, int center_y, int half_size_px) +{ + const float cube_half = 1.0f; + + SCP_vector faces; + faces.reserve(6); + faces.push_back({OrbitCamera::SnapDirection::Front, + "Front", + vm_vec_new(0.0f, 0.0f, cube_half), + vm_vec_new(0.0f, 0.0f, 1.0f), + {vm_vec_new(-cube_half, cube_half, cube_half), + vm_vec_new(cube_half, cube_half, cube_half), + vm_vec_new(cube_half, -cube_half, cube_half), + vm_vec_new(-cube_half, -cube_half, cube_half)}}); + faces.push_back({OrbitCamera::SnapDirection::Back, + "Back", + vm_vec_new(0.0f, 0.0f, -cube_half), + vm_vec_new(0.0f, 0.0f, -1.0f), + {vm_vec_new(cube_half, cube_half, -cube_half), + vm_vec_new(-cube_half, cube_half, -cube_half), + vm_vec_new(-cube_half, -cube_half, -cube_half), + vm_vec_new(cube_half, -cube_half, -cube_half)}}); + faces.push_back({OrbitCamera::SnapDirection::Top, + "Top", + vm_vec_new(0.0f, cube_half, 0.0f), + vm_vec_new(0.0f, 1.0f, 0.0f), + {vm_vec_new(-cube_half, cube_half, -cube_half), + vm_vec_new(cube_half, cube_half, -cube_half), + vm_vec_new(cube_half, cube_half, cube_half), + vm_vec_new(-cube_half, cube_half, cube_half)}}); + faces.push_back({OrbitCamera::SnapDirection::Bottom, + "Bottom", + vm_vec_new(0.0f, -cube_half, 0.0f), + vm_vec_new(0.0f, -1.0f, 0.0f), + {vm_vec_new(-cube_half, -cube_half, cube_half), + vm_vec_new(cube_half, -cube_half, cube_half), + vm_vec_new(cube_half, -cube_half, -cube_half), + vm_vec_new(-cube_half, -cube_half, -cube_half)}}); + faces.push_back({OrbitCamera::SnapDirection::Left, + "Left", + vm_vec_new(-cube_half, 0.0f, 0.0f), + vm_vec_new(-1.0f, 0.0f, 0.0f), + {vm_vec_new(-cube_half, cube_half, -cube_half), + vm_vec_new(-cube_half, cube_half, cube_half), + vm_vec_new(-cube_half, -cube_half, cube_half), + vm_vec_new(-cube_half, -cube_half, -cube_half)}}); + faces.push_back({OrbitCamera::SnapDirection::Right, + "Right", + vm_vec_new(cube_half, 0.0f, 0.0f), + vm_vec_new(1.0f, 0.0f, 0.0f), + {vm_vec_new(cube_half, cube_half, cube_half), + vm_vec_new(cube_half, cube_half, -cube_half), + vm_vec_new(cube_half, -cube_half, -cube_half), + vm_vec_new(cube_half, -cube_half, cube_half)}}); + + vec3d camera_dir; + vm_vec_copy_scale(&camera_dir, &forward, -1.0f); + + for (auto& face : faces) { + face.visible = vm_vec_dot(&face.normal_world, &camera_dir) > 0.0f; + + vec3d center_view; + center_view.xyz.x = vm_vec_dot(&face.center_world, &right); + center_view.xyz.y = vm_vec_dot(&face.center_world, &up); + center_view.xyz.z = vm_vec_dot(&face.center_world, &camera_dir); + face.center_screen_x = center_x + fl2i(center_view.xyz.x * half_size_px); + face.center_screen_y = center_y - fl2i(center_view.xyz.y * half_size_px); + face.center_depth = center_view.xyz.z; + + for (int i = 0; i < 4; ++i) { + vec3d view; + view.xyz.x = vm_vec_dot(&face.corners_world[i], &right); + view.xyz.y = vm_vec_dot(&face.corners_world[i], &up); + view.xyz.z = vm_vec_dot(&face.corners_world[i], &camera_dir); + face.corners_screen_x[i] = center_x + fl2i(view.xyz.x * half_size_px); + face.corners_screen_y[i] = center_y - fl2i(view.xyz.y * half_size_px); + } + } + + std::sort(faces.begin(), faces.end(), [](const WidgetFaceProjection& a, const WidgetFaceProjection& b) { + return a.center_depth < b.center_depth; + }); + + return faces; +} + +OrbitCamera::SnapDirection pick_direction_for_axis(const vec3d& axis, const SCP_vector& excluded) +{ + const std::array all_dirs = {OrbitCamera::SnapDirection::Front, + OrbitCamera::SnapDirection::Back, + OrbitCamera::SnapDirection::Top, + OrbitCamera::SnapDirection::Bottom, + OrbitCamera::SnapDirection::Left, + OrbitCamera::SnapDirection::Right}; + + float best_dot = -FLT_MAX; + auto best_dir = OrbitCamera::SnapDirection::Front; + + for (auto dir : all_dirs) { + if (std::find(excluded.begin(), excluded.end(), dir) != excluded.end()) { + continue; + } + + const auto normal = snap_direction_normal(dir); + const float dir_dot = vm_vec_dot(&normal, &axis); + if (dir_dot > best_dot) { + best_dot = dir_dot; + best_dir = dir; + } + } + + return best_dir; +} + +struct AdjacentLabel { + OrbitCamera::SnapDirection direction; + const char* label; + int x = 0; + int y = 0; + int w = 0; + int h = 0; +}; + +std::array build_adjacent_labels(const vec3d& forward, const vec3d& right, const vec3d& up, int center_x, int center_y, int cube_half_px) +{ + vec3d camera_dir; + vm_vec_copy_scale(&camera_dir, &forward, -1.0f); + + const auto primary = pick_direction_for_axis(camera_dir, {}); + const auto opposite = pick_direction_for_axis(vm_vec_new(-camera_dir.xyz.x, -camera_dir.xyz.y, -camera_dir.xyz.z), {primary}); + + const auto up_dir = pick_direction_for_axis(up, {primary, opposite}); + const auto down_dir = pick_direction_for_axis(vm_vec_new(-up.xyz.x, -up.xyz.y, -up.xyz.z), {primary, opposite, up_dir}); + const auto right_dir = pick_direction_for_axis(right, {primary, opposite, up_dir, down_dir}); + const auto left_dir = + pick_direction_for_axis(vm_vec_new(-right.xyz.x, -right.xyz.y, -right.xyz.z), {primary, opposite, up_dir, down_dir, right_dir}); + + static constexpr int padding_x = 5; + static constexpr int padding_y = 2; + static constexpr int margin = 6; + + std::array labels = {{ + {up_dir, snap_direction_label(up_dir)}, + {down_dir, snap_direction_label(down_dir)}, + {left_dir, snap_direction_label(left_dir)}, + {right_dir, snap_direction_label(right_dir)}, + }}; + + for (auto& label : labels) { + int text_w = 0; + int text_h = 0; + gr_get_string_size(&text_w, &text_h, label.label); + label.w = text_w + (padding_x * 2); + label.h = text_h + (padding_y * 2); + } + + labels[0].x = center_x - (labels[0].w / 2); + labels[0].y = center_y - cube_half_px - margin - labels[0].h; + + labels[1].x = center_x - (labels[1].w / 2); + labels[1].y = center_y + cube_half_px + margin; + + labels[2].x = center_x - cube_half_px - margin - labels[2].w; + labels[2].y = center_y - (labels[2].h / 2); + + labels[3].x = center_x + cube_half_px + margin; + labels[3].y = center_y - (labels[3].h / 2); + + return labels; +} +} + +void OrbitCamera::handleInput( + int dx, int dy, int dz, bool, bool lmbPressed, bool rmbDown, int modifierKeys, int mouseX, int mouseY) { + if (getLabManager()->Renderer->getShowOrientationWidget() && lmbPressed && handleOrientationWidgetClick(mouseX, mouseY)) { + return; + } + if (dx == 0 && dy == 0 && dz == 0) return; @@ -64,43 +351,136 @@ void OrbitCamera::handleInput(int dx, int dy, int dz, bool, bool rmbDown, int mo updateCamera(); } -void OrbitCamera::resetView() +OrbitCamera::WidgetLayout OrbitCamera::getWidgetLayout() { - phi = DEFAULT_PHI; - theta = DEFAULT_THETA; - distance = DEFAULT_DISTANCE; + const int size = WIDGET_CUBE_HALF_SIZE * 4; + const int left = gr_screen.center_offset_x + gr_screen.center_w - size - WIDGET_MARGIN; + const int top = gr_screen.center_offset_y + WIDGET_MARGIN; + return {size, left, top, left + size / 2, top + size / 2, WIDGET_CUBE_HALF_SIZE}; +} + +bool OrbitCamera::handleOrientationWidgetClick(int mouseX, int mouseY) +{ + const auto layout = getWidgetLayout(); + + vec3d forward, right, up; + get_orbit_view_basis(phi, theta, forward, right, up); + + const auto faces = build_widget_faces(forward, right, up, layout.center_x, layout.center_y, layout.cube_half); + for (auto it = faces.rbegin(); it != faces.rend(); ++it) { + if (!it->visible) { + continue; + } + + if (point_in_convex_quad(mouseX, mouseY, it->corners_screen_x, it->corners_screen_y)) { + snapToDirection(it->direction); + return true; + } + } + + const auto adjacent_labels = build_adjacent_labels(forward, right, up, layout.center_x, layout.center_y, layout.cube_half); + return std::any_of(adjacent_labels.begin(), adjacent_labels.end(), [&](const AdjacentLabel& label) { + if (point_in_rect(mouseX, mouseY, label.x, label.y, label.w, label.h)) { + snapToDirection(label.direction); + return true; + } + return false; + }); +} + +bool OrbitCamera::isOverlayHit(int mouseX, int mouseY) const +{ + const auto layout = getWidgetLayout(); + if (point_in_rect(mouseX, mouseY, layout.left, layout.top, layout.size, layout.size)) { + return true; + } + + vec3d forward, right, up; + get_orbit_view_basis(phi, theta, forward, right, up); + const auto adjacent_labels = build_adjacent_labels(forward, right, up, layout.center_x, layout.center_y, layout.cube_half); + return std::any_of(adjacent_labels.begin(), adjacent_labels.end(), [&](const AdjacentLabel& label) { + return point_in_rect(mouseX, mouseY, label.x, label.y, label.w, label.h); + }); +} + +void OrbitCamera::snapToDirection(SnapDirection direction) +{ + static constexpr float POLE_EPSILON = 0.01f; + pan_offset = vmd_zero_vector; + distance = getObjectFitDistance(); - displayedObjectChanged(); + switch (direction) { + case SnapDirection::Front: + phi = PI_2; + theta = PI_2; + break; + case SnapDirection::Back: + phi = PI_2; + theta = -PI_2; + break; + case SnapDirection::Top: + phi = POLE_EPSILON; + break; + case SnapDirection::Bottom: + phi = PI - POLE_EPSILON; + break; + case SnapDirection::Left: + phi = PI_2; + theta = PI; + break; + case SnapDirection::Right: + phi = PI_2; + theta = 0.0f; + break; + } + + updateCamera(); } -void OrbitCamera::displayedObjectChanged() { - float distance_multiplier = 1.6f; +float OrbitCamera::getObjectFitDistance() +{ + static constexpr float distance_multiplier = 1.6f; + float fit_distance = DEFAULT_DISTANCE; if (getLabManager()->CurrentObject != -1) { object* obj = &Objects[getLabManager()->CurrentObject]; - - // Reset camera panning - pan_offset = vmd_zero_vector; - // Ships and Missiles use the object radius to get a camera distance - distance = obj->radius * distance_multiplier; + fit_distance = obj->radius * distance_multiplier; // Beams use the muzzle radius if (obj->type == OBJ_BEAM) { - weapon_info* wip = &Weapon_info[Beams[obj->instance].weapon_info_index]; - if (wip != nullptr) { - distance = wip->b_info.beam_muzzle_radius * distance_multiplier; + const int wip_idx = Beams[obj->instance].weapon_info_index; + if (wip_idx >= 0) { + fit_distance = Weapon_info[wip_idx].b_info.beam_muzzle_radius * distance_multiplier; } // Lasers use the laser length } else if (obj->type == OBJ_WEAPON) { - weapon_info* wip = &Weapon_info[Weapons[obj->instance].weapon_info_index]; - if (wip != nullptr && wip->render_type == WRT_LASER) { - distance = wip->laser_length * distance_multiplier; + const int wip_idx = Weapons[obj->instance].weapon_info_index; + if (wip_idx >= 0 && Weapon_info[wip_idx].render_type == WRT_LASER) { + fit_distance = Weapon_info[wip_idx].laser_length * distance_multiplier; } } } + return fit_distance; +} + +void OrbitCamera::resetView() +{ + phi = DEFAULT_PHI; + theta = DEFAULT_THETA; + distance = DEFAULT_DISTANCE; + pan_offset = vmd_zero_vector; + + displayedObjectChanged(); +} + +void OrbitCamera::displayedObjectChanged() { + // Reset camera panning + pan_offset = vmd_zero_vector; + distance = getObjectFitDistance(); + updateCamera(); } @@ -113,16 +493,19 @@ void OrbitCamera::updateCamera() { vm_vec_scale(&new_position, distance); - object* obj = &Objects[getLabManager()->CurrentObject]; - vec3d target = obj->pos; + vec3d target = vmd_zero_vector; + if (getLabManager()->CurrentObject != -1) { + object* obj = &Objects[getLabManager()->CurrentObject]; + target = obj->pos; - if (obj->type == OBJ_WEAPON) { - weapon_info* wip = &Weapon_info[Weapons[obj->instance].weapon_info_index]; - if (wip != nullptr && wip->render_type == WRT_LASER) { - // Offset target by half the laser length forward along the facing - vec3d forward; - vm_vec_copy_normalize(&forward, &obj->orient.vec.fvec); - vm_vec_scale_add2(&target, &forward, wip->laser_length * 0.5f); + if (obj->type == OBJ_WEAPON) { + const int wip_idx = Weapons[obj->instance].weapon_info_index; + if (wip_idx >= 0 && Weapon_info[wip_idx].render_type == WRT_LASER) { + // Offset target by half the laser length forward along the facing + vec3d fwd; + vm_vec_copy_normalize(&fwd, &obj->orient.vec.fvec); + vm_vec_scale_add2(&target, &fwd, Weapon_info[wip_idx].laser_length * 0.5f); + } } } @@ -136,3 +519,63 @@ void OrbitCamera::updateCamera() { cam->set_rotation_facing(&target); } } + +void OrbitCamera::renderOverlay() const +{ + const auto layout = getWidgetLayout(); + + color background; + gr_init_alphacolor(&background, 24, 24, 24, 96); + gr_set_color_fast(&background); + gr_rect(layout.left, layout.top, layout.size, layout.size, GR_RESIZE_NONE); + + int mouse_x = 0; + int mouse_y = 0; + mouse_get_pos(&mouse_x, &mouse_y); + + vec3d forward, right, up; + get_orbit_view_basis(phi, theta, forward, right, up); + const auto faces = build_widget_faces(forward, right, up, layout.center_x, layout.center_y, layout.cube_half); + for (const auto& face : faces) { + if (!face.visible) { + continue; + } + + const bool hovered = point_in_convex_quad(mouse_x, mouse_y, face.corners_screen_x, face.corners_screen_y); + + color edge_color; + gr_init_alphacolor(&edge_color, hovered ? 255 : 210, hovered ? 255 : 210, hovered ? 255 : 210, hovered ? 255 : 170); + gr_set_color_fast(&edge_color); + + for (int i = 0; i < 4; ++i) { + const int next = (i + 1) % 4; + gr_line(face.corners_screen_x[i], face.corners_screen_y[i], face.corners_screen_x[next], face.corners_screen_y[next], GR_RESIZE_NONE); + } + + int text_w = 0; + int text_h = 0; + gr_get_string_size(&text_w, &text_h, face.label); + gr_set_color_fast(hovered ? &Color_white : &Color_grey); + gr_string(face.center_screen_x - (text_w / 2), face.center_screen_y - (text_h / 2), face.label, GR_RESIZE_NONE); + } + + const auto adjacent_labels = build_adjacent_labels(forward, right, up, layout.center_x, layout.center_y, layout.cube_half); + for (const auto& label : adjacent_labels) { + const bool hovered = point_in_rect(mouse_x, mouse_y, label.x, label.y, label.w, label.h); + + color label_bg; + gr_init_alphacolor(&label_bg, 24, 24, 24, hovered ? 180 : 120); + gr_set_color_fast(&label_bg); + gr_rect(label.x, label.y, label.w, label.h, GR_RESIZE_NONE); + + gr_set_color_fast(hovered ? &Color_white : &Color_silver); + gr_string(label.x + 5, label.y + 2, label.label, GR_RESIZE_NONE); + } + + const char* subtitle = "Camera orientation"; + int subtitle_w = 0; + int subtitle_h = 0; + gr_get_string_size(&subtitle_w, &subtitle_h, subtitle); + gr_set_color_fast(&Color_silver); + gr_string(layout.center_x - (subtitle_w / 2), layout.top + layout.size + 8, subtitle, GR_RESIZE_NONE); +} diff --git a/code/lab/renderer/lab_cameras.h b/code/lab/renderer/lab_cameras.h index c6be8caa769..12965c0b36f 100644 --- a/code/lab/renderer/lab_cameras.h +++ b/code/lab/renderer/lab_cameras.h @@ -30,10 +30,12 @@ class LabCamera { /// Mouse delta on the x axis /// Mouse delta on the y axis /// Mouse wheel delta - /// State of the left mouse button + /// Whether the left mouse button is currently held + /// Whether the left mouse button was pressed this frame (rising edge only) /// State of the right mouse button /// State of the various modifier keys. See keys.h - virtual void handleInput(int dx, int dy, int dz, bool lmbDown, bool rmbDown, int modifierKeys) = 0; + virtual void handleInput( + int dx, int dy, int dz, bool lmbDown, bool lmbPressed, bool rmbDown, int modifierKeys, int mouseX, int mouseY) = 0; /// /// Called by the lab manager when the displayed object changes @@ -57,10 +59,18 @@ class LabCamera { /// Returns the distance from the camera to the point of interest, used to size the orthographic frustum. virtual float getCameraDistance() const { return 0.0f; } + + /// Render any 2D overlays associated with this camera (e.g. orientation widgets). + virtual void renderOverlay() const {} + + /// Returns true if the given screen-space point is over an interactive camera overlay control. + virtual bool isOverlayHit(int /*mouseX*/, int /*mouseY*/) const { return false; } }; class OrbitCamera : public LabCamera { public: + enum class SnapDirection { Front, Back, Top, Bottom, Left, Right }; + OrbitCamera() : LabCamera(cam_create("Lab orbit camera")) {} SCP_string getUsageInfo() override { @@ -77,7 +87,8 @@ class OrbitCamera : public LabCamera { return ss.str(); } - void handleInput(int dx, int dy, int dz, bool /*lmbDown*/, bool rmbDown, int modifierKeys) override; + void handleInput( + int dx, int dy, int dz, bool /*lmbDown*/, bool lmbPressed, bool rmbDown, int modifierKeys, int mouseX, int mouseY) override; void resetView() override; @@ -88,12 +99,26 @@ class OrbitCamera : public LabCamera { void updateCamera() override; float getCameraDistance() const override { return distance; } + void renderOverlay() const override; + bool isOverlayHit(int mouseX, int mouseY) const override; private: + static constexpr int WIDGET_CUBE_HALF_SIZE = 30; + static constexpr int WIDGET_MARGIN = 14; + static constexpr float DEFAULT_DISTANCE = 100.0f; static constexpr float DEFAULT_PHI = 1.24f; static constexpr float DEFAULT_THETA = 2.25f; + struct WidgetLayout { + int size, left, top, center_x, center_y, cube_half; + }; + static WidgetLayout getWidgetLayout(); + + bool handleOrientationWidgetClick(int mouseX, int mouseY); + void snapToDirection(SnapDirection direction); + static float getObjectFitDistance(); + float distance = DEFAULT_DISTANCE; float phi = DEFAULT_PHI; float theta = DEFAULT_THETA; diff --git a/code/lab/renderer/lab_renderer.cpp b/code/lab/renderer/lab_renderer.cpp index a3f9edb861b..eec2c2d1813 100644 --- a/code/lab/renderer/lab_renderer.cpp +++ b/code/lab/renderer/lab_renderer.cpp @@ -368,6 +368,10 @@ void LabRenderer::renderHud(float) { "Current Team Color: %s", currentTeamColor.c_str()); } + + if (showOrientationWidget) { + labCamera->renderOverlay(); + } } void LabRenderer::useBackground(const SCP_string& mission_name) { diff --git a/code/lab/renderer/lab_renderer.h b/code/lab/renderer/lab_renderer.h index 2ea8fc02536..a15bd1320e2 100644 --- a/code/lab/renderer/lab_renderer.h +++ b/code/lab/renderer/lab_renderer.h @@ -172,6 +172,8 @@ class LabRenderer { void resetView(); void setRenderFlag(LabRenderFlag flag, bool value) { renderFlags.set(flag, value); } + void setShowOrientationWidget(bool value) { showOrientationWidget = value; } + bool getShowOrientationWidget() const { return showOrientationWidget; } static float setAmbientFactor(float factor) { ltp::lab_set_ambient(factor); @@ -227,6 +229,7 @@ class LabRenderer { SCP_string currentTeamColor; std::unique_ptr labCamera; + bool showOrientationWidget = true; float cameraDistance;