# Conflicts:
#	docs/parity-checklist.md
This commit is contained in:
Claude
2026-10-11 11:47:25 +00:00
4 changed files with 38 additions and 4 deletions
+4 -2
View File
@@ -528,7 +528,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Drag Crop Handle",
[],
None,
"{handle: topLeft|topRight|bottomRight|bottomLeft|top|right|bottom|left|move | 0..8, from: [x,y], to: [x,y] (normalized, straightened frame), start?: [x0,y0,x1,y1] (default: the current crop)} — drags a crop handle; stops at the image edge, keeps the opposite edges, honours the aspect lock",
"{handle: topLeft|topRight|bottomRight|bottomLeft|top|right|bottom|left|move | 0..8, from: [x,y], to: [x,y] (normalized, straightened frame), start?: [x0,y0,x1,y1] (default: the current crop), keepRatio?: bool (default false)} — drags a crop handle; stops at the image edge, keeps the opposite edges, honours the aspect lock; keepRatio holds the frame's current proportions for this drag even with the lock off (the UI sends it while Shift is held)",
has_active,
|s, p| {
let id = active(s, "crop.drag")?;
@@ -553,11 +553,13 @@ pub fn specs() -> Vec<CommandSpec> {
}
_ => return Err(bad("crop.drag", "`start` needs 4 numbers")),
};
let keep_ratio = bool_or(p, "keepRatio", false);
edit(s, "crop.drag", "Crop", |d| {
let start = CropGeometry { rect: given.unwrap_or(d.crop.geometry.rect), angle: d.crop.geometry.angle };
// A locked crop keeps the shape it has: the lock stores a width:height tuple whose
// orientation is reinterpreted per image, so the frame itself is the truth.
let ratio = d.crop.aspect.and(Some(start.rect_px(w, h).aspect())).filter(|a| a.is_finite() && *a > 0.0);
// `keepRatio` (Shift held) asks for the same thing for one drag without touching the lock.
let ratio = (d.crop.aspect.is_some() || keep_ratio).then(|| start.rect_px(w, h).aspect()).filter(|a| a.is_finite() && *a > 0.0);
d.crop.geometry = drag_crop(start, handle, from, to, w, h, ratio);
Ok(())
})
+30
View File
@@ -557,6 +557,36 @@ fn crop_drag_keeps_a_locked_ratio_on_every_handle() {
}
}
// Feature: Shift while dragging keeps the current proportions (issue 614)
#[test]
fn crop_drag_keep_ratio_holds_the_shape_with_the_lock_off() {
let mut s = demo();
s.execute("crop.set", &json!({"rect": [0.1, 0.2, 0.7, 0.6]})).unwrap();
assert!(active_dev(&s).crop.aspect.is_none(), "lock is off");
let r = active_dev(&s).crop.geometry.rect;
let want = r.aspect();
for (handle, to) in [("left", [r.x0 + 0.1, 0.5]), ("top", [0.5, r.y0 + 0.05]), ("bottomRight", [1.3, 1.3]), ("right", [0.9, 0.5])] {
s.execute("crop.drag", &json!({"handle": handle, "from": [0.5, 0.5], "to": to, "start": [r.x0, r.y0, r.x1, r.y1], "keepRatio": true}))
.unwrap();
let g = active_dev(&s).crop.geometry.rect;
assert!((g.aspect() - want).abs() < 1e-6, "{handle}: {} vs {want}", g.aspect());
assert!(active_dev(&s).crop.aspect.is_none(), "the lock toggle is untouched");
}
}
#[test]
fn crop_drag_without_keep_ratio_and_lock_off_is_free() {
let mut s = demo();
s.execute("crop.set", &json!({"rect": [0.1, 0.2, 0.7, 0.6]})).unwrap();
let r = active_dev(&s).crop.geometry.rect;
let want = r.aspect();
s.execute("crop.drag", &json!({"handle": "right", "from": [0.7, 0.4], "to": [0.9, 0.4], "start": [r.x0, r.y0, r.x1, r.y1]})).unwrap();
assert!((active_dev(&s).crop.geometry.rect.aspect() - want).abs() > 1e-3, "ratio changes freely");
s.execute("crop.drag", &json!({"handle": "right", "from": [0.7, 0.4], "to": [0.9, 0.4], "start": [r.x0, r.y0, r.x1, r.y1], "keepRatio": false}))
.unwrap();
assert!((active_dev(&s).crop.geometry.rect.aspect() - want).abs() > 1e-3, "keepRatio false is the same");
}
#[test]
fn crop_drag_rejects_unknown_handles_and_bad_points() {
let mut s = demo();