mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-08 15:29:48 -10:00
582 lines
17 KiB
C++
582 lines
17 KiB
C++
#include "globalincs/pstypes.h"
|
|
#include "graphics/2d.h"
|
|
#include "io/key.h"
|
|
#include "io/mouse.h"
|
|
#include "lab/renderer/lab_cameras.h"
|
|
#include "lab/labv2_internal.h"
|
|
|
|
|
|
LabCamera::~LabCamera() {
|
|
cam_delete(FS_camera);
|
|
}
|
|
|
|
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<WidgetFaceProjection> 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<WidgetFaceProjection> 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<OrbitCamera::SnapDirection>& excluded)
|
|
{
|
|
const std::array<OrbitCamera::SnapDirection, 6> 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<AdjacentLabel, 4> 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<AdjacentLabel, 4> 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;
|
|
|
|
if (dz > 0) {
|
|
for (int i = 0; i < dz; ++i) {
|
|
distance *= 0.9f;
|
|
}
|
|
} else if (dz < 0) {
|
|
for (int i = 0; i < -dz; ++i) {
|
|
distance *= 1.1f;
|
|
}
|
|
}
|
|
CLAMP(distance, 1.0f, 10000000.0f);
|
|
|
|
if (rmbDown) {
|
|
if (modifierKeys & KEY_SHIFTED) {
|
|
const float pan_factor = distance / 500.0f;
|
|
|
|
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);
|
|
|
|
vec3d view_forward;
|
|
vm_vec_copy_scale(&view_forward, &camera_offset, -1.0f);
|
|
|
|
vec3d world_up = vmd_y_vector;
|
|
vec3d view_right;
|
|
vm_vec_cross(&view_right, &world_up, &view_forward);
|
|
|
|
if (vm_vec_mag_squared(&view_right) <= 1e-6f) {
|
|
world_up = vmd_x_vector;
|
|
vm_vec_cross(&view_right, &world_up, &view_forward);
|
|
}
|
|
|
|
vm_vec_normalize_safe(&view_right);
|
|
|
|
vec3d view_up;
|
|
vm_vec_cross(&view_up, &view_forward, &view_right);
|
|
vm_vec_normalize_safe(&view_up);
|
|
|
|
vm_vec_scale_add2(&pan_offset, &view_right, -dx * pan_factor);
|
|
vm_vec_scale_add2(&pan_offset, &view_up, dy * pan_factor);
|
|
} else {
|
|
theta -= dx / 100.0f;
|
|
phi -= dy / 100.0f;
|
|
|
|
CLAMP(phi, 0.01f, PI - 0.01f);
|
|
}
|
|
}
|
|
|
|
updateCamera();
|
|
}
|
|
|
|
OrbitCamera::WidgetLayout OrbitCamera::getWidgetLayout()
|
|
{
|
|
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();
|
|
|
|
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();
|
|
}
|
|
|
|
float OrbitCamera::getObjectFitDistance()
|
|
{
|
|
static constexpr float distance_multiplier = 1.6f;
|
|
float fit_distance = DEFAULT_DISTANCE;
|
|
|
|
if (getLabManager()->CurrentObject != -1) {
|
|
object* obj = &Objects[getLabManager()->CurrentObject];
|
|
// Ships and Missiles use the object radius to get a camera distance
|
|
fit_distance = obj->radius * distance_multiplier;
|
|
|
|
// Beams use the muzzle radius
|
|
if (obj->type == OBJ_BEAM) {
|
|
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) {
|
|
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();
|
|
}
|
|
|
|
void OrbitCamera::updateCamera() {
|
|
auto cam = FS_camera.getCamera();
|
|
vec3d new_position;
|
|
new_position.xyz.x = sinf(phi) * cosf(theta);
|
|
new_position.xyz.y = cosf(phi);
|
|
new_position.xyz.z = sinf(phi) * sinf(theta);
|
|
|
|
vm_vec_scale(&new_position, distance);
|
|
|
|
vec3d target = vmd_zero_vector;
|
|
if (getLabManager()->CurrentObject != -1) {
|
|
object* obj = &Objects[getLabManager()->CurrentObject];
|
|
target = obj->pos;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
vm_vec_add2(&target, &pan_offset);
|
|
vm_vec_add2(&new_position, &target);
|
|
|
|
cam->set_position(&new_position);
|
|
|
|
// If these are the same then that's not great so do nothing and use the last facing value
|
|
if (!vm_vec_same(&new_position, &target)) {
|
|
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);
|
|
}
|