//! Alignment, distribution and stacking order (depth). use super::{always, num, opt_str, CmdResult, Command}; use crate::{Engine, EngineError}; use rustfig_geom::Rect; use rustfig_model::{ObjectId, MAX_DEPTH}; use serde_json::{json, Value}; use std::sync::Arc; pub(super) fn commands() -> Vec { vec![ Command { id: "arrange.align", label: "Align", menu: "", shortcut: "", params: "{\"ids\"?: [id], \"h\"?: none|left|center|right, \"v\"?: none|top|center|bottom} — defaults from the indicator", mutates: true, enabled: always, run: align, }, Command { id: "arrange.align_canvas", label: "Align to Canvas", menu: "", shortcut: "", params: "{\"ids\"?: [id] (all objects), \"h\"?: none|left|center|right, \"v\"?: none|top|center|bottom} — each object is aligned to the page (xfig's Align mode, middle button; init_align_canvas), by the indicator's align modes", mutates: true, enabled: |_| true, run: align_canvas, }, Command { id: "arrange.distribute", label: "Distribute", menu: "", shortcut: "", params: "{\"ids\"?: [id], \"axis\": horizontal|vertical} — equal gaps between objects", mutates: true, enabled: always, run: distribute, }, Command { id: "arrange.to_front", label: "Bring to Front", menu: "", shortcut: "", params: "{\"ids\"?: [id]}", mutates: true, enabled: always, run: |e, p| restack(e, p, Stack::Front), }, Command { id: "arrange.forward", label: "Bring Forward", menu: "", shortcut: "", params: "{\"ids\"?: [id]} — depth - 1", mutates: true, enabled: always, run: |e, p| restack(e, p, Stack::Forward), }, Command { id: "arrange.backward", label: "Send Backward", menu: "", shortcut: "", params: "{\"ids\"?: [id]} — depth + 1", mutates: true, enabled: always, run: |e, p| restack(e, p, Stack::Backward), }, Command { id: "arrange.to_back", label: "Send to Back", menu: "", shortcut: "", params: "{\"ids\"?: [id]}", mutates: true, enabled: always, run: |e, p| restack(e, p, Stack::Back), }, Command { id: "arrange.set_depth", label: "Set depth", menu: "", shortcut: "", params: "{\"ids\"?: [id], \"depth\": 0-999}", mutates: true, enabled: has_selection_or_ids, run: set_depth, }, ] } fn has_selection_or_ids(_: &Engine) -> bool { true } fn targets(e: &Engine, p: &Value) -> Result, EngineError> { let ids = e.target_ids(p)?; if ids.is_empty() { return Err(EngineError::bad_params("no objects selected")); } Ok(ids .iter() .filter_map(|i| e.doc().get(*i).map(|o| (*i, o.geometry_bbox()))) .filter(|(_, r)| !r.is_empty()) .collect()) } fn translate(e: &mut Engine, moves: Vec<(ObjectId, f64, f64)>) { // Smart links: the ends attached to each object follow it. crate::links::translate_each(e, &moves); } fn align(e: &mut Engine, p: &Value) -> CmdResult { let h = opt_str(p, "h")? .map(str::to_string) .unwrap_or_else(|| e.indicator.align_h.clone()); let v = opt_str(p, "v")? .map(str::to_string) .unwrap_or_else(|| e.indicator.align_v.clone()); let items = targets(e, p)?; let all = items.iter().fold(Rect::EMPTY, |r, (_, b)| r.union(b)); let mut moves = Vec::new(); for (id, b) in &items { let dx = match h.as_str() { "left" => all.min.x - b.min.x, "center" => all.center().x - b.center().x, "right" => all.max.x - b.max.x, "none" | "" | "distribute_centres" | "distribute_edges" | "abut" => 0.0, other => { return Err(EngineError::bad_params(format!( "unknown horizontal alignment {other:?}" ))) } }; let dy = match v.as_str() { "top" => all.min.y - b.min.y, "center" => all.center().y - b.center().y, "bottom" => all.max.y - b.max.y, "none" | "" | "distribute_centres" | "distribute_edges" | "abut" => 0.0, other => return Err(EngineError::bad_params(format!("unknown vertical alignment {other:?}"))), }; moves.push((*id, dx, dy)); } // xfig's distribute_horizontally / _vertically for the last three choices. for (horizontal, mode) in [(true, h.as_str()), (false, v.as_str())] { let Some(mode) = Spread::from_name(mode) else { continue }; for (m, d) in moves.iter_mut().zip(spread(&items, horizontal, mode)) { if horizontal { m.1 = d; } else { m.2 = d; } } } translate(e, moves); Ok(json!({"count": items.len()})) } /// How objects are spread along one axis (xfig's last three align choices). #[derive(Clone, Copy)] enum Spread { /// Equal distance between the objects' centres. Centres, /// Equal gaps between the objects' edges. Edges, /// Edge to edge, from the first object on. Abut, } impl Spread { fn from_name(s: &str) -> Option { match s { "distribute_centres" => Some(Spread::Centres), "distribute_edges" => Some(Spread::Edges), "abut" => Some(Spread::Abut), _ => None, } } } /// The shift of each of `items` (in their order) along one axis. fn spread(items: &[(ObjectId, Rect)], horizontal: bool, mode: Spread) -> Vec { let lo = |r: &Rect| if horizontal { r.min.x } else { r.min.y }; let size = |r: &Rect| if horizontal { r.width() } else { r.height() }; let mid = |r: &Rect| lo(r) + size(r) / 2.0; let key = |i: usize| match (items.get(i), mode) { (Some((_, r)), Spread::Centres) => mid(r), (Some((_, r)), _) => lo(r), _ => 0.0, }; let mut order: Vec = (0..items.len()).collect(); order.sort_by(|a, b| key(*a).total_cmp(&key(*b))); let mut shifts = vec![0.0; items.len()]; let (Some(&first), Some(&last)) = (order.first(), order.last()) else { return shifts; }; let (Some((_, rf)), Some((_, rl))) = (items.get(first), items.get(last)) else { return shifts; }; let n = order.len(); let total: f64 = items.iter().map(|(_, r)| size(r)).sum(); let gap = match mode { Spread::Edges if n > 1 => (lo(rl) + size(rl) - lo(rf) - total) / (n - 1) as f64, _ => 0.0, }; let step = if n > 1 { (mid(rl) - mid(rf)) / (n - 1) as f64 } else { 0.0 }; let mut pos = lo(rf); for (k, i) in order.iter().enumerate() { let Some((_, r)) = items.get(*i) else { continue }; let d = match mode { Spread::Centres => mid(rf) + step * k as f64 - mid(r), _ => pos - lo(r), }; pos += size(r) + gap; if let Some(s) = shifts.get_mut(*i) { *s = if d.is_finite() { d } else { 0.0 }; } } shifts } /// Align every object (not each other) to the page: xfig's /// `init_align_canvas` in `e_align.c`. The page is the paper size, swapped in /// landscape; distribute and abut have no meaning against the canvas. fn align_canvas(e: &mut Engine, p: &Value) -> CmdResult { let h = opt_str(p, "h")?.map_or_else(|| e.indicator.align_h.clone(), str::to_string); let v = opt_str(p, "v")?.map_or_else(|| e.indicator.align_v.clone(), str::to_string); if !matches!( h.as_str(), "none" | "" | "left" | "center" | "right" | "distribute_centres" | "distribute_edges" | "abut" ) { return Err(EngineError::bad_params(format!("unknown horizontal alignment {h:?}"))); } if !matches!( v.as_str(), "none" | "" | "top" | "center" | "bottom" | "distribute_centres" | "distribute_edges" | "abut" ) { return Err(EngineError::bad_params(format!("unknown vertical alignment {v:?}"))); } let ids: Vec = match p.get("ids") { Some(_) => e.target_ids(p)?, None => e.doc().objects.iter().map(|o| o.id).collect(), }; let (pw, ph) = e.doc().page_size(); let page = Rect::from_points([rustfig_geom::Point::ZERO, rustfig_geom::Point::new(pw, ph)]); let mut moves = Vec::new(); for id in ids { let Some(o) = e.doc().get(id) else { continue }; let b = o.geometry_bbox(); if b.is_empty() { continue; } let dx = match h.as_str() { "left" => page.min.x - b.min.x, "center" => page.center().x - b.center().x, "right" => page.max.x - b.max.x, _ => 0.0, }; let dy = match v.as_str() { "top" => page.min.y - b.min.y, "center" => page.center().y - b.center().y, "bottom" => page.max.y - b.max.y, _ => 0.0, }; if dx.is_finite() && dy.is_finite() && (dx != 0.0 || dy != 0.0) { moves.push((id, dx, dy)); } } let n = moves.len(); translate(e, moves); Ok(json!({"count": n})) } fn distribute(e: &mut Engine, p: &Value) -> CmdResult { let horizontal = match opt_str(p, "axis")?.unwrap_or("horizontal") { "horizontal" | "h" | "x" => true, "vertical" | "v" | "y" => false, other => return Err(EngineError::bad_params(format!("unknown axis {other:?}"))), }; let mut items = targets(e, p)?; if items.len() < 3 { return Ok(json!({"count": items.len()})); } let lo = |r: &Rect| if horizontal { r.min.x } else { r.min.y }; let size = |r: &Rect| if horizontal { r.width() } else { r.height() }; items.sort_by(|a, b| lo(&a.1).total_cmp(&lo(&b.1))); let (Some(first), Some(last)) = (items.first(), items.last()) else { return Ok(json!({"count": 0})); }; let start = lo(&first.1); let end = lo(&last.1) + size(&last.1); let total: f64 = items.iter().map(|(_, r)| size(r)).sum(); let gap = (end - start - total) / (items.len() - 1) as f64; let mut pos = start; let mut moves = Vec::new(); for (id, r) in &items { let d = pos - lo(r); moves.push(if horizontal { (*id, d, 0.0) } else { (*id, 0.0, d) }); pos += size(r) + gap; } let n = moves.len(); translate(e, moves); Ok(json!({"count": n})) } enum Stack { Front, Forward, Backward, Back, } fn restack(e: &mut Engine, p: &Value, how: Stack) -> CmdResult { let ids = e.target_ids(p)?; let others: Vec = e .doc() .objects .iter() .filter(|o| !ids.contains(&o.id)) .map(|o| o.depth()) .collect(); let min = others.iter().copied().min().unwrap_or(50); let max = others.iter().copied().max().unwrap_or(50); for o in e.doc_mut().objects.iter_mut() { if !ids.contains(&o.id) { continue; } let d = o.depth(); let nd = match how { Stack::Front => (min - 1).min(d), Stack::Forward => d - 1, Stack::Backward => d + 1, Stack::Back => (max + 1).max(d), }; Arc::make_mut(o).set_depth(nd.clamp(0, MAX_DEPTH)); } Ok(json!({"count": ids.len()})) } fn set_depth(e: &mut Engine, p: &Value) -> CmdResult { let ids = e.target_ids(p)?; let d = (num(p, "depth")?.round() as i32).clamp(0, MAX_DEPTH); for o in e.doc_mut().objects.iter_mut() { if ids.contains(&o.id) { Arc::make_mut(o).set_depth(d); } } Ok(json!({"count": ids.len()})) } .