mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
LR-CROP-AUTO: Auto straighten turns by at most 10° on at least 3 lines, and says when it finds nothing
Measured on 68 photos straightened by hand in Lightroom Classic and 300 left as shot, Auto straighten turned 79 of the 300 by more than 1° (65 by more than 2°): the edges of planks, a branch or a bird's wing were taken for the horizon. It now answers only for at most 10° and on at least 3 agreeing lines (Upright Level is unchanged): 19 of the 300, with the same 14 of the 68 levelled within 0.5° and 1 instead of 3 more than 1° off. When it finds nothing, the app says so instead of leaving the photo without a word. LR-CROP-AUTO goes to 🟡: it still leaves most tilted photos alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
479a05a0be
commit
693c35931d
@@ -488,3 +488,39 @@ fn level_degrees_sign_matches_crop_angle() {
|
||||
let deg = crate::upright::level_degrees(&src, &SourceInfo::default(), &DevelopSettings::default()).expect("lines found");
|
||||
assert!((deg + 6.0).abs() < 0.5, "{deg}");
|
||||
}
|
||||
|
||||
/// A "horizon" of `deg` degrees: sky above, below it bands of `band` px (0: one edge only).
|
||||
fn tilted_scene(deg: f32, band: f32) -> Rgb32f {
|
||||
let (w, h) = (900usize, 600usize);
|
||||
let t = deg.to_radians().tan();
|
||||
Rgb32f::from_fn(w, h, |x, y| {
|
||||
let yh = h as f32 / 2.0 + (x as f32 - w as f32 / 2.0) * t;
|
||||
if (y as f32) < yh {
|
||||
[0.6, 0.7, 0.9]
|
||||
} else if band > 0.0 && (((y as f32 - yh) / band) as i32) % 2 == 1 {
|
||||
[0.1, 0.2, 0.3]
|
||||
} else {
|
||||
[0.05, 0.1, 0.2]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Auto straighten acts on several agreeing lines only: one edge (the frame of a window, a branch)
|
||||
/// turned straight photos in a measurement on real ones, so it is left alone.
|
||||
#[test]
|
||||
fn auto_straighten_needs_more_than_one_line() {
|
||||
let level = |src: &Rgb32f| crate::upright::level_degrees(src, &SourceInfo::default(), &DevelopSettings::default());
|
||||
assert_eq!(level(&tilted_scene(4.0, 0.0)), None, "one edge");
|
||||
let deg = level(&tilted_scene(4.0, 30.0)).expect("a horizon and the bands below it");
|
||||
assert!((deg + 4.0).abs() < 0.5, "{deg}");
|
||||
}
|
||||
|
||||
/// Auto straighten turns by at most `STRAIGHTEN_MAX_DEGREES`: larger "levels" came from texture
|
||||
/// (planks, a bird's wing) on photos that were straight.
|
||||
#[test]
|
||||
fn auto_straighten_leaves_steep_lines_alone() {
|
||||
let level = |src: &Rgb32f| crate::upright::level_degrees(src, &SourceInfo::default(), &DevelopSettings::default());
|
||||
assert_eq!(level(&tilted_scene(20.0, 30.0)), None, "20° is not a horizon to level");
|
||||
let deg = level(&tilted_scene(8.0, 30.0)).expect("8° is");
|
||||
assert!((deg + 8.0).abs() < 0.5, "{deg}");
|
||||
}
|
||||
|
||||
@@ -375,6 +375,12 @@ fn focal_from_vps(v: [f64; 3], h: [f64; 3]) -> Option<f64> {
|
||||
|
||||
/// Pure rotation (degrees, clockwise +) that levels the dominant lines.
|
||||
fn level_angle(vert: &[Segment], horiz: &[Segment]) -> Option<f64> {
|
||||
level_angle_within(vert, horiz, 90.0, 1)
|
||||
}
|
||||
|
||||
/// [`level_angle`] that only answers for at most `max` degrees, backed by at least `min_lines`
|
||||
/// agreeing segments.
|
||||
fn level_angle_within(vert: &[Segment], horiz: &[Segment], max: f64, min_lines: usize) -> Option<f64> {
|
||||
let dev = |s: &Segment, vertical: bool| {
|
||||
let (dx, dy) = (s.b.x - s.a.x, s.b.y - s.a.y);
|
||||
// signed deviation in degrees of the segment from the axis
|
||||
@@ -406,8 +412,9 @@ fn level_angle(vert: &[Segment], horiz: &[Segment]) -> Option<f64> {
|
||||
})?;
|
||||
// consensus: most of the line length must agree with the median within 2°, otherwise the "lines" are scene
|
||||
// texture (tree flanks, ridges) rather than a horizon or plumb lines
|
||||
let agree: f64 = v.iter().filter(|(d, _)| (d - median.0).abs() <= 2.0).map(|x| x.1).sum();
|
||||
(agree >= 0.5 * total).then_some(-median.0)
|
||||
let agreeing: Vec<_> = v.iter().filter(|(d, _)| (d - median.0).abs() <= 2.0).collect();
|
||||
let agree: f64 = agreeing.iter().map(|x| x.1).sum();
|
||||
(agree >= 0.5 * total && agreeing.len() >= min_lines && median.0.abs() <= max).then_some(-median.0)
|
||||
}
|
||||
|
||||
fn rot_z_h(deg: f64) -> Homography {
|
||||
@@ -569,11 +576,27 @@ pub fn analyze_source(src: &Rgb32f, info: &SourceInfo, s: &DevelopSettings) -> A
|
||||
a
|
||||
}
|
||||
|
||||
/// The most Auto straighten turns a photo (degrees). Measured on 68 photos straightened by hand
|
||||
/// in Lightroom Classic and 300 left as shot: none was turned by more than 6°, while a larger
|
||||
/// "level" came from texture (wooden planks, a branch, a bird's wing) and turned photos that were
|
||||
/// straight.
|
||||
pub const STRAIGHTEN_MAX_DEGREES: f64 = 10.0;
|
||||
|
||||
/// The fewest agreeing lines Auto straighten acts on: one line (the frame's edge, a branch) said
|
||||
/// nothing about the horizon in that measurement.
|
||||
pub const STRAIGHTEN_MIN_LINES: usize = 3;
|
||||
|
||||
/// The rotation (degrees, clockwise +) that levels `src`'s dominant horizon/plumb lines, if they agree
|
||||
/// (the consensus rule of Upright Level). Used by the crop panel's Auto straighten.
|
||||
/// (the consensus rule of Upright Level), within [`STRAIGHTEN_MAX_DEGREES`] and backed by at least
|
||||
/// [`STRAIGHTEN_MIN_LINES`] lines. Used by the crop panel's Auto straighten.
|
||||
pub fn level_degrees(src: &Rgb32f, info: &SourceInfo, s: &DevelopSettings) -> Option<f64> {
|
||||
let a = analyze_source(src, info, s);
|
||||
level_angle(&a.vertical, &a.horizontal)
|
||||
straighten_angle(&a)
|
||||
}
|
||||
|
||||
/// [`level_degrees`] of an analysis.
|
||||
pub fn straighten_angle(a: &Analysis) -> Option<f64> {
|
||||
level_angle_within(&a.vertical, &a.horizontal, STRAIGHTEN_MAX_DEGREES, STRAIGHTEN_MIN_LINES)
|
||||
}
|
||||
|
||||
/// The automatic Upright correction for `mode` on `src` (identity for Off and Guided).
|
||||
|
||||
@@ -445,6 +445,7 @@
|
||||
"Download the SAM 3 Model?": "SAM-3-Modell herunterladen?",
|
||||
"Downloads": "Downloads",
|
||||
"Drag along the horizon; double-click for Auto": "Entlang des Horizonts ziehen; Doppelklick für Automatik",
|
||||
"No horizon or vertical lines to level by": "Kein Horizont und keine senkrechten Linien zum Ausrichten gefunden",
|
||||
"Drag over an eye on the photo; the pupil inside is found automatically.": "Ziehe über ein Auge im Foto; die Pupille darin wird automatisch gefunden.",
|
||||
"Draw Guides": "Hilfslinien zeichnen",
|
||||
"Duplicate": "Duplizieren",
|
||||
|
||||
@@ -812,6 +812,18 @@ impl LightcraftApp {
|
||||
self.toast_for(ctx, text, TOAST_SECONDS);
|
||||
}
|
||||
|
||||
/// Auto straighten (the crop panel's Auto, a double-click with the Straighten tool), saying so
|
||||
/// when it finds nothing to level by instead of leaving the photo as it is without a word.
|
||||
pub fn auto_straighten(&mut self, ctx: &egui::Context) {
|
||||
match self.run("crop.autoStraighten", serde_json::json!({})) {
|
||||
Ok(r) if r.get("changed").and_then(Value::as_bool) == Some(false) => {
|
||||
self.toast(ctx, crate::i18n::tr("No horizon or vertical lines to level by"));
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => self.toast(ctx, e),
|
||||
}
|
||||
}
|
||||
|
||||
/// A toast that stays `secs` seconds (messages that say where to look or what to do next).
|
||||
pub fn toast_for(&mut self, ctx: &egui::Context, text: impl Into<String>, secs: f64) {
|
||||
let t = ctx.input(|i| i.time);
|
||||
|
||||
@@ -3047,7 +3047,7 @@ mod preview_geometry_tests {
|
||||
fn straighten_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response, held: bool) {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
if !held && resp.double_clicked() {
|
||||
let _ = app.run("crop.autoStraighten", json!({}));
|
||||
app.auto_straighten(ui.ctx());
|
||||
app.ui.tool.clear();
|
||||
app.gesture = None;
|
||||
return;
|
||||
|
||||
@@ -317,7 +317,7 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
app.ui.tool = if on { String::new() } else { "straighten".into() };
|
||||
}
|
||||
if text_button(ui, "straightenAuto", crate::i18n::tr("Auto"), false).clicked() {
|
||||
let _ = app.run("crop.autoStraighten", json!({}));
|
||||
app.auto_straighten(ui.ctx());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -454,3 +454,31 @@ fn a_drag_from_just_inside_a_handle_resizes_not_moves() {
|
||||
assert!(r.x0 > 0.0, "the left edge moved in: {r:?}");
|
||||
assert!((r.x1 - 1.0).abs() < 1e-6, "the right edge stays: {r:?}");
|
||||
}
|
||||
|
||||
/// Auto on a photo without a horizon or plumb lines says so, instead of leaving the photo as it is
|
||||
/// without a word.
|
||||
#[test]
|
||||
fn auto_straighten_says_when_it_finds_nothing() {
|
||||
let dir = std::env::temp_dir().join(format!("lc-ui-auto-straighten-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let img = lightcraft_raster::Rgba8 { width: 64, height: 48, data: vec![[90, 120, 160, 255]; 64 * 48] };
|
||||
let bytes = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
|
||||
let path = dir.join("flat.png");
|
||||
std::fs::write(&path, bytes).unwrap();
|
||||
let mut session = lightcraft_engine::Session::new().with_fs();
|
||||
session.execute("library.import", &json!({"paths": [path.to_string_lossy()]})).unwrap();
|
||||
let app = LightcraftApp::new(session, Services { png: None, ..Default::default() });
|
||||
let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
|
||||
for (method, params) in [("ui.set", json!({"view": "detail"})), ("engine.execute", json!({"command": "panel.crop"}))] {
|
||||
let r = h.request(method, params, T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
}
|
||||
h.settle(SETTLE);
|
||||
let r = h.request("ui.clickWidget", json!({"id": "button:straightenAuto"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
assert!(h.app.ui.toast.as_ref().is_some_and(|t| t.0 == "No horizon or vertical lines to level by"), "{:?}", h.app.ui.toast);
|
||||
assert_eq!(angle(&h), 0.0);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Lightroom parity checklist
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (LR-CROP-AUTO: Auto straighten turns by at most 10° on at least 3 agreeing lines and says when it finds nothing; ✅ → 🟡; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
|
||||
The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we
|
||||
have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good
|
||||
@@ -33,7 +33,7 @@ cited here still exists. Formerly docs/parity.md.
|
||||
| E. Metadata (META) | 5 | 1 | 0 | 0 | 2/2 (100%) | 2/2 (100%) |
|
||||
| F. Edit panel — global adjustments (EDIT) | 44 | 2 | 3 | 1 | 28/28 (100%) | 13/14 (93%) |
|
||||
| G. Profiles (PROF) | 7 | 1 | 3 | 0 | 3/4 (75%) | 3/3 (100%) |
|
||||
| H. Crop & rotate (CROP) | 11 | 0 | 0 | 1 | 8/8 (100%) | 3/3 (100%) |
|
||||
| H. Crop & rotate (CROP) | 10 | 1 | 0 | 1 | 8/8 (100%) | 2/3 (67%) |
|
||||
| I. Remove / healing (REM) | 8 | 1 | 2 | 2 | 4/4 (100%) | 2/3 (67%) |
|
||||
| J. Red eye (EYE) | 2 | 0 | 0 | 0 | — | 1/1 (100%) |
|
||||
| K. Masking (MASK) | 15 | 5 | 5 | 0 | 7/8 (88%) | 7/7 (100%) |
|
||||
@@ -53,9 +53,9 @@ cited here still exists. Formerly docs/parity.md.
|
||||
| Y. Menus | 85 | 0 | 4 | 8 | 47/47 (100%) | 23/23 (100%) |
|
||||
| Z. Keyboard shortcuts (desktop) | 78 | 4 | 2 | 1 | 49/52 (94%) | 24/26 (92%) |
|
||||
| Lightroom Classic extras | 40 | 25 | 21 | 8 | — | 24/26 (92%) |
|
||||
| **Total** | 422 | 63 | 57 | 35 | 196/203 (97%) | 148/161 (92%) |
|
||||
| **Total** | 421 | 64 | 57 | 35 | 196/203 (97%) | 147/161 (91%) |
|
||||
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **83.7%** of 542 in-scope rows — P0 98.3% of 203 · P1 95.0% of 161 · P2 56.5% of 177.
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **83.6%** of 542 in-scope rows — P0 98.3% of 203 · P1 94.7% of 161 · P2 56.5% of 177.
|
||||
<!-- /parity:summary -->
|
||||
|
||||
## Top gaps and shortcut notes
|
||||
@@ -250,7 +250,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| LR-CROP-RECT | Crop rectangle | P0 | ✅ | `cmd:crop.set`, `cmd:crop.drag`, `crates/geom/src/crop_drag.rs`, `crates/ui-egui/src/panels/detail.rs` | double-click inside the box applies the crop (like Return); on a straightened photo a moved box slides along the tilted edges to where the pointer asks, so a box that fills the photo's height still reaches its sides (issue #2299) |
|
||||
| LR-CROP-ASPECT | Aspect ratios | P0 | ✅ | `cmd:crop.aspect`, `cmd:crop.rotateAspect`, `crates/ui-egui/src/panels/right.rs` | menu with presets + custom W × H (a preset takes the crop's orientation, portrait after X; a custom ratio is used as entered, issue #2253), visible Lock toggle (also A), button names the active ratio; every handle honours the lock, holding Shift while dragging a handle keeps the frame's proportions for that drag (issue #614), and the drag stops at the image edge instead of shifting the frame (issue #295); "Original" is the photo's own shape (no separate As Shot); the last chosen aspect is saved as a crop tool default, resolving Original per photo and preserving existing crops; opening Crop or browsing never edits a photo: the default applies only when a crop gesture starts, within its single undo step; a crop starts locked to the photo's own shape until the Lock is switched off once, then as last set (#991) |
|
||||
| LR-CROP-STRAIGHTEN | Straighten tool | P0 | ✅ | `cmd:crop.straighten`, `cmd:crop.autoStraighten`, `crates/ui-egui/src/panels/detail.rs` (`rotate_cursor`, `angle_readout`), `crates/ui-egui/src/panels/right.rs` (Angle field), `crates/ui-egui/src/tests_crop_rotate.rs` | Straighten Tool button: drag along a horizon/vertical; double-click or Auto levels automatically. Dragging outside the crop box rotates it: there the pointer is a curved double arrow that bulges away from the side of the box it is on, diagonally off a corner and following a rotated box (`rotate_cursor_turn`, tested by `rotate_cursor_faces_away_from_the_crop_box`) (with ⌘ held, the straighten crosshair), and while rotating the angle shows next to it, kept on the canvas (issue #534); an exact angle goes in the Angle field under Straighten (`cropAngleField`: applies on Return or leaving it, Esc keeps the old angle, takes a decimal comma; a drag on it is one undo step) or by clicking the Straighten value (its hover says so). Every way of setting the angle (rotate handle, Straighten slider, Angle field, `crop.straighten`, `crop.set {angle}`, and the generic `develop.set` / `develop.adjust` / `develop.quickAdjust` / `develop.resetControl` on `crop.angle`, as MCP, the control channel and `lightcraft-cli render --set` use them) keeps the crop if it still fits, else shrinks it about its own centre, keeping its aspect, until it fits; the full photo (or a crop an earlier turn shrank to the largest of its aspect) stays the largest, so it grows back towards 0° (issue #1388), and no other crop ever grows (issue #1794: a crop dragged in from a corner turned into nearly the whole photo) (`CropGeometry::with_angle`, `crates/engine/src/tests_crop_angle.rs`; before issue #742 the generic path left the box, and the export, past the photo with smeared edge pixels in the corners). The dimming outside the box is the photo minus the (rotated) box, drawn as one mesh (`outside_crop_mesh`), so rotating never dims wedges inside the box or doubles them outside (issue #731) |
|
||||
| LR-CROP-AUTO | Auto straighten | P1 | ✅ | `cmd:crop.autoStraighten` | crop-angle leveling from detected horizon/plumb lines (consensus required) |
|
||||
| LR-CROP-AUTO | Auto straighten | P1 | 🟡 | `cmd:crop.autoStraighten` | crop-angle leveling from detected horizon/plumb lines (consensus required); turns by at most 10° (`STRAIGHTEN_MAX_DEGREES`) and only on at least 3 agreeing lines (`STRAIGHTEN_MIN_LINES`), and says so when it finds nothing (`auto_straighten_says_when_it_finds_nothing`). Measured on 68 photos straightened by hand in Lightroom Classic and 300 left as shot: it levels 14 of the 68 within 0.5° (1 more than 1° off) and turns 19 of the 300 by more than 1° (before the limits: 79, some by 20° or more). 🟡 because it still leaves most tilted photos alone: a soft sea horizon among wave crests, and verticals that converge in perspective, aren't used yet |
|
||||
| LR-CROP-ANGLE | Angle slider | P0 | ✅ | `ctl:crop.angle`, `crates/ui-egui/src/panels/right.rs` | the Straighten slider; an exact angle can also be typed in the Angle field under it (see LR-CROP-STRAIGHTEN); setting `crop.angle` through `develop.set` fits the crop inside the photo as Straighten does (issue #742) |
|
||||
| LR-GUIDED-SELECT | Guide selection in Guided Upright mode | P0 | ✅ | `cmd:tool.guidedUpright`, `cmd:geometry.guides`, `crates/ui-egui/src/panels/detail.rs`, `crates/ui-egui/src/state.rs` (`selected_guide`), `crates/ui-egui/src/tests_guided_guides.rs` | a press near a guide's line selects it (`selected_guide = Some((photo id, index))`; another press on it deselects), a press on an empty spot starts a new guide at that point, and an endpoint drags to reshape the guide (`Gesture::GuideEndpoint`, one undo step; clicking an endpoint only selects); tested in `tests_guided_guides` 8/8 |
|
||||
| LR-GUIDED-DELETE | Delete removes the selected guide alone | P0 | ✅ | `cmd:photo.delete`, `crates/ui-egui/src/shortcuts.rs`, `crates/ui-egui/src/tests_guided_guides.rs` | with the Guided Upright tool open (`RightPanel::Crop && tool == "guidedUpright"`), Delete / Backspace removes only the selected guide and clears the selection; with nothing selected it removes nothing (the photo itself is never deleted while the tool is open); tested in `tests_guided_guides` |
|
||||
@@ -927,3 +927,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 2026-10-16 | minor | LR-VIEW-PHOTOGRID: when the photos shown change by those an edit touched (an import's batch, a capture time, a crop), the grid reads only those photos' dates and shapes again instead of every photo of the list (a 16-photo batch at 181,749 photos: grid frame 101 ms → 7 ms); status unchanged |
|
||||
| 2026-10-19 | minor | LRC-LIB-LIBFOLDERS: the sidebar trees indent by a narrower step (14 / 12 / 10 / 8 pt) when the deepest open row would not fit the panel, instead of cutting its name or scrolling sideways at once (issue #2298); status unchanged (✅) |
|
||||
| 2026-10-15 | patch | LR-EXP-DIM: Width & Height (`fit: widthHeight`) limits the width and the height as the photo stands, beside Dimensions (W × H, either orientation), as in Lightroom (#1620); status unchanged (✅) |
|
||||
| 2026-10-22 | minor | LR-CROP-AUTO: Auto straighten turns by at most 10° and only on at least 3 agreeing lines, and says when it finds nothing to level by; measured on 68 photos straightened by hand and 300 left as shot, unwanted turns of more than 1° fell from 79 to 19 of the 300. Downgraded ✅ → 🟡: it still leaves most tilted photos alone |
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# What's new in LightCraft
|
||||
|
||||
## 2026-10-22
|
||||
|
||||
### Auto straighten leaves straight photos alone
|
||||
- Auto straighten (Auto in the crop panel, or a double-click with the Straighten tool) no longer turns a photo by
|
||||
more than 10°, and acts only when at least three lines agree. Before, it often turned photos that were straight:
|
||||
the edges of wooden planks, a branch or a bird's wing were taken for the horizon, sometimes for a 25° turn.
|
||||
- When it finds no horizon or vertical lines, it says so instead of doing nothing without a word.
|
||||
|
||||
## 2026-10-21
|
||||
|
||||
### Sharper resampling in HDR and panorama merges
|
||||
|
||||
Reference in New Issue
Block a user