mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Fix #2143: selected ruler guides align to a key object
A lone object selected with ruler guides can be the key (the Selection tool's click on it), and Align to the key moves the other selected guides to its edge or centre: vertical guides to left, centre or right, horizontal ones to top, centre or bottom. Parallel guides line up on a key guide. Aligning to the selection or the artboard leaves guides alone.
This commit is contained in:
@@ -175,13 +175,16 @@ impl Selection {
|
||||
refs.retain(|&(si, seg)| path.subpaths.get(si).is_some_and(|sp| seg < sp.segment_count()));
|
||||
!refs.is_empty() && self.objects.contains(id) && self.anchors.contains_key(id)
|
||||
});
|
||||
// The key object stays while another selected object lies outside it.
|
||||
// Guides on a hidden or locked layer can't stay selected.
|
||||
self.guides.retain(|i| doc.guides.get(*i).is_some_and(|g| doc.guide_editable(g)));
|
||||
// The key object stays while another selected object lies outside it, or while ruler
|
||||
// guides are selected for it to line up (#2143).
|
||||
let keep = self.key.and_then(|k| found.get(&k)).and_then(|p| doc.node_at(p)).is_some_and(|n| {
|
||||
let mut inside = BTreeSet::new();
|
||||
n.walk(&mut |c| {
|
||||
inside.insert(c.id);
|
||||
});
|
||||
self.objects.iter().any(|o| !inside.contains(o))
|
||||
self.objects.iter().any(|o| !inside.contains(o)) || (self.objects.contains(&n.id) && !self.guides.is_empty())
|
||||
});
|
||||
if !keep {
|
||||
self.key = None;
|
||||
@@ -190,8 +193,6 @@ impl Selection {
|
||||
self.target = None;
|
||||
}
|
||||
self.slices.retain(|id| doc.is_slice(*id));
|
||||
// Guides on a hidden or locked layer can't stay selected.
|
||||
self.guides.retain(|i| doc.guides.get(*i).is_some_and(|g| doc.guide_editable(g)));
|
||||
// A key guide needs objects to line up on it, and stays selected.
|
||||
if self.key.is_some() || self.objects.is_empty() || self.key_guide.is_some_and(|k| !self.guides.contains(&k)) {
|
||||
self.key_guide = None;
|
||||
|
||||
@@ -243,7 +243,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Align",
|
||||
["Window", "Align"],
|
||||
None,
|
||||
"{horizontal?: \"left\"|\"center\"|\"right\", vertical?: \"top\"|\"center\"|\"bottom\", to?: \"selection\"|\"artboard\"|\"key\" (default: the key object when the selection has one, select.key, else the selection; a key guide, select.key {guide}, stays put and the objects line up on its line: a vertical guide sets where left, center and right go and vertical alignment leaves them, a horizontal one the other way round), artboard?: index (0-based; with to: \"artboard\", the artboard to align to; the app passes the active one; default: the artboard under the center of the selection, else the first), bounds?: \"preview\"|\"geometric\" (default: the Use Preview Bounds preference; preview bounds take in strokes), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} With anchor points selected (Direct Selection), the points align instead: to their own bounds, or to the artboard (#1180)",
|
||||
"{horizontal?: \"left\"|\"center\"|\"right\", vertical?: \"top\"|\"center\"|\"bottom\", to?: \"selection\"|\"artboard\"|\"key\" (default: the key object when the selection has one, select.key, else the selection; a key guide, select.key {guide}, stays put and the objects line up on its line: a vertical guide sets where left, center and right go and vertical alignment leaves them, a horizontal one the other way round; aligning to the key, the other selected ruler guides move too: a vertical guide to the key's left edge, center or right edge, a horizontal one to its top, center or bottom), artboard?: index (0-based; with to: \"artboard\", the artboard to align to; the app passes the active one; default: the artboard under the center of the selection, else the first), bounds?: \"preview\"|\"geometric\" (default: the Use Preview Bounds preference; preview bounds take in strokes), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} With anchor points selected (Direct Selection), the points align instead: to their own bounds, or to the artboard (#1180)",
|
||||
has_selection,
|
||||
align
|
||||
),
|
||||
@@ -1251,12 +1251,17 @@ fn items_bounds(d: &Document, ids: &[NodeId], preview: bool) -> Vec<(NodeId, Rec
|
||||
ids.iter().filter_map(|id| Some((*id, d.bounds_of(&[*id], preview)?))).collect()
|
||||
}
|
||||
|
||||
/// Move each object of `moves` by its offset, as undo step `label`; pattern tiles move too when
|
||||
/// [`transform_patterns`] says so for `ids`. When every move is a rounding error (1e-9 pt or less
|
||||
/// on both axes), the document and its undo history stay as they are.
|
||||
fn apply_moves(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], moves: &[(NodeId, Vec2)]) -> Result<Value> {
|
||||
/// Move each object of `moves` by its offset and each ruler guide of `guides` (index, position)
|
||||
/// to its position, as undo step `label`; pattern tiles move too when [`transform_patterns`] says
|
||||
/// so for `ids`. When every move is a rounding error (1e-9 pt or less on both axes), the document
|
||||
/// and its undo history stay as they are.
|
||||
fn apply_moves(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], moves: &[(NodeId, Vec2)], guides: &[(usize, f64)]) -> Result<Value> {
|
||||
let sc = Scaling { patterns: transform_patterns(s, p, ids)?, ..Scaling::default() };
|
||||
if moves.iter().all(|(_, dv)| dv.x.abs() <= 1e-9 && dv.y.abs() <= 1e-9) {
|
||||
let guides: Vec<(usize, f64)> = {
|
||||
let d = &s.doc()?.doc;
|
||||
guides.iter().copied().filter(|&(i, pos)| d.guides.get(i).is_some_and(|g| (g.pos - pos).abs() > 1e-9)).collect()
|
||||
};
|
||||
if moves.iter().all(|(_, dv)| dv.x.abs() <= 1e-9 && dv.y.abs() <= 1e-9) && guides.is_empty() {
|
||||
return ok();
|
||||
}
|
||||
s.edit(label, |d, _| {
|
||||
@@ -1265,6 +1270,11 @@ fn apply_moves(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], moves: &
|
||||
n.transform(Affine::translate(*dv), sc);
|
||||
}
|
||||
}
|
||||
for &(i, pos) in &guides {
|
||||
if let Some(g) = d.guides.get_mut(i) {
|
||||
g.pos = pos;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
@@ -1375,7 +1385,35 @@ fn align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
apply_moves(s, "Align", p, &ids, &moves)
|
||||
let guides = guide_moves(s, p, r, h, v)?;
|
||||
apply_moves(s, "Align", p, &ids, &moves, &guides)
|
||||
}
|
||||
|
||||
/// The selected ruler guides that Align to the key object (or key guide) moves, and where: a
|
||||
/// vertical guide goes to the left edge, centre or right edge of `r` that `h` names, a horizontal
|
||||
/// one to the top, centre or bottom that `v` names. The key guide stays put, and so do guides
|
||||
/// when aligning to the selection or the artboard.
|
||||
fn guide_moves(s: &Session, p: &Value, r: Rect, h: Option<&str>, v: Option<&str>) -> Result<Vec<(usize, f64)>> {
|
||||
if align_to(s, p) != Some("key") {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let st = s.doc()?;
|
||||
let at = |vertical: bool| match (vertical, h, v) {
|
||||
(true, Some("left"), _) => Some(r.x0),
|
||||
(true, Some("center"), _) => Some(r.center().x),
|
||||
(true, Some("right"), _) => Some(r.x1),
|
||||
(false, _, Some("top")) => Some(r.y0),
|
||||
(false, _, Some("center")) => Some(r.center().y),
|
||||
(false, _, Some("bottom")) => Some(r.y1),
|
||||
_ => None,
|
||||
};
|
||||
Ok(st
|
||||
.selection
|
||||
.guides
|
||||
.iter()
|
||||
.filter(|&&i| Some(i) != st.selection.key_guide)
|
||||
.filter_map(|&i| Some((i, at(st.doc.guides.get(i)?.vertical)?)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// [`align`] of the selected anchor points (whole paths among them give all theirs): each point
|
||||
@@ -1540,7 +1578,7 @@ fn distribute(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(*id, if horiz { Vec2::new(delta, 0.0) } else { Vec2::new(0.0, delta) })
|
||||
})
|
||||
.collect();
|
||||
apply_moves(s, "Distribute", p, &ids, &moves)
|
||||
apply_moves(s, "Distribute", p, &ids, &moves, &[])
|
||||
}
|
||||
|
||||
fn distribute_spacing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -1585,7 +1623,7 @@ fn distribute_spacing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let fixed = spacing.and_then(|_| deltas.iter().find(|(id, _)| Some(*id) == key)).map_or(0.0, |k| k.1);
|
||||
let moves: Vec<(NodeId, Vec2)> =
|
||||
deltas.into_iter().map(|(id, d)| (id, if horiz { Vec2::new(d - fixed, 0.0) } else { Vec2::new(0.0, d - fixed) })).collect();
|
||||
apply_moves(s, "Distribute Spacing", p, &ids, &moves)
|
||||
apply_moves(s, "Distribute Spacing", p, &ids, &moves, &[])
|
||||
}
|
||||
|
||||
fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -66,7 +66,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Cancel Key Object",
|
||||
[],
|
||||
None,
|
||||
"{id?: a selected top-level object (not a layer, a compound-path member or an object inside another selected object); one of two or more (the Selection tool: a click on one object of a selection of several; on the key again: no id), guide?: index (a selected ruler guide, guide.list, with one or more objects selected: the Selection tool's click on a selected guide; Align lines the objects up on it and the guide stays put)} the key object Align aligns to and Distribute Spacing spaces from; without `id` or `guide` it cancels the key, as does a selection that holds only the key and objects inside it (one object, or a group key and its own members), or a key guide with no object selected or taken out of the selection; any other id or guide is an error that leaves the key as it was → {key, guide?} (the resulting key object, or null; guide: the key guide, left out without one)",
|
||||
"{id?: a selected top-level object (not a layer, a compound-path member or an object inside another selected object); one of two or more or the only one with ruler guides selected (the Selection tool: a click on one object of a selection of several, or of one with guides; on the key again: no id), guide?: index (a selected ruler guide, guide.list, with one or more objects selected: the Selection tool's click on a selected guide; Align lines the objects up on it and the guide stays put)} the key object Align aligns to and Distribute Spacing spaces from; without `id` or `guide` it cancels the key, as does a selection that holds only the key and objects inside it (one object, or a group key and its own members) and no ruler guide, or a key guide with no object selected or taken out of the selection; any other id or guide is an error that leaves the key as it was → {key, guide?} (the resulting key object, or null; guide: the key guide, left out without one)",
|
||||
has_doc,
|
||||
key
|
||||
),
|
||||
|
||||
@@ -775,6 +775,10 @@ pub(crate) fn deselect_guides(d: &Document, sel: &mut Selection) -> bool {
|
||||
}
|
||||
sel.guides.clear();
|
||||
sel.key_guide = None;
|
||||
// A key object kept only for the guides to line up on goes with them.
|
||||
if sel.key.is_some_and(|k| sel.objects.iter().all(|o| *o == k)) {
|
||||
sel.key = None;
|
||||
}
|
||||
any
|
||||
}
|
||||
|
||||
|
||||
@@ -127,3 +127,59 @@ fn selected_anchor_points_align_to_a_key_guide() {
|
||||
assert_eq!(p.x, 400.0);
|
||||
assert_eq!(path.subpaths[0].anchors[1].p.x, 140.0, "the other points stay");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_guides_align_to_a_clicked_key_object() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0);
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 500})).unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": false, "pos": 400})).unwrap();
|
||||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||||
// Click the object, Shift-click both guides, then click the object again: one object is
|
||||
// enough to be the key while guides are selected.
|
||||
click(&mut s, 110.0, 110.0, false);
|
||||
click(&mut s, 500.0, 300.0, true);
|
||||
click(&mut s, 300.0, 400.0, true);
|
||||
assert_eq!(s.doc().unwrap().selection.guides, vec![0, 1]);
|
||||
click(&mut s, 110.0, 110.0, false);
|
||||
assert_eq!(s.doc().unwrap().selection.key, Some(a), "the object is the key");
|
||||
let before = bounds(&s, a);
|
||||
s.execute("object.align", &json!({"horizontal": "center", "vertical": "center"})).unwrap();
|
||||
let guides = |s: &Session| s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect::<Vec<_>>();
|
||||
assert_eq!(guides(&s), [120.0, 110.0], "the vertical guide on the centre's x, the horizontal one on its y");
|
||||
assert_eq!(bounds(&s, a), before, "the key stays put");
|
||||
s.execute("object.align", &json!({"horizontal": "right"})).unwrap();
|
||||
assert_eq!(guides(&s), [140.0, 110.0], "horizontal alignment moves only the vertical guide");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(guides(&s), [120.0, 110.0], "one undo step");
|
||||
// Aligning to the artboard leaves the guides alone.
|
||||
s.execute("object.align", &json!({"horizontal": "left", "to": "artboard"})).unwrap();
|
||||
assert_eq!(guides(&s), [120.0, 110.0]);
|
||||
// Deselecting the guides lets the lone key go.
|
||||
s.execute("guide.select", &json!({"indexes": [0, 1], "toggle": true})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![a]);
|
||||
assert_eq!(s.doc().unwrap().selection.key, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guides_align_to_a_key_object_among_others_and_parallel_ones_to_a_key_guide() {
|
||||
let mut s = session();
|
||||
let (a, b) = (rect(&mut s, 100.0, 100.0), rect(&mut s, 300.0, 250.0));
|
||||
for pos in [500, 600] {
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": pos})).unwrap();
|
||||
}
|
||||
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
s.execute("guide.select", &json!({"indexes": [0, 1], "toggle": true})).unwrap();
|
||||
s.execute("select.key", &json!({"id": b.0})).unwrap();
|
||||
s.execute("object.align", &json!({"horizontal": "left"})).unwrap();
|
||||
assert_eq!(bounds(&s, a).x0, 300.0);
|
||||
assert!(s.doc().unwrap().doc.guides.iter().all(|g| g.pos == 300.0));
|
||||
// Aligning to the selection leaves guides alone.
|
||||
s.execute("object.align", &json!({"horizontal": "right", "to": "selection"})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.guides.iter().all(|g| g.pos == 300.0));
|
||||
// A key guide stays put, and the other vertical guide lines up on it.
|
||||
s.execute("guide.move", &json!({"index": 1, "pos": 700})).unwrap();
|
||||
s.execute("select.key", &json!({"guide": 1})).unwrap();
|
||||
s.execute("object.align", &json!({"horizontal": "center"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect::<Vec<_>>(), [700.0, 700.0]);
|
||||
}
|
||||
|
||||
@@ -48,9 +48,9 @@ enum State {
|
||||
/// Shift-pressed on this selected object: released without a drag, it leaves the
|
||||
/// selection.
|
||||
deselect: Option<NodeId>,
|
||||
/// Pressed (no modifier) on this object of a selection of several: released without a
|
||||
/// drag, it becomes the key object that Align aligns to if `can_be_key` allows it (the key
|
||||
/// again: no key).
|
||||
/// Pressed (no modifier) on this object of a selection of several, or of one with ruler
|
||||
/// guides: released without a drag, it becomes the key object that Align aligns to if
|
||||
/// `can_be_key` allows it (the key again: no key).
|
||||
key: Option<NodeId>,
|
||||
},
|
||||
Scaling {
|
||||
@@ -582,7 +582,7 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
} else if !cx.selection.contains(top) {
|
||||
out.push(Action::Exec("select.set".into(), json!({ "ids": [top.0] })));
|
||||
} else if cx.selection.objects.len() > 1 && !m.cmd && !m.alt {
|
||||
} else if (cx.selection.objects.len() > 1 || !cx.selection.guides.is_empty()) && !m.cmd && !m.alt {
|
||||
key = Some(top);
|
||||
}
|
||||
self.state = State::Moving { start: grab, began: false, deselect, key };
|
||||
@@ -1442,7 +1442,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// #541: a click on one object of a selection of several makes it the key object; a click on
|
||||
/// the key again lets it go. A drag moves them all, a lone object has no key.
|
||||
/// the key again lets it go. A drag moves them all, a lone object has no key unless ruler
|
||||
/// guides are selected with it.
|
||||
#[test]
|
||||
fn a_click_on_a_selected_object_makes_it_the_key() {
|
||||
let (d, id) = doc_with_area_type();
|
||||
@@ -1466,10 +1467,13 @@ mod tests {
|
||||
t.pointer(&c, &ev(PointerKind::Down, 150.0, 150.0));
|
||||
t.pointer(&c, &ev(PointerKind::Drag, 170.0, 150.0));
|
||||
assert_eq!(t.pointer(&c, &ev(PointerKind::Up, 170.0, 150.0)), vec![Action::Commit]);
|
||||
// One object selected: a click on it does nothing.
|
||||
// One object selected: a click on it does nothing, unless ruler guides are selected
|
||||
// with it for it to be the key of (#2143).
|
||||
let mut one = Selection::default();
|
||||
one.add(rect);
|
||||
assert!(click(&one, 150.0, 150.0).is_empty());
|
||||
one.guides = vec![0];
|
||||
assert_eq!(click(&one, 150.0, 150.0), vec![Action::Exec("select.key".into(), json!({"id": rect.0}))]);
|
||||
}
|
||||
|
||||
/// In isolation mode a click makes only a selected top-level object the key, as `select.key`
|
||||
|
||||
+4
-2
@@ -1082,8 +1082,10 @@ The id must name a selected top-level object.
|
||||
Layers, compound-path members and objects inside another selected object are not top-level objects.
|
||||
Any other id is an error naming that value, and the key is left unchanged.
|
||||
A null `id` is an error too.
|
||||
A click with the Selection tool on a selected top-level object, while two or more objects are selected, runs `select.key {id}`, and a click on the key object again runs `select.key {}`.
|
||||
On a single selected object, or on a group selected with only its own members, `select.key` sets no key.
|
||||
A click with the Selection tool on a selected top-level object, while two or more objects are selected or ruler guides are selected with it, runs `select.key {id}`, and a click on the key object again runs `select.key {}`.
|
||||
On a single selected object with no ruler guide selected, or on a group selected with only its own members, `select.key` sets no key.
|
||||
With a key object (or a key guide), `object.align` moves the other selected ruler guides too: a vertical guide goes to the key's left edge, centre or right edge, a horizontal one to its top, centre or bottom (#2143).
|
||||
Aligning to the selection or the artboard leaves guides where they are.
|
||||
`select.key {guide}` makes a selected ruler guide (its `guide.list` index) the key instead, while one or more objects are selected (#2143).
|
||||
`object.align` then lines the objects up on the guide and the guide stays put: a vertical guide sets where their left edges, centres or right edges go and vertical alignment leaves them where they are, a horizontal guide the other way round.
|
||||
Selected anchor points align to it the same way. Distribute Spacing doesn't space from a key guide.
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user