Let a crop be trimmed from one edge
Four bar handles at the midpoints of the sides, each moving only its own side along the axis across it. The overlay reports an edge as 0.5 on the axis it does not move, so the anchor Rust takes is the middle of the far side. Under a ratio lock the dragged axis leads. with_aspect grew the short axis onto the ratio, which for an edge pulled inward made the untouched axis the leader and pushed the edge straight back out.
This commit is contained in:
@@ -260,8 +260,9 @@ impl CropRect {
|
|||||||
///
|
///
|
||||||
/// `anchor` is the point of the rect that stays put, in the rect's own
|
/// `anchor` is the point of the rect that stays put, in the rect's own
|
||||||
/// `0..1` coordinates: `(1.0, 1.0)` while the top-left handle is dragged,
|
/// `0..1` coordinates: `(1.0, 1.0)` while the top-left handle is dragged,
|
||||||
/// so the far corner is the one that does not move, and `(0.5, 0.5)` when
|
/// so the far corner is the one that does not move, `(0.0, 0.5)` while
|
||||||
/// a ratio is chosen and the composition should stay where it is.
|
/// the right-hand edge is dragged, and `(0.5, 0.5)` when a ratio is
|
||||||
|
/// chosen and the composition should stay where it is.
|
||||||
///
|
///
|
||||||
/// **The rect grows onto the ratio rather than shrinking onto it.** The
|
/// **The rect grows onto the ratio rather than shrinking onto it.** The
|
||||||
/// axis that is short is extended; the long one is never trimmed. Fitting
|
/// axis that is short is extended; the long one is never trimmed. Fitting
|
||||||
@@ -269,6 +270,7 @@ impl CropRect {
|
|||||||
/// along one axis alone would be immediately clamped back by the other,
|
/// along one axis alone would be immediately clamped back by the other,
|
||||||
/// and the handle would simply refuse to move. The result is then scaled
|
/// and the handle would simply refuse to move. The result is then scaled
|
||||||
/// down, both axes together, only as far as the frame's edge demands.
|
/// down, both axes together, only as far as the frame's edge demands.
|
||||||
|
/// The exception is an edge: see the note in the body.
|
||||||
pub fn with_aspect(self, frame_w: u32, frame_h: u32, ratio: f32, anchor: (f32, f32)) -> Self {
|
pub fn with_aspect(self, frame_w: u32, frame_h: u32, ratio: f32, anchor: (f32, f32)) -> Self {
|
||||||
let rect = self.normalised();
|
let rect = self.normalised();
|
||||||
let ratio = finite(ratio, 0.0);
|
let ratio = finite(ratio, 0.0);
|
||||||
@@ -286,8 +288,19 @@ impl CropRect {
|
|||||||
let px = rect.x + ax * rect.width;
|
let px = rect.x + ax * rect.width;
|
||||||
let py = rect.y + ay * rect.height;
|
let py = rect.y + ay * rect.height;
|
||||||
|
|
||||||
let mut w = rect.width.max(rect.height * r);
|
// An anchor in the middle of one side is an *edge* being dragged, and
|
||||||
let mut h = w / r;
|
// then the axis across that edge leads: it is the only one the user
|
||||||
|
// moved. Growing the short axis instead would take the other side
|
||||||
|
// for the leader whenever the edge went inward, and the edge would be
|
||||||
|
// pushed straight back out — a handle that only ever grows the crop.
|
||||||
|
let (mut w, mut h) = if ax == 0.5 && ay != 0.5 {
|
||||||
|
(rect.height * r, rect.height)
|
||||||
|
} else if ay == 0.5 && ax != 0.5 {
|
||||||
|
(rect.width, rect.width / r)
|
||||||
|
} else {
|
||||||
|
let w = rect.width.max(rect.height * r);
|
||||||
|
(w, w / r)
|
||||||
|
};
|
||||||
|
|
||||||
// Scaled to fit, never clamped to fit: clamping one axis against the
|
// Scaled to fit, never clamped to fit: clamping one axis against the
|
||||||
// frame would break the very ratio this exists to hold.
|
// frame would break the very ratio this exists to hold.
|
||||||
@@ -2255,6 +2268,42 @@ mod tests {
|
|||||||
assert!((c.y - start.y).abs() < 1e-5, "{c:?}");
|
assert!((c.y - start.y).abs() < 1e-5, "{c:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_locked_edge_leads_and_the_far_side_stays_put() {
|
||||||
|
// An edge dragged inward under a lock must narrow the crop. With the
|
||||||
|
// short axis leading, the untouched height would win and push the
|
||||||
|
// edge straight back out.
|
||||||
|
let start = CropRect {
|
||||||
|
x: 0.2,
|
||||||
|
y: 0.2,
|
||||||
|
width: 0.4,
|
||||||
|
height: 0.6,
|
||||||
|
};
|
||||||
|
// Right edge held, dragged in: the left side and the vertical
|
||||||
|
// centre stay, the width is what was asked for.
|
||||||
|
let c = start.with_aspect(4000, 4000, 1.0, (0.0, 0.5));
|
||||||
|
assert!((c.x - start.x).abs() < 1e-5, "{c:?}");
|
||||||
|
assert!((c.width - start.width).abs() < 1e-5, "{c:?}");
|
||||||
|
assert!((c.height - start.width).abs() < 1e-5, "{c:?}");
|
||||||
|
assert!(
|
||||||
|
(c.y + c.height / 2.0 - (start.y + start.height / 2.0)).abs() < 1e-5,
|
||||||
|
"{c:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Top edge held: the bottom and the horizontal centre stay, the
|
||||||
|
// height is what was asked for.
|
||||||
|
let c = start.with_aspect(4000, 4000, 1.0, (0.5, 1.0));
|
||||||
|
assert!(
|
||||||
|
(c.y + c.height - (start.y + start.height)).abs() < 1e-5,
|
||||||
|
"{c:?}"
|
||||||
|
);
|
||||||
|
assert!((c.width - c.height).abs() < 1e-5, "{c:?}");
|
||||||
|
assert!(
|
||||||
|
(c.x + c.width / 2.0 - (start.x + start.width / 2.0)).abs() < 1e-5,
|
||||||
|
"{c:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_locked_rect_grows_onto_the_ratio_rather_than_shrinking_onto_it() {
|
fn a_locked_rect_grows_onto_the_ratio_rather_than_shrinking_onto_it() {
|
||||||
// Shrinking to fit makes a one-axis drag do nothing at all: the other
|
// Shrinking to fit makes a one-axis drag do nothing at all: the other
|
||||||
|
|||||||
@@ -1042,9 +1042,10 @@ fn wire_zoom_pan_crop(
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
};
|
};
|
||||||
// The overlay reports the corner it is *holding*; the point
|
// The overlay reports the handle it is *holding*; the point
|
||||||
// that must not move is the opposite one. A move reports no
|
// that must not move is the opposite one — the far corner, or
|
||||||
// corner at all, and keeps the shape it already has — there is
|
// for an edge the middle of the far side. A move reports no
|
||||||
|
// handle at all, and keeps the shape it already has — there is
|
||||||
// nothing to reshape, and reshaping about a centre would drag
|
// nothing to reshape, and reshaping about a centre would drag
|
||||||
// an over-moved rect smaller instead of sliding it along the
|
// an over-moved rect smaller instead of sliding it along the
|
||||||
// edge.
|
// edge.
|
||||||
|
|||||||
+59
-34
@@ -37,15 +37,17 @@ export component CropOverlay inherits Rectangle {
|
|||||||
in property <float> crop-w: 1.0;
|
in property <float> crop-w: 1.0;
|
||||||
in property <float> crop-h: 1.0;
|
in property <float> crop-h: 1.0;
|
||||||
|
|
||||||
/// A dragged crop rect — `x`, `y`, `width`, `height`, then the corner the
|
/// A dragged crop rect — `x`, `y`, `width`, `height`, then the handle the
|
||||||
/// drag is holding as a `0`/`1` pair, or `-1, -1` where the whole rect is
|
/// drag is holding as a point of the rect: a `0`/`1` pair for a corner,
|
||||||
/// being moved and its shape must not be touched.
|
/// `0.5` on the axis an edge does not move, or `-1, -1` where the whole
|
||||||
|
/// rect is being moved and its shape must not be touched.
|
||||||
///
|
///
|
||||||
/// The corner is what an aspect lock needs and cannot infer: reshaping a
|
/// The handle is what an aspect lock needs and cannot infer: reshaping a
|
||||||
/// rect onto a ratio has to know which of its corners is nailed down, and
|
/// rect onto a ratio has to know which point of it is nailed down, and
|
||||||
/// only the handle that took the press knows that. It is reported as the
|
/// only the handle that took the press knows that. It is reported as the
|
||||||
/// held corner rather than as the fixed one because that is what is
|
/// held point rather than as the fixed one because that is what is
|
||||||
/// written here — Rust takes the opposite corner.
|
/// written here — Rust takes the opposite one, which for an edge is the
|
||||||
|
/// middle of the far side.
|
||||||
callback crop-changed(float, float, float, float, float, float);
|
callback crop-changed(float, float, float, float, float, float);
|
||||||
|
|
||||||
/// TRACES: FR-DEV-17
|
/// TRACES: FR-DEV-17
|
||||||
@@ -189,24 +191,43 @@ export component CropOverlay inherits Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Corner handles. Each drags one corner while the opposite
|
// Handles: the four edges, then the four corners.
|
||||||
// stays put, which is the only behaviour that lets a crop
|
//
|
||||||
// be shaped rather than merely moved.
|
// A corner drags one corner while the opposite stays put, which is the
|
||||||
for corner in [
|
// only behaviour that lets a crop be shaped rather than merely moved. An
|
||||||
|
// edge drags that one side along the axis across it and leaves the other
|
||||||
|
// three where they are — the way to trim a strip off one side without
|
||||||
|
// disturbing the corner you had already placed.
|
||||||
|
//
|
||||||
|
// Each is a point of the rect in its own `0..1` coordinates; `0.5` on an
|
||||||
|
// axis is an edge's midpoint and means "this axis does not move". The
|
||||||
|
// edges come first because among overlapping siblings the last declared
|
||||||
|
// takes the press, and on a rect dragged small the corners are the ones
|
||||||
|
// that must stay reachable.
|
||||||
|
for handle in [
|
||||||
|
{ hx: 0.5, hy: 0.0 },
|
||||||
|
{ hx: 0.5, hy: 1.0 },
|
||||||
|
{ hx: 0.0, hy: 0.5 },
|
||||||
|
{ hx: 1.0, hy: 0.5 },
|
||||||
{ hx: 0.0, hy: 0.0 },
|
{ hx: 0.0, hy: 0.0 },
|
||||||
{ hx: 1.0, hy: 0.0 },
|
{ hx: 1.0, hy: 0.0 },
|
||||||
{ hx: 0.0, hy: 1.0 },
|
{ hx: 0.0, hy: 1.0 },
|
||||||
{ hx: 1.0, hy: 1.0 },
|
{ hx: 1.0, hy: 1.0 },
|
||||||
]: Rectangle {
|
]: Rectangle {
|
||||||
property <length> size: 18px;
|
property <bool> across: handle.hx == 0.5;
|
||||||
x: parent.rx + corner.hx * parent.rw - self.size / 2;
|
property <bool> along: handle.hy == 0.5;
|
||||||
y: parent.ry + corner.hy * parent.rh - self.size / 2;
|
// A corner is a square; an edge is a bar lying along its side, with
|
||||||
width: self.size;
|
// a hit area as long as the bar and as deep as a corner's.
|
||||||
height: self.size;
|
property <length> grip: 18px;
|
||||||
|
property <length> reach: 36px;
|
||||||
|
width: self.across ? self.reach : self.grip;
|
||||||
|
height: self.along ? self.reach : self.grip;
|
||||||
|
x: parent.rx + handle.hx * parent.rw - self.width / 2;
|
||||||
|
y: parent.ry + handle.hy * parent.rh - self.height / 2;
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 12px;
|
width: parent.across ? 24px : (parent.along ? 5px : 12px);
|
||||||
height: 12px;
|
height: parent.along ? 24px : (parent.across ? 5px : 12px);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
@@ -214,7 +235,11 @@ export component CropOverlay inherits Rectangle {
|
|||||||
TouchArea {
|
TouchArea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
mouse-cursor: (corner.hx == corner.hy)
|
mouse-cursor: parent.across
|
||||||
|
? MouseCursor.ns-resize
|
||||||
|
: parent.along
|
||||||
|
? MouseCursor.ew-resize
|
||||||
|
: (handle.hx == handle.hy)
|
||||||
? MouseCursor.nwse-resize
|
? MouseCursor.nwse-resize
|
||||||
: MouseCursor.nesw-resize;
|
: MouseCursor.nesw-resize;
|
||||||
|
|
||||||
@@ -246,27 +271,27 @@ export component CropOverlay inherits Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Movement as a fraction of the frame, live while
|
// Movement as a fraction of the frame, live while the handle is
|
||||||
// the handle is held.
|
// held — and nothing along an axis the handle does not move, so
|
||||||
property <float> dx: self.fraction-x(self.mouse-x) - self.from-x;
|
// an edge dragged at a slant still only moves its own side.
|
||||||
property <float> dy: self.fraction-y(self.mouse-y) - self.from-y;
|
property <float> dx: parent.across ? 0 : self.fraction-x(self.mouse-x) - self.from-x;
|
||||||
|
property <float> dy: parent.along ? 0 : self.fraction-y(self.mouse-y) - self.from-y;
|
||||||
|
|
||||||
moved => {
|
moved => {
|
||||||
if (!self.pressed) {
|
if (!self.pressed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Dragging a left/top handle moves the origin
|
// Dragging a left/top handle moves the origin and shrinks the
|
||||||
// and shrinks the extent by the same amount;
|
// extent by the same amount; a right/bottom handle moves only
|
||||||
// a right/bottom handle moves only the extent.
|
// the extent. Rust clamps the result, so an over-drag slides
|
||||||
// Rust clamps the result, so an over-drag
|
// rather than inverting.
|
||||||
// slides rather than inverting.
|
|
||||||
root.crop-changed(
|
root.crop-changed(
|
||||||
corner.hx == 0 ? self.ox + self.dx : self.ox,
|
handle.hx == 0 ? self.ox + self.dx : self.ox,
|
||||||
corner.hy == 0 ? self.oy + self.dy : self.oy,
|
handle.hy == 0 ? self.oy + self.dy : self.oy,
|
||||||
corner.hx == 0 ? self.ow - self.dx : self.ow + self.dx,
|
handle.hx == 0 ? self.ow - self.dx : self.ow + self.dx,
|
||||||
corner.hy == 0 ? self.oh - self.dy : self.oh + self.dy,
|
handle.hy == 0 ? self.oh - self.dy : self.oh + self.dy,
|
||||||
corner.hx,
|
handle.hx,
|
||||||
corner.hy,
|
handle.hy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user