--- a/plugins/cube/cube.cpp +++ b/plugins/cube/cube.cpp @@ -493,17 +493,14 @@ 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; + float cube_rotation_radius() + { + // A pose-independent envelope about the geometric center. It keeps + // wide faces clear of the camera without following individual corners. + const bool stack = (mouse_overview_active || render_at_rest) && !vertical_transition; + const float rows = stack ? output->wset()->get_workspace_grid_size().height : 1; + return std::sqrt(identity_z_offset * identity_z_offset + + geometry_aspect * geometry_aspect * 0.25f + rows * rows * 0.25f); } bool refresh_cube_geometry() @@ -1290,7 +1287,7 @@ animation.cube_animation.rotation.set(current_rotation, current_rotation); animation.cube_animation.offset_y.set(current_offset_y, current_offset_y); animation.cube_animation.offset_z.restart_with_end( - zoom_opt + identity_z_offset + front_face_distance()); + zoom_opt + cube_rotation_radius() + front_face_distance()); animation.cube_animation.zoom.set(current_zoom, current_zoom); animation.cube_animation.ease_deformation.restart_with_end(1); @@ -1370,7 +1367,7 @@ } /* Update the view matrix used in the next frame */ - glm::mat4 cube_view_matrix(float zoom, bool nominal_camera = false) + glm::mat4 cube_view_matrix(float zoom) { const float pitch = animation.cube_animation.offset_y; const bool stack = (mouse_overview_active || render_at_rest) && !vertical_transition; @@ -1378,29 +1375,16 @@ 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), + // Rotation must not move the camera. Only entry/exit and explicit + // zoom change its distance; yaw and pitch orbit the same cube center. + const float distance = float(animation.cube_animation.offset_z) + + cube_rotation_radius() * (1.0f / zoom - 1.0f); + auto camera = glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.f, -distance)); - - auto rotation = glm::rotate(glm::mat4(1.0), - (float)animation.cube_animation.offset_y, - glm::vec3(1., 0., 0.)); - - auto view = glm::lookAt(glm::vec3(0., 0., 0.), - glm::vec3(0., 0., -animation.cube_animation.offset_z), - glm::vec3(0., 1., 0.)); - - 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; + auto rotation = glm::rotate(glm::mat4(1.f), pitch, glm::vec3(1, 0, 0)); + auto pivot = glm::translate(glm::mat4(1.f), glm::vec3(0, center, 0)); + auto unpivot = glm::translate(glm::mat4(1.f), glm::vec3(0, -center, 0)); + return camera * pivot * rotation * unpivot; } void update_view_matrix() @@ -1410,32 +1394,18 @@ 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; + // Fit the rotation-invariant envelope once, rather than refitting each + // pose. Otherwise its changing outline creates a second zoom pulse. + // Wheel zoom and the configured camera offset stay independent of fit. + const float radius = cube_rotation_radius(); + const float distance = radius + front_face_distance(); + const float rows = output->wset()->get_workspace_grid_size().height; + const float center = std::abs((rows - 1) / 2 - float(overview_row)); + const float tangent = distance * distance - radius * radius; + const float horizontal = animation.projection[0][0] * radius / std::sqrt(tangent); + const float vertical = animation.projection[1][1] * + (center * distance + radius * std::sqrt(tangent + center * center)) / tangent; + return 0.8f / std::max({1.0f, horizontal, vertical}); } glm::mat4 output_transform(const wf::render_target_t& target)