Perspective Selection: a move of loose art keeps its plane when 4 clears the active plane

The move drag remembers the plane it places loose art on. Pressing 4
during the drag, or dragging on after it, previews and commits the move
on that plane instead of dropping it. A 1-3 key pressed before the drag
threshold also sets the plane the move uses.
This commit is contained in:
dotCooCoo
2026-10-31 19:43:16 +07:00
committed by bflatastic
parent c15874ba6e
commit ae76455723
3 changed files with 77 additions and 11 deletions
+33
View File
@@ -281,6 +281,39 @@ fn shift_drag_keeps_its_displacement_after_a_plane_key() {
}
}
#[test]
fn loose_art_move_survives_clearing_the_active_plane() {
for (drag_after_key, switch_before_drag) in [(false, false), (true, false), (false, true)] {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 800, "height": 600, "created": null})).unwrap();
s.execute("perspective.grid.preset", &json!({"kind": 2})).unwrap();
s.execute("perspective.grid.snap", &json!({"on": false})).unwrap();
s.execute("perspective.plane.set", &json!({"plane": "right"})).unwrap();
let result = s.execute("shape.rectangle", &json!({"x": 450, "y": 380, "width": 60, "height": 60})).unwrap();
let id = NodeId(result["id"].as_u64().unwrap());
assert_eq!(attached(&s, id), None);
let before = s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap();
let (from, to) = (before.center(), before.center() + Vec2::new(30.0, 15.0));
let v = ViewInfo::default();
s.select_tool("perspectiveSelection", v).unwrap();
let undo_before = undo_len(&s);
s.pointer(&PointerEvent::new(PointerKind::Down, from.x, from.y), v).unwrap();
if switch_before_drag {
s.tool_key(ToolKey::Digit(2), Mods::default(), v).unwrap();
}
s.pointer(&PointerEvent::new(PointerKind::Drag, to.x, to.y), v).unwrap();
s.tool_key(ToolKey::Digit(4), Mods::default(), v).unwrap();
assert_eq!(grid(&s).plane, Plane::None);
if drag_after_key {
s.pointer(&PointerEvent::new(PointerKind::Drag, to.x + 10.0, to.y), v).unwrap();
}
s.pointer(&PointerEvent::new(PointerKind::Up, to.x, to.y), v).unwrap();
assert_eq!(attached(&s, id), Some((if switch_before_drag { Plane::Ground } else { Plane::Right }, 0.0)));
assert_ne!(s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap(), before);
assert_eq!(undo_len(&s), undo_before + 1);
}
}
#[test]
fn cmd_alt_shift_drag_copies_on_a_plane_axis() {
let (mut s, id) = persp_session();
+25
View File
@@ -1286,6 +1286,31 @@ mod tests {
assert_eq!(params["constrain"], json!(true));
}
#[test]
fn loose_move_preview_keeps_its_plane_when_the_active_plane_is_cleared() {
let (mut d, id) = doc_with_rect();
let mut g = grid(2);
g.plane = Plane::Right;
g.store(&mut d);
let mut selection = Selection::default();
selection.add(id);
let p = paint();
let from = d.node(id).unwrap().geometric_bounds().unwrap().center();
let to = from + Vec2::new(30.0, 15.0);
let mut tool = PerspectiveSelectionTool::default();
let c = cx(&d, &selection, &p);
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, from.x, from.y));
let actions = tool.pointer(&c, &PointerEvent::new(PointerKind::Drag, to.x, to.y));
assert!(matches!(actions.last(), Some(Action::Preview(_, params)) if params["plane"] == "right"));
g.plane = Plane::None;
g.store(&mut d);
let c = cx(&d, &selection, &p);
let actions = tool.key(&c, ToolKey::Digit(4), Mods::default());
assert!(matches!(actions.last(), Some(Action::Preview(_, params)) if params["plane"] == "right"), "{actions:?}");
let actions = tool.pointer(&c, &PointerEvent::new(PointerKind::Drag, to.x + 10.0, to.y));
assert!(matches!(actions.last(), Some(Action::Preview(_, params)) if params["plane"] == "right"), "{actions:?}");
}
#[test]
fn selection_move_recomputes_shift_copy_and_perpendicular() {
let (mut d, id) = doc_with_rect();
+19 -11
View File
@@ -120,7 +120,7 @@ enum SelDrag {
Idle,
/// Pressed on an object: moves `ids` once the pointer leaves the drag threshold. `perp`: along
/// the plane's normal (5 pressed while dragging); `copy` and `shift`: modifiers at the last event.
Move { ids: Vec<NodeId>, from: Point, last: Point, began: bool, perp: bool, copy: bool, shift: bool },
Move { ids: Vec<NodeId>, from: Point, last: Point, plane: Plane, began: bool, perp: bool, copy: bool, shift: bool },
/// Dragging `handle` of the perspective bounding box `rect` (plane space, on `plane` at `depth`).
Scale { ids: Vec<NodeId>, plane: Plane, depth: f64, rect: Rect, handle: BoxHandle },
/// Dragging a plane widget.
@@ -164,9 +164,11 @@ pub(super) fn resize_cursor(d: Vec2) -> Cursor {
impl PerspectiveSelectionTool {
/// The `perspective.move` preview for the move drag (and its measurement label).
fn move_preview(&mut self, cx: &ToolContext) -> Option<Action> {
let SelDrag::Move { ids, from, last, perp, copy, shift, .. } = &self.drag else { return None };
let SelDrag::Move { ids, from, last, plane, perp, copy, shift, .. } = &self.drag else { return None };
let g = PerspectiveGrid::current(cx.doc);
let at = ids.first().and_then(|id| g.attachment_of(cx.doc, *id)).or((g.plane != Plane::None).then_some((g.plane, g.offset(g.plane))));
let move_plane = if g.plane == Plane::None { *plane } else { g.plane };
let at =
ids.first().and_then(|id| g.attachment_of(cx.doc, *id)).or((move_plane != Plane::None).then_some((move_plane, g.offset(move_plane))));
let constrained = if *shift && !*perp {
at.and_then(|(plane, depth)| {
let delta = g.plane_delta(plane, depth, *from, *last)?;
@@ -179,8 +181,8 @@ impl PerspectiveSelectionTool {
if *shift && !*perp {
v["constrain"] = json!(true);
}
if g.plane != Plane::None {
v["plane"] = json!(g.plane.id());
if move_plane != Plane::None {
v["plane"] = json!(move_plane.id());
}
self.measure = at.and_then(|(pl, depth)| {
let text = if *perp {
@@ -230,24 +232,26 @@ impl Tool for PerspectiveSelectionTool {
vec![top]
};
// Selecting an object in perspective makes its plane the active one.
let mut plane = g.plane;
if let Some((pl, _)) = g.attachment_of(cx.doc, top)
&& pl != g.plane
{
acts.push(Action::Exec("perspective.plane.set".into(), json!({"plane": pl.id()})));
plane = pl;
}
self.drag = SelDrag::Move { ids, from: p, last: p, began: false, perp: false, copy: m.alt, shift: m.shift };
self.drag = SelDrag::Move { ids, from: p, last: p, plane, began: false, perp: false, copy: m.alt, shift: m.shift };
acts
}
(PointerKind::Drag, SelDrag::Move { ids, from, began, perp, .. }) => {
(PointerKind::Drag, SelDrag::Move { ids, from, plane, began, perp, .. }) => {
if !began && p.distance(from) < cx.tol(3.0) {
self.drag = SelDrag::Move { ids, from, last: p, began, perp, copy: m.alt, shift: m.shift };
self.drag = SelDrag::Move { ids, from, last: p, plane, began, perp, copy: m.alt, shift: m.shift };
return vec![];
}
let mut out = vec![];
if !began {
out.push(Action::Begin(if m.alt { "Copy in Perspective" } else { "Move in Perspective" }.into()));
}
self.drag = SelDrag::Move { ids, from, last: p, began: true, perp, copy: m.alt, shift: m.shift };
self.drag = SelDrag::Move { ids, from, last: p, plane, began: true, perp, copy: m.alt, shift: m.shift };
out.extend(self.move_preview(cx));
out
}
@@ -307,10 +311,14 @@ impl Tool for PerspectiveSelectionTool {
return if *began { self.move_preview(cx).into_iter().collect() } else { vec![] };
}
if matches!(key, ToolKey::Digit(1..=4))
&& let SelDrag::Move { began: true, shift, .. } = &mut self.drag
&& let SelDrag::Move { began, plane, shift, .. } = &mut self.drag
{
*shift = mods.shift;
return self.move_preview(cx).into_iter().collect();
let active = PerspectiveGrid::current(cx.doc).plane;
if active != Plane::None {
*plane = active;
}
return if *began { self.move_preview(cx).into_iter().collect() } else { vec![] };
}
let (dx, dy) = match key {
ToolKey::Left => (-1, 0),