G11: Align panel moves ruler guides to artboard (#3463)

## Summary
object.align now moves selected ruler guides when aligning to the artboard,
matching Illustrator and the existing Distribute behaviour. Guide-only
selection can run align; the Align panel enables its buttons when only
guides are selected. Guides still stay put when objects are in the selection
and Align To is Selection; key-guide behaviour is unchanged.

## Tests
- engine tests_keyguide: guides_selected_alone_align_to_artboard; updated
  selected_guides_align_to_a_clicked_key_object for artboard align
- ui-egui align panel: the_align_buttons_align_guides_selected_alone_to_artboard

Closes storytold/vectorcraft#3463

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-10-22 05:20:10 +07:00
committed by bflatastic
co-authored by nrq6993-a11y
parent 430741f074
commit c8082facb1
3 changed files with 77 additions and 15 deletions
+36 -11
View File
@@ -252,8 +252,8 @@ 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; 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,
"{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; aligning to the artboard moves selected guides to its edges too (#3463), and with only guides selected aligning to the selection moves them to their collective bounds), 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_or_guides,
align
),
cmd!(
@@ -1361,11 +1361,22 @@ fn align_reference(s: &Session, p: &Value, ids: &[NodeId], preview: bool, own: O
Ok((r, true, true))
}
/// Bounds of the selected ruler guides (each guide is a zero-thickness line), for Align to
/// Selection when no objects are selected.
fn selected_guides_bounds(st: &crate::DocState) -> Option<Rect> {
st.selection.guides.iter().filter_map(|&i| st.doc.guides.get(i)).fold(None, |acc: Option<Rect>, g| {
let r = if g.vertical { Rect::new(g.pos, 0.0, g.pos, 0.0) } else { Rect::new(0.0, g.pos, 0.0, g.pos) };
Some(acc.map(|a| a.union(r)).unwrap_or(r))
})
}
fn reference_rect(s: &Session, p: &Value, ids: &[NodeId], preview: bool) -> Result<Rect> {
let st = s.doc()?;
match align_to(s, p) {
Some("artboard") => {
let b = effects::preview::bounds_of(&st.doc, ids, preview).unwrap_or_default();
let b = effects::preview::bounds_of(&st.doc, ids, preview)
.or_else(|| ids.is_empty().then(|| selected_guides_bounds(st)).flatten())
.unwrap_or_default();
// Align to Artboard uses artboard `artboard` (the app passes the active one). Without
// it, or with an index the document doesn't have, it uses the artboard under the center
// of the selection, or the first one when no artboard is there.
@@ -1377,7 +1388,13 @@ fn reference_rect(s: &Session, p: &Value, ids: &[NodeId], preview: bool) -> Resu
let k = st.selection.key.ok_or_else(|| EngineError::Other("no key object".into()))?;
effects::preview::bounds_of(&st.doc, &[k], preview).ok_or(EngineError::NoNode(k))
}
_ => effects::preview::bounds_of(&st.doc, ids, preview).ok_or_else(|| EngineError::Other("nothing to align".into())),
_ => {
if ids.is_empty() {
selected_guides_bounds(st).ok_or_else(|| EngineError::Other("nothing to align".into()))
} else {
effects::preview::bounds_of(&st.doc, ids, preview).ok_or_else(|| EngineError::Other("nothing to align".into()))
}
}
}
}
@@ -1443,15 +1460,22 @@ fn align(s: &mut Session, p: &Value) -> Result<Value> {
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.
/// The selected ruler guides that Align 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. Align to the key object (or key guide) moves them too, and the key guide stays put.
/// Align to the artboard moves them (#3463). Align to the selection moves them when only guides are
/// selected; with objects in the selection they stay put.
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") {
let st = s.doc()?;
let to = align_to(s, p);
let move_guides = match to {
Some("key" | "artboard") => true,
Some("selection") => st.selection.is_empty(),
_ => false,
};
if !move_guides {
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),
@@ -1461,11 +1485,12 @@ fn guide_moves(s: &Session, p: &Value, r: Rect, h: Option<&str>, v: Option<&str>
(false, _, Some("bottom")) => Some(r.y1),
_ => None,
};
let skip_key = to == Some("key");
Ok(st
.selection
.guides
.iter()
.filter(|&&i| Some(i) != st.selection.key_guide)
.filter(|&&i| !skip_key || Some(i) != st.selection.key_guide)
.filter_map(|&i| Some((i, at(st.doc.guides.get(i)?.vertical)?)))
.collect())
}