Files
vectorcraft/crates/engine/src/cmd/object/align.rs
T

528 lines
25 KiB
Rust

use super::transform::transform_patterns;
use super::{
Affine, Document, EngineError, NodeId, OrientedBox, Point, Rect, Result, Scaling, Session, Value, Vec2, bad, check_numbers, check_switches,
effects, has_multi, ok, selected_roots, str_param,
};
impl Session {
/// The bounds of `ids` the Transform panel, the bounding box, Align and Distribute measure:
/// with Use Preview Bounds on, visual bounds as drawn (strokes and geometry effects included,
/// see [`effects::preview`]), else geometric bounds.
pub fn transform_bounds(&self, ids: &[NodeId]) -> Option<Rect> {
effects::preview::bounds_of(&self.doc().ok()?.doc, ids, self.prefs.use_preview_bounds)
}
/// [`Session::transform_bounds`] square to the objects' own angle (their rotated bounding
/// box, see [`Document::oriented_bounds`]).
pub fn transform_box(&self, ids: &[NodeId]) -> Option<OrientedBox> {
effects::preview::oriented_bounds(&self.doc().ok()?.doc, ids, self.prefs.use_preview_bounds)
}
}
/// Measure with preview (visual) bounds? The `bounds` param, else Use Preview Bounds.
fn preview_bounds(s: &Session, p: &Value, cmd: &str) -> Result<bool> {
match choice(p, cmd, "bounds", &["preview", "geometric"])? {
None => Ok(s.prefs.use_preview_bounds),
Some(b) => Ok(b == "preview"),
}
}
/// Param `key` of the Align commands, Shear and `text.setFormat`'s lists: one of `values`, or
/// `None` when it is missing or null. Any other value is an error that lists `values`.
pub(crate) fn choice<'a>(p: &'a Value, cmd: &str, key: &str, values: &[&str]) -> Result<Option<&'a str>> {
let Some(v) = p.get(key).filter(|v| !v.is_null()) else { return Ok(None) };
match v.as_str() {
Some(s) if values.contains(&s) => Ok(Some(s)),
_ => Err(bad(cmd, format!("`{key}` must be one of {}", values.join(", ")))),
}
}
/// `horizontal` and `vertical` of `object.align` and `object.distribute`, each checked by [`choice`].
fn direction<'a>(p: &'a Value, cmd: &str) -> Result<(Option<&'a str>, Option<&'a str>)> {
Ok((choice(p, cmd, "horizontal", &["left", "center", "right"])?, choice(p, cmd, "vertical", &["top", "center", "bottom"])?))
}
/// `spacing` of `object.distribute` and `object.distributeSpacing`: a finite number, or `None`
/// when it is missing or null. Any other value is an error.
fn spacing(p: &Value, cmd: &str) -> Result<Option<f64>> {
p.get("spacing")
.filter(|v| !v.is_null())
.map(|v| v.as_f64().filter(|g| g.is_finite()).ok_or_else(|| bad(cmd, "`spacing` must be a number")))
.transpose()
}
/// `ids` with their bounds (`preview`: visual bounds as drawn, see [`effects::preview::bounds_of`]).
fn items_bounds(d: &Document, ids: &[NodeId], preview: bool) -> Vec<(NodeId, Rect)> {
ids.iter().filter_map(|id| Some((*id, effects::preview::bounds_of(d, &[*id], preview)?))).collect()
}
/// 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() };
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, _| {
for (id, dv) in moves {
if let Some(n) = d.node_mut(*id) {
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()
}
/// What `object.align` aligns to: `to`, else the key object (or key guide) when there is one,
/// else the selection.
fn align_to<'a>(s: &Session, p: &'a Value) -> Option<&'a str> {
str_param(p, "to").or_else(|| s.doc().ok()?.selection.has_key().then_some("key"))
}
/// The key guide `object.align` lines the objects up on (aligning to the key with one set,
/// [`vectorcraft_doc::Selection::key_guide`]): its position, and whether it is vertical.
fn key_guide(s: &Session, p: &Value) -> Option<(f64, bool)> {
let st = s.doc().ok()?;
let g = st.doc.guides.get(st.selection.key_guide?)?;
(align_to(s, p) == Some("key")).then_some((g.pos, g.vertical))
}
/// Where Align puts the objects, `r`, and the axes it moves them along (x, y): a key guide gives
/// the line across its axis only (a vertical guide fixes x, a horizontal one y), so the other
/// axis stays as it is.
fn align_reference(s: &Session, p: &Value, ids: &[NodeId], preview: bool, own: Option<Rect>) -> Result<(Rect, bool, bool)> {
if let Some((pos, vertical)) = key_guide(s, p) {
return Ok((Rect::new(pos, pos, pos, pos), vertical, !vertical));
}
let r = match own {
Some(b) if align_to(s, p) != Some("artboard") => b,
_ => reference_rect(s, p, ids, preview)?,
};
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)
.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.
let named = p.get("artboard").and_then(Value::as_u64).and_then(|i| usize::try_from(i).ok()).and_then(|i| st.doc.artboards.get(i));
let i = st.doc.artboard_at(b.center()).unwrap_or(0);
Ok(named.or(st.doc.artboards.get(i)).map(|a| a.rect).unwrap_or(b))
}
Some("key") => {
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))
}
_ => {
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()))
}
}
}
}
/// Returns the artboard for `to: "artboard"` of `object.distribute` and `object.distributeSpacing`
/// (command `cmd`), the same artboard as `object.align` uses ([`reference_rect`]). It returns
/// `None` for the other choices. A `to` outside the choices is an error. Both commands call it
/// before they space selected anchor points, and they space those points without the artboard.
fn distribute_to(s: &Session, p: &Value, ids: &[NodeId], preview: bool, cmd: &str) -> Result<Option<Rect>> {
match choice(p, cmd, "to", &["selection", "artboard", "key"])? {
Some("artboard") => reference_rect(s, p, ids, preview).map(Some),
_ => Ok(None),
}
}
/// Returns the selected root (one of `ids`) that is the key object or contains it, such as the
/// compound path or selected group the key is in. Align to the key object, and Distribute and
/// Distribute Spacing with a spacing, keep it in place.
fn key_root(s: &Session, ids: &[NodeId]) -> Option<NodeId> {
let st = s.doc().ok()?;
let ancestry = st.doc.ancestry(st.selection.key?)?;
ids.iter().copied().find(|id| ancestry.contains(id))
}
pub(super) fn align(s: &mut Session, p: &Value) -> Result<Value> {
let ids = selected_roots(s)?;
let preview = preview_bounds(s, p, "object.align")?;
let (h, v) = direction(p, "object.align")?;
choice(p, "object.align", "to", &["selection", "artboard", "key"])?;
check_switches(p, &["patterns"], "object.align")?;
check_numbers(p, &["artboard"], "object.align")?;
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, along_x, along_y) = align_reference(s, p, &ids, preview, None)?;
let (h, v) = (h.filter(|_| along_x), v.filter(|_| along_y));
let key = key_root(s, &ids);
let moves: Vec<(NodeId, Vec2)> = {
let d = &s.doc()?.doc;
items_bounds(d, &ids, preview)
.into_iter()
.filter(|(id, _)| Some(*id) != key || align_to(s, p) != Some("key"))
.map(|(id, b)| {
let dx = match h {
Some("left") => r.x0 - b.x0,
Some("center") => r.center().x - b.center().x,
Some("right") => r.x1 - b.x1,
_ => 0.0,
};
let dy = match v {
Some("top") => r.y0 - b.y0,
Some("center") => r.center().y - b.center().y,
Some("bottom") => r.y1 - b.y1,
_ => 0.0,
};
(id, Vec2::new(dx, dy))
})
.collect()
};
let guides = guide_moves(s, p, r, h, v)?;
apply_moves(s, "Align", p, &ids, &moves, &guides)
}
/// 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)>> {
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 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,
};
let skip_key = to == Some("key");
Ok(st
.selection
.guides
.iter()
.filter(|&&i| !skip_key || 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
/// 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 = crate::cmd::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
// (or to a key guide).
let (r, along_x, along_y) = align_reference(s, p, ids, false, Some(bounds))?;
let (h, v) = (h.filter(|_| along_x), v.filter(|_| along_y));
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)
};
if pts.iter().all(|q| {
let dv = target(*q) - *q;
dv.x.abs() <= 1e-9 && dv.y.abs() <= 1e-9
}) {
return ok();
}
s.edit("Align", |d, _| {
for (id, refs) in &targets {
let path = crate::cmd::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()
}
/// Distribute needs two or more objects and ruler guides, or anchor points selected with Direct
/// Selection (they may all be on one path).
pub(super) fn can_distribute(s: &Session) -> std::result::Result<(), String> {
if s.active().is_some_and(|st| !st.selection.anchors.is_empty() || st.selection.len() + st.selection.guides.len() >= 2) {
Ok(())
} else {
has_multi(s)
}
}
/// What Distribute and Distribute Spacing space: a selected object, or a selected ruler guide
/// across the axis (#1597), which has no size.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Spaced {
Object(NodeId),
Guide(usize),
}
/// The selected objects `ids` with their bounds (`preview`: visual bounds), and the selected
/// ruler guides across the axis (vertical ones when `horiz`) with their line as their bounds.
/// Guides along the axis are left out, so they stay where they are.
fn spaced_items(s: &Session, ids: &[NodeId], preview: bool, horiz: bool) -> Result<Vec<(Spaced, Rect)>> {
let st = s.doc()?;
let objects = items_bounds(&st.doc, ids, preview).into_iter().map(|(id, r)| (Spaced::Object(id), r));
let guides = st.selection.guides.iter().filter_map(|&i| {
let g = st.doc.guides.get(i).filter(|g| g.vertical == horiz)?;
Some((Spaced::Guide(i), Rect::new(g.pos, g.pos, g.pos, g.pos)))
});
Ok(objects.chain(guides).collect())
}
/// The index in `items` of what Distribute and Distribute Spacing keep in place with a spacing:
/// the selected root that is the key object or contains it ([`key_root`]), or the key guide when
/// aligning to the key (#2143).
fn spaced_key(s: &Session, p: &Value, ids: &[NodeId], items: &[(Spaced, Rect)]) -> Option<usize> {
let key = match key_root(s, ids) {
Some(id) => Spaced::Object(id),
None => key_guide(s, p).and(s.doc().ok()?.selection.key_guide).map(Spaced::Guide)?,
};
items.iter().position(|(it, _)| *it == key)
}
/// Whether the key is a guide along the axis (a horizontal guide for a horizontal distribution),
/// aligning to the key. It has no line there, so a spacing from it leaves everything where it
/// is, as Align does.
fn key_guide_along(s: &Session, p: &Value, horiz: bool) -> bool {
key_guide(s, p).is_some_and(|(_, vertical)| vertical != horiz)
}
/// Moves each of `deltas` that far along the axis (x when `horiz`) as undo step `label`, as
/// [`apply_moves`] does: an object by that offset, a ruler guide to its new position.
fn apply_spaced(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], horiz: bool, deltas: &[(Spaced, f64)]) -> Result<Value> {
let along = |d: f64| if horiz { Vec2::new(d, 0.0) } else { Vec2::new(0.0, d) };
let (mut moves, mut guides) = (vec![], vec![]);
let d = &s.doc()?.doc;
for &(it, delta) in deltas {
match it {
Spaced::Object(id) => moves.push((id, along(delta))),
Spaced::Guide(i) => guides.extend(d.guides.get(i).map(|g| (i, g.pos + delta))),
}
}
apply_moves(s, label, p, ids, &moves, &guides)
}
/// [`distribute`] and [`distribute_spacing`] of the selected anchor points (whole paths among them
/// give all theirs): the points between the first and the last along the axis are spaced evenly,
/// or `gap` apart from the first. A point has no size, so every Distribute option of an axis
/// gives the same result.
fn distribute_anchors(s: &mut Session, label: &str, horiz: bool, gap: Option<f64>) -> Result<Value> {
let targets = crate::cmd::path::anchor_targets(s)?;
let d = &s.doc()?.doc;
let along = |q: Point| if horiz { q.x } else { q.y };
let mut pts: Vec<(NodeId, usize, usize, f64)> = targets
.iter()
.flat_map(|(id, refs)| {
refs.iter().filter_map(move |&(si, ai)| Some((*id, si, ai, along(d.node(*id)?.path_data()?.subpaths.get(si)?.anchors.get(ai)?.p))))
})
.collect();
pts.sort_by(|a, b| a.3.total_cmp(&b.3));
let (Some(first), Some(last)) = (pts.first().map(|q| q.3), pts.last().map(|q| q.3)) else {
return ok();
};
// Two points are the ends already; with a gap the second is placed from the first.
if pts.len() < if gap.is_some() { 2 } else { 3 } {
return ok();
}
let step = gap.unwrap_or((last - first) / (pts.len() - 1) as f64);
let moves: Vec<(NodeId, usize, usize, f64)> =
pts.iter().enumerate().map(|(i, &(id, si, ai, at))| (id, si, ai, first + step * i as f64 - at)).collect();
if moves.iter().all(|m| m.3.abs() <= 1e-9) {
return ok();
}
s.edit(label, |d, _| {
for &(id, si, ai, delta) in &moves {
if let Some(a) = crate::cmd::path::path_mut(d, id)?.anchor_mut(si, ai) {
a.translate(if horiz { Vec2::new(delta, 0.0) } else { Vec2::new(0.0, delta) });
}
}
Ok(())
})?;
ok()
}
pub(super) fn distribute(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &["patterns"], "object.distribute")?;
check_numbers(p, &["artboard"], "object.distribute")?;
let ids = selected_roots(s)?;
let preview = preview_bounds(s, p, "object.distribute")?;
let board = distribute_to(s, p, &ids, preview, "object.distribute")?;
let spacing = spacing(p, "object.distribute")?;
// Anchor points selected with Direct Selection are distributed themselves, as Align aligns them.
if !s.doc()?.selection.anchors.is_empty() {
return match direction(p, "object.distribute")? {
(None, None) => Err(bad("object.distribute", "give horizontal or vertical")),
(h, _) => distribute_anchors(s, "Distribute", h.is_some(), None),
};
}
let (horiz, key): (bool, fn(&Rect, bool) -> f64) = match direction(p, "object.distribute")? {
(Some(h), _) => (
true,
match h {
"left" => |r: &Rect, _| r.x0,
"right" => |r: &Rect, _| r.x1,
_ => |r: &Rect, _| r.center().x,
},
),
(_, Some(v)) => (
false,
match v {
"top" => |r: &Rect, _| r.y0,
"bottom" => |r: &Rect, _| r.y1,
_ => |r: &Rect, _| r.center().y,
},
),
_ => return Err(bad("object.distribute", "give horizontal or vertical")),
};
// The selected guides across the axis are spaced with the objects (#1597). A guide has no
// size, so its left edge, center and right edge are all its line.
let mut items = spaced_items(s, &ids, preview, horiz)?;
items.sort_by(|a, b| key(&a.1, horiz).total_cmp(&key(&b.1, horiz)));
let n = items.len();
if spacing.is_some() && key_guide_along(s, p, horiz) {
return ok();
}
// With a spacing, the key object (or the compound path or selected group it is in) or the key
// guide stays where it is, and the lines are placed `spacing` apart, outward from its line, in
// their order. Without a key the spacing is not used. `from_key` is the key's index in
// `items` and the spacing.
let from_key = spacing.zip(spaced_key(s, p, &ids, &items)).map(|(g, k)| (k, g));
// Two items are enough on an artboard, where they go to its edges, and when they are spaced
// from a key.
if n < 2 || (n < 3 && board.is_none() && from_key.is_none()) {
return ok();
}
let (first_box, last_box) = (&items[0].1, &items[n - 1].1);
// `first` and `last` are the lines that the first and last items end on. Without an artboard
// the first and last items stay where they are. On an artboard the first item moves until
// its left (top) edge is on the artboard's left (top) edge, and the last item moves until its
// right (bottom) edge is on the artboard's right (bottom) edge.
let (first, last) = match board {
Some(r) if horiz => (key(first_box, horiz) + r.x0 - first_box.x0, key(last_box, horiz) + r.x1 - last_box.x1),
Some(r) => (key(first_box, horiz) + r.y0 - first_box.y0, key(last_box, horiz) + r.y1 - last_box.y1),
None => (key(first_box, horiz), key(last_box, horiz)),
};
let deltas: Vec<(Spaced, f64)> = items
.iter()
.enumerate()
.map(|(i, (it, r))| {
let target = match from_key {
Some((k, g)) => key(&items[k].1, horiz) + g * (i as f64 - k as f64),
None => first + (last - first) * i as f64 / (n - 1) as f64,
};
(*it, target - key(r, horiz))
})
.collect();
apply_spaced(s, "Distribute", p, &ids, horiz, &deltas)
}
pub(super) fn distribute_spacing(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &["patterns"], "object.distributeSpacing")?;
check_numbers(p, &["artboard"], "object.distributeSpacing")?;
let ids = selected_roots(s)?;
let horiz = choice(p, "object.distributeSpacing", "axis", &["horizontal", "vertical"])? != Some("vertical");
let preview = preview_bounds(s, p, "object.distributeSpacing")?;
let board = distribute_to(s, p, &ids, preview, "object.distributeSpacing")?;
let spacing = spacing(p, "object.distributeSpacing")?;
if !s.doc()?.selection.anchors.is_empty() {
return distribute_anchors(s, "Distribute Spacing", horiz, spacing);
}
// The selected guides across the axis are spaced with the objects, as items with no size (#1597).
let mut items = spaced_items(s, &ids, preview, horiz)?;
items.sort_by(|a, b| if horiz { a.1.x0.total_cmp(&b.1.x0) } else { a.1.y0.total_cmp(&b.1.y0) });
if spacing.is_some() && key_guide_along(s, p, horiz) {
return ok();
}
let n = items.len();
let &[(_, first), .., (_, last)] = items.as_slice() else {
// Selected guides along the axis have nothing to space.
return if s.doc()?.selection.guides.is_empty() { Err(EngineError::Other("select two or more objects to distribute".into())) } else { ok() };
};
let size = |r: &Rect| if horiz { r.width() } else { r.height() };
let start = |r: &Rect| if horiz { r.x0 } else { r.y0 };
// The items start at `lo`, and without a spacing the last one ends at `hi`. These are the
// first item's left (top) edge and the last one's right (bottom) edge, or the artboard's
// edges with Align to Artboard and no spacing.
let (lo, hi) = match board.filter(|_| spacing.is_none()) {
Some(r) if horiz => (r.x0, r.x1),
Some(r) => (r.y0, r.y1),
None if horiz => (first.x0, last.x1),
None => (first.y0, last.y1),
};
let gap = match spacing {
Some(g) => g,
None => (hi - lo - items.iter().map(|i| size(&i.1)).sum::<f64>()) / (n - 1) as f64,
};
let mut pos = lo;
let mut deltas = vec![];
for (it, r) in &items {
deltas.push((*it, pos - start(r)));
pos += size(r) + gap;
}
// With a spacing the key stays where it is: the key object, with the compound path or
// selected group it is in, or the key guide (#2143). The others are spaced from it.
let fixed = spacing.and_then(|_| spaced_key(s, p, &ids, &items)).and_then(|k| deltas.get(k)).map_or(0.0, |k| k.1);
let deltas: Vec<(Spaced, f64)> = deltas.into_iter().map(|(it, d)| (it, d - fixed)).collect();
apply_spaced(s, "Distribute Spacing", p, &ids, horiz, &deltas)
}