mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 21:23:02 +00:00
This commit is contained in:
commit
d8ea8061ee
4 files changed
+385
-5
No files matched your search
@@ -3,7 +3,8 @@
|
||||
//! - **Default mode** (Photoshop's default, Use Classic Mode off): the crop box stays upright on
|
||||
//! screen and the image turns behind it. A drag outside the box turns the image about the box's
|
||||
//! centre (⇧ in 15° steps; the readout beside the pointer is the image's turn, clockwise
|
||||
//! positive), a drag inside moves the image under the box (the box stays put; with View › Snap
|
||||
//! positive) and keeps the frame inside the turned image ([`keep_inside`]; not with ⌘ held), a
|
||||
//! drag inside moves the image under the box (the box stays put; with View › Snap
|
||||
//! on, the upright frame's edges and centre snap as a Classic Mode frame's do, #8580), the arrow
|
||||
//! keys nudge the image, and the handles resize the box on screen as usual. With **Auto Center
|
||||
//! Preview** on (the default), a box being resized keeps its centre where it is on screen (also
|
||||
@@ -114,6 +115,74 @@ pub fn image_turn(frame_angle: f64) -> f64 {
|
||||
if a == 0.0 { 0.0 } else { a }
|
||||
}
|
||||
|
||||
/// How far (document px) a turned frame's corner may lie past the canvas and still count as on
|
||||
/// it: rounding, not a visible gap.
|
||||
const FIT_TOLERANCE: f64 = 1e-6;
|
||||
|
||||
/// Frame `rect` turned `deg` degrees about its centre, kept inside a `cw`×`ch` canvas (#10869):
|
||||
/// `rect` itself when it fits; else moved the shortest way in; else shrunk about its centre,
|
||||
/// keeping its proportions, to the largest size that fits anywhere, then moved in. `None` when a
|
||||
/// size or the angle isn't finite, or a size isn't positive.
|
||||
pub fn kept_inside(rect: [f64; 4], deg: f64, cw: f64, ch: f64) -> Option<[f64; 4]> {
|
||||
let (w, h) = (rect[2] - rect[0], rect[3] - rect[1]);
|
||||
if ![w, h, cw, ch, deg].iter().all(|v| v.is_finite()) || w <= 0.0 || h <= 0.0 || cw <= 0.0 || ch <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let (s, c) = deg.to_radians().sin_cos();
|
||||
let (s, c) = (s.abs(), c.abs());
|
||||
// Half the turned frame's bounds: its corners are on the canvas when these are.
|
||||
let (ex, ey) = ((w * c + h * s) / 2.0, (w * s + h * c) / 2.0);
|
||||
let m = crate::crop_ui::center(rect);
|
||||
// A frame that fits stays bit for bit, also one whose corners a rounding error puts a hair
|
||||
// past the edges (a frame fitted by an earlier turn touches them).
|
||||
let fits = |v: f64, e: f64, len: f64| v - e >= -FIT_TOLERANCE && v + e <= len + FIT_TOLERANCE;
|
||||
if fits(m[0], ex, cw) && fits(m[1], ey, ch) {
|
||||
return Some(rect);
|
||||
}
|
||||
// The scale that makes the bounds fit the canvas; a rounding error short of 1 (180°) fits.
|
||||
let k = (cw / (2.0 * ex)).min(ch / (2.0 * ey));
|
||||
let k = if k >= 1.0 - 1e-9 { 1.0 } else { k };
|
||||
// Each axis on its own: the nearest centre that keeps the bounds on the canvas.
|
||||
let along = |v: f64, e: f64, len: f64| if 2.0 * e >= len { len / 2.0 } else { v.clamp(e, len - e) };
|
||||
let n = [along(m[0], ex * k, cw), along(m[1], ey * k, ch)];
|
||||
let r = if k == 1.0 {
|
||||
// Kept size: moved by exactly the offset.
|
||||
let d = [n[0] - m[0], n[1] - m[1]];
|
||||
[rect[0] + d[0], rect[1] + d[1], rect[2] + d[0], rect[3] + d[1]]
|
||||
} else {
|
||||
let (hw, hh) = (w * k / 2.0, h * k / 2.0);
|
||||
[n[0] - hw, n[1] - hh, n[0] + hw, n[1] + hh]
|
||||
};
|
||||
r.iter().all(|v| v.is_finite()).then_some(r)
|
||||
}
|
||||
|
||||
/// A default-mode turn keeps the frame inside the turned image, as in Photoshop (#10869): the
|
||||
/// frame the drag started from, `rect` (turned `from` degrees then), keeps its size and place
|
||||
/// when it fits the image at the frame's angle now, else [`kept_inside`] moves it in or shrinks
|
||||
/// it. Each step starts from the press frame, so turning back gives the frame back. The view
|
||||
/// moves with the frame's centre, so the box stays put on screen. With ⌘ (Ctrl) `held` the frame
|
||||
/// stays as it was at the press and ↵ fills the uncovered corners.
|
||||
///
|
||||
/// Not measured in Photoshop, so they keep the frame as before: Fill set to Content-Aware Fill,
|
||||
/// and a frame that already reached past the canvas at the press.
|
||||
pub fn keep_inside(app: &mut PhotocraftApp, rect: [f64; 4], from: f64, held: bool) {
|
||||
let Some(now) = finite_frame(app) else { return };
|
||||
let Some((cw, ch)) = app.session.active().map(|st| (f64::from(st.doc.size.width), f64::from(st.doc.size.height))) else { return };
|
||||
let inside_at_press = kept_inside(rect, from, cw, ch) == Some(rect);
|
||||
let want = if held || app.ui.tool_options.crop_content_aware || !inside_at_press {
|
||||
rect
|
||||
} else {
|
||||
kept_inside(rect, crate::crop_ui::angle(app), cw, ch).unwrap_or(rect)
|
||||
};
|
||||
if want == now {
|
||||
return;
|
||||
}
|
||||
// Moves the frame's centre and the view together (the box stays put), then sets the size.
|
||||
let (a, b) = (crate::crop_ui::center(now), crate::crop_ui::center(want));
|
||||
shift_image(app, [a[0] - b[0], a[1] - b[1]]);
|
||||
app.ui.crop_rect = Some(want);
|
||||
}
|
||||
|
||||
/// A live-centred resize ([`live_resize`]): the frame `rect` and handle (`hx`, `hy`) at the press,
|
||||
/// grabbed at document point `start`; the pointer now at `q` (in the press view); the frame's turn
|
||||
/// `deg`, the aspect `ratio` to hold and ⌥ (resize about the centre).
|
||||
|
||||
@@ -98,8 +98,9 @@ pub enum CropDrag {
|
||||
MoveImage { start: [f64; 2], rect: [f64; 4], view: Option<crate::crop_mode::PressView> },
|
||||
/// Default mode, outside the box: the image turns about the frame's `center` (which stays put
|
||||
/// on screen). `grab` is the pointer's direction from it at the press (radians, in the press
|
||||
/// `view`), `from` the frame's angle then (degrees).
|
||||
TurnImage { center: [f64; 2], grab: f64, from: f64, view: Option<crate::crop_mode::PressView> },
|
||||
/// `view`), `from` the frame's angle then (degrees) and `rect` the frame then, which the turn
|
||||
/// keeps inside the turned image (`crop_mode::keep_inside`).
|
||||
TurnImage { center: [f64; 2], grab: f64, from: f64, rect: [f64; 4], view: Option<crate::crop_mode::PressView> },
|
||||
}
|
||||
|
||||
/// What the pointer is over, relative to a crop frame.
|
||||
@@ -950,7 +951,7 @@ fn outside_drag(app: &PhotocraftApp, p: [f64; 2], rect: [f64; 4], deg: f64) -> C
|
||||
let c = center(rect);
|
||||
let grab = (p[1] - c[1]).atan2(p[0] - c[0]);
|
||||
if crate::crop_mode::moves_image(app) {
|
||||
CropDrag::TurnImage { center: c, grab, from: deg, view: crate::crop_mode::press_view(app) }
|
||||
CropDrag::TurnImage { center: c, grab, from: deg, rect, view: crate::crop_mode::press_view(app) }
|
||||
} else {
|
||||
// Outside the frame a drag turns it, as in Photoshop (#1792).
|
||||
CropDrag::Rotate { center: c, grab, from: deg }
|
||||
@@ -1018,13 +1019,14 @@ fn update(app: &mut PhotocraftApp, p: [f64; 2], mods: Modifiers) {
|
||||
fn follow_view_drag(app: &mut PhotocraftApp, p: [f64; 2], mods: Modifiers) -> bool {
|
||||
match app.crop.drag {
|
||||
Some(CropDrag::Rotate { center, grab, from }) => app.ui.crop_angle = turned_angle(center, grab, from, p, mods.shift),
|
||||
Some(CropDrag::TurnImage { center, grab, from, view }) => {
|
||||
Some(CropDrag::TurnImage { center, grab, from, rect, view }) => {
|
||||
// The image turns with the pointer's sweep on screen, so the frame turns against it.
|
||||
let at = crate::crop_mode::to_press(app, view, p);
|
||||
let turned = from - ((at[1] - center[1]).atan2(at[0] - center[0]) - grab).to_degrees();
|
||||
if turned.is_finite() {
|
||||
app.ui.crop_angle = normalized_angle(if mods.shift { (turned / 15.0).round() * 15.0 } else { turned });
|
||||
crate::crop_mode::sync_frame(app);
|
||||
crate::crop_mode::keep_inside(app, rect, from, mods.command);
|
||||
}
|
||||
}
|
||||
Some(CropDrag::MoveImage { start, rect, view }) => {
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
//! Crop tool, default mode: turning the image keeps the frame inside the turned image (#10869).
|
||||
//!
|
||||
//! Measured in the reference application on an 800 × 500 document, turning by a drag outside the
|
||||
//! frame with Auto Center Preview off: the frame keeps its size where it fits the turned image,
|
||||
//! else moves the shortest way into it, else shrinks about its centre keeping its proportions.
|
||||
//! With Ctrl (⌘) held the frame isn't adjusted and the uncovered corners get the Fill.
|
||||
//! PhotoCraft never adjusted the frame, so ↵ filled the corners with the background colour.
|
||||
//!
|
||||
//! Driven as an agent does, through `ui.pointer` (document px as the view showed them at the
|
||||
//! press).
|
||||
|
||||
use photocraft_ui_egui::PhotocraftApp;
|
||||
use photocraft_ui_egui::canvas::ViewXform;
|
||||
use photocraft_ui_egui::control::{ControlRequest, Outcome, handle};
|
||||
use photocraft_ui_egui::crop_mode::kept_inside;
|
||||
use photocraft_ui_egui::crop_ui::{angle, center, corners};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// The document's size.
|
||||
const W: f64 = 800.0;
|
||||
const H: f64 = 500.0;
|
||||
/// The whole canvas, the default frame.
|
||||
const CANVAS: [f64; 4] = [0.0, 0.0, W, H];
|
||||
|
||||
/// An 800 × 500 white document with a #1050e0 background colour, the Crop tool in the default
|
||||
/// mode with Auto Center Preview off, and `frame` as the pending crop frame: the default frame
|
||||
/// when it is the whole canvas, else drawn from its bottom-right corner to its top-left one (a
|
||||
/// press on the default frame's corner handle would resize it).
|
||||
fn app(frame: [f64; 4]) -> (PhotocraftApp, egui::Context) {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
app.run("file.new", json!({"width": W, "height": H, "background": "white"})).expect("new");
|
||||
app.run("tools.setColors", json!({"background": "#1050e0"})).expect("colors");
|
||||
app.sync_views();
|
||||
app.ui.extras.snap = false;
|
||||
app.ui.view.show.smart_guides = false;
|
||||
app.ui.tool_options.crop_shield.auto_center_preview = false;
|
||||
control(&mut app, &ctx, "ui.set", json!({"tool": "crop", "zoom": 1.0, "center": [W / 2.0, H / 2.0]}));
|
||||
photocraft_ui_egui::crop_ui::ensure_frame(&mut app);
|
||||
assert!(!app.ui.tool_options.crop_shield.classic_mode, "Photoshop's default mode");
|
||||
if frame != CANVAS {
|
||||
let events = json!([
|
||||
{"kind": "down", "x": frame[2], "y": frame[3]},
|
||||
{"kind": "move", "x": frame[0], "y": frame[1]},
|
||||
{"kind": "up", "x": frame[0], "y": frame[1]},
|
||||
]);
|
||||
control(&mut app, &ctx, "ui.pointer", json!({"events": events}));
|
||||
}
|
||||
assert_eq!(app.ui.crop_rect, Some(frame));
|
||||
(app, ctx)
|
||||
}
|
||||
|
||||
/// Runs one control-channel request, which must succeed.
|
||||
fn control(app: &mut PhotocraftApp, ctx: &egui::Context, method: &str, params: Value) {
|
||||
let (req, _rx) = ControlRequest::new(method, params.clone());
|
||||
assert!(matches!(handle(app, ctx, &req), Outcome::Done(_)), "{method} {params}");
|
||||
}
|
||||
|
||||
/// The point 500 px right of the frame's centre turned `deg` degrees clockwise about it: a press
|
||||
/// there is outside every frame below, and moving to it from the unturned point turns the image
|
||||
/// `deg` degrees clockwise.
|
||||
fn around(frame: [f64; 4], deg: f64) -> [f64; 2] {
|
||||
let c = center(frame);
|
||||
let (s, k) = deg.to_radians().sin_cos();
|
||||
[c[0] + 500.0 * k, c[1] + 500.0 * s]
|
||||
}
|
||||
|
||||
/// A turn of the image: a press outside `frame`, then for each `(deg, modifiers)` step a move to
|
||||
/// `deg` with those modifiers held (one `ui.pointer` call each, so a key can be pressed during
|
||||
/// the drag); `each` runs after every step. Releases at the last step's point with its modifiers.
|
||||
fn turn(app: &mut PhotocraftApp, ctx: &egui::Context, frame: [f64; 4], steps: &[(f64, Value)], mut each: impl FnMut(&PhotocraftApp, f64)) {
|
||||
let p = around(frame, 0.0);
|
||||
control(app, ctx, "ui.pointer", json!({"events": [{"kind": "down", "x": p[0], "y": p[1]}]}));
|
||||
for (deg, mods) in steps {
|
||||
let p = around(frame, *deg);
|
||||
control(app, ctx, "ui.pointer", json!({"modifiers": mods, "events": [{"kind": "move", "x": p[0], "y": p[1]}]}));
|
||||
each(app, *deg);
|
||||
}
|
||||
let (deg, mods) = steps.last().expect("a step");
|
||||
let p = around(frame, *deg);
|
||||
control(app, ctx, "ui.pointer", json!({"modifiers": mods, "events": [{"kind": "up", "x": p[0], "y": p[1]}]}));
|
||||
}
|
||||
|
||||
/// No modifier held.
|
||||
fn plain() -> Value {
|
||||
json!({})
|
||||
}
|
||||
|
||||
/// The pending frame.
|
||||
fn frame(app: &PhotocraftApp) -> [f64; 4] {
|
||||
app.ui.crop_rect.expect("a frame")
|
||||
}
|
||||
|
||||
/// The pending frame's corners, turned with it, all lie on the canvas.
|
||||
fn inside(app: &PhotocraftApp) -> bool {
|
||||
corners(frame(app), angle(app)).iter().all(|c| (-1e-6..=W + 1e-6).contains(&c[0]) && (-1e-6..=H + 1e-6).contains(&c[1]))
|
||||
}
|
||||
|
||||
/// Where the frame's centre shows on screen.
|
||||
fn on_screen(app: &PhotocraftApp) -> egui::Pos2 {
|
||||
let c = center(frame(app));
|
||||
ViewXform::active(app).expect("a view").to_screen(c[0] as f32, c[1] as f32)
|
||||
}
|
||||
|
||||
/// ↵: commits the crop and returns the document's new size.
|
||||
fn committed(app: &mut PhotocraftApp) -> (u32, u32) {
|
||||
photocraft_ui_egui::canvas::commit_crop(app);
|
||||
let size = app.session.active().expect("document").doc.size;
|
||||
(size.width, size.height)
|
||||
}
|
||||
|
||||
/// The flattened document's four corner pixels and its middle one.
|
||||
fn corners_and_middle(app: &PhotocraftApp) -> [[f32; 4]; 5] {
|
||||
let doc = &app.session.active().expect("document").doc;
|
||||
let flat = photocraft_compose::flatten(doc);
|
||||
let (w, h) = (doc.size.width as usize, doc.size.height as usize);
|
||||
let px = |x: usize, y: usize| flat.px[y * w + x];
|
||||
[px(0, 0), px(w - 1, 0), px(w - 1, h - 1), px(0, h - 1), px(w / 2, h / 2)]
|
||||
}
|
||||
|
||||
/// The background colour #1050e0 rather than the white image: its red is 16/255. A corner that
|
||||
/// the turned image covers only in part still has far more red than that.
|
||||
fn background(p: [f32; 4]) -> bool {
|
||||
p[0] < 0.5 && (p[2] - 224.0 / 255.0).abs() < 0.02
|
||||
}
|
||||
|
||||
/// White: the image.
|
||||
fn white(p: [f32; 4]) -> bool {
|
||||
p[..3].iter().all(|&v| v > 0.98)
|
||||
}
|
||||
|
||||
/// The frame's angle is `want` degrees, up to the `f32` rounding of the view the pointer is read
|
||||
/// through.
|
||||
fn near_angle(deg: f64, want: f64) -> bool {
|
||||
(deg - want).abs() < 1e-4
|
||||
}
|
||||
|
||||
/// The size of the largest frame of the canvas's proportions that fits inside the canvas turned
|
||||
/// `deg` degrees.
|
||||
fn fitted_size(deg: f64) -> (f64, f64) {
|
||||
let (s, c) = deg.to_radians().sin_cos();
|
||||
let (s, c) = (s.abs(), c.abs());
|
||||
let k = (W / (W * c + H * s)).min(H / (W * s + H * c));
|
||||
(W * k, H * k)
|
||||
}
|
||||
|
||||
/// The issue's first two rows: the whole-canvas frame shrinks about its centre, keeping its
|
||||
/// proportions, to the largest size that fits (687 × 429 measured at 6.3°, 580 × 362 at the 15°
|
||||
/// that ⇧ snaps to), live while dragging, with the box put on screen. ↵ crops to it and no corner
|
||||
/// shows the background colour.
|
||||
#[test]
|
||||
fn turning_the_whole_canvas_frame_shrinks_it_inside_the_turned_image() {
|
||||
for (deg, shift, measured) in [(6.3, false, (687.0, 429.0)), (13.0, true, (580.0, 362.0))] {
|
||||
let what = format!("turned {deg}°, shift {shift}");
|
||||
let (mut app, ctx) = app(CANVAS);
|
||||
let screen = on_screen(&app);
|
||||
// ⇧ pressed after the press, as measured: the turn snaps to 15°.
|
||||
let steps = [(deg / 2.0, plain()), (deg, json!({"shift": shift}))];
|
||||
turn(&mut app, &ctx, CANVAS, &steps, |app, step| {
|
||||
assert!(inside(app), "{what}: at {step}° the frame {:?} turned {} leaves the image", frame(app), angle(app));
|
||||
assert!(on_screen(app).distance(screen) < 0.01, "{what}: at {step}° the box moved on screen to {:?}", on_screen(app));
|
||||
});
|
||||
let turned = if shift { 15.0 } else { deg };
|
||||
assert!(near_angle(angle(&app), -turned), "{what}: the frame's angle is {}", angle(&app));
|
||||
let (w, h) = fitted_size(angle(&app));
|
||||
let f = frame(&app);
|
||||
let (fw, fh) = (f[2] - f[0], f[3] - f[1]);
|
||||
assert!((fw - w).abs() < 1e-6 && (fh - h).abs() < 1e-6, "{what}: {fw} × {fh}, want {w} × {h}");
|
||||
assert!((fw - measured.0).abs() <= 3.0 && (fh - measured.1).abs() <= 3.0, "{what}: {fw} × {fh} is far from the reference's {measured:?}");
|
||||
let c = center(f);
|
||||
assert!((c[0] - W / 2.0).abs() < 1e-6 && (c[1] - H / 2.0).abs() < 1e-6, "{what}: centred, not at {c:?}");
|
||||
let (cw, ch) = committed(&mut app);
|
||||
assert!((f64::from(cw) - w).abs() <= 1.0 && (f64::from(ch) - h).abs() <= 1.0, "{what}: committed {cw} × {ch}");
|
||||
for p in corners_and_middle(&app) {
|
||||
assert!(!background(p), "{what}: a corner shows the background colour: {p:?}");
|
||||
}
|
||||
assert!(white(corners_and_middle(&app)[4]), "{what}: the middle is the image");
|
||||
}
|
||||
}
|
||||
|
||||
/// The issue's Ctrl row: with Ctrl (⌘) held the frame keeps the whole canvas and ↵ fills the
|
||||
/// corners the turn uncovered. Pressed part way through the turn, Ctrl gives the press frame back
|
||||
/// at once.
|
||||
#[test]
|
||||
fn with_command_held_the_turn_keeps_the_frame() {
|
||||
for late in [false, true] {
|
||||
let (mut app, ctx) = app(CANVAS);
|
||||
let first = if late { plain() } else { json!({"command": true}) };
|
||||
turn(&mut app, &ctx, CANVAS, &[(3.0, first), (6.0, json!({"command": true}))], |app, step| {
|
||||
if late && step == 3.0 {
|
||||
assert!(frame(app) != CANVAS && inside(app), "Ctrl not held yet: the frame fits, {:?}", frame(app));
|
||||
} else {
|
||||
assert_eq!(frame(app), CANVAS, "late {late}: at {step}°");
|
||||
}
|
||||
});
|
||||
assert!(near_angle(angle(&app), -6.0), "late {late}: {}", angle(&app));
|
||||
assert_eq!(committed(&mut app), (800, 500), "late {late}");
|
||||
let [corner @ .., middle] = corners_and_middle(&app);
|
||||
for p in corner {
|
||||
assert!(background(p), "late {late}: an uncovered corner is {p:?}");
|
||||
}
|
||||
assert!(white(middle), "late {late}: the middle is the image");
|
||||
}
|
||||
}
|
||||
|
||||
/// The issue's fourth row: a 300 × 201 frame at (100, 99) still lies inside the image turned
|
||||
/// 11.3° about its centre, so it stays exactly as it was.
|
||||
#[test]
|
||||
fn a_frame_that_fits_the_turned_image_keeps_its_size_and_place() {
|
||||
let start = [100.0, 99.0, 400.0, 300.0];
|
||||
let (mut app, ctx) = app(start);
|
||||
turn(&mut app, &ctx, start, &[(5.0, plain()), (11.3, plain())], |app, step| {
|
||||
assert_eq!(frame(app), start, "at {step}°");
|
||||
});
|
||||
assert!(near_angle(angle(&app), -11.3), "{}", angle(&app));
|
||||
assert_eq!(committed(&mut app), (300, 201));
|
||||
for p in corners_and_middle(&app) {
|
||||
assert!(white(p), "{p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The issue's last row: a 300 × 200 frame in the top-left corner, turned 11.1°, keeps its size
|
||||
/// and moves the shortest way into the turned image: its turned corners end up touching the
|
||||
/// canvas's left and top edges, not further in. The box stays put on screen while the image
|
||||
/// slides under it.
|
||||
#[test]
|
||||
fn a_frame_in_the_corner_moves_in_without_shrinking() {
|
||||
let start = [0.0, 0.0, 300.0, 200.0];
|
||||
let (mut app, ctx) = app(start);
|
||||
let screen = on_screen(&app);
|
||||
turn(&mut app, &ctx, start, &[(5.0, plain()), (11.1, plain())], |app, step| {
|
||||
assert!(inside(app), "at {step}° the frame {:?} leaves the image", frame(app));
|
||||
assert!(on_screen(app).distance(screen) < 0.01, "at {step}° the box moved on screen to {:?}", on_screen(app));
|
||||
});
|
||||
let f = frame(&app);
|
||||
assert!(((f[2] - f[0]) - 300.0).abs() < 1e-9 && ((f[3] - f[1]) - 200.0).abs() < 1e-9, "the size is kept: {f:?}");
|
||||
let turned = corners(f, angle(&app));
|
||||
let left = turned.iter().map(|c| c[0]).fold(f64::INFINITY, f64::min);
|
||||
let top = turned.iter().map(|c| c[1]).fold(f64::INFINITY, f64::min);
|
||||
assert!(left.abs() < 1e-9 && top.abs() < 1e-9, "moved the shortest way: the turned frame's left {left}, top {top}");
|
||||
assert_eq!(committed(&mut app), (300, 200));
|
||||
for p in corners_and_middle(&app) {
|
||||
assert!(!background(p), "a corner shows the background colour: {p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every step of the turn starts from the frame at the press, so turning back within the same
|
||||
/// drag gives the whole frame back instead of keeping the smallest size it passed through.
|
||||
#[test]
|
||||
fn turning_back_gives_the_frame_back() {
|
||||
let (mut app, ctx) = app(CANVAS);
|
||||
turn(&mut app, &ctx, CANVAS, &[(10.0, plain()), (0.0, plain())], |_, _| {});
|
||||
assert_eq!((frame(&app), angle(&app)), (CANVAS, 0.0));
|
||||
}
|
||||
|
||||
/// A second turn starts from the frame the first one fitted, whose turned corners touch the
|
||||
/// canvas's edges: turned further it shrinks again, turned back upright it stays as it is.
|
||||
#[test]
|
||||
fn a_second_turn_starts_from_the_fitted_frame() {
|
||||
for second in [10.0, -6.3] {
|
||||
let (mut app, ctx) = app(CANVAS);
|
||||
turn(&mut app, &ctx, CANVAS, &[(6.3, plain())], |_, _| {});
|
||||
let first = frame(&app);
|
||||
turn(&mut app, &ctx, first, &[(second, plain())], |_, _| {});
|
||||
let f = frame(&app);
|
||||
let what = format!("then {second}°: {f:?} turned {}", angle(&app));
|
||||
assert!(near_angle(angle(&app), -(6.3 + second)), "{what}");
|
||||
assert!(inside(&app), "{what}");
|
||||
if second > 0.0 {
|
||||
let want = fitted_size(angle(&app));
|
||||
assert!(((f[2] - f[0]) - want.0).abs() < 1e-6 && ((f[3] - f[1]) - want.1).abs() < 1e-6, "{what}: want {want:?}");
|
||||
} else {
|
||||
assert_eq!(f, first, "{what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame `kept_inside` fitted is left exactly as it is at the same angle, at every tenth of a
|
||||
/// degree up to 90°. The fitted frame's turned corners touch the canvas's edges, and at many
|
||||
/// angles rounding puts them a hair past: without the fit's tolerance such a frame would count as
|
||||
/// reaching past the canvas, and a second turn would no longer keep it inside.
|
||||
#[test]
|
||||
fn a_fitted_frame_fits_at_its_own_angle() {
|
||||
let off_centre = [0.0, 0.0, 300.0, 200.0];
|
||||
for i in 1..900 {
|
||||
let deg = f64::from(i) / 10.0;
|
||||
for start in [CANVAS, off_centre] {
|
||||
let f = kept_inside(start, deg, W, H).expect("a frame");
|
||||
assert_eq!(kept_inside(f, deg, W, H), Some(f), "{start:?} at {deg}°");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Not measured, so they keep today's behaviour: with Fill set to Content-Aware Fill, with a
|
||||
/// frame already reaching past the canvas at the press, and in Classic Mode (where the frame
|
||||
/// turns over the image) the frame keeps its size and place.
|
||||
#[test]
|
||||
fn content_aware_fill_a_frame_past_the_canvas_and_classic_mode_keep_the_frame() {
|
||||
let past = [-50.0, 100.0, 300.0, 300.0];
|
||||
for (what, start) in [("content-aware", CANVAS), ("past the canvas", past), ("classic", CANVAS)] {
|
||||
let (mut app, ctx) = app(start);
|
||||
app.ui.tool_options.crop_content_aware = what == "content-aware";
|
||||
app.ui.tool_options.crop_shield.classic_mode = what == "classic";
|
||||
turn(&mut app, &ctx, start, &[(6.3, plain())], |_, _| {});
|
||||
assert!(angle(&app).abs() > 6.0, "{what}: turned, {}", angle(&app));
|
||||
assert_eq!(frame(&app), start, "{what}");
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod crop_default_mode;
|
||||
mod crop_overlay_icons;
|
||||
mod crop_popup_keys;
|
||||
mod crop_shift_corner;
|
||||
mod crop_turn_keeps_frame_inside;
|
||||
mod custom_print_paper;
|
||||
mod display_shadows;
|
||||
mod drag_preview_canvas;
|
||||
|
||||
Reference in new issue
Block a user