Snap: dimension guides follow the final edge

Shift plus Alt restores the aspect from the original center after the size snaps. A line keeps the runner delta on both axes. Dimension guides are redrawn on the committed side. Grouped own size decomposes the absolute transform once.
This commit is contained in:
Cameron Gilroy
2026-10-06 09:05:59 -04:00
committed by Brandon Thomas
parent 40f3b6454a
commit 8e7cff341f
5 changed files with 419 additions and 58 deletions
+4 -15
View File
@@ -170,23 +170,12 @@ impl Tool for FrameTool {
pointer: b,
},
);
if hit.length_delta[1].is_none() {
b.y += hit.delta.y;
}
if hit.length_delta[0].is_none() {
// Position and length both land on the endpoint. Rescaling the segment would leave the other axis.
if hit.delta.x.is_finite() {
b.x += hit.delta.x;
}
if let Some(diff) = hit.length_delta[0]
&& diff.is_finite()
{
let v = b - sa;
let cur = v.hypot();
if cur > 1e-9 {
let next = cur + diff;
if next.is_finite() {
b = sa + v * (next / cur);
}
}
if hit.delta.y.is_finite() {
b.y += hit.delta.y;
}
self.guides = hit.guides;
}
+77 -10
View File
@@ -198,6 +198,77 @@ fn apply_aspect(from: Rect, handle: usize, rect: Rect, s: f64) -> Rect {
Rect::new(x0, y0, x1, y1)
}
/// Snap changes the size. Shift then restores the aspect, and Alt places that size on the original center.
///
/// Nudging one edge of an already centered rect walks the center. Mirroring that nudge doubles a length match.
fn finish_snapped_resize(
from: Rect,
handle: usize,
proposed: Rect,
mods: Mods,
driver_x: bool,
x_edges: [bool; 3],
y_edges: [bool; 3],
hit: &crate::Snap,
) -> Rect {
if !mods.alt {
let mut to = crate::snap::nudge_edges(proposed, x_edges, y_edges, hit.delta);
if mods.shift && from.width() > 1e-9 && from.height() > 1e-9 {
let s = if driver_x { (to.x1 - to.x0) / from.width() } else { (to.y1 - to.y0) / from.height() };
if s.is_finite() {
to = apply_aspect(from, handle, to, s);
}
}
return to;
}
let mut w = proposed.x1 - proposed.x0;
let mut h = proposed.y1 - proposed.y0;
w += centered_size_delta(x_edges, hit.delta.x, hit.length_delta[0]);
h += centered_size_delta(y_edges, hit.delta.y, hit.length_delta[1]);
if mods.shift && from.width() > 1e-9 && from.height() > 1e-9 {
if driver_x {
let s = w / from.width();
if s.is_finite() {
h = from.height() * s;
}
} else {
let s = h / from.height();
if s.is_finite() {
w = from.width() * s;
}
}
}
if !(w.is_finite() && h.is_finite()) {
return proposed;
}
let c = from.center();
Rect::new(c.x - w / 2.0, c.y - h / 2.0, c.x + w / 2.0, c.y + h / 2.0)
}
/// Length match: add the difference once. Position match: the flagged edge moves and the other side mirrors.
fn centered_size_delta(edges: [bool; 3], delta: f64, length_delta: Option<f64>) -> f64 {
if let Some(diff) = length_delta.filter(|d| d.is_finite()) {
return diff;
}
let edge = axis_size_delta(edges, delta);
if edge.is_finite() { edge * 2.0 } else { 0.0 }
}
/// Signed change in `hi - lo` when one flagged edge moves by `delta`.
fn axis_size_delta(edges: [bool; 3], delta: f64) -> f64 {
if !delta.is_finite() {
return 0.0;
}
let [low_edge, _, high_edge] = edges;
if high_edge && !low_edge {
delta
} else if low_edge && !high_edge {
-delta
} else {
0.0
}
}
fn apply_alt(from: Rect, handle: usize, rect: Rect) -> Rect {
let (x0, y0, x1, y1) = (rect.x0, rect.y0, rect.x1, rect.y1);
let c = from.center();
@@ -424,29 +495,25 @@ impl Tool for SelectionTool {
}
let exclude = cx.selection.items.clone();
let (pad_x, pad_y) = single_stroke_pad(cx);
let hit = crate::snap::snap(
let proposed = to;
let mut hit = crate::snap::snap(
cx,
SnapRequest {
spread,
gesture: Gesture::Resize,
rect: to,
rect: proposed,
x_edges,
y_edges,
exclude: &exclude,
copying: false,
lengths: [axis_length(x_edges, to.width(), pad_x), axis_length(y_edges, to.height(), pad_y)],
lengths: [axis_length(x_edges, proposed.width(), pad_x), axis_length(y_edges, proposed.height(), pad_y)],
angle: None,
radius: 0.0,
pointer,
},
);
to = crate::snap::nudge_edges(to, x_edges, y_edges, hit.delta);
if ev.mods.shift && !ev.mods.alt && from.width() > 1e-9 && from.height() > 1e-9 {
let s = if driver_x { (to.x1 - to.x0) / from.width() } else { (to.y1 - to.y0) / from.height() };
if s.is_finite() {
to = apply_aspect(from, handle, to, s);
}
}
to = finish_snapped_resize(from, handle, proposed, ev.mods, driver_x, x_edges, y_edges, &hit);
crate::snap::lay_dimension_guides(&mut hit.guides, cx.layout.xf(spread), proposed, to, x_edges, y_edges, hit.length_delta);
self.guides = hit.guides;
}
vec![Action::Preview(
+129 -25
View File
@@ -8,7 +8,7 @@ use std::collections::HashSet;
use designcraft_doc::{Item, ItemId, Orientation, PageSide, Selection, SpreadId, SpreadRef, StrokeAlign};
use designcraft_geom::snap::snap_to_grid;
use designcraft_geom::{Point, Rect, Vec2, format_measure};
use designcraft_geom::{Affine, Point, Rect, Vec2, format_measure};
pub use crate::{Gesture, Snap, SnapRequest, SnapView};
@@ -384,10 +384,11 @@ fn pass_spacing(_cx: &ToolContext, _req: &SnapRequest<'_>, _axis: Axis, _tol: f6
/// Other items' own side lengths, on resize and create.
///
/// Own size is the path box in item space, inflated the way [`Item::visible_bounds`] inflates,
/// then multiplied by the absolute scale (ancestors included, rotation dropped). Width and height
/// are both candidates for whichever length is set. The closest length inside the zone wins.
/// Delta is the length difference applied from the moving edge. A rotated rectangle contributes
/// its side lengths, not the axis-aligned box.
/// then scaled by the absolute transform. That transform is the parent chain composed with the
/// item and decomposed once, so a rotated child inside a non-uniform group keeps the spread-space
/// sides. Rotation is dropped. Width and height are both candidates for whichever length is set.
/// The closest length inside the zone wins. Delta is the length difference applied from the moving
/// edge. A rotated rectangle contributes its side lengths, not the axis-aligned box.
fn pass_dimensions(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) -> Option<AxisHit> {
if !matches!(req.gesture, Gesture::Resize | Gesture::Create) || !cx.snap.smart_guides || !cx.snap.smart_dimensions {
return None;
@@ -441,8 +442,15 @@ fn length_edge_delta(flags: [bool; 3], diff: f64) -> f64 {
/// Guide along the side whose length matched, after the edge has moved.
fn dimension_mark(req: &SnapRequest<'_>, axis: Axis, diff: f64, text: String) -> DimMark {
let rect = req.rect;
let flags = axis_flags(req, axis);
let (a, b) = dimension_segment(req.rect, axis, axis_flags(req, axis), diff).unwrap_or((Point::ORIGIN, Point::ORIGIN));
DimMark { a, b, text }
}
/// Spread-space segment along the matched side. `diff` 0 is the side of `rect` itself.
fn dimension_segment(rect: Rect, axis: Axis, flags: [bool; 3], diff: f64) -> Option<(Point, Point)> {
if !diff.is_finite() || !rect_finite(rect) {
return None;
}
let (a, b) = match axis {
Axis::X => {
let (x0, x1) = grown_span(rect.x0, rect.x1, flags, diff);
@@ -455,7 +463,59 @@ fn dimension_mark(req: &SnapRequest<'_>, axis: Axis, diff: f64, text: String) ->
(Point::new(x, y0), Point::new(x, y1))
}
};
DimMark { a, b, text }
(point_finite(a) && point_finite(b)).then_some((a, b))
}
fn point_finite(p: Point) -> bool {
p.x.is_finite() && p.y.is_finite()
}
/// Move dimension guides onto the rect the gesture commits.
///
/// Snap draws the guide on `pre` plus the length difference along the flagged edge. Aspect re-lock
/// and a centered scale move that side afterwards. The guide then follows the committed side.
pub(crate) fn lay_dimension_guides(
guides: &mut Vec<Overlay>,
xf: Affine,
pre: Rect,
committed: Rect,
x_edges: [bool; 3],
y_edges: [bool; 3],
length_delta: [Option<f64>; 2],
) {
if !rect_finite(committed) {
return;
}
let axes = [(Axis::X, x_edges, length_delta[0]), (Axis::Y, y_edges, length_delta[1])];
for (axis, edges, diff) in axes {
let Some(diff) = diff else { continue };
let Some((old_a, old_b)) = dimension_segment(pre, axis, edges, diff) else { continue };
let Some((new_a, new_b)) = dimension_segment(committed, axis, edges, 0.0) else { continue };
let (oa, ob) = (xf * old_a, xf * old_b);
let (na, nb) = (xf * new_a, xf * new_b);
if !point_finite(na) || !point_finite(nb) {
continue;
}
let mut found = false;
for g in guides.iter_mut() {
if let Overlay::Guide { a, b } = g
&& segment_same(*a, *b, oa, ob)
{
*a = na;
*b = nb;
found = true;
break;
}
}
if !found {
guides.push(Overlay::Guide { a: na, b: nb });
}
}
}
fn segment_same(a0: Point, b0: Point, a1: Point, b1: Point) -> bool {
let close = |p: Point, q: Point| (p.x - q.x).abs() <= 1e-6 && (p.y - q.y).abs() <= 1e-6;
(close(a0, a1) && close(b0, b1)) || (close(a0, b1) && close(b0, a1))
}
fn grown_span(a0: f64, a1: f64, flags: [bool; 3], diff: f64) -> (f64, f64) {
@@ -477,7 +537,7 @@ fn dimension_lengths(cx: &ToolContext, req: &SnapRequest<'_>) -> Vec<f64> {
let mut seen = HashSet::new();
if let Some(sp) = cx.doc.spread(req.spread) {
for item in &sp.items {
push_own_lengths(&mut out, cx.selection, req, item, 1.0, 1.0, false, 0, &mut seen);
push_own_lengths(&mut out, cx.selection, req, item, Affine::IDENTITY, false, 0, &mut seen);
}
}
if !layout_edits_parents(cx) {
@@ -491,8 +551,7 @@ fn push_own_lengths(
selection: &Selection,
req: &SnapRequest<'_>,
item: &Item,
ancestor_sx: f64,
ancestor_sy: f64,
ancestor: Affine,
skipped: bool,
depth: usize,
seen: &mut HashSet<ItemId>,
@@ -500,29 +559,32 @@ fn push_own_lengths(
if depth >= ALIGN_GROUP_DEPTH || !seen.insert(item.id) || item.hidden {
return;
}
let abs = ancestor * item.xf;
if !xf_finite(abs) {
return;
}
let skip = skipped || align_skips_item(selection, req, item.id);
if item.is_group() && !item.children().is_empty() {
let decomposed = designcraft_geom::decompose::decompose(item.xf);
let sx = ancestor_sx * decomposed.scale_x.abs();
let sy = ancestor_sy * decomposed.scale_y.abs();
if !sx.is_finite() || !sy.is_finite() {
return;
}
for child in item.children() {
push_own_lengths(out, selection, req, child, sx, sy, skip, depth + 1, seen);
push_own_lengths(out, selection, req, child, abs, skip, depth + 1, seen);
}
return;
}
if skip {
return;
}
let Some((w, h)) = leaf_own_size(item, ancestor_sx, ancestor_sy) else { return };
let Some((w, h)) = leaf_own_size(item, abs) else { return };
out.push(w);
out.push(h);
}
/// Path bounds in item space, stroke outset, then absolute scale. Rotation is not applied.
fn leaf_own_size(item: &Item, ancestor_sx: f64, ancestor_sy: f64) -> Option<(f64, f64)> {
fn xf_finite(m: Affine) -> bool {
m.as_coeffs().iter().all(|c| c.is_finite())
}
/// Path bounds in item space, stroke outset, then one decompose of the absolute transform.
/// Rotation is not applied to the side lengths.
fn leaf_own_size(item: &Item, abs: Affine) -> Option<(f64, f64)> {
let local = item.path.bounds()?;
if !rect_finite(local) {
return None;
@@ -535,9 +597,9 @@ fn leaf_own_size(item: &Item, ancestor_sx: f64, ancestor_sy: f64) -> Option<(f64
if !rect_finite(local) {
return None;
}
let decomposed = designcraft_geom::decompose::decompose(item.xf);
let sx = ancestor_sx * decomposed.scale_x.abs();
let sy = ancestor_sy * decomposed.scale_y.abs();
let decomposed = designcraft_geom::decompose::decompose(abs);
let sx = decomposed.scale_x.abs();
let sy = decomposed.scale_y.abs();
if !sx.is_finite() || !sy.is_finite() {
return None;
}
@@ -601,7 +663,7 @@ fn push_parent_length_chain(
if parent.pages.len() > 1 && parent.page_at_x(item.bounds().center().x) != Some(page_idx) {
continue;
}
push_own_lengths(out, cx.selection, req, item, 1.0, 1.0, false, 0, seen_items);
push_own_lengths(out, cx.selection, req, item, Affine::IDENTITY, false, 0, seen_items);
}
}
let Some(next_id) = next else { return };
@@ -1278,4 +1340,46 @@ mod tests {
assert!((hit.delta.x - 3.0).abs() < 1e-6);
assert!(matches!(hit.guides.last(), Some(Overlay::Measure { text, .. }) if text.contains("200")));
}
#[test]
fn grouped_child_matches_spread_side() {
// Group scale 2 on X and 1 on Y. Child rotated -90 degrees (decompose reports +90).
// Spread-space sides are local width times 1 (100) and local height times 2 (80).
// 82 is 2 pt from 80 and far from 200, which is what per-node scales would offer.
let mut doc = Document::new(&NewDocument::default());
let layer = doc.default_layer();
let child_id = ItemId(doc.alloc());
let mut child = Item::new(child_id, layer, Shape::Rectangle, shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 40.0)));
child.xf = Affine::rotate((-90.0_f64).to_radians());
let group_id = ItemId(doc.alloc());
let mut group = Item::new(group_id, layer, Shape::Group, designcraft_geom::PathData::default());
group.xf = Affine::scale_non_uniform(2.0, 1.0);
group.content = designcraft_doc::Content::Group { items: vec![std::sync::Arc::new(child)] };
doc.insert_item(SpreadRef::Doc(0), group, None).unwrap();
let cache = Cache::new();
let layout = CanvasLayout::new(&doc, false);
let sel = Selection::default();
let mut cx = ctx_on(&doc, &sel, &cache, &layout);
cx.snap.snap_to_guides = false;
cx.snap.snap_to_document_grid = false;
cx.snap.align_edges = false;
cx.snap.align_centers = false;
let hit = snap(
&cx,
SnapRequest {
spread: SpreadRef::Doc(0),
gesture: Gesture::Resize,
rect: Rect::new(200.0, 200.0, 282.0, 240.0),
x_edges: [false, false, true],
y_edges: [false, false, false],
exclude: &[],
copying: false,
lengths: [Some(82.0), None],
angle: None,
radius: 0.0,
pointer: Point::new(282.0, 220.0),
},
);
assert!((hit.delta.x - -2.0).abs() < 1e-6, "delta.x = {}, want -2 toward local height times 2", hit.delta.x);
}
}
+191 -2
View File
@@ -1,7 +1,8 @@
use designcraft_compose::Cache;
use designcraft_doc::build::NewDocument;
use designcraft_doc::{Document, ParaFormat, Selection, SpreadRef};
use designcraft_geom::{Rect, Unit};
use designcraft_doc::{Document, Item, ItemId, ParaFormat, Selection, Shape, SpreadRef};
use designcraft_geom::shapes;
use designcraft_geom::{Point, Rect, Unit};
use super::*;
@@ -292,3 +293,191 @@ fn turned_spread_maps_points_both_ways() {
let dv = slot.delta_to_spread(designcraft_geom::Vec2::new(10.0, 0.0));
assert!(dv.x.abs() < 1e-9 && (dv.y + 10.0).abs() < 1e-9 || (dv.y - 10.0).abs() < 1e-9, "{dv:?}");
}
fn add_rect(doc: &mut Document, rect: Rect) -> ItemId {
let layer = doc.default_layer();
let id = ItemId(doc.alloc());
let item = Item::new(id, layer, Shape::Rectangle, shapes::rectangle(rect));
doc.insert_item(SpreadRef::Doc(0), item, None).unwrap();
id
}
/// Dimensions on, ruler guides and the document grid off. Alignment is optional.
fn dims_ctx<'a>(d: &'a Document, s: &'a Selection, c: &'a Cache, l: &'a CanvasLayout, align: bool) -> ToolContext<'a> {
let mut cx = ctx(d, s, c, l);
cx.snap = SnapView::FACTORY;
cx.snap.snap_to_guides = false;
cx.snap.snap_to_document_grid = false;
cx.snap.align_edges = align;
cx.snap.align_centers = align;
cx
}
fn spread_pt(l: &CanvasLayout, x: f64, y: f64) -> Point {
l.to_canvas(SpreadRef::Doc(0), Point::new(x, y))
}
fn near(a: Point, b: Point) -> bool {
(a.x - b.x).abs() < 1e-4 && (a.y - b.y).abs() < 1e-4
}
fn guide_matches(overlays: &[Overlay], a: Point, b: Point) -> bool {
overlays.iter().any(|o| match o {
Overlay::Guide { a: ga, b: gb } => (near(*ga, a) && near(*gb, b)) || (near(*ga, b) && near(*gb, a)),
_ => false,
})
}
fn guide_list(overlays: &[Overlay]) -> String {
overlays
.iter()
.filter_map(|o| match o {
Overlay::Guide { a, b } => Some(format!("({:.4},{:.4})-({:.4},{:.4})", a.x, a.y, b.x, b.y)),
_ => None,
})
.collect::<Vec<_>>()
.join("; ")
}
#[test]
fn shift_alt_resize_keeps_aspect_and_center() {
// 100 by 50, center (150, 125), aspect 2. Corner handle, Shift and Alt.
// The centered width is 160. Another item's side is 163, inside the zone.
let mut doc = Document::new(&NewDocument::default());
let moving = add_rect(&mut doc, Rect::new(100.0, 100.0, 200.0, 150.0));
add_rect(&mut doc, Rect::new(400.0, 400.0, 563.0, 430.0));
let sel = Selection::items(vec![moving]);
let cache = Cache::new();
let layout = CanvasLayout::new(&doc, false);
let cx = dims_ctx(&doc, &sel, &cache, &layout, false);
let mut t = create("selection");
let mods = Mods { shift: true, alt: true, ..Mods::default() };
let down = spread_pt(&layout, 200.0, 150.0);
let drag = spread_pt(&layout, 230.0, 155.0);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, down.x, down.y).with_mods(mods));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, drag.x, drag.y).with_mods(mods));
let params = match &a[..] {
[Action::Preview(cmd, p)] => {
assert_eq!(cmd, "transform.resize");
p
}
other => panic!("{other:?}"),
};
let to = params["to"].as_array().unwrap();
let (x0, y0, x1, y1) = (to[0].as_f64().unwrap(), to[1].as_f64().unwrap(), to[2].as_f64().unwrap(), to[3].as_f64().unwrap());
let (w, h) = (x1 - x0, y1 - y0);
let (cx0, cy0) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
assert!((cx0 - 150.0).abs() < 1e-4 && (cy0 - 125.0).abs() < 1e-4, "center walked to ({cx0}, {cy0}) rect {to:?}");
assert!((w / h - 2.0).abs() < 1e-4, "aspect {} rect {to:?}", w / h);
assert!((w - 163.0).abs() < 1e-4, "driving length {w}, want the matched 163, not twice the correction");
}
#[test]
fn line_length_keeps_the_crossed_guide() {
// Diagonal from (100, 100) to (180, 140). Length is about 89.44, near a 92 side.
// Endpoint y is 2 pt from the other item's top, so Y is a position snap and X is the length.
let mut doc = Document::new(&NewDocument::default());
add_rect(&mut doc, Rect::new(400.0, 142.0, 492.0, 180.0));
let sel = Selection::default();
let cache = Cache::new();
let layout = CanvasLayout::new(&doc, false);
let cx = dims_ctx(&doc, &sel, &cache, &layout, true);
let mut t = create("line");
let a = spread_pt(&layout, 100.0, 100.0);
let b = spread_pt(&layout, 180.0, 140.0);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, a.x, a.y));
let actions = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, b.x, b.y));
let params = actions
.iter()
.find_map(|act| match act {
Action::Preview(cmd, p) if cmd == "line.create" => Some(p),
_ => None,
})
.unwrap();
let end = params["b"].as_array().unwrap();
let (bx, by) = (end[0].as_f64().unwrap(), end[1].as_f64().unwrap());
let len = 80.0_f64.hypot(40.0);
let expect_x = 180.0 + (92.0 - len);
assert!((by - 142.0).abs() < 1e-4, "endpoint y {by} left the guide at 142 (x {bx})");
assert!((bx - expect_x).abs() < 1e-4, "endpoint x {bx}, want {expect_x} from the length delta on X only");
}
#[test]
fn dimension_guide_lies_on_the_committed_side() {
let mut problems = Vec::new();
// Centered scale. Width 120 grows to the matched 123, so each side moves 1.5, not 3.
let mut doc = Document::new(&NewDocument::default());
let moving = add_rect(&mut doc, Rect::new(100.0, 100.0, 200.0, 160.0));
add_rect(&mut doc, Rect::new(400.0, 400.0, 523.0, 440.0));
let sel = Selection::items(vec![moving]);
let cache = Cache::new();
let layout = CanvasLayout::new(&doc, false);
let cx = dims_ctx(&doc, &sel, &cache, &layout, false);
let mut t = create("scale");
let center = spread_pt(&layout, 150.0, 130.0);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, center.x + 50.0, center.y + 30.0));
let drag = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, center.x + 60.0, center.y + 40.0));
let (sx, sy) = match &drag[..] {
[Action::Preview(cmd, p)] => {
assert_eq!(cmd, "transform.scale");
(p["sx"].as_f64().unwrap(), p["sy"].as_f64().unwrap())
}
other => panic!("{other:?}"),
};
let committed = Rect::new(150.0 - 50.0 * sx.abs(), 130.0 - 30.0 * sy.abs(), 150.0 + 50.0 * sx.abs(), 130.0 + 30.0 * sy.abs());
let xf = layout.xf(SpreadRef::Doc(0));
let bottom = (xf * Point::new(committed.x0, committed.y1), xf * Point::new(committed.x1, committed.y1));
let overlays = t.overlays(&cx);
let scale_guides = overlays.iter().filter(|o| matches!(o, Overlay::Guide { .. })).count();
if scale_guides != 1 || !guide_matches(&overlays, bottom.0, bottom.1) {
problems.push(format!(
"scale guide is not on the committed bottom ({:.4},{:.4})-({:.4},{:.4}), sx {sx} sy {sy}, {scale_guides} guides [{}]",
bottom.0.x,
bottom.0.y,
bottom.1.x,
bottom.1.y,
guide_list(&overlays)
));
}
// Shift resize. Width snaps 130 to 133, then the height follows the aspect and the bottom moves.
let mut doc = Document::new(&NewDocument::default());
let moving = add_rect(&mut doc, Rect::new(100.0, 100.0, 200.0, 150.0));
add_rect(&mut doc, Rect::new(400.0, 400.0, 533.0, 430.0));
let sel = Selection::items(vec![moving]);
let cache = Cache::new();
let layout = CanvasLayout::new(&doc, false);
let cx = dims_ctx(&doc, &sel, &cache, &layout, false);
let mut t = create("selection");
let mods = Mods { shift: true, ..Mods::default() };
let down = spread_pt(&layout, 200.0, 150.0);
let drag_at = spread_pt(&layout, 230.0, 155.0);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, down.x, down.y));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, drag_at.x, drag_at.y).with_mods(mods));
let to = match &a[..] {
[Action::Preview(cmd, p)] => {
assert_eq!(cmd, "transform.resize");
p["to"].as_array().unwrap()
}
other => panic!("{other:?}"),
};
let rect = Rect::new(to[0].as_f64().unwrap(), to[1].as_f64().unwrap(), to[2].as_f64().unwrap(), to[3].as_f64().unwrap());
let xf = layout.xf(SpreadRef::Doc(0));
let y = rect.y0.max(rect.y1);
let (x0, x1) = (rect.x0.min(rect.x1), rect.x0.max(rect.x1));
let side = (xf * Point::new(x0, y), xf * Point::new(x1, y));
let overlays = t.overlays(&cx);
let shift_guides = overlays.iter().filter(|o| matches!(o, Overlay::Guide { .. })).count();
if shift_guides != 1 || !guide_matches(&overlays, side.0, side.1) {
problems.push(format!(
"shift guide is not on the committed side ({:.4},{:.4})-({:.4},{:.4}), rect {to:?}, {shift_guides} guides [{}]",
side.0.x,
side.0.y,
side.1.x,
side.1.y,
guide_list(&overlays)
));
}
assert!(problems.is_empty(), "{}", problems.join(" | "));
}
+18 -6
View File
@@ -98,18 +98,18 @@ impl Tool for XformTool {
if w0 > 1e-9 && h0 > 1e-9 {
let center = from.center();
let (mut w, mut h) = (w0 * sx.abs(), h0 * sy.abs());
let to = Rect::new(center.x - w / 2.0, center.y - h / 2.0, center.x + w / 2.0, center.y + h / 2.0);
let pre = Rect::new(center.x - w / 2.0, center.y - h / 2.0, center.x + w / 2.0, center.y + h / 2.0);
let pointer = cx.layout.to_spread(spread, ev.pos);
let x_edges = if x_drives { edge_toward(pointer.x, center.x) } else { [false, false, false] };
let y_edges = if x_drives && ev.mods.shift { [false, false, false] } else { edge_toward(pointer.y, center.y) };
let exclude = cx.selection.items.clone();
let (pad_x, pad_y) = scale_stroke_pad(cx);
let hit = crate::snap::snap(
let mut hit = crate::snap::snap(
cx,
SnapRequest {
spread,
gesture: Gesture::Resize,
rect: to,
rect: pre,
x_edges,
y_edges,
exclude: &exclude,
@@ -141,9 +141,21 @@ impl Tool for XformTool {
w = w0 * (h / h0);
}
}
if w.is_finite() && h.is_finite() && w.abs() > 1e-9 && h.abs() > 1e-9 {
sx = (w / w0) * sign_x;
sy = (h / h0) * sign_y;
if w.is_finite() && h.is_finite() {
let committed = Rect::new(center.x - w / 2.0, center.y - h / 2.0, center.x + w / 2.0, center.y + h / 2.0);
crate::snap::lay_dimension_guides(
&mut hit.guides,
cx.layout.xf(spread),
pre,
committed,
x_edges,
y_edges,
hit.length_delta,
);
if w.abs() > 1e-9 && h.abs() > 1e-9 {
sx = (w / w0) * sign_x;
sy = (h / h0) * sign_y;
}
}
self.guides = hit.guides;
}