//! Live Corners: the widgets inside the corners of the selected paths. The Selection tool shows //! them on live rectangles and polygons, Direct Selection on any path (a star, a pen path) at each //! corner anchor: one that isn't smooth, where two sides, straight or curved, meet at an angle. Dragging one rounds (or //! sharpens) the corners whose widgets show together, on every selected path (#938): every corner //! of a path selected whole, those with a selected anchor when Direct Selection picked some. Alt-clicking a widget cycles their corner //! kind (round, inverted round, chamfer); double-clicking one opens the Corners dialog. The //! Selection and Direct Selection tools share this, and the canvas draws the widgets from the same //! geometry. use serde_json::{Map, Value, json}; use vectorcraft_doc::{Document, LiveCorners, LiveShape, NodeId, NodeKind, Selection}; use vectorcraft_geom::corners::{Corner, cut_corners}; use vectorcraft_geom::shapes::CornerKind; use vectorcraft_geom::{Affine, BezPath, PathData, Point, Vec2}; use crate::{Action, Overlay, PointerEvent, ToolContext}; /// The dialog a double-click on a corner widget opens: `{id, corners}`. pub const DIALOG: &str = "corners"; /// Widgets sit at least this far inside their corner along each side of a right angle (screen px; /// √2 times it along the bisector, at any angle), further in once the radius is. const MIN_INSET_PX: f64 = 10.0; /// Corners with a side shorter than this on screen (px) hide their widgets. const MIN_SIDE_PX: f64 = 3.0 * MIN_INSET_PX; /// One corner's widget. #[derive(Clone, Copy, Debug, PartialEq)] struct Widget { /// The corner, in the corners' own space. corner: Corner, /// The radius it is set to (document units), the one it is drawn with (no larger than its /// neighbours leave room for) and its kind. radius: f64, drawn: f64, kind: CornerKind, /// The angle between its sides in the document, in degrees. angle: f64, /// The widget's centre in the document. at: Point, } /// The corner widgets of one selected path. #[derive(Clone, Debug, PartialEq)] struct PathWidgets { id: NodeId, /// Maps the corners' space into the document (a live rectangle's own; else none). xf: Affine, /// The widgets that show (and which a drag edits), in anchor order. widgets: Vec, /// Every corner of the path shows its widget. all: bool, } impl PathWidgets { /// The widgets of path `id` at `zoom` (screen px per document point), if it has any: any path /// with Direct Selection (`any_path`), live rectangles and polygons with Selection. `id` is /// editable ([`Document::is_editable`]). fn of(doc: &Document, selection: &Selection, id: NodeId, zoom: f64, any_path: bool) -> Option { if selection.segments.contains_key(&id) { return None; } let NodeKind::Path { path, live, .. } = &doc.node(id)?.kind else { return None }; if !any_path && !matches!(live.as_deref(), Some(LiveShape::Rectangle { .. } | LiveShape::Polygon { .. })) { return None; } let lc = LiveCorners::new(path, live.as_deref())?; let det = lc.xf.determinant().abs(); if det < 1e-12 { return None; } // Screen pixels per unit of the corners' space (a rectangle's may be skewed). let scale = zoom * det.sqrt(); let picked = lc.picked(selection.partial(id)); let widgets: Vec<_> = lc .corners .iter() .filter(|c| picked.contains(&c.index) && c.lu.min(c.lv) * scale >= MIN_SIDE_PX) .map(|c| { // On the bisector at the centre of the arc as drawn, at least the minimum inset // in and at most half the shorter side. let (radius, drawn) = (lc.radius(c.index), lc.drawn_radius(c.index)); let k = (drawn / c.sin()).max(MIN_INSET_PX / (scale * (1.0 + c.cos()).sqrt())).min(c.lu.min(c.lv) / 2.0); Widget { corner: *c, radius, drawn, kind: lc.kind(c.index), angle: angle_in(lc.xf, c), at: lc.xf * c.on_bisector(k) } }) .collect(); let all = widgets.len() == lc.corners.len(); (!widgets.is_empty()).then_some(Self { id, xf: lc.xf, widgets, all }) } /// The corners whose widgets show: anchor indices of the path's uncut outline. fn corners(&self) -> Vec { self.widgets.iter().map(|w| w.corner.index).collect() } } /// The corner widgets of the selected paths that have some. #[derive(Clone, Debug, PartialEq)] pub struct CornerWidgets { paths: Vec, } impl CornerWidgets { /// The widgets at `zoom` (screen px per document point), if the selection has them: on any /// path with Direct Selection (`any_path`), on live rectangles and polygons with Selection. pub fn of(doc: &Document, selection: &Selection, zoom: f64, any_path: bool) -> Option { let editable = doc.editable(); let paths: Vec<_> = selection.objects.iter().filter(|id| editable(**id)).filter_map(|id| PathWidgets::of(doc, selection, *id, zoom, any_path)).collect(); (!paths.is_empty()).then_some(Self { paths }) } /// Selection & Anchor Display → Hide Corner Widget for angles greater than: the widgets of /// corners wider than `max` degrees (in the document) hide; none if all do. pub fn within_angle(mut self, max: f64) -> Option { for p in &mut self.paths { let n = p.widgets.len(); p.widgets.retain(|w| w.angle <= max + 1e-9); p.all &= p.widgets.len() == n; } self.paths.retain(|p| !p.widgets.is_empty()); (!self.paths.is_empty()).then_some(self) } /// The widgets on show: [`Self::of`] with View → Show Corner Widget `on`, less those wider /// than `max_angle` ([`Self::within_angle`]). pub fn showing(doc: &Document, selection: &Selection, zoom: f64, any_path: bool, on: bool, max_angle: f64) -> Option { if !on { return None; } Self::of(doc, selection, zoom, any_path)?.within_angle(max_angle) } /// The widgets the active tool can drag (Direct Selection: `any_path`). pub fn for_tool(cx: &ToolContext, any_path: bool) -> Option { Self::showing(cx.doc, cx.selection, cx.zoom, any_path, cx.corner_widgets, cx.corner_widget_max_angle) } /// The centres of the widgets that show. pub fn visible(&self) -> impl Iterator + '_ { self.visible_on().map(|(_, at)| at) } /// The centres of the widgets that show, each with the object it belongs to. pub fn visible_on(&self) -> impl Iterator + '_ { self.paths.iter().flat_map(|p| p.widgets.iter().map(move |w| (p.id, w.at))) } /// The corners whose widgets show, path after path: anchor indices of each path's uncut /// outline. pub fn corners(&self) -> Vec { self.paths.iter().flat_map(PathWidgets::corners).collect() } /// The widget nearest to `p` within `tol` (document units): its path's and its own index. fn hit(&self, p: Point, tol: f64) -> Option<(usize, usize)> { let d = |&(i, k): &(usize, usize)| self.widget(i, k).map_or(f64::INFINITY, |w| w.at.distance(p)); self.paths .iter() .enumerate() .flat_map(|(i, path)| (0..path.widgets.len()).map(move |k| (i, k))) .filter(|ik| d(ik) <= tol) .min_by(|a, b| d(a).total_cmp(&d(b))) } fn widget(&self, path: usize, k: usize) -> Option<&Widget> { self.paths.get(path).and_then(|p| p.widgets.get(k)) } /// `object.setLiveShape` params setting `key` on the shown corners (every corner of a path /// unless some are hidden, which `corners` then leaves out): one path by its `id`, several as /// `items`. fn command(&self, key: &str, value: Value) -> Value { let corners = |p: &PathWidgets| (!p.all).then(|| p.corners()); let mut out = Map::new(); match self.paths.as_slice() { [p] => { out.insert("id".into(), json!(p.id.0)); if let Some(c) = corners(p) { out.insert("corners".into(), json!(c)); } } paths => { let items: Vec = paths .iter() .map(|p| match corners(p) { Some(c) => json!({ "id": p.id.0, "corners": c }), None => json!({ "id": p.id.0 }), }) .collect(); out.insert("items".into(), Value::Array(items)); } } out.insert(key.into(), value); Value::Object(out) } } /// The angle between corner `c`'s sides once `xf` maps them into the document, in degrees. fn angle_in(xf: Affine, c: &Corner) -> f64 { let [a, b, cc, d, _, _] = xf.as_coeffs(); let map = |v: Vec2| Vec2::new(a * v.x + cc * v.y, b * v.x + d * v.y); let (u, v) = (map(c.u), map(c.v)); u.cross(v).abs().atan2(u.dot(v)).to_degrees() } /// Is `p` over a corner widget the active tool would drag (Direct Selection: `any_path`)? pub fn over_widget(cx: &ToolContext, p: Point, any_path: bool) -> bool { CornerWidgets::for_tool(cx, any_path).and_then(|w| w.hit(p, cx.tol(5.0))).is_some() } /// A double-click on a corner widget opens the Corners dialog for the shown corners of its path. pub fn double_click(cx: &ToolContext, p: Point, any_path: bool) -> Option { let w = CornerWidgets::for_tool(cx, any_path)?; let (i, _) = w.hit(p, cx.tol(5.0))?; let path = w.paths.get(i)?; Some(Action::Dialog(DIALOG.into(), json!({ "id": path.id.0, "corners": path.corners() }))) } /// A path's uncut outline with its corners' radii and kinds, and the corners' space (to outline /// the corners at their largest). #[derive(Clone, Debug)] struct Outline { base: PathData, /// The corners of `base`. corners: Vec, radii: Vec, kinds: Vec, xf: Affine, } /// Dragging a corner widget: the radius follows the pointer along the corner's bisector. An /// Alt-click (no drag) cycles the corner kind instead. #[derive(Clone, Debug)] pub struct CornerDrag { widgets: CornerWidgets, /// The dragged widget: its path's and its own index among the shown ones. widget: (usize, usize), /// Each shown path's outline, in the order of [`CornerWidgets`]' paths. outlines: Vec, start: Point, began: bool, alt: bool, radius: f64, /// The pointer went past fully round: the dragged corner is at its largest radius. at_limit: bool, at: Point, /// Direct Selection: what a click (no drag) on the pressed widget selects. click: Option, } /// Direct Selection: the drawn anchors of a widget's corner, which a click on the widget selects /// (Shift: toggles), as a click on the corner's anchor does. #[derive(Clone, Debug)] struct CornerClick { id: NodeId, anchors: Vec<(usize, usize)>, shift: bool, } impl CornerDrag { /// The drag has left the click: the corner radius is changing. pub fn adjusting(&self) -> bool { self.began } /// Start a drag when the press `ev` is on a widget of the selection (Direct Selection: /// `any_path`). pub fn hit(cx: &ToolContext, ev: &PointerEvent, any_path: bool) -> Option { let (widgets, p) = (CornerWidgets::for_tool(cx, any_path)?, ev.pos); let widget = widgets.hit(p, cx.tol(5.0))?; let radius = widgets.widget(widget.0, widget.1)?.radius; let click = any_path .then(|| { let path = widgets.paths.get(widget.0)?; let corner = path.widgets.get(widget.1)?.corner.index; let lc = cx.doc.node(path.id).and_then(LiveCorners::of)?; Some(CornerClick { id: path.id, anchors: lc.anchors_of(&[corner].into()).into_iter().collect(), shift: ev.mods.shift }) }) .flatten(); let outlines = widgets .paths .iter() .map(|w| { let lc = cx.doc.node(w.id).and_then(LiveCorners::of)?; let n = lc.base.anchor_count(); let (radii, kinds) = ((0..n).map(|k| lc.radius(k)).collect(), (0..n).map(|k| lc.kind(k)).collect()); Some(Outline { corners: lc.corners.clone(), base: lc.base.into_owned(), radii, kinds, xf: w.xf }) }) .collect::>>()?; Some(Self { widgets, widget, outlines, start: p, began: false, alt: ev.mods.alt, radius, at_limit: false, at: p, click }) } /// The start radius (as drawn: no larger than fits) plus the pointer's travel along the /// corner's bisector (in the corners' own units), from square to fully round; and whether the /// travel reached fully round. fn radius_at(&self, p: Point) -> (f64, bool) { let (path, k) = self.widget; let (Some(w), Some(xf)) = (self.widgets.widget(path, k), self.widgets.paths.get(path).map(|p| p.xf)) else { return (self.radius, self.at_limit); }; let inv = xf.inverse(); let max = self.max_radius(path, &w.corner); let r = w.drawn + w.corner.radius_change(inv * p - inv * self.start); (r.min(max).max(0.0), max > 0.0 && r >= max) } /// The largest radius `corner` of shown path `path` draws while the drag sets every shown /// corner of that path to one radius: up to the cuts of its neighbours, as far as its own /// sides when they are square (#2332). fn max_radius(&self, path: usize, corner: &Corner) -> f64 { let (Some(shown), Some(o)) = (self.widgets.paths.get(path), self.outlines.get(path)) else { return corner.max_radius() }; corner.max_shared_radius(&o.corners, &o.radii, |k| shown.widgets.iter().any(|w| w.corner.index == k)) } /// The cuts of the edited corners that are at their largest radius, in document coordinates. fn corners_at_limit(&self) -> BezPath { let mut out = BezPath::new(); for (n, (shown, o)) in self.widgets.paths.iter().zip(&self.outlines).enumerate() { let shown = &shown.widgets; let mut radii = o.radii.clone(); for w in shown { if let Some(r) = radii.get_mut(w.corner.index) { *r = self.radius; } } let full = |k: &usize| shown.iter().any(|w| w.corner.index == *k && self.radius >= self.max_radius(n, &w.corner) - 1e-9); let (path, sources) = cut_corners(&o.base, &radii, &o.kinds); for (sp, from) in path.transformed(o.xf).subpaths.iter().zip(&sources) { // A corner's cut is the segment between its two anchors (the last one, for a // closed subpath's first corner). for i in 0..sp.segment_count() { let (a, b) = (from.get(i), from.get(i + 1).or(from.first())); if a == b && a.is_some_and(full) { let c = sp.segment(i); out.move_to(c.p0); out.curve_to(c.p1, c.p2, c.p3); } } } } out } pub fn drag(&mut self, cx: &ToolContext, p: Point) -> Vec { let mut out = vec![]; if !self.began { if p.distance(self.start) < cx.tol(3.0) { return out; } self.began = true; out.push(Action::Begin("Corner Radius".into())); } (self.radius, self.at_limit) = self.radius_at(p); self.at = p; out.push(Action::Preview("object.setLiveShape".into(), self.widgets.command("radius", json!(self.radius)))); out } pub fn finish(self) -> Vec { if self.began { vec![Action::Commit] } else if let Some(w) = self.widgets.widget(self.widget.0, self.widget.1).filter(|_| self.alt) { vec![Action::Exec("object.setLiveShape".into(), self.widgets.command("kind", json!(w.kind.next())))] } else if let Some(c) = self.click { // Direct Selection: the click selects the widget's corner, so the next drag on this // widget rounds that corner alone (its widget is then the only one showing). let mode = if c.shift { "toggle" } else { "set" }; vec![Action::Exec("select.anchors".into(), json!({"id": c.id.0, "anchors": c.anchors, "mode": mode}))] } else { vec![] } } /// The radius readout next to the pointer; at the largest radius, the corners there in red. pub fn overlays(&self, cx: &ToolContext) -> Vec { if !self.began { return vec![]; } let mut out = vec![]; if cx.measurement_labels { out.push(Overlay::Measure { p: self.at, text: format!("Radius: {}", cx.len(self.radius)) }); } if self.at_limit { out.push(Overlay::Path { path: self.corners_at_limit(), color: crate::builder::HIGHLIGHT_RED, width: 2.0, dashed: false }); } out } } #[cfg(test)] mod tests;