Integration: lock a Shift selection drag to multiples of 45° (#6254)

Photoshop constrains a Shift-mid-drag selection move to multiples of 45°
("Move a selection border"), and the Move tool's drag of selected pixels
already uses TRANSFORM_DIRECTIONS (8). selection_drag_to used
MOVE_DIRECTIONS (4), so a near-diagonal Shift drag (delta 13,11) snapped
to the horizontal axis (13,0) instead of 45° (12,12).

Test: shift_constrains_a_selection_drag_to_an_axis gains a diagonal case
(fails with 4 directions); the floating-drag test moves to (34,22) so it
stays an axis case with 8 directions.

Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Mann
2026-10-22 21:56:36 +02:00
co-authored by mroxso Claude Opus 5.5
parent b7eabd4990
commit 4efebbe2bf
3 changed files with 15 additions and 11 deletions
+2 -2
View File
@@ -267,7 +267,7 @@ pub struct Drag {
/// every Move-tool drag with a selection does (`move_ui::moves_selected_pixels_with`).
pub sel_move: Option<bool>,
/// The last ⇧-locked offset of a selection-tool drag of the selection (`move_mods`, #6231);
/// keeps the locked axis stable between events. The Move tool's own drags use `move_mods`.
/// keeps the locked direction stable between events. The Move tool's own drags use `move_mods`.
pub sel_axis: Option<[f64; 2]>,
pub lasso: Option<crate::lasso_ui::Lasso>,
}
@@ -5724,7 +5724,7 @@ fn tool_move(app: &mut PhotocraftApp, x: f64, y: f64, pressure: f32, mods: egui:
// ⇧: straight 0/45/90° strokes, 45° gradient angles (stroke_constraint.rs).
let last = d.points.last().map_or(d.start, |p| [p[0], p[1]]);
let [x, y] = crate::stroke_constraint::constrain(d.tool, &mut d.constrain, d.start, last, [x, y], mods.shift, zoom);
// ⇧: a selection-tool drag of the selection locks to the dominant axis (#6231). The Move
// ⇧: a selection-tool drag of the selection locks to multiples of 45° (#6231). The Move
// tool's own drags were already locked by `move_mods::filter_event`.
let [x, y] = crate::move_mods::selection_drag_to(d, [x, y], mods.shift && tool != Tool::Move);
let moved = d.points.last().is_none_or(|p| (p[0] - x).abs() + (p[1] - y).abs() > 0.25);
+8 -5
View File
@@ -655,8 +655,8 @@ fn drag_inside_the_selection_moves_the_outline() {
assert_eq!(sel(&app), None);
}
/// #6231: ⇧ held during a selection-tool drag of the selection locks the move to the dominant
/// axis (⇧ at the press still means add-to-selection, so the lock only applies mid-drag).
/// #6231: ⇧ held during a selection-tool drag of the selection locks the move to multiples of
/// 45° (⇧ at the press still means add-to-selection, so the lock only applies mid-drag).
#[test]
fn shift_constrains_a_selection_drag_to_an_axis() {
use crate::canvas::{ToolEvent, tool_event};
@@ -674,9 +674,12 @@ fn shift_constrains_a_selection_drag_to_an_axis() {
// Without ⇧ the drag is free (the outline already sits at (30,26,50,46)).
drag(&mut app, [40.0, 30.0], [50.0, 20.0], Modifiers::NONE);
assert_eq!(sel(&app), Some(Rect::new(40, 16, 60, 36)), "plain drag moves both axes");
// A near-diagonal ⇧-drag locks to 45°, as in Photoshop: (13,11) projects onto (12,12).
drag_shift(&mut app, [50.0, 26.0], [63.0, 37.0]);
assert_eq!(sel(&app), Some(Rect::new(52, 28, 72, 48)), "diagonal drag locks to 45°");
}
/// #6231: ⇧ also locks a ⌘-drag of the selected pixels to the dominant axis.
/// #6231: ⇧ also locks a ⌘-drag of the selected pixels to multiples of 45°.
#[test]
fn shift_constrains_a_floating_selection_drag() {
use crate::canvas::{ToolEvent, tool_event};
@@ -684,8 +687,8 @@ fn shift_constrains_a_floating_selection_drag() {
let offset = |app: &PhotocraftApp| photocraft_engine::float_cmds::floating(app.session.active().unwrap()).map(|f| f.offset);
let cmd_shift = Modifiers::COMMAND | Modifiers::SHIFT;
tool_event(&mut app, ToolEvent::Down { x: 20.0, y: 20.0, pressure: 1.0 }, Modifiers::COMMAND);
tool_event(&mut app, ToolEvent::Move { x: 34.0, y: 28.0, pressure: 1.0 }, cmd_shift);
tool_event(&mut app, ToolEvent::Up { x: 34.0, y: 28.0 }, cmd_shift);
tool_event(&mut app, ToolEvent::Move { x: 34.0, y: 22.0, pressure: 1.0 }, cmd_shift);
tool_event(&mut app, ToolEvent::Up { x: 34.0, y: 22.0 }, cmd_shift);
assert_eq!(offset(&app), Some((14, 0)), "locked to the horizontal axis");
app.run("select.drop", json!({})).unwrap();
}
+5 -4
View File
@@ -152,18 +152,19 @@ fn drag_to(app: &mut PhotocraftApp, p: [f64; 2], mods: egui::Modifiers) -> [f64;
[start[0] + d[0], start[1] + d[1]]
}
/// ⇧ while a selection tool drags the selection (#6231): lock the move to the dominant axis, as
/// the Move tool's layer drags do. The caller skips this when [`filter_event`] has already
/// ⇧ while a selection tool drags the selection (#6231): lock the move to multiples of 45°, as
/// Photoshop does ("Move a selection border") and as the Move tool's drags of selected pixels do
/// ([`TRANSFORM_DIRECTIONS`]). The caller skips this when [`filter_event`] has already
/// rewritten the event (the Move tool itself); it covers the `sel_move` drags the marquee and
/// lasso tools start, including ⌘-drags of the selected pixels. The locked offset is kept on the
/// drag between events so the axis doesn't flicker near 45°.
/// drag between events so the direction doesn't flicker between two neighbours.
pub(crate) fn selection_drag_to(d: &mut crate::canvas::Drag, p: [f64; 2], shift: bool) -> [f64; 2] {
if d.sel_move.is_none() || !shift {
d.sel_axis = None;
return p;
}
let delta = [p[0] - d.start[0], p[1] - d.start[1]];
let locked = constrain(delta, d.sel_axis, MOVE_DIRECTIONS);
let locked = constrain(delta, d.sel_axis, TRANSFORM_DIRECTIONS);
d.sel_axis = Some(locked);
[d.start[0] + locked[0], d.start[1] + locked[1]]
}