# Conflicts:
#	docs/whats-new.md
This commit is contained in:
Brandon Thomas committed 2026-10-13 11:49:12 -04:00
commit 8bcbcd918b
3 files changed
+54 -2

No files matched your search

+23 -2
View File
@@ -167,10 +167,12 @@ impl Frame {
}
}
/// Aspect ratio (w/h) of the output.
/// Aspect ratio (w/h) of the output. A crop narrower than a source pixel (or of no size, from
/// an agent or a sidecar) counts each side as one pixel, so the aspect stays finite: 0/0 made
/// it NaN, and the loupe laid the photo out at a NaN size.
pub fn aspect(&self) -> f64 {
let r = self.crop.rect_px(self.ow, self.oh);
r.width() / r.height()
r.width().max(1.0) / r.height().max(1.0)
}
/// Size of the (cropped) output at the source's own resolution, in pixels.
@@ -383,6 +385,25 @@ mod tests {
assert!((back.x - 50.0).abs() < 1e-6 && (back.y - 50.0).abs() < 1e-6);
}
/// A crop of no size (or under a pixel wide) gave a NaN or infinite aspect; each side now
/// counts as at least one source pixel.
#[test]
fn degenerate_crops_have_a_finite_aspect() {
let mut s = DevelopSettings::default();
for (rect, want) in [
(Rect::new(0.5, 0.5, 0.5, 0.5), 1.0),
(Rect::new(0.0, 0.0, 1e-9, 1.0), 1.0 / 200.0),
(Rect::new(0.0, 0.5, 1.0, 0.5), 400.0),
(Rect::new(0.25, 0.0, 0.75, 1.0), 1.0),
] {
s.crop.geometry.rect = rect;
let f = Frame::new(400, 200, &s, true);
assert!((f.aspect() - want).abs() < 1e-9, "{rect:?}: {}", f.aspect());
let (w, h) = f.fit(1000, 1000);
assert!((1..=1000).contains(&w) && (1..=1000).contains(&h), "{rect:?}: {w}×{h}");
}
}
#[test]
fn flip_mirrors() {
let mut s = DevelopSettings::default();
+27
View File
@@ -108,3 +108,30 @@ fn zoomed_before_after_views_stay_in_their_panes() {
}
}
}
/// A crop of no size (or a hair wide), from an agent or a sidecar, gave the loupe a NaN aspect:
/// the photo was laid out at a NaN size, which egui's debug checks stop on (a panic in a debug
/// build) and nothing could be drawn or clicked. It now lays out at a finite size, fitted and at
/// 100 %.
#[test]
fn a_crop_of_no_size_lays_out_at_a_finite_size() {
let mut h = detail();
for rect in [[0.5, 0.5, 0.5, 0.5], [0.0, 0.0, 1e-9, 1.0], [0.0, 0.3, 1.0, 0.3]] {
let r = h.request("engine.execute", json!({"command": "crop.set", "params": {"rect": rect}}), T);
assert_eq!(r["ok"], true, "{r}");
for zoom in ["view.zoomFit", "view.zoom100"] {
let r = h.request("engine.execute", json!({"command": zoom}), T);
assert_eq!(r["ok"], true, "{zoom}: {r}");
h.settle(SETTLE);
let img = h.app.image_rect.expect("the loupe laid the photo out");
assert!(img.min.is_finite() && img.max.is_finite(), "{rect:?} at {zoom}: {img:?}");
let r = h.request(
"ui.pointer",
json!({"events": [{"kind": "down", "x": 0.5, "y": 0.5}, {"kind": "drag", "x": 0.6, "y": 0.4}, {"kind": "up", "x": 0.6, "y": 0.4}]}),
T,
);
assert_eq!(r["ok"], true, "{r}");
h.step();
}
}
}
+4
View File
@@ -20,6 +20,10 @@
- Importing a preset zip that unpacks to far more than any preset (a "zip bomb") is refused with a message naming
the file, instead of asking for gigabytes of memory, which could end LightCraft.
### Reliability
- A crop of no size (from an agent or a sidecar) no longer gives the Detail view a photo of no size to lay out
(its width over its height was 0 / 0, which stopped debug builds); it shows as a single pixel's colour.
### Slider drags on large raws
- Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you
let go. On a 64 MP raw the first of those renders decoded the whole file again, which held up the loupe's