--- a/plugins/cube/cube.cpp +++ b/plugins/cube/cube.cpp @@ -407,6 +407,7 @@ float identity_z_offset = 0.0f; int geometry_faces = 0; int geometry_rows = 0; + float geometry_aspect = 1.0f; bool vertical_transition = false; bool mouse_overview_active = false; @@ -485,19 +486,43 @@ (get_num_faces() == 2 ? 1e-3f : 0.0f) : Z_OFFSET_NEAR; } + float output_aspect_ratio() + { + const auto geometry = output->get_relative_geometry(); + return std::max(1.0f, float(geometry.width)) / + std::max(1.0f, float(geometry.height)); + } + + float cube_front_extent(float yaw) + { + float extent = 0; + for (int face = 0; face < get_num_faces(); face++) + { + const float angle = face * animation.side_angle + yaw; + extent = std::max(extent, identity_z_offset * std::cos(angle) + + geometry_aspect * 0.5f * std::abs(std::sin(angle))); + } + + return extent; + } + bool refresh_cube_geometry() { auto grid = output->wset()->get_workspace_grid_size(); int faces = grid.width; - if ((faces == geometry_faces) && (grid.height == geometry_rows)) + const float aspect = output_aspect_ratio(); + if ((faces == geometry_faces) && (grid.height == geometry_rows) && + (aspect == geometry_aspect)) { return false; } float old_offset = identity_z_offset; + geometry_aspect = aspect; + animation.projection = glm::perspective(45.0f, aspect, 0.1f, 100.f); animation.side_angle = 2 * M_PI / float(faces); identity_z_offset = faces == 1 ? 0.0f : - 0.5 / std::tan(animation.side_angle / 2); + 0.5f * aspect / std::tan(animation.side_angle / 2); if (geometry_faces) { // Preserve the camera's distance from the cube when a new @@ -668,7 +693,7 @@ #endif } - animation.projection = glm::perspective(45.0f, 1.f, 0.1f, 100.f); + animation.projection = glm::perspective(45.0f, output_aspect_ratio(), 0.1f, 100.f); } wf::signal::connection_t on_cube_control = [=] (cube_control_signal *d) @@ -1345,10 +1370,25 @@ } /* Update the view matrix used in the next frame */ - void update_view_matrix() - { + glm::mat4 cube_view_matrix(float zoom, bool nominal_camera = false) + { + const float pitch = animation.cube_animation.offset_y; + const bool stack = (mouse_overview_active || render_at_rest) && !vertical_transition; + const float rows = stack ? output->wset()->get_workspace_grid_size().height : 1; + const float row = mouse_overview_active ? float(overview_row) : + output->wset()->get_current_workspace().y; + const float center = stack ? ((rows - 1) / 2 - row) / zoom : 0; + // Keep the nearest corner in front of the camera as the regular + // polygon turns. Anchor zoom to that surface, rather than scaling + // a wide cube through a stationary camera. + const float support = std::cos(pitch) * + cube_front_extent(animation.cube_animation.rotation) + + std::abs(std::sin(pitch)) * rows * 0.5f; + const float base = nominal_camera ? identity_z_offset + front_face_distance() : + float(animation.cube_animation.offset_z); + const float distance = base + support / zoom - identity_z_offset; auto zoom_translate = glm::translate(glm::mat4(1.f), - glm::vec3(0.f, 0.f, -animation.cube_animation.offset_z)); + glm::vec3(0.f, 0.f, -distance)); auto rotation = glm::rotate(glm::mat4(1.0), (float)animation.cube_animation.offset_y, @@ -1358,24 +1398,44 @@ glm::vec3(0., 0., -animation.cube_animation.offset_z), glm::vec3(0., 1., 0.)); - animation.view = zoom_translate * rotation * view; - if ((mouse_overview_active || render_at_rest) && !vertical_transition) - { - const float pitch = animation.cube_animation.offset_y; - const float zoom = animation.cube_animation.zoom; - const float rows = output->wset()->get_workspace_grid_size().height; - const float row = mouse_overview_active ? float(overview_row) : - output->wset()->get_current_workspace().y; - const float center = ((rows - 1) / 2 - row) / zoom; - auto pivot = glm::translate(glm::mat4(1.0), glm::vec3(0, center, 0)); - auto unpivot = glm::translate(glm::mat4(1.0), glm::vec3(0, -center, 0)); - // Orbit the complete stack about its middle. Move the camera back - // as its height turns into depth, keeping the near faces visible. - const float distance = float(animation.cube_animation.offset_z) + - std::abs(std::sin(pitch)) * rows / (2 * zoom); - zoom_translate = glm::translate(glm::mat4(1.0), glm::vec3(0, 0, -distance)); - animation.view = zoom_translate * pivot * rotation * unpivot * view; - } + auto pivot = glm::translate(glm::mat4(1.0), glm::vec3(0, center, 0)); + auto unpivot = glm::translate(glm::mat4(1.0), glm::vec3(0, -center, 0)); + return zoom_translate * pivot * rotation * unpivot * view; + } + + void update_view_matrix() + { + animation.view = cube_view_matrix(animation.cube_animation.zoom); + } + + float overview_fit() + { + // Measure the projected outline with neutral wheel zoom and camera + // distance. Fitting model dimensions again would double-count the + // camera's pullback, making tilted wide desktops unnecessarily tiny. + // User zoom and camera distance remain free to change the fitted size. + auto vp = animation.projection * cube_view_matrix(1.0f, true); + const float last = output->wset()->get_workspace_grid_size().height - 1; + float extent = 1.0f; + for (float row : {0.0f, last}) + { + for (int face = 0; face < get_num_faces(); face++) + { + auto matrix = vp * calculate_model_matrix(face, row - float(overview_row)); + for (float x : {-0.5f, 0.5f}) + { + for (float y : {-0.5f, 0.5f}) + { + auto corner = matrix * glm::vec4(x, y, 0, 1); + const float w = std::max(1e-3f, corner.w); + extent = std::max(extent, std::max(std::abs(corner.x), + std::abs(corner.y)) / w); + } + } + } + } + + return 0.8f / extent; } glm::mat4 output_transform(const wf::render_target_t& target) @@ -1394,10 +1454,7 @@ { // Fit every row while keeping the selected row centered. Scale // the complete projected stack uniformly, preserving face shape. - float center = overview_row; - float last = output->wset()->get_workspace_grid_size().height - 1; - float extent = 2 * std::max(center, last - center) + 1; - float fit = 0.8f / std::max(1.0f, extent); + float fit = overview_fit(); float scale = 1 + float(overview_mix) * (fit - 1); auto frame = glm::scale(glm::mat4(1.0), glm::vec3(scale, scale, 1)); // Overview rows share one camera and horizontal axis. Their @@ -1428,7 +1485,11 @@ auto translation = glm::translate(glm::mat4(1.0), glm::vec3(0, row_offset, identity_z_offset + additional_z)); - return rotation * translation; + // One workspace is aspect units wide and one unit tall. Its regular + // polygon radius uses the same width, so four faces form a square + // footprint without stretching the desktop textures. + auto face_size = glm::scale(glm::mat4(1.0), glm::vec3(geometry_aspect, 1, 1)); + return rotation * translation * face_size; } /* Render the sides of the cube, using the given culling mode - cw or ccw */