mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 05:57:09 -06:00
LR-MASK-COLORRANGE: a colour-range mask keeps at most 64 samples however it is loaded (#2556)
Render cost is pixels x samples (CPU and GPU), and only mask.add / mask.update capped the list, so a sidecar's lc:settings, a preset, develop.merge, render --settings or a catalog / history load could carry thousands and stall a render or lose the GPU device. MAX_COLOR_RANGE_SAMPLES moves to lightcraft-develop and ColorRange::samples deserializes through a capped visitor (the first 64 in order, the rest skipped as IgnoredAny); the CPU and GPU renders also take at most 64 for settings built in code.
This commit is contained in:
@@ -271,6 +271,54 @@ mod tests {
|
||||
assert_eq!(s.grading.blending, 50.0);
|
||||
}
|
||||
|
||||
/// Settings with one colour-range mask of `n` distinct samples (built in code, so not capped).
|
||||
fn color_range_settings(n: usize) -> DevelopSettings {
|
||||
let samples = (0..n).map(|i| [i as f64 / 1000.0, 0.1, -0.1]).collect();
|
||||
let mut s = DevelopSettings::default();
|
||||
s.masks.push(Mask {
|
||||
id: 1,
|
||||
components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape: MaskShape::ColorRange { samples, refine: 50.0 } }],
|
||||
..Default::default()
|
||||
});
|
||||
s
|
||||
}
|
||||
|
||||
fn color_range_samples(s: &DevelopSettings) -> &[[f64; 3]] {
|
||||
match s.masks.first().and_then(|m| m.components.first()).map(|c| &c.shape) {
|
||||
Some(MaskShape::ColorRange { samples, .. }) => samples,
|
||||
other => panic!("expected a colour range, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue #2556: `mask.add`/`mask.update` capped a colour range's samples, but a sidecar `lc:settings`, a
|
||||
/// preset, `develop.merge`, `render --settings` or a catalog / history load took any number (render cost is
|
||||
/// pixels x samples). Every load now keeps the first [`MAX_COLOR_RANGE_SAMPLES`], in order.
|
||||
#[test]
|
||||
fn color_range_samples_are_capped_however_settings_load() {
|
||||
let big = color_range_settings(1000);
|
||||
let want = &color_range_samples(&big)[..MAX_COLOR_RANGE_SAMPLES];
|
||||
let v = big.to_json();
|
||||
let loaded = DevelopSettings::from_json(&v).unwrap();
|
||||
assert_eq!(color_range_samples(&loaded).len(), MAX_COLOR_RANGE_SAMPLES);
|
||||
assert_eq!(color_range_samples(&loaded), want);
|
||||
let from_str: DevelopSettings = serde_json::from_str(&v.to_string()).unwrap();
|
||||
assert_eq!(color_range_samples(&from_str), want);
|
||||
// `develop.merge`, `render --settings` and presets merge a partial
|
||||
let merged = DevelopSettings::default().merged(&json!({"masks": v["masks"]})).unwrap();
|
||||
assert_eq!(color_range_samples(&merged), want);
|
||||
let preset =
|
||||
Preset { id: "p".into(), name: "P".into(), group: "G".into(), settings: json!({"masks": v["masks"]}), favorite: false, builtin: false };
|
||||
assert_eq!(color_range_samples(&preset.apply(&DevelopSettings::default(), 1.0)), want);
|
||||
// a capped list round-trips, and a short one is unchanged
|
||||
assert_eq!(DevelopSettings::from_json(&loaded.to_json()).unwrap(), loaded);
|
||||
let few = color_range_settings(2);
|
||||
assert_eq!(DevelopSettings::from_json(&few.to_json()).unwrap(), few);
|
||||
// a list that isn't samples is still refused
|
||||
let mut bad = v.clone();
|
||||
bad["masks"][0]["components"][0]["shape"]["samples"] = json!([[0.1, 0.2]]);
|
||||
assert!(DevelopSettings::from_json(&bad).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_partial() {
|
||||
let s = DevelopSettings::default();
|
||||
|
||||
@@ -921,6 +921,35 @@ impl Default for BrushStroke {
|
||||
}
|
||||
}
|
||||
|
||||
/// Most samples a colour-range mask keeps. It compares every pixel against every sample, so render and export cost
|
||||
/// is pixels x samples; the interactive sampler stops at 5, and a sidecar, preset or `mask.add` with thousands could
|
||||
/// stall a render or lose the GPU device (issue #2556).
|
||||
pub const MAX_COLOR_RANGE_SAMPLES: usize = 64;
|
||||
|
||||
/// Deserialize a colour range's samples, keeping the first [`MAX_COLOR_RANGE_SAMPLES`] (the rest are skipped without
|
||||
/// being stored), however the settings arrive: sidecar, catalog, history, preset or merge.
|
||||
fn de_color_range_samples<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<[f64; 3]>, D::Error> {
|
||||
struct Capped;
|
||||
impl<'de> serde::de::Visitor<'de> for Capped {
|
||||
type Value = Vec<[f64; 3]>;
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str("a list of OkLab colours")
|
||||
}
|
||||
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<[f64; 3]>, A::Error> {
|
||||
let mut out = Vec::new();
|
||||
while out.len() < MAX_COLOR_RANGE_SAMPLES {
|
||||
match seq.next_element::<[f64; 3]>()? {
|
||||
Some(s) => out.push(s),
|
||||
None => return Ok(out),
|
||||
}
|
||||
}
|
||||
while seq.next_element::<serde::de::IgnoredAny>()?.is_some() {}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
d.deserialize_seq(Capped)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
pub enum MaskShape {
|
||||
@@ -941,8 +970,9 @@ pub enum MaskShape {
|
||||
feather: f64,
|
||||
invert: bool,
|
||||
},
|
||||
/// Colour range: sampled OkLab colours with a refine amount 0..100.
|
||||
/// Colour range: sampled OkLab colours (at most [`MAX_COLOR_RANGE_SAMPLES`]) with a refine amount 0..100.
|
||||
ColorRange {
|
||||
#[serde(deserialize_with = "de_color_range_samples")]
|
||||
samples: Vec<[f64; 3]>,
|
||||
refine: f64,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Masking and Remove (spot) commands on the active photo.
|
||||
|
||||
use lightcraft_develop::{BrushStroke, LocalAdjustments, Mask, MaskComponent, MaskOp, MaskShape, RedEye, Spot, SpotMode};
|
||||
use lightcraft_develop::{BrushStroke, LocalAdjustments, MAX_COLOR_RANGE_SAMPLES, Mask, MaskComponent, MaskOp, MaskShape, RedEye, Spot, SpotMode};
|
||||
use lightcraft_geom::Point;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -8,11 +8,6 @@ use super::{CommandSpec, bad, bool_or, cmd, f64_or, has_active, ok, point, str_p
|
||||
use crate::segment::{Click, MAX_CLICKS};
|
||||
use crate::{Result, Session};
|
||||
|
||||
/// A colour-range mask compares every pixel against every sample, so render and export cost is
|
||||
/// pixels x samples. The interactive sampler stops at 5; automation (`mask.add`/`mask.update`)
|
||||
/// passed its `samples` array straight through, so a few hundred samples on a large render hung.
|
||||
const MAX_COLOR_RANGE_SAMPLES: usize = 64;
|
||||
|
||||
/// Caps a colour-range shape's sample list at [`MAX_COLOR_RANGE_SAMPLES`] (render cost is pixels x samples).
|
||||
fn cap_color_range_samples(shape: &mut MaskShape) {
|
||||
if let MaskShape::ColorRange { samples, .. } = shape {
|
||||
|
||||
@@ -980,6 +980,23 @@ mod tests {
|
||||
assert_eq!(sc.keywords, Some(vec!["Places|Lisbon".to_string()]));
|
||||
}
|
||||
|
||||
/// Issue #2556: a sidecar's `lc:settings` colour-range mask keeps at most `MAX_COLOR_RANGE_SAMPLES` samples.
|
||||
#[test]
|
||||
fn sidecar_color_range_samples_are_capped() {
|
||||
use lightcraft_develop::{MAX_COLOR_RANGE_SAMPLES, Mask, MaskComponent, MaskOp, MaskShape};
|
||||
let samples: Vec<[f64; 3]> = (0..1000).map(|i| [i as f64 / 1000.0, 0.1, -0.1]).collect();
|
||||
let mut d = DevelopSettings::default();
|
||||
let shape = MaskShape::ColorRange { samples: samples.clone(), refine: 50.0 };
|
||||
d.masks.push(Mask { id: 1, components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape }], ..Default::default() });
|
||||
let xmp = lightcraft_meta::write_xmp(&lightcraft_meta::Metadata::default(), Some(&d.to_json().to_string()));
|
||||
let sc = super::parse_sidecar(&xmp, crate::crs::Target::Rendered).unwrap();
|
||||
let Some(DevelopPatch::Full(s)) = sc.develop else { panic!("expected full settings") };
|
||||
let Some(MaskShape::ColorRange { samples: got, .. }) = s.masks.first().and_then(|m| m.components.first()).map(|c| &c.shape) else {
|
||||
panic!("expected a colour range")
|
||||
};
|
||||
assert_eq!(got.as_slice(), &samples[..MAX_COLOR_RANGE_SAMPLES]);
|
||||
}
|
||||
|
||||
use super::*;
|
||||
use lightcraft_catalog::Meta;
|
||||
|
||||
|
||||
@@ -1017,8 +1017,10 @@ fn masks(cx: &mut Cx<'_>, lin: &Buf, prep: &Prep, plan: &Plan<'_>, host: &mut Ho
|
||||
}
|
||||
MaskShape::ColorRange { samples, refine } => {
|
||||
let tol = lightcraft_pipeline::masks::color_range_tolerance(*refine);
|
||||
// at most the CPU's samples (`MAX_COLOR_RANGE_SAMPLES`): cost is pixels x samples
|
||||
let samples = samples.iter().take(lightcraft_develop::MAX_COLOR_RANGE_SAMPLES);
|
||||
p.extend([tol.to_bits(), ev.exp2().to_bits(), samples.len() as u32]);
|
||||
aux.extend(samples.iter().flat_map(|s| s.map(|v| v as f32)));
|
||||
aux.extend(samples.flat_map(|s| s.map(|v| v as f32)));
|
||||
Some(3)
|
||||
}
|
||||
MaskShape::Brush { strokes } => {
|
||||
|
||||
@@ -251,7 +251,9 @@ pub fn shape_alpha(shape: &MaskShape, frame: &Frame, w: usize, h: usize, img: &R
|
||||
}
|
||||
MaskShape::ColorRange { samples, refine } => {
|
||||
let tol = color_range_tolerance(*refine);
|
||||
let samples: Vec<[f32; 3]> = samples.iter().map(|s| [s[0] as f32, s[1] as f32, s[2] as f32]).collect();
|
||||
// (a settings value built in code can skip the deserializer's cap: cost is pixels x samples)
|
||||
let samples: Vec<[f32; 3]> =
|
||||
samples.iter().take(lightcraft_develop::MAX_COLOR_RANGE_SAMPLES).map(|s| [s[0] as f32, s[1] as f32, s[2] as f32]).collect();
|
||||
for (v, c) in out.data.iter_mut().zip(&img.data) {
|
||||
let lab = lightcraft_color::perceptual::oklab_from_2020(tonemap_for_select(c.map(|v| v * gain)));
|
||||
let d = samples
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Lightroom parity checklist
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; 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; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; 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-30 · **Change:** patch (LR-MASK-COLORRANGE: a colour-range mask keeps at most 64 samples however it is loaded, #2556; earlier: LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; 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; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; 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
|
||||
@@ -299,7 +299,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| LR-MASK-BRUSH | Brush mask | P0 | 🟡 | `cmd:tool.brush`, `cmd:mask.brushStroke` (`autoMask`), `crates/pipeline/src/masks.rs`, `crates/ui-egui/src/brush_live.rs` | size/feather/flow/density/erase; Create New Mask starts a separate brush in paint mode; the stroke shows while the button is held with the committed stroke's dab spacing, feather and flow, so fast strokes have no gaps (#517); coverage builds up by passes, not hold time (holding the brush still adds nothing, live or committed, #518); Auto Mask: dabs weighted by similarity to the colour under the dab centre, refined by a guided filter on luminance (CPU + GPU); ⇧-click draws a straight stroke from where the last stroke ended (`connect`), tapering size, feather and flow from that stroke's values when they changed; no A/B brushes; pen pressure (Brush ▸ Pen Pressure, on by default; `pressure` per point in `mask.brushStroke`) scales each dab's size (a tenth to all of it) and flow, live and committed alike, where winit reports a pen force (Windows; not macOS / Linux tablets yet); no tilt |
|
||||
| LR-MASK-LINEAR | Linear gradient | P0 | ✅ | `cmd:tool.linear`, `cmd:mask.update` | |
|
||||
| LR-MASK-RADIAL | Radial gradient | P0 | ✅ | `cmd:tool.radial`, `cmd:mask.update`, `crates/ui-egui/src/panels/detail.rs` | drag the selected ellipse's body or centre pin to move; edge grips resize, outer grip rotates; rotated and compound radial components, hidden pins, outside hit rejection and one-step undo tested |
|
||||
| LR-MASK-COLORRANGE | Colour range | P1 | ✅ | `cmd:mask.add` (`colorRange`), `cmd:mask.sampleColor`, `cmd:mask.update`, `crates/pipeline/src/lib.rs` (`color_range_sample`) | Color tile → click the photo to sample (⇧-click adds, up to 5; samples taken in the space the mask compares in, respecting section eyes and the requested HDR/SDR rendition), Pick button, Refine slider |
|
||||
| LR-MASK-COLORRANGE | Colour range | P1 | ✅ | `cmd:mask.add` (`colorRange`), `cmd:mask.sampleColor`, `cmd:mask.update`, `crates/pipeline/src/lib.rs` (`color_range_sample`) | Color tile → click the photo to sample (⇧-click adds, up to 5; samples taken in the space the mask compares in, respecting section eyes and the requested HDR/SDR rendition), Pick button, Refine slider; at most 64 samples (`MAX_COLOR_RANGE_SAMPLES`, render cost is pixels × samples) however the mask arrives: `mask.add` / `mask.update`, a sidecar's `lc:settings`, a preset, `develop.merge`, `render --settings` or the catalog and history keep the first 64 (issue #2556) |
|
||||
| LR-MASK-LUMRANGE | Luminance range | P1 | ✅ | `cmd:mask.add` (`luminanceRange`), `cmd:mask.update`, `crates/ui-egui/src/panels/masking.rs` (`range_controls`) | range bar with two handles (the pressed endpoint stays selected through fast/crossing drags; one undo step per drag), Smoothness, Show Luminance Map (B&W photo with the range tinted) |
|
||||
| LR-MASK-DEPTHRANGE | Depth range | P2 | ⬜ | | shape exists, needs depth data |
|
||||
| LR-MASK-COMBINE | Add / subtract / intersect | P0 | ✅ | `cmd:mask.addComponent`, `cmd:tool.brush` | Add/Subtract > Brush creates a component with the chosen operation; Erase removes paint within that brush component |
|
||||
@@ -959,3 +959,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 2026-10-23 | patch | LR-BEHAV-ACCESS: the name field of New Album, New Smart Album, Rename and the other one-field dialogs takes the keyboard focus once when the dialog opens instead of every frame, so Tab and Shift+Tab reach the check box, Cancel and OK; 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 |
|
||||
| 2026-10-23 | patch | LRC-LIB-FILTERBAR: showing an album (`library.source`) while the filter holds an album drops the filter's album, as showing a library folder already dropped its folder filter; before, the old album stayed in the filter unseen (the filter bar has no album chip) and narrowed the next source such as All Photos; the row's note says so; status unchanged (✅) |
|
||||
| 2026-10-30 | patch | LR-MASK-COLORRANGE: a colour-range mask keeps its first 64 samples however it is loaded (sidecar `lc:settings`, presets, `develop.merge`, `render --settings`, catalog and history), not only through `mask.add` / `mask.update`, so a document with thousands can't stall a render or lose the GPU device (issue #2556); status unchanged (✅) |
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# What's new in LightCraft
|
||||
|
||||
## 2026-10-30
|
||||
|
||||
### A colour-range mask keeps at most 64 samples, however it arrives
|
||||
- A colour-range mask compares every pixel with every colour it has sampled, so a mask with thousands of samples
|
||||
could stall a render or export, or lose the GPU for the rest of the session. `mask.add` and `mask.update` already
|
||||
kept the first 64; a sidecar's `lc:settings`, a preset, `develop.merge`, `render --settings` and the catalog and
|
||||
edit history now keep the first 64 too, in order, and the extra ones are skipped as they are read. Masks made in
|
||||
the app (up to 5 samples) are unchanged.
|
||||
|
||||
## 2026-10-24
|
||||
|
||||
### Relative-date filters and smart albums follow the clock
|
||||
|
||||
Reference in New Issue
Block a user