mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 04:07:12 -06:00
Merge pull request #1216 from storytold/fix/1180-align-anchors
#1180: Align aligns the selected anchor points
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -167,7 +167,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), 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)}",
|
||||
"{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), 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
|
||||
),
|
||||
@@ -1080,6 +1080,10 @@ fn align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if h.is_none() && v.is_none() {
|
||||
return Err(bad("object.align", "give horizontal or vertical"));
|
||||
}
|
||||
// Anchor points selected with Direct Selection align themselves, as in Illustrator (#1180).
|
||||
if !s.doc()?.selection.anchors.is_empty() {
|
||||
return align_anchors(s, p, &ids, (h, v));
|
||||
}
|
||||
let r = reference_rect(s, p, &ids, preview)?;
|
||||
let key = key_root(s, &ids);
|
||||
let moves: Vec<(NodeId, Vec2)> = {
|
||||
@@ -1107,6 +1111,49 @@ fn align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
apply_moves(s, "Align", p, &ids, &moves)
|
||||
}
|
||||
|
||||
/// [`align`] of the selected anchor points (whole paths among them give all theirs): each point
|
||||
/// moves to the edge or centre of the points' bounds, or of the artboard with Align To Artboard.
|
||||
fn align_anchors(s: &mut Session, p: &Value, ids: &[NodeId], (h, v): (Option<&str>, Option<&str>)) -> Result<Value> {
|
||||
let targets = super::path::anchor_targets(s)?;
|
||||
let d = &s.doc()?.doc;
|
||||
let pts: Vec<Point> = targets
|
||||
.iter()
|
||||
.flat_map(|(id, refs)| refs.iter().filter_map(move |(si, ai)| d.node(*id)?.path_data()?.subpaths.get(*si)?.anchors.get(*ai).map(|a| a.p)))
|
||||
.collect();
|
||||
let Some(bounds) = pts.iter().skip(1).fold(pts.first().map(|q| Rect::from_points(*q, *q)), |b, q| b.map(|b| b.union_pt(*q))) else {
|
||||
return ok();
|
||||
};
|
||||
// shortcut: no key anchor yet, so Align To Key Object aligns to the points' bounds as Selection does.
|
||||
let r = if align_to(s, p) == Some("artboard") { reference_rect(s, p, ids, false)? } else { bounds };
|
||||
let target = |q: Point| {
|
||||
let x = match h {
|
||||
Some("left") => r.x0,
|
||||
Some("center") => r.center().x,
|
||||
Some("right") => r.x1,
|
||||
_ => q.x,
|
||||
};
|
||||
let y = match v {
|
||||
Some("top") => r.y0,
|
||||
Some("center") => r.center().y,
|
||||
Some("bottom") => r.y1,
|
||||
_ => q.y,
|
||||
};
|
||||
Point::new(x, y)
|
||||
};
|
||||
s.edit("Align", |d, _| {
|
||||
for (id, refs) in &targets {
|
||||
let path = super::path::path_mut(d, *id)?;
|
||||
for &(si, ai) in refs {
|
||||
if let Some(a) = path.anchor_mut(si, ai) {
|
||||
a.translate(target(a.p) - a.p);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
}
|
||||
|
||||
fn distribute(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = selected_roots(s)?;
|
||||
let preview = preview_bounds(s, p, "object.distribute")?;
|
||||
|
||||
@@ -116,7 +116,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
]
|
||||
}
|
||||
|
||||
fn path_mut(d: &mut vectorcraft_doc::Document, id: NodeId) -> Result<&mut PathData> {
|
||||
pub(super) fn path_mut(d: &mut vectorcraft_doc::Document, id: NodeId) -> Result<&mut PathData> {
|
||||
let n = d.node_mut(id).ok_or(EngineError::NoNode(id))?;
|
||||
match &mut n.kind {
|
||||
NodeKind::Path { path, live, .. } => {
|
||||
@@ -378,9 +378,9 @@ fn join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
type AnchorTargets = Vec<(NodeId, Vec<(usize, usize)>)>;
|
||||
pub(super) type AnchorTargets = Vec<(NodeId, Vec<(usize, usize)>)>;
|
||||
|
||||
fn anchor_targets(s: &Session) -> Result<AnchorTargets> {
|
||||
pub(super) fn anchor_targets(s: &Session) -> Result<AnchorTargets> {
|
||||
let st = s.doc()?;
|
||||
let mut out = vec![];
|
||||
for id in &st.selection.objects {
|
||||
|
||||
@@ -1700,6 +1700,27 @@ fn file_list_lists_a_folder_within_the_read_root() {
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// #1180: with anchor points selected (Direct Selection), Align moves those points, not the whole
|
||||
/// path: to the points' own bounds, or to the artboard.
|
||||
#[test]
|
||||
fn align_moves_selected_anchor_points() {
|
||||
let mut s = session();
|
||||
let id = NodeId(s.execute("path.create", &json!({"d": "M100 40 L200 60 L150 160 Z"})).unwrap()["id"].as_u64().unwrap());
|
||||
let anchors = |s: &Session| -> Vec<(f64, f64)> {
|
||||
s.doc().unwrap().doc.node(id).unwrap().path_data().unwrap().anchors().map(|(_, _, a)| (a.p.x, a.p.y)).collect()
|
||||
};
|
||||
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 0], [0, 1]], "mode": "set"})).unwrap();
|
||||
s.execute("object.align", &json!({"vertical": "top", "to": "selection"})).unwrap();
|
||||
assert_eq!(anchors(&s), [(100.0, 40.0), (200.0, 40.0), (150.0, 160.0)]);
|
||||
s.execute("object.align", &json!({"horizontal": "center", "to": "artboard"})).unwrap();
|
||||
let mid = s.doc().unwrap().doc.artboards[0].rect.center().x;
|
||||
assert_eq!(anchors(&s), [(mid, 40.0), (mid, 40.0), (150.0, 160.0)], "the third point stays");
|
||||
// A path selected whole still moves as an object.
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("object.align", &json!({"vertical": "top", "to": "artboard"})).unwrap();
|
||||
assert_eq!(anchors(&s)[2].1 - anchors(&s)[0].1, 120.0, "its shape kept");
|
||||
}
|
||||
|
||||
/// #1197: Make Compound Path cuts holes where the paths in front overlap the backmost one, also
|
||||
/// when they were drawn running the same way.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user