mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 12:08:10 -06:00
LR-CROP-STRAIGHTEN: an Angle field in the Crop panel for an exact rotation (#534)
The issue asks for a numeric field for an exact, repeatable rotation. Typing into the Straighten value worked, but it was hidden behind a hover hint. The Crop panel now has an Angle field under Straighten (a drag value in degrees, −45° to 45°, two decimals): click and type, or drag it. A drag is one undo step, like the slider's (begin / endInteraction). "Angle" is translated in all seven locales. Headless tests: typing -3.25 into the field sets exactly -3.25°; a six-step drag on it adds one undo step (it added five before the interaction was wrapped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e88be40e8c
commit
b401b56da4
@@ -1888,5 +1888,6 @@
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions",
|
||||
"Click to type a value": "Klicken, um einen Wert einzugeben"
|
||||
"Click to type a value": "Klicken, um einen Wert einzugeben",
|
||||
"Angle": "Winkel"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,6 @@
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions",
|
||||
"Click to type a value": "Hacer clic para escribir un valor"
|
||||
"Click to type a value": "Hacer clic para escribir un valor",
|
||||
"Angle": "Ángulo"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,6 @@
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions",
|
||||
"Click to type a value": "クリックして値を入力"
|
||||
"Click to type a value": "クリックして値を入力",
|
||||
"Angle": "角度"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,6 @@
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions",
|
||||
"Click to type a value": "Clique para digitar um valor"
|
||||
"Click to type a value": "Clique para digitar um valor",
|
||||
"Angle": "Ângulo"
|
||||
}
|
||||
|
||||
@@ -1406,5 +1406,6 @@
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions",
|
||||
"Click to type a value": "Щёлкните, чтобы ввести значение"
|
||||
"Click to type a value": "Щёлкните, чтобы ввести значение",
|
||||
"Angle": "Угол"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,6 @@
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions",
|
||||
"Click to type a value": "点击以输入数值"
|
||||
"Click to type a value": "点击以输入数值",
|
||||
"Angle": "角度"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,6 @@
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions",
|
||||
"Click to type a value": "按一下以輸入數值"
|
||||
"Click to type a value": "按一下以輸入數值",
|
||||
"Angle": "角度"
|
||||
}
|
||||
|
||||
@@ -185,6 +185,32 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
if let Some(spec) = ang {
|
||||
let out = slider(ui, &spec, d.crop.geometry.angle, true, Some("Straighten"));
|
||||
super::edit::apply_slider_out(app, &spec, out, |app, v| app.run("crop.straighten", json!({"angle": v})));
|
||||
// an exact angle, typed (issue #534): a field of its own, besides the slider's value
|
||||
padded(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(crate::i18n::tr("Angle"));
|
||||
let mut angle = d.crop.geometry.angle;
|
||||
let r = ui.add(
|
||||
egui::DragValue::new(&mut angle)
|
||||
.range(spec.min..=spec.max)
|
||||
.clamp_existing_to_range(false)
|
||||
.speed(0.05)
|
||||
.fixed_decimals(2)
|
||||
.suffix("°"),
|
||||
);
|
||||
crate::widgets::register(ui.ctx(), "cropAngleField", r.rect);
|
||||
// a drag on the field is one undo step, like the slider's
|
||||
if r.drag_started() {
|
||||
let _ = app.run("develop.beginInteraction", json!({"label": "Straighten"}));
|
||||
}
|
||||
if r.changed() && angle.is_finite() {
|
||||
let _ = app.run("crop.straighten", json!({"angle": (angle.clamp(spec.min, spec.max) * 100.0).round() / 100.0}));
|
||||
}
|
||||
if r.drag_stopped() {
|
||||
let _ = app.run("develop.endInteraction", json!({}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
padded(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -128,6 +128,25 @@ fn command_held_shows_the_straighten_crosshair() {
|
||||
assert!(!has(&h, "cropRotateCursor"));
|
||||
}
|
||||
|
||||
/// The Crop panel has an angle field: click it, type an exact angle, Return.
|
||||
#[test]
|
||||
fn the_crop_panel_has_an_angle_field() {
|
||||
let mut h = crop_tool();
|
||||
let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
for key in ["a", "Backspace"] {
|
||||
let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
}
|
||||
let r = h.request("ui.text", json!({"text": "-3.25"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
let r = h.request("ui.key", json!({"key": "Enter"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
assert!((angle(&h) + 3.25).abs() < 1e-9, "typed -3.25°, got {}", angle(&h));
|
||||
}
|
||||
|
||||
/// Rotating with the pointer at the canvas's corner keeps the angle readout on the canvas (and so
|
||||
/// on screen), rather than spilling past its edge.
|
||||
#[test]
|
||||
@@ -147,3 +166,15 @@ fn the_readout_stays_on_the_canvas() {
|
||||
let readout = h.app.widgets.iter().find(|(w, _)| w == "cropAngleReadout").map(|(_, r)| *r).expect("the readout while rotating");
|
||||
assert!(canvas.contains_rect(readout), "{readout:?} in {canvas:?}");
|
||||
}
|
||||
|
||||
/// Dragging the angle field changes the angle as one undo step, like the slider.
|
||||
#[test]
|
||||
fn dragging_the_angle_field_is_one_undo_step() {
|
||||
let mut h = crop_tool();
|
||||
let undo = h.app.session.undo.len();
|
||||
let r = h.request("ui.dragWidget", json!({"id": "cropAngleField", "dx": 40.0, "dy": 0.0, "steps": 6}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
assert!(angle(&h) > 0.0, "dragged right: {}", angle(&h));
|
||||
assert_eq!(h.app.session.undo.len(), undo + 1, "one step for the whole drag");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user