Search code examples
mathrustgraphicsgeometrylinear-algebra

Rotating a Rectangle Around Its Center by Dragging a Corner with Cursor


I’m implementing a feature for rotating shapes using corner handles. The rotation works fine for square shapes, but I'm facing an issue with non-square shapes (rectangles with aspect ratios different from 1:1). When rotating these shapes by dragging a corner handle, they unexpectedly "snap" or rotate, not accounting for the shape's aspect ratio. This results in a misalignment between the cursor's initial click point and the shape's rotation behavior.

I suspect the problem lies in my rotation calculation, which fails to consider the aspect ratio, leading to incorrect angle offsets during rotation.

How can I correctly calculate the rotation, taking into account the shape's aspect ratio, to ensure smooth and expected behavior when users rotate non-square shapes?

Maybe I’m also trying to solve a problem of a problem so the core problem I want to solve is basically how to rotate a rectangle around the center via corner handles. You can see the behaviour in this video.

Code:

pub fn handle_rotating(
    composition: &CompositionRes,
    selected_nodes_query: &mut Query<
        (
            &mut RelativeTransformMixin,
            &AbsoluteTransformMixin,
            &mut DimensionMixin,
        ),
        With<Selected>,
    >,
    event: &CursorMovedOnComposition,
    corner: u8,
    initial_rotation: f32,
) {
    let CursorMovedOnComposition {
        position: cursor_position,
        ..
    } = event;
    let cursor_position = transform_point_to_view_box(composition, cursor_position, true);

    selected_nodes_query.for_each_mut(
        |(mut relative_transform_mixin, absolute_transform_mixin, dimension_mixin)| {
            let relative_pivot_point = Vec2::new(
                dimension_mixin.width as f32 / 2.0,
                dimension_mixin.height as f32 / 2.0,
            );
            let absolute_pivot_point =
                apply_transform_to_point(absolute_transform_mixin.0, relative_pivot_point);

            // Determine rotation offset based on corner
            let rotation_offset_in_radians: f32 = match corner {
                _ if corner == (HandleSide::Top as u8 | HandleSide::Left as u8) => {
                    (-135.0 as f32).to_radians()
                }
                _ if corner == (HandleSide::Top as u8 | HandleSide::Right as u8) => {
                    (-45.0 as f32).to_radians()
                }
                _ if corner == (HandleSide::Bottom as u8 | HandleSide::Right as u8) => {
                    (45.0 as f32).to_radians()
                }
                _ if corner == (HandleSide::Bottom as u8 | HandleSide::Left as u8) => {
                    (135.0 as f32).to_radians()
                }
                _ => 0.0,
            };

            // Calculate rotation based on the corner
            let rotation_angle =
                calculate_rotation(initial_rotation, &cursor_position, &absolute_pivot_point);
            let final_rotation_angle =
                rotation_angle + rotation_offset_in_radians - initial_rotation;
            relative_transform_mixin.0 = set_rotation(
                relative_transform_mixin.0,
                final_rotation_angle,
                relative_pivot_point,
            );
        },
    );
}

fn calculate_rotation(
    initial_angle_in_radians: f32,
    cursor_point: &Vec2,
    pivot_point: &Vec2,
) -> f32 {
    // Calculate the angle from the pivot point to the current cursor position
    let current_angle = (cursor_point.y - pivot_point.y).atan2(cursor_point.x - pivot_point.x);

    // Calculate the raw angle difference
    let angle_diff = current_angle - initial_angle_in_radians;

    return -angle_diff;
}

Solution

  • The rotation_offset_in_radians values are set wrong. The angles corresponding to the various corners assume that the shape is a square, and thus, as seen from the center, the corners are spaced 90 degrees apart. This is not true in a rectangle:

    Angles in a square, angles in a rectangle

    What you need to do is to use the aspect ratio of the rectangle (or its actual dimensions, makes no difference), in combination with e.g. atan2 to figure out the angle corresponding to each corner.

    This gives the same angles as the original code when the width and height are the same:

    let width: f32 = dimension_mixin.width as f32;
    let height: f32 = dimension_mixin.height as f32;
    let rotation_offset_in_radians: f32 = match corner {
        _ if corner == (HandleSide::Top as u8 | HandleSide::Left as u8) => f32::atan2(-height, -width),
        _ if corner == (HandleSide::Top as u8 | HandleSide::Right as u8) => f32::atan2(-height, width),
        _ if corner == (HandleSide::Bottom as u8 | HandleSide::Right as u8) => f32::atan2(height, width),
        _ if corner == (HandleSide::Bottom as u8 | HandleSide::Left as u8) => f32::atan2(height, -width),
        _ => 0.0,
    };