mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 14:16:49 -06:00
Direct Selection: a Shift-drag keeps a selected point; snapping keeps the 45° constraint
Shift pressed on a selected anchor point keeps it selected, so the press can become a constrained drag of all the selected points; a Shift-click still takes it away, on release. With Shift, a snap moves the points only along the constrained direction. The canvas looks selected points up in a set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d2001f2597
commit
1dc1dbf31a
+23
-12
@@ -279,6 +279,8 @@ pub struct SelectionTool {
|
||||
guides: Vec<Overlay>,
|
||||
/// Selected item to toggle if this Shift press never becomes a drag.
|
||||
shift_release: Option<u64>,
|
||||
/// Selected anchor point to take away if this Shift press never becomes a drag.
|
||||
anchor_release: Option<(ItemId, usize, usize)>,
|
||||
/// The loaded text cursor.
|
||||
loaded: Option<Loaded>,
|
||||
/// Frame whose live corners are being edited (diamonds shown).
|
||||
@@ -287,7 +289,7 @@ pub struct SelectionTool {
|
||||
|
||||
impl SelectionTool {
|
||||
pub fn new(direct: bool) -> Self {
|
||||
Self { direct, drag: Drag::None, hover_handle: None, guides: vec![], shift_release: None, loaded: None, live: None }
|
||||
Self { direct, drag: Drag::None, hover_handle: None, guides: vec![], shift_release: None, anchor_release: None, loaded: None, live: None }
|
||||
}
|
||||
|
||||
/// [`bracket_at`] for this tool. With Direct Selection an anchor under the pointer wins: the
|
||||
@@ -811,7 +813,9 @@ impl Tool for SelectionTool {
|
||||
let mut points = cx.selection.anchors.clone();
|
||||
let was = points.contains(&me);
|
||||
if ev.mods.shift && was {
|
||||
points.retain(|a| *a != me);
|
||||
// Kept selected so a Shift-drag can constrain it; a click still takes it
|
||||
// away, on release, as for objects.
|
||||
self.anchor_release = Some(me);
|
||||
} else if ev.mods.shift {
|
||||
points.push(me);
|
||||
} else if !was {
|
||||
@@ -820,10 +824,6 @@ impl Tool for SelectionTool {
|
||||
if points != cx.selection.anchors {
|
||||
out.push(Action::Exec("selection.set".into(), anchor_selection(cx, &points)));
|
||||
}
|
||||
if ev.mods.shift && was {
|
||||
self.drag = Drag::None;
|
||||
return out;
|
||||
}
|
||||
others = points.iter().filter(|a| **a != me).map(|(i, s, a)| (i.0, *s, *a)).collect();
|
||||
}
|
||||
let label = if handle.is_some() {
|
||||
@@ -1165,6 +1165,9 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
Drag::Anchor { id, si, ai, handle, start, at, others } => {
|
||||
let mut d = p - start;
|
||||
if d.hypot() >= cx.tol(3.0) {
|
||||
self.anchor_release = None;
|
||||
}
|
||||
// Shift: horizontal, vertical or at 45°.
|
||||
if ev.mods.shift {
|
||||
d = constrain_45(d);
|
||||
@@ -1196,13 +1199,15 @@ impl Tool for SelectionTool {
|
||||
},
|
||||
);
|
||||
// dx and dy stay in canvas space. Snap.delta is spread space.
|
||||
let corr = spread_delta_to_canvas(cx.layout.xf(spread), hit.delta);
|
||||
if corr.x.is_finite() {
|
||||
dx += corr.x;
|
||||
}
|
||||
if corr.y.is_finite() {
|
||||
dy += corr.y;
|
||||
let mut corr = spread_delta_to_canvas(cx.layout.xf(spread), hit.delta);
|
||||
corr = Vec2::new(if corr.x.is_finite() { corr.x } else { 0.0 }, if corr.y.is_finite() { corr.y } else { 0.0 });
|
||||
// Shift: the snap moves the points along the constrained direction only.
|
||||
if ev.mods.shift && d.hypot() > 1e-9 {
|
||||
let dir = d / d.hypot();
|
||||
corr = dir * corr.dot(dir);
|
||||
}
|
||||
dx += corr.x;
|
||||
dy += corr.y;
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": dx, "dy": dy});
|
||||
@@ -1219,8 +1224,14 @@ impl Tool for SelectionTool {
|
||||
PointerKind::Up => {
|
||||
self.guides.clear();
|
||||
let release = self.shift_release.take();
|
||||
let anchor_release = self.anchor_release.take();
|
||||
let d = std::mem::replace(&mut self.drag, Drag::None);
|
||||
match d {
|
||||
// A Shift-click on a selected point that never became a drag takes it away.
|
||||
Drag::Anchor { .. } if anchor_release.is_some() => {
|
||||
let points: Vec<_> = cx.selection.anchors.iter().copied().filter(|a| Some(*a) != anchor_release).collect();
|
||||
vec![Action::Commit, Action::Exec("selection.set".into(), anchor_selection(cx, &points))]
|
||||
}
|
||||
Drag::Move { .. }
|
||||
| Drag::Resize { .. }
|
||||
| Drag::ContentResize { .. }
|
||||
|
||||
@@ -1980,9 +1980,19 @@ fn direct_selection_marquee_selects_points_of_several_frames_and_drags_them_toge
|
||||
// Shift keeps the move horizontal.
|
||||
assert_eq!((p["dx"].as_f64(), p["dy"].as_f64()), (Some(36.0), Some(0.0)));
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x + 36.0, at.y)), vec![Action::Commit]);
|
||||
// Shift+click on a selected point takes it out of the selection; the arrow keys move the points.
|
||||
// A Shift-drag that starts on a selected point keeps it: all four move, constrained.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(shift));
|
||||
let drag = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, at.x + 3.0, at.y + 30.0).with_mods(shift));
|
||||
let Some(Action::Preview(_, p)) = drag.last() else { panic!("{drag:?}") };
|
||||
assert_eq!(p["points"].as_array().map(Vec::len), Some(4), "{p}");
|
||||
assert!(p["dx"].as_f64().unwrap().abs() < 1e-9 && (p["dy"].as_f64().unwrap() - 30.0).abs() < 1e-9, "vertical: {p}");
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x, at.y + 30.0).with_mods(shift)), vec![Action::Commit]);
|
||||
// Shift+click on a selected point takes it out of the selection, on release; the arrow keys
|
||||
// move the points.
|
||||
let out = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(shift));
|
||||
let Some(Action::Exec(cmd, p)) = out.first() else { panic!("{out:?}") };
|
||||
assert!(!out.iter().any(|a| matches!(a, Action::Exec(..))), "kept while it may become a drag: {out:?}");
|
||||
let out = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x, at.y).with_mods(shift));
|
||||
let Some(Action::Exec(cmd, p)) = out.last() else { panic!("{out:?}") };
|
||||
assert_eq!(cmd, "selection.set");
|
||||
assert_eq!(p["anchors"].as_array().map(Vec::len), Some(3), "{p}");
|
||||
let keys = t.key(&cx, ToolKey::Right, Mods::default());
|
||||
|
||||
@@ -1279,6 +1279,8 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum
|
||||
}
|
||||
let direct = matches!(app.session.tool_id(), "directSelection" | "pen" | "addAnchor" | "deleteAnchor" | "convertDirection");
|
||||
if direct {
|
||||
// Looked up once per anchor drawn: a large marquee selection stays linear.
|
||||
let chosen_set: std::collections::HashSet<(designcraft_doc::ItemId, usize, usize)> = sel.anchors.iter().copied().collect();
|
||||
for id in &sel.items {
|
||||
let (Some(it), Some((a, _))) = (doc.item(*id), item_canvas_xf(doc, layout, *id)) else { continue };
|
||||
let m = a * it.xf;
|
||||
@@ -1287,7 +1289,7 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum
|
||||
for (ai, an) in sp.anchors.iter().enumerate() {
|
||||
let p = xf.to_screen(m * an.p);
|
||||
// A selected anchor point is solid, the others hollow (InDesign).
|
||||
let chosen = sel.anchors.contains(&(*id, si, ai));
|
||||
let chosen = chosen_set.contains(&(*id, si, ai));
|
||||
for (h, has) in [(an.h_in, an.has_in()), (an.h_out, an.has_out())] {
|
||||
if has {
|
||||
let hp = xf.to_screen(m * h);
|
||||
|
||||
Reference in New Issue
Block a user