mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
G1: Group Selection Shift-click deselects compound path members
Match the Selection tool: Shift on an already selected hit removes it on release without a drag, including one path of a compound path. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Cursor
parent
891793b3a4
commit
61f35cd037
+70
-13
@@ -62,6 +62,8 @@ enum State {
|
||||
start: Point,
|
||||
grab: Option<Point>,
|
||||
began: bool,
|
||||
/// Group Selection: Shift-click on a selected object toggles it off on release.
|
||||
deselect: Option<NodeId>,
|
||||
},
|
||||
/// Dragging a curved segment: it follows the pointer (`path.reshapeSegment`).
|
||||
Segment {
|
||||
@@ -452,14 +454,25 @@ impl Tool for DirectSelectionTool {
|
||||
} else {
|
||||
h.leaf
|
||||
};
|
||||
self.state = State::MoveObject { start: p, grab: None, began: false };
|
||||
if cx.selection.contains(target) {
|
||||
vec![]
|
||||
} else if ev.mods.shift || cx.selection.contains(h.leaf) {
|
||||
vec![Action::Exec("select.add".into(), json!({"ids": [target.0]}))]
|
||||
} else {
|
||||
vec![Action::Exec("select.set".into(), json!({"ids": [target.0]}))]
|
||||
let mut deselect = None;
|
||||
let mut out = vec![];
|
||||
if ev.mods.shift {
|
||||
// Shift on a selected member deselects what was hit (a compound path's part),
|
||||
// not the next ancestor a plain click would add (#2441).
|
||||
if cx.selection.contains(h.leaf) {
|
||||
deselect = Some(h.leaf);
|
||||
} else {
|
||||
out.push(Action::Exec("select.toggle".into(), json!({"id": target.0})));
|
||||
}
|
||||
} else if !cx.selection.contains(target) {
|
||||
if cx.selection.contains(h.leaf) {
|
||||
out.push(Action::Exec("select.add".into(), json!({"ids": [target.0]})));
|
||||
} else {
|
||||
out.push(Action::Exec("select.set".into(), json!({"ids": [target.0]})));
|
||||
}
|
||||
}
|
||||
self.state = State::MoveObject { start: p, grab: None, began: false, deselect };
|
||||
out
|
||||
}
|
||||
(PointerKind::DoubleClick, _) if self.group => release_guide_at(cx, p, ev.mods).into_iter().collect(),
|
||||
(PointerKind::DoubleClick, _) if !self.group => {
|
||||
@@ -522,7 +535,7 @@ impl Tool for DirectSelectionTool {
|
||||
}
|
||||
// Alt picks the whole path (and Alt-drag copies it), as on a segment.
|
||||
if ev.mods.alt {
|
||||
self.state = State::MoveObject { start: p, grab: Some(grab), began: false };
|
||||
self.state = State::MoveObject { start: p, grab: Some(grab), began: false, deselect: None };
|
||||
let whole = cx.selection.contains(id) && cx.selection.partial(id).is_none();
|
||||
return if whole { vec![] } else { vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))] };
|
||||
}
|
||||
@@ -533,7 +546,7 @@ impl Tool for DirectSelectionTool {
|
||||
// Paths all selected whole move as objects, held by this anchor, so Alt can turn
|
||||
// the copy on and off during the drag as with the Selection tool.
|
||||
self.state = if already && cx.selection.anchors.is_empty() {
|
||||
State::MoveObject { start: p, grab: Some(grab), began: false }
|
||||
State::MoveObject { start: p, grab: Some(grab), began: false, deselect: None }
|
||||
} else {
|
||||
State::MoveAnchors { start: p, grab, began: false }
|
||||
};
|
||||
@@ -586,7 +599,7 @@ impl Tool for DirectSelectionTool {
|
||||
}
|
||||
}
|
||||
// Clicking the fill selects the whole leaf path (all anchors).
|
||||
self.state = State::MoveObject { start: p, grab: None, began: false };
|
||||
self.state = State::MoveObject { start: p, grab: None, began: false, deselect: None };
|
||||
if ev.mods.shift {
|
||||
return vec![Action::Exec("select.toggle".into(), json!({"id": h.leaf.0}))];
|
||||
}
|
||||
@@ -638,14 +651,14 @@ impl Tool for DirectSelectionTool {
|
||||
));
|
||||
out
|
||||
}
|
||||
(PointerKind::Drag, State::MoveObject { start, grab, began }) => {
|
||||
(PointerKind::Drag, State::MoveObject { start, grab, began, .. }) => {
|
||||
let mut out = vec![];
|
||||
if !began {
|
||||
if p.distance(start) < cx.tol(3.0) {
|
||||
return out;
|
||||
}
|
||||
out.push(Action::Begin(if ev.mods.alt { "Copy".into() } else { "Move".into() }));
|
||||
self.state = State::MoveObject { start, grab, began: true };
|
||||
self.state = State::MoveObject { start, grab, began: true, deselect: None };
|
||||
if grab.is_some() {
|
||||
// Alt can go down or up at any time: gather both while nothing has moved.
|
||||
self.anchor_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection)));
|
||||
@@ -754,10 +767,25 @@ impl Tool for DirectSelectionTool {
|
||||
self.state = State::Idle;
|
||||
b.finish()
|
||||
}
|
||||
(PointerKind::Up, State::MoveObject { began, deselect, .. }) => {
|
||||
self.state = State::Idle;
|
||||
self.guides.clear();
|
||||
(self.anchor_snap, self.copy_snap, self.move_snap) = (None, None, None);
|
||||
match (began, deselect, self.click.take(), self.click_segment.take()) {
|
||||
(true, ..) => vec![Action::Commit],
|
||||
(false, Some(id), _, _) => vec![Action::Exec("select.toggle".into(), json!({"id": id.0}))],
|
||||
(false, None, Some((id, si, ai)), _) => {
|
||||
vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[si, ai]], "mode": "set"}))]
|
||||
}
|
||||
(false, None, None, Some((id, si, seg))) => {
|
||||
vec![Action::Exec("select.segments".into(), json!({"id": id.0, "segments": [[si, seg]]}))]
|
||||
}
|
||||
(false, None, None, None) => vec![],
|
||||
}
|
||||
}
|
||||
(
|
||||
PointerKind::Up,
|
||||
State::MoveAnchors { began, .. }
|
||||
| State::MoveObject { began, .. }
|
||||
| State::Segment { began, .. }
|
||||
| State::SpinePoint { began, .. }
|
||||
| State::SpineSegment { began, .. }
|
||||
@@ -1377,6 +1405,35 @@ mod tests {
|
||||
assert_eq!(click(&s, 25.0, 100.0), vec![Action::Exec("select.add".into(), json!({"ids": [compound.0]}))]);
|
||||
}
|
||||
|
||||
/// Group Selection Shift-click on a selected compound-path member deselects it on release.
|
||||
#[test]
|
||||
fn group_selection_shift_click_deselects_a_compound_path_member() {
|
||||
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
|
||||
let l = d.layers[0].id;
|
||||
let (outer, hole, compound) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
|
||||
let rect = |id, a: f64, b: f64| {
|
||||
std::sync::Arc::new(vectorcraft_doc::Node::path(id, vectorcraft_geom::shapes::rectangle(Rect::new(a, a, b, b)), Default::default()))
|
||||
};
|
||||
let mut c = vectorcraft_doc::Node::new(
|
||||
compound,
|
||||
vectorcraft_doc::NodeKind::Compound {
|
||||
children: vec![rect(outer, 0.0, 200.0), rect(hole, 50.0, 150.0)],
|
||||
rule: vectorcraft_geom::FillRule::EvenOdd,
|
||||
},
|
||||
);
|
||||
c.appearance = vectorcraft_doc::Appearance::default_art().into();
|
||||
d.insert(Some(l), 0, c).unwrap();
|
||||
let p = paint();
|
||||
let mut s = Selection::default();
|
||||
s.set([outer]);
|
||||
let shift = Mods { shift: true, ..Mods::default() };
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = DirectSelectionTool::new(true);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 25.0, 100.0).with_mods(shift));
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 25.0, 100.0).with_mods(shift));
|
||||
assert_eq!(a, vec![Action::Exec("select.toggle".into(), json!({"id": outer.0}))]);
|
||||
}
|
||||
|
||||
/// A path inside an inner group, a sibling path outside it, and the groups' ids.
|
||||
/// The inside path is the rectangle at (100, 100); the outside one at (300, 300).
|
||||
fn nested_group_doc() -> (vectorcraft_doc::Document, NodeId, NodeId, NodeId, NodeId) {
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Convert selected anchors to corner/smooth from the Control bar and Properties | done | |
|
||||
| Cut path at selected anchors, Remove Anchor Points, Delete removes segments | done | Open-path anchor Delete removes adjoining segments without bridging the gap; targeted core and full-frame Delete/Backspace regressions pass for survivor selection and Undo/Redo. Explicit Remove Anchor Points keeps its reconnect/refit behavior; #1278: segment Delete preserves neighboring edges and endpoint anchors; a second Delete removes the remainder; explicit anchor Delete retains its behavior; #1674: Delete removes every selected anchor of a closed path and objects selected whole along with them, and keeps the remainder selected; #2241: Remove Anchor Points then selects the next anchor along the path (the one before at an open end), so it can be run again without picking each anchor |
|
||||
| Direct Selection and Group Selection pick only inside the isolated object in isolation mode | done | #1666 |
|
||||
| Group Selection picks one path of a compound path (the outer shape or a hole) and moves it on its own; a second click adds the compound path | done | #2441, in part (Shift-click to deselect is not covered) |
|
||||
| Group Selection picks one path of a compound path (the outer shape or a hole) and moves it on its own; a second click adds the compound path | done | #2441; Shift-click on a selected member deselects it on release |
|
||||
| Escape cancels a Direct Selection drag | done | #1803 (#1743) |
|
||||
| Live Corners widgets (radius readout, Alt-click cycles kind, double-click the Corners dialog) | done | a Direct Selection click on a widget selects its corner (#1239); a press-and-drag on a widget of a path selected whole still rounds every corner (#908), and adding widgets to the selection by Shift-click (#1232) is missing; the bounding box hides while a widget is dragged with the Selection tool (#1229); a round corner next to a curved side curves smoothly instead of sticking out of the shape (#1512); a corner rounds until its cut reaches its neighbours' cuts, so one corner of a square alone reaches the whole side (#2332) |
|
||||
| Corners between curved sides (#1144), Relative rounding | partial | Relative rounding missing |
|
||||
|
||||
Reference in New Issue
Block a user