mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
committed by
bflatastic
co-authored by
nrq6993-a11y
parent
430741f074
commit
c8082facb1
@@ -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())
|
||||
}
|
||||
|
||||
@@ -153,9 +153,9 @@ fn selected_guides_align_to_a_clicked_key_object() {
|
||||
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.
|
||||
// Aligning to the artboard moves the vertical guide to the artboard's left edge.
|
||||
s.execute("object.align", &json!({"horizontal": "left", "to": "artboard"})).unwrap();
|
||||
assert_eq!(guides(&s), [120.0, 110.0]);
|
||||
assert_eq!(guides(&s), [0.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]);
|
||||
@@ -310,6 +310,18 @@ fn positions(s: &Session) -> Vec<f64> {
|
||||
s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guides_selected_alone_align_to_artboard() {
|
||||
let mut s = guides_alone(&[100.0, 400.0], &[50.0, 300.0]);
|
||||
s.execute("object.align", &json!({"horizontal": "left", "to": "artboard", "artboard": 0})).unwrap();
|
||||
assert_eq!(positions(&s), [0.0, 0.0, 50.0, 300.0]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("object.align", &json!({"horizontal": "right", "to": "artboard", "artboard": 0})).unwrap();
|
||||
assert_eq!(&positions(&s)[..2], [800.0, 800.0]);
|
||||
s.execute("object.align", &json!({"vertical": "top", "to": "artboard", "artboard": 0})).unwrap();
|
||||
assert_eq!(positions(&s), [800.0, 800.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distribute_spaces_guides_selected_without_objects() {
|
||||
let mut s = guides_alone(&[100.0, 150.0, 500.0], &[50.0, 60.0, 400.0]);
|
||||
|
||||
@@ -223,6 +223,8 @@ fn align_to(app: &VectorcraftApp) -> AlignTo {
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let n = selection_len(app);
|
||||
let guides = selected_guides(app);
|
||||
let can_align = n + guides >= 1;
|
||||
let to = align_to(app);
|
||||
// `spacing` is the Distribute Spacing value. The Distribute Objects and Distribute Spacing
|
||||
// buttons send it while there is a key object or a key guide (#2143).
|
||||
@@ -236,12 +238,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
for (i, (icon, tip, axis, v)) in ALIGN.iter().enumerate() {
|
||||
if i == 3 {
|
||||
ui.add_space(6.0);
|
||||
if widgets::icon_button_enabled(ui, "dc-al-center", tl!("Horizontal & Vertical Align Center"), false, n >= 1, 32.0).clicked() {
|
||||
if widgets::icon_button_enabled(ui, "dc-al-center", tl!("Horizontal & Vertical Align Center"), false, can_align, 32.0).clicked() {
|
||||
run_button(app, Button::AlignCenters).ok();
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
if widgets::icon_button_enabled(ui, icon, tl!(tip), false, n >= 1, 32.0).clicked() {
|
||||
if widgets::icon_button_enabled(ui, icon, tl!(tip), false, can_align, 32.0).clicked() {
|
||||
run_button(app, Button::Align(axis, v)).ok();
|
||||
}
|
||||
}
|
||||
@@ -883,6 +885,29 @@ mod tests {
|
||||
assert_eq!(app.session.active().unwrap().doc.guides[0].pos, 100.0, "the guide stays put");
|
||||
}
|
||||
|
||||
/// Three vertical ruler guides selected without objects: Align Objects buttons are on, and
|
||||
/// Horizontal Align Left puts every guide on the artboard's left edge (#3463).
|
||||
#[test]
|
||||
fn the_align_buttons_align_guides_selected_alone_to_artboard() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
for pos in [20, 50, 180] {
|
||||
app.run("guide.add", json!({"vertical": true, "pos": pos})).unwrap();
|
||||
}
|
||||
app.run("guide.select", json!({"indexes": [0, 1, 2]})).unwrap();
|
||||
app.ui.align_to = AlignTo::Artboard;
|
||||
let ctx = egui::Context::default();
|
||||
frame_events(&ctx, &mut app, vec![], show);
|
||||
let row = widget_rects(&ctx, egui::vec2(32.0, 32.0));
|
||||
let top = row.iter().map(|r| r.top()).fold(f32::MAX, f32::min);
|
||||
let mut row: Vec<egui::Rect> = row.into_iter().filter(|r| r.top() == top).collect();
|
||||
row.sort_by(|a, b| a.left().total_cmp(&b.left()));
|
||||
assert!(row[0].width() > 0.0, "Horizontal Align Left is enabled");
|
||||
click(&ctx, &mut app, row[0].center(), show);
|
||||
let pos = |app: &VectorcraftApp| app.session.active().unwrap().doc.guides.iter().map(|g| g.pos).collect::<Vec<_>>();
|
||||
assert_eq!(pos(&app), [0.0, 0.0, 0.0], "{}", app.ui.status);
|
||||
}
|
||||
|
||||
/// Three vertical ruler guides selected without objects: the Distribute buttons are on, and
|
||||
/// Horizontal Distribute Center puts the middle guide halfway between the other two, or with
|
||||
/// Align to Artboard spaces them from its left edge to its right edge, as a public forum answer
|
||||
|
||||
Reference in New Issue
Block a user