selection: spans on the physical pixel grid; a box inside one output is its plain crop (fractional pointer corners made single shots 1 px short and resampled, and doubled seam rows)
This commit is contained in:
1 parent
857c1f0b65
commit
42bca37ed8
2 files changed
+93
-14
No files matched your search
+92
-13
@@ -189,8 +189,9 @@ pub struct Part {
|
|||||||
pub output: usize,
|
pub output: usize,
|
||||||
/// In that output's frame, physical pixels.
|
/// In that output's frame, physical pixels.
|
||||||
pub rect: PixelRect,
|
pub rect: PixelRect,
|
||||||
/// Its top-left corner and size in the whole image.
|
/// Its top-left corner and size in the whole image (a corner can sit a
|
||||||
pub at: (u32, u32),
|
/// pixel left of or above the image when scales differ).
|
||||||
|
pub at: (i64, i64),
|
||||||
pub size: (u32, u32),
|
pub size: (u32, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +221,12 @@ impl Span {
|
|||||||
/// The drag from `a` to `b` over `outputs` (each output's geometry and frame
|
/// The drag from `a` to `b` over `outputs` (each output's geometry and frame
|
||||||
/// size), at the largest scale among the outputs it covers. `None` for a
|
/// size), at the largest scale among the outputs it covers. `None` for a
|
||||||
/// click (under [`MIN_PIXELS`]) or a rectangle over no output.
|
/// click (under [`MIN_PIXELS`]) or a rectangle over no output.
|
||||||
|
///
|
||||||
|
/// A box inside one output is that output's plain crop, pixel for pixel.
|
||||||
|
/// Otherwise the image lies on the physical pixel grid at that scale: its
|
||||||
|
/// edges are the corners rounded outwards, and each part sits where its own
|
||||||
|
/// pixels fall on that grid, so neighbouring parts meet without a gap or an
|
||||||
|
/// overlap even when the corners are fractional (as pointer positions are).
|
||||||
pub fn span(a: Point, b: Point, outputs: &[(&OutputGeom, (u32, u32))]) -> Option<Span> {
|
pub fn span(a: Point, b: Point, outputs: &[(&OutputGeom, (u32, u32))]) -> Option<Span> {
|
||||||
let (x0, y0) = (a.x.min(b.x), a.y.min(b.y));
|
let (x0, y0) = (a.x.min(b.x), a.y.min(b.y));
|
||||||
let (x1, y1) = (a.x.max(b.x), a.y.max(b.y));
|
let (x1, y1) = (a.x.max(b.x), a.y.max(b.y));
|
||||||
@@ -231,13 +238,36 @@ pub fn span(a: Point, b: Point, outputs: &[(&OutputGeom, (u32, u32))]) -> Option
|
|||||||
Some((i, r, *w as f64 / g.width as f64))
|
Some((i, r, *w as f64 / g.width as f64))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
let inside = |g: &OutputGeom| {
|
||||||
|
x0 >= g.x as f64
|
||||||
|
&& y0 >= g.y as f64
|
||||||
|
&& x1 <= (g.x + g.width) as f64
|
||||||
|
&& y1 <= (g.y + g.height) as f64
|
||||||
|
};
|
||||||
|
if let Some(&(i, rect, _)) = covered.iter().find(|c| inside(outputs[c.0].0)) {
|
||||||
|
let size = (rect.width, rect.height);
|
||||||
|
if size.0 < MIN_PIXELS || size.1 < MIN_PIXELS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let part = Part {
|
||||||
|
output: i,
|
||||||
|
rect,
|
||||||
|
at: (0, 0),
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
return Some(Span {
|
||||||
|
parts: vec![part],
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
}
|
||||||
let scale = covered.iter().map(|c| c.2).fold(0.0, f64::max);
|
let scale = covered.iter().map(|c| c.2).fold(0.0, f64::max);
|
||||||
if scale == 0.0 {
|
if scale == 0.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let (left, top) = ((x0 * scale).floor(), (y0 * scale).floor());
|
||||||
let size = (
|
let size = (
|
||||||
((x1 - x0) * scale).ceil() as u32,
|
((x1 * scale).ceil() - left) as u32,
|
||||||
((y1 - y0) * scale).ceil() as u32,
|
((y1 * scale).ceil() - top) as u32,
|
||||||
);
|
);
|
||||||
if size.0 < MIN_PIXELS || size.1 < MIN_PIXELS {
|
if size.0 < MIN_PIXELS || size.1 < MIN_PIXELS {
|
||||||
return None;
|
return None;
|
||||||
@@ -246,20 +276,20 @@ pub fn span(a: Point, b: Point, outputs: &[(&OutputGeom, (u32, u32))]) -> Option
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(i, rect, own)| {
|
.map(|(i, rect, own)| {
|
||||||
let g = outputs[i].0;
|
let g = outputs[i].0;
|
||||||
let at = (
|
|
||||||
((x0.max(g.x as f64) - x0) * scale).floor() as u32,
|
|
||||||
((y0.max(g.y as f64) - y0) * scale).floor() as u32,
|
|
||||||
);
|
|
||||||
let k = scale / own;
|
let k = scale / own;
|
||||||
let part = (
|
let at = (
|
||||||
((rect.width as f64 * k).round() as u32).min(size.0 - at.0.min(size.0)),
|
(g.x as f64 * scale + rect.x as f64 * k).round() as i64 - left as i64,
|
||||||
((rect.height as f64 * k).round() as u32).min(size.1 - at.1.min(size.1)),
|
(g.y as f64 * scale + rect.y as f64 * k).round() as i64 - top as i64,
|
||||||
|
);
|
||||||
|
let size = (
|
||||||
|
(rect.width as f64 * k).round() as u32,
|
||||||
|
(rect.height as f64 * k).round() as u32,
|
||||||
);
|
);
|
||||||
Part {
|
Part {
|
||||||
output: i,
|
output: i,
|
||||||
rect,
|
rect,
|
||||||
at,
|
at,
|
||||||
size: part,
|
size,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -626,7 +656,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(s.single(), None);
|
assert_eq!(s.single(), None);
|
||||||
// The parts meet without a gap or an overlap.
|
// The parts meet without a gap or an overlap.
|
||||||
assert_eq!(s.parts[1].at.1 + s.parts[1].size.1, s.parts[0].at.1);
|
assert_eq!(s.parts[1].at.1 + s.parts[1].size.1 as i64, s.parts[0].at.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -684,6 +714,55 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fractional_box_on_one_output_is_its_plain_crop() {
|
||||||
|
let (laptop, monitor) = layout();
|
||||||
|
let outputs = [(&laptop, (1366, 768)), (&monitor, (1920, 1080))];
|
||||||
|
let (a, b) = (p(100.6, 100.6), p(300.2, 250.2));
|
||||||
|
let s = span(a, b, &outputs).unwrap();
|
||||||
|
let crop = clip(a, b, &laptop, 1366, 768).unwrap();
|
||||||
|
assert_eq!(s.single(), Some((0, crop)));
|
||||||
|
assert_eq!(s.size, (crop.width, crop.height));
|
||||||
|
// A 1.25-scale output: the crop is what clip gives, not a resampled one.
|
||||||
|
let o = out("A", 0, 0, 1000, 800);
|
||||||
|
let scaled = [(&o, (1250, 1000))];
|
||||||
|
let (a, b) = (p(101.0, 101.0), p(301.0, 251.0));
|
||||||
|
assert_eq!(
|
||||||
|
span(a, b, &scaled).unwrap().single(),
|
||||||
|
Some((0, clip(a, b, &o, 1250, 1000).unwrap()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fractional_seams_meet_exactly_and_fill_the_image() {
|
||||||
|
let (laptop, monitor) = layout();
|
||||||
|
let outputs = [(&laptop, (1366, 768)), (&monitor, (1920, 1080))];
|
||||||
|
for (top, bottom) in [(-300.4, 500.6), (-300.6, 500.6), (-300.5, 500.2)] {
|
||||||
|
let s = span(p(100.3, bottom), p(600.7, top), &outputs).unwrap();
|
||||||
|
let (laptop_part, monitor_part) = (&s.parts[0], &s.parts[1]);
|
||||||
|
assert_eq!(monitor_part.at.1, 0, "{top}: the monitor starts the image");
|
||||||
|
assert_eq!(
|
||||||
|
monitor_part.at.1 + monitor_part.size.1 as i64,
|
||||||
|
laptop_part.at.1,
|
||||||
|
"{top}: no doubled or missing row at the seam"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
laptop_part.at.1 + laptop_part.size.1 as i64,
|
||||||
|
s.size.1 as i64,
|
||||||
|
"{top}: the last row is covered"
|
||||||
|
);
|
||||||
|
assert_eq!(laptop_part.size.0, s.size.0, "{top}: full width");
|
||||||
|
}
|
||||||
|
// Mixed scales with a fractional corner: the parts tile the width.
|
||||||
|
let a = out("A", 0, 0, 100, 100);
|
||||||
|
let b = out("B", 100, 0, 100, 100);
|
||||||
|
let mixed = [(&a, (200, 200)), (&b, (100, 100))];
|
||||||
|
let s = span(p(50.3, 10.0), p(150.7, 60.0), &mixed).unwrap();
|
||||||
|
assert_eq!(s.parts[0].at.0, 0);
|
||||||
|
assert_eq!(s.parts[0].at.0 + s.parts[0].size.0 as i64, s.parts[1].at.0);
|
||||||
|
assert_eq!(s.parts[1].at.0 + s.parts[1].size.0 as i64, s.size.0 as i64);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_tiny_span_is_a_click() {
|
fn a_tiny_span_is_a_click() {
|
||||||
let (laptop, monitor) = layout();
|
let (laptop, monitor) = layout();
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ pub fn stitch(frames: &[Frame], span: &Span) -> RgbaImage {
|
|||||||
imageops::FilterType::Triangle,
|
imageops::FilterType::Triangle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
imageops::replace(&mut image, &crop, part.at.0 as i64, part.at.1 as i64);
|
imageops::replace(&mut image, &crop, part.at.0, part.at.1);
|
||||||
}
|
}
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user