//! Live Corners on any path: the corners of its uncut outline (a live rectangle's or polygon's, a //! live path's, or a plain path's own), each one's radius and kind, and the anchors of the path as //! drawn that belong to each. [`set_corners`] edits them. use std::borrow::Cow; use std::collections::BTreeSet; use vectorcraft_geom::corners::{Corner, cut_corners, path_corners}; use vectorcraft_geom::shapes::{self, CornerKind}; use vectorcraft_geom::{Affine, PathData, Rect}; use crate::node::{LiveShape, Node, NodeKind}; use crate::selection::AnchorRef; /// The Live Corners of a path. #[derive(Clone, Debug)] pub struct LiveCorners<'a> { /// The path with its corners uncut, in the corners' own space: a live rectangle's (its size /// and radii in document units, see [`LiveShape::folded`]), else the document's. pub base: Cow<'a, PathData>, /// Maps `base` into the document. pub xf: Affine, /// The corners that can be cut, in anchor order ([`path_corners`] of `base`). pub corners: Vec, radii: Vec, kinds: Vec, /// For each anchor of the path as drawn (per subpath), the anchor of `base` it comes from. sources: Vec>, } impl<'a> LiveCorners<'a> { /// The Live Corners of `path` and its live shape; `None` for shapes without corners (ellipses, /// lines). pub fn new(path: &'a PathData, live: Option<&'a LiveShape>) -> Option { let (base, xf, radii, kinds) = match live { None => (Cow::Borrowed(path), Affine::IDENTITY, vec![], vec![]), Some(l @ LiveShape::Rectangle { .. }) => { let LiveShape::Rectangle { w, h, radii, kinds, xf } = l.folded() else { return None }; (Cow::Owned(shapes::rectangle(Rect::new(0.0, 0.0, w, h))), xf, radii.to_vec(), kinds.to_vec()) } Some(l @ LiveShape::Polygon { radii, kinds, .. }) => (Cow::Owned(l.polygon_outline()), Affine::IDENTITY, radii.clone(), kinds.clone()), Some(LiveShape::Path { base, radii, kinds }) => (Cow::Borrowed(base), Affine::IDENTITY, radii.clone(), kinds.clone()), Some(LiveShape::Ellipse { .. } | LiveShape::Line { .. }) => return None, }; let sources = if radii.iter().any(|r| *r > 0.0) { cut_corners(&base, &radii, &kinds).1 } else { let mut first = 0; base.subpaths .iter() .map(|sp| { first += sp.anchors.len(); (first - sp.anchors.len()..first).collect() }) .collect() }; Some(Self { corners: path_corners(&base), base, xf, radii, kinds, sources }) } /// The Live Corners of a path object. pub fn of(node: &'a Node) -> Option { let NodeKind::Path { path, live, .. } = &node.kind else { return None }; Self::new(path, live.as_deref()) } /// The corner at anchor `index` of `base`. pub fn corner(&self, index: usize) -> Option<&Corner> { self.corners.binary_search_by_key(&index, |c| c.index).ok().and_then(|i| self.corners.get(i)) } /// The radius corner `index` is set to, in document units (drawn no larger than fits). pub fn radius(&self, index: usize) -> f64 { self.radii.get(index).copied().unwrap_or(0.0) } /// The kind corner `index` is cut with. pub fn kind(&self, index: usize) -> CornerKind { self.kinds.get(index).copied().unwrap_or_default() } /// Every corner. pub fn all(&self) -> BTreeSet { self.corners.iter().map(|c| c.index).collect() } /// The corners holding one of `anchors` (of the path as drawn). pub fn corners_of(&self, anchors: &BTreeSet) -> BTreeSet { anchors.iter().filter_map(|(si, ai)| self.sources.get(*si)?.get(*ai).copied()).filter(|k| self.corner(*k).is_some()).collect() } /// The corners Live Corners edit for a selection: those holding a selected anchor when the /// path is partly selected (Direct Selection), else every corner. pub fn picked(&self, partial: Option<&BTreeSet>) -> BTreeSet { let picked = partial.map(|a| self.corners_of(a)).unwrap_or_default(); if picked.is_empty() { self.all() } else { picked } } /// The anchors of the path as drawn that belong to `corners` (to keep them selected as the /// path changes). pub fn anchors_of(&self, corners: &BTreeSet) -> BTreeSet { let mut out = BTreeSet::new(); for (si, from) in self.sources.iter().enumerate() { out.extend(from.iter().enumerate().filter(|(_, k)| corners.contains(k)).map(|(ai, _)| (si, ai))); } out } /// For each anchor of the path as drawn (per subpath), the anchor of `base` it comes from. pub fn sources(&self) -> &[Vec] { &self.sources } /// The radius (document units) and kind `corners` share (each `None` when they differ, or /// for no corners). pub fn style(&self, corners: &BTreeSet) -> (Option, Option) { fn shared(mut it: impl Iterator, same: impl Fn(T, T) -> bool) -> Option { let first = it.next()?; it.all(|x| same(x, first)).then_some(first) } let radius = shared(corners.iter().map(|k| self.radius(*k)), |a, b| (a - b).abs() < 1e-9); (radius, shared(corners.iter().map(|k| self.kind(*k)), |a, b| a == b)) } } /// Set `radius` (document units; negative: 0) and/or `kind` on the `corners` (anchor indices of /// the uncut outline, see [`LiveCorners`]) of a path and its live shape, then cut the path again. /// A path that isn't a live shape keeps its uncut outline as a [`LiveShape::Path`], which is a /// plain path again once no corner is cut. Ellipses and lines have no corners: they stay as they /// are. pub fn set_corners(path: &mut PathData, live: &mut Option>, corners: &BTreeSet, radius: Option, kind: Option) { fn edit(radii: &mut [f64], kinds: &mut [CornerKind], corners: &BTreeSet, radius: Option, kind: Option) { for k in corners { if let (Some(r), Some(slot)) = (radius, radii.get_mut(*k)) { *slot = r.max(0.0); } if let (Some(kind), Some(slot)) = (kind, kinds.get_mut(*k)) { *slot = kind; } } } /// One radius and kind per corner of an outline of `n` anchors while editing; none saved /// when all are sharp or round. fn edit_vec( n: usize, radii: &mut Vec, kinds: &mut Vec, corners: &BTreeSet, radius: Option, kind: Option, ) { radii.resize(n, 0.0); kinds.resize(n, CornerKind::Round); edit(radii, kinds, corners, radius, kind); if radii.iter().all(|r| *r <= 0.0) { radii.clear(); } if kinds.iter().all(|k| *k == CornerKind::Round) { kinds.clear(); } } let l = live.get_or_insert_with(|| Box::new(LiveShape::Path { base: path.clone(), radii: vec![], kinds: vec![] })); // Radii are document lengths and corners circular, also on a rectangle from a file that kept // an uneven scale in its transform (#442). l.fold_scale(); match &mut **l { LiveShape::Rectangle { radii, kinds, .. } => edit(radii, kinds, corners, radius, kind), LiveShape::Polygon { sides, radii, kinds, .. } => edit_vec(*sides as usize, radii, kinds, corners, radius, kind), LiveShape::Path { base, radii, kinds } => edit_vec(base.anchor_count(), radii, kinds, corners, radius, kind), LiveShape::Ellipse { .. } | LiveShape::Line { .. } => return, } match live.as_deref_mut() { Some(LiveShape::Path { base, radii, kinds }) if radii.is_empty() && kinds.is_empty() => { *path = std::mem::take(base); *live = None; } Some(l) => *path = l.to_path(), None => {} } } #[cfg(test)] mod tests { use super::*; use vectorcraft_geom::{Point, SubPath}; fn set(v: &[usize]) -> BTreeSet { v.iter().copied().collect() } fn anchors(v: &[usize]) -> BTreeSet { v.iter().map(|ai| (0, *ai)).collect() } #[test] fn rectangle_corners_map_to_their_anchors() { let l = LiveShape::Rectangle { w: 100.0, h: 50.0, radii: [0.0, 10.0, 0.0, 0.0], kinds: Default::default(), xf: Affine::IDENTITY }; let path = l.to_path(); let c = LiveCorners::new(&path, Some(&l)).unwrap(); // Anchors: top-left, the top-right corner's two, bottom-right, bottom-left. assert_eq!(c.sources(), [vec![0, 1, 1, 2, 3]]); assert_eq!(path.anchor_count(), 5); assert_eq!(c.corners_of(&anchors(&[2, 4])), set(&[1, 3])); assert!(c.corners_of(&anchors(&[9])).is_empty(), "out of range"); assert_eq!(c.anchors_of(&set(&[1, 3])), anchors(&[1, 2, 4])); // Partly selected: the corners of the selected anchors; wholly (or no anchor): all four. assert_eq!(c.picked(Some(&anchors(&[0]))), set(&[0])); assert_eq!(c.picked(None), set(&[0, 1, 2, 3])); assert_eq!(c.picked(Some(&anchors(&[]))), set(&[0, 1, 2, 3])); assert_eq!(c.style(&set(&[1])), (Some(10.0), Some(CornerKind::Round))); assert_eq!(c.style(&set(&[0, 1])), (None, Some(CornerKind::Round))); assert_eq!(c.style(&set(&[0, 2, 3])), (Some(0.0), Some(CornerKind::Round))); let e = LiveShape::Ellipse { w: 1.0, h: 1.0, pie: (0.0, 360.0), xf: Affine::IDENTITY }; assert!(LiveCorners::new(&e.to_path(), Some(&e)).is_none()); } /// #511: a plain path (a star) rounds its corners and keeps them editable; with none cut it /// is plain again. #[test] fn a_plain_path_keeps_its_cut_corners_live() { let star = shapes::star(Point::new(100.0, 100.0), 50.0, 25.0, 5, 0.0); let (mut path, mut live) = (star.clone(), None); set_corners(&mut path, &mut live, &set(&[0, 2]), Some(5.0), None); let Some(LiveShape::Path { base, radii, kinds }) = live.as_deref() else { panic!("live corners: {live:?}") }; assert_eq!((base, radii.len(), kinds.len()), (&star, 10, 0)); assert_eq!(path.anchor_count(), 12, "two corners cut"); let c = LiveCorners::new(&path, live.as_deref()).unwrap(); assert_eq!(c.style(&set(&[0, 2])), (Some(5.0), Some(CornerKind::Round))); assert_eq!(c.corners_of(&anchors(&[0, 11])), set(&[0]), "the first corner's cut ends the path"); // Kinds alone keep it live; squaring every corner makes it the plain star again. set_corners(&mut path, &mut live, &set(&[0, 2]), Some(0.0), Some(CornerKind::Chamfer)); assert!(matches!(live.as_deref(), Some(LiveShape::Path { radii, .. }) if radii.is_empty())); set_corners(&mut path, &mut live, &set(&[0, 2]), None, Some(CornerKind::Round)); assert_eq!((path, live), (star, None)); } /// A polygon's corners are cut in the document and survive a change of sides when shared. #[test] fn polygon_corners_follow_their_sides() { let mut l = LiveShape::Polygon { radius: 50.0, sides: 6, xf: Affine::translate((100.0, 100.0)), radii: vec![], kinds: vec![] }; let (mut path, mut live) = (l.to_path(), Some(Box::new(l.clone()))); set_corners(&mut path, &mut live, &set(&[0, 1, 2, 3, 4, 5]), Some(8.0), None); assert_eq!(path.anchor_count(), 12); l = *live.clone().unwrap(); l.set_sides(8); assert!(matches!(&l, LiveShape::Polygon { radii, .. } if *radii == vec![8.0; 8])); assert_eq!(l.to_path().anchor_count(), 16); // Mixed radii: the new corners are sharp. set_corners(&mut path, &mut live, &set(&[1]), Some(3.0), None); let mut l = *live.unwrap(); l.set_sides(7); assert!(matches!(&l, LiveShape::Polygon { radii, .. } if *radii == [8.0, 3.0, 8.0, 8.0, 8.0, 8.0, 0.0])); // An uneven scale keeps the cuts circular: the radius scales by the mean scale. let mut p = l.clone(); assert!(p.transform(Affine::scale_non_uniform(4.0, 1.0))); assert!(matches!(&p, LiveShape::Polygon { radii, .. } if (radii[0] - 16.0).abs() < 1e-9)); assert!(!p.transform(Affine::translate((5.0, 0.0))), "a move needs no new cuts"); } #[test] fn open_paths_and_curves_keep_their_ends_and_smooth_anchors() { let pen = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(100.0, 0.0), Point::new(100.0, 80.0)], false)); let c = LiveCorners::new(&pen, None).unwrap(); assert_eq!(c.all(), set(&[1])); assert_eq!(c.picked(Some(&anchors(&[0]))), set(&[1]), "an end is no corner: every corner"); let (mut path, mut live) = (pen.clone(), None); set_corners(&mut path, &mut live, &c.all(), Some(10.0), None); assert_eq!(path.anchor_count(), 4); assert_eq!(path.subpaths[0].anchors[0].p, Point::new(0.0, 0.0)); assert_eq!(path.subpaths[0].anchors[3].p, Point::new(100.0, 80.0)); } }