Snap: Alt-drag skips preview copies and Shift follows the turned view
This commit is contained in:
parent
a2451243ad
commit
253f1a7a10
|
|
@ -106,6 +106,27 @@ fn cmd_move_does_not_snap() {
|
|||
assert!(b.x0 > 80.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_move_on_a_turned_spread_stays_on_the_line() {
|
||||
use designcraft_tools::{PointerEvent, PointerKind};
|
||||
let mut s = session();
|
||||
s.execute("frame.create", &json!({"rect": [80.0, 100.0, 140.0, 140.0], "content": "unassigned"})).unwrap();
|
||||
s.execute("layout.rotateSpreadView", &json!({"angle": 90})).unwrap();
|
||||
// Vertical guide sits on the spread axis that a screen-horizontal Shift lock must not offer.
|
||||
s.execute("guide.add", &json!({"orientation": "vertical", "position": 82.0, "page": 0})).unwrap();
|
||||
s.set_tool("selection");
|
||||
let v = ViewInfo::at_zoom(1.0);
|
||||
let b0 = s.doc().unwrap().doc.spreads[0].items[0].bounds();
|
||||
let c = s.layout().to_canvas(designcraft_doc::SpreadRef::Doc(0), b0.center());
|
||||
let shift = designcraft_tools::Mods { shift: true, ..Default::default() };
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, c.x, c.y).with_mods(shift), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, c.x + 60.0, c.y + 1.0).with_mods(shift), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, c.x + 60.0, c.y + 1.0).with_mods(shift), v).unwrap();
|
||||
let b = s.doc().unwrap().doc.spreads[0].items[0].bounds();
|
||||
assert!((b.x0 - b0.x0).abs() < 1e-6, "locked screen axis jumped to {b:?}");
|
||||
assert!((b.y0 - b0.y0).abs() > 1.0, "free axis did not move: {b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pages_and_layers() {
|
||||
let mut s = session();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! the 8 bounding-box handles resize (Shift keeps proportions, Alt from centre, Cmd scales content),
|
||||
//! empty-canvas drags make a marquee, double-clicking a text frame switches to the Type tool.
|
||||
|
||||
use designcraft_doc::SpreadRef;
|
||||
use designcraft_doc::{ItemId, SpreadRef};
|
||||
use designcraft_geom::{Point, Rect, Vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ use crate::{Action, Cursor, Gesture, Mods, Overlay, PointerEvent, PointerKind, S
|
|||
enum Drag {
|
||||
None,
|
||||
Pending { start: Point, hit: bool },
|
||||
Move { start: Point, origin_spread: SpreadRef, bounds0: Option<Rect> },
|
||||
Move { start: Point, origin_spread: SpreadRef, bounds0: Option<Rect>, exclude: Vec<ItemId> },
|
||||
Resize { handle: usize, start: Point, from: Rect, spread: SpreadRef },
|
||||
Rotate { center: Point, start_angle: f64 },
|
||||
Marquee { start: Point, cur: Point },
|
||||
|
|
@ -251,7 +251,8 @@ impl Tool for SelectionTool {
|
|||
self.shift_release = None;
|
||||
if hit && !cx.selection.items.is_empty() {
|
||||
let sr = cx.selection.items.first().and_then(|i| cx.doc.find(*i)).map(|l| l.spread).unwrap_or(SpreadRef::Doc(0));
|
||||
self.drag = Drag::Move { start, origin_spread: sr, bounds0: cx.selection_bounds() };
|
||||
let exclude = cx.selection.items.clone();
|
||||
self.drag = Drag::Move { start, origin_spread: sr, bounds0: cx.selection_bounds(), exclude };
|
||||
let label = if ev.mods.alt { "Duplicate" } else { "Move" };
|
||||
let mut v = vec![Action::Begin(label.into())];
|
||||
v.extend(self.pointer(cx, ev));
|
||||
|
|
@ -261,24 +262,30 @@ impl Tool for SelectionTool {
|
|||
vec![]
|
||||
}
|
||||
}
|
||||
Drag::Move { start, origin_spread, bounds0 } => {
|
||||
Drag::Move { start, origin_spread, bounds0, exclude } => {
|
||||
let mut d: Vec2 = p - start;
|
||||
let mut x_edges = [true, true, true];
|
||||
let mut y_edges = [true, true, true];
|
||||
let mut screen_x_locked = false;
|
||||
let mut screen_y_locked = false;
|
||||
if ev.mods.shift {
|
||||
if d.x.abs() > d.y.abs() {
|
||||
d.y = 0.0;
|
||||
y_edges = [false, false, false];
|
||||
screen_y_locked = true;
|
||||
} else {
|
||||
d.x = 0.0;
|
||||
x_edges = [false, false, false];
|
||||
screen_x_locked = true;
|
||||
}
|
||||
}
|
||||
// 180 degrees keeps the screen axis. 90 and 270 swap it into the other spread axis.
|
||||
let odd_turn = cx.layout.slot(origin_spread).is_some_and(|slot| slot.rotation % 2 == 1);
|
||||
let spread_x_locked = if odd_turn { screen_y_locked } else { screen_x_locked };
|
||||
let spread_y_locked = if odd_turn { screen_x_locked } else { screen_y_locked };
|
||||
let x_edges = if spread_x_locked { [false; 3] } else { [true; 3] };
|
||||
let y_edges = if spread_y_locked { [false; 3] } else { [true; 3] };
|
||||
self.guides.clear();
|
||||
// The movement in the spread's own coordinates (its view may be turned).
|
||||
let mut ds = cx.layout.delta_to_spread(origin_spread, d);
|
||||
let b0s = bounds0.map(|b| cx.layout.xf(origin_spread).inverse().transform_rect_bbox(b));
|
||||
// Command suspends snapping. A Shift-locked axis is not offered.
|
||||
// Command suspends snapping. A Shift-locked spread axis is not offered.
|
||||
if !ev.mods.cmd
|
||||
&& cx.snap.any()
|
||||
&& let Some(b0) = b0s
|
||||
|
|
@ -291,7 +298,7 @@ impl Tool for SelectionTool {
|
|||
rect: b0 + ds,
|
||||
x_edges,
|
||||
y_edges,
|
||||
exclude: &cx.selection.items,
|
||||
exclude: &exclude,
|
||||
copying: ev.mods.alt,
|
||||
lengths: [None, None],
|
||||
angle: None,
|
||||
|
|
|
|||
|
|
@ -263,8 +263,9 @@ fn pass_guides(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) ->
|
|||
///
|
||||
/// Edges match edges only while `align_edges` is on. Centers match centers only while
|
||||
/// `align_centers` is on, and only when the request's middle flag is set. Hidden items are
|
||||
/// skipped. Excluded items are skipped unless `copying` is set, so an Alt-drag can meet the
|
||||
/// original. Parent items and group children are not targets.
|
||||
/// skipped. Without copying, excluded items are skipped. With copying, that list is the
|
||||
/// drag-start set and stays a target. Live selection items outside that set are preview
|
||||
/// copies and are skipped. Parent items and group children are not targets.
|
||||
fn pass_align(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) -> Option<AxisHit> {
|
||||
if !cx.snap.smart_guides || (!cx.snap.align_edges && !cx.snap.align_centers) {
|
||||
return None;
|
||||
|
|
@ -276,7 +277,7 @@ fn pass_align(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) ->
|
|||
offer_box(&mut best, req.rect, flags, axis, page.bounds(), tol, cx.snap.align_edges, cx.snap.align_centers);
|
||||
}
|
||||
for item in &sp.items {
|
||||
if item.hidden || (!req.copying && req.exclude.contains(&item.id)) {
|
||||
if item.hidden || align_skips_item(cx, req, item.id) {
|
||||
continue;
|
||||
}
|
||||
offer_box(&mut best, req.rect, flags, axis, item.visible_bounds(), tol, cx.snap.align_edges, cx.snap.align_centers);
|
||||
|
|
@ -284,6 +285,11 @@ fn pass_align(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) ->
|
|||
best.map(|b| b.hit)
|
||||
}
|
||||
|
||||
/// Drag-start ids stay targets while copying. Preview copies are the live selection minus that set.
|
||||
fn align_skips_item(cx: &ToolContext, req: &SnapRequest<'_>, id: ItemId) -> bool {
|
||||
if req.copying { cx.selection.items.contains(&id) && !req.exclude.contains(&id) } else { req.exclude.contains(&id) }
|
||||
}
|
||||
|
||||
fn axis_flags(req: &SnapRequest<'_>, axis: Axis) -> [bool; 3] {
|
||||
match axis {
|
||||
Axis::X => req.x_edges,
|
||||
|
|
@ -552,4 +558,40 @@ mod tests {
|
|||
);
|
||||
assert!((hit.delta.x - -2.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_copy_does_not_stick_to_the_preview() {
|
||||
let mut doc = Document::new(&NewDocument::default());
|
||||
let layer = doc.default_layer();
|
||||
let make = |doc: &mut Document, rect: Rect| {
|
||||
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
|
||||
};
|
||||
let original = make(&mut doc, Rect::new(100.0, 40.0, 160.0, 80.0));
|
||||
let ghost = make(&mut doc, Rect::new(161.0, 40.0, 221.0, 80.0));
|
||||
let cache = Cache::new();
|
||||
let layout = CanvasLayout::new(&doc, false);
|
||||
let sel = Selection::items(vec![ghost]);
|
||||
let mut cx = ctx_on(&doc, &sel, &cache, &layout);
|
||||
cx.snap.snap_to_guides = false;
|
||||
let hit = snap(
|
||||
&cx,
|
||||
SnapRequest {
|
||||
spread: SpreadRef::Doc(0),
|
||||
gesture: Gesture::Move,
|
||||
rect: Rect::new(161.0, 40.0, 221.0, 80.0),
|
||||
x_edges: [true, false, false],
|
||||
y_edges: [false, false, false],
|
||||
exclude: &[original],
|
||||
copying: true,
|
||||
lengths: [None, None],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer: Point::new(0.0, 0.0),
|
||||
},
|
||||
);
|
||||
assert!((hit.delta.x - -1.0).abs() < 1e-6, "delta.x = {}", hit.delta.x);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue