mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Merge pull request #4361 from dotCooCoo/fix/curvature-delete-reshapes
Curvature: deleting a point re-curves the path through the remaining points
This commit is contained in:
@@ -109,13 +109,14 @@ pub(super) fn remove_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?;
|
||||
let si = usize_opt(p, "subpath", C)?.unwrap_or(0);
|
||||
let ai = usize_req(p, "anchor", C)?;
|
||||
let curvature = switch_or(p, "curvature", false, C)?;
|
||||
let removed = s.edit("Delete Anchor Point", |d, sel| {
|
||||
let sp = path_mut(d, id)?.subpaths.get(si).ok_or_else(|| EngineError::Other("no such subpath".into()))?;
|
||||
if ai >= sp.anchors.len() {
|
||||
return Err(EngineError::Other("no such anchor".into()));
|
||||
}
|
||||
let mut emptied = Beside::replacing(d, &[id]);
|
||||
let removed = remove_anchors_of(d, sel, &mut emptied, id, [(si, ai)])?;
|
||||
let removed = remove_anchors_of(d, sel, &mut emptied, id, [(si, ai)], curvature)?;
|
||||
emptied.finish(d)?;
|
||||
Ok(removed)
|
||||
})?;
|
||||
@@ -141,7 +142,7 @@ pub(super) fn remove_anchors(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let shape: Vec<(usize, bool)> =
|
||||
d.node(id).and_then(Node::path_data).map(|p| p.subpaths.iter().map(|sp| (sp.anchors.len(), sp.closed)).collect()).unwrap_or_default();
|
||||
let next = following_anchors(&shape, &anchors);
|
||||
if remove_anchors_of(d, sel, &mut emptied, id, anchors)? {
|
||||
if remove_anchors_of(d, sel, &mut emptied, id, anchors, false)? {
|
||||
removed += 1;
|
||||
} else if !next.is_empty() {
|
||||
// The next anchor along is selected, so the next Remove Anchor Points goes on from
|
||||
@@ -180,14 +181,16 @@ fn following_anchors(shape: &[(usize, bool)], removed: &[(usize, usize)]) -> BTr
|
||||
}
|
||||
|
||||
/// Remove `anchors` (subpath, anchor) of path `id` without opening it, refitting the curve round
|
||||
/// each one. A subpath left with one anchor is removed, and a path left with no subpath is removed
|
||||
/// through `emptied`. True when the path went.
|
||||
/// each one, or with `curvature` re-curving it as the Curvature tool curves its points
|
||||
/// ([`vectorcraft_tools::draw2::curvature_remove`]). A subpath left with one anchor is removed,
|
||||
/// and a path left with no subpath is removed through `emptied`. True when the path went.
|
||||
fn remove_anchors_of(
|
||||
d: &mut Document,
|
||||
sel: &mut Selection,
|
||||
emptied: &mut Beside,
|
||||
id: NodeId,
|
||||
anchors: impl IntoIterator<Item = (usize, usize)>,
|
||||
curvature: bool,
|
||||
) -> Result<bool> {
|
||||
let mut anchors: Vec<(usize, usize)> = anchors.into_iter().collect();
|
||||
// The last first, so the indexes still to come stay valid.
|
||||
@@ -196,7 +199,11 @@ fn remove_anchors_of(
|
||||
let path = path_mut(d, id)?;
|
||||
for (si, ai) in anchors {
|
||||
if let Some(sp) = path.subpaths.get_mut(si) {
|
||||
po::remove_anchor(sp, ai);
|
||||
if curvature {
|
||||
vectorcraft_tools::draw2::curvature_remove(sp, ai);
|
||||
} else {
|
||||
po::remove_anchor(sp, ai);
|
||||
}
|
||||
}
|
||||
}
|
||||
path.subpaths.retain(|sp| sp.anchors.len() >= 2);
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Delete Anchor Point",
|
||||
[],
|
||||
None,
|
||||
"{id, subpath?, anchor} remove one anchor, re-fitting the curve",
|
||||
"{id, subpath?, anchor, curvature?} remove one anchor, re-fitting the curve (with curvature: true, the curve where it was follows the remaining anchors, as the Curvature tool curves its points)",
|
||||
has_doc,
|
||||
remove_anchor
|
||||
),
|
||||
|
||||
@@ -597,6 +597,40 @@ fn curvature_click_first_point_closes() {
|
||||
assert_eq!(p.anchor_count(), 3);
|
||||
}
|
||||
|
||||
/// Delete on a point of a finished Curvature path re-curves the rest through the remaining points,
|
||||
/// as if they had been drawn without it, in one undo step.
|
||||
#[test]
|
||||
fn curvature_delete_curves_through_the_remaining_points() {
|
||||
let mut s = session();
|
||||
let v = view();
|
||||
s.select_tool("curvature", v).unwrap();
|
||||
let pts = [(100.0, 300.0), (200.0, 200.0), (300.0, 320.0), (400.0, 220.0), (500.0, 300.0)];
|
||||
for (x, y) in pts {
|
||||
gesture(&mut s, &[(x, y)], Mods::default());
|
||||
}
|
||||
s.tool_key(ToolKey::Escape, Mods::default(), v).unwrap();
|
||||
let id = paths(&s)[0].0;
|
||||
let before = path(&s, id).subpaths[0].clone();
|
||||
let remaining = |gone: usize| -> Vec<(Point, bool)> {
|
||||
pts.iter().enumerate().filter(|&(i, _)| i != gone).map(|(_, &(x, y))| (Point::new(x, y), false)).collect()
|
||||
};
|
||||
for (gone, &(x, y)) in pts.iter().enumerate() {
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
let steps = undo_steps(&s);
|
||||
gesture(&mut s, &[(x, y)], Mods::default());
|
||||
s.tool_key(ToolKey::Delete, Mods::default(), v).unwrap();
|
||||
assert_eq!(undo_steps(&s), steps + 1, "one step");
|
||||
let want = vectorcraft_tools::draw2::curvature_spline(&remaining(gone), false);
|
||||
let got = path(&s, id).subpaths[0].clone();
|
||||
assert_eq!(got.anchors.len(), want.anchors.len());
|
||||
for (a, b) in got.anchors.iter().zip(&want.anchors) {
|
||||
assert!(a.p.distance(b.p) < 1e-9 && a.h_in.distance(b.h_in) < 1e-6 && a.h_out.distance(b.h_out) < 1e-6, "point {gone}: {a:?} != {b:?}");
|
||||
}
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(path(&s, id).subpaths[0], before);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Curvature tool edits a path another tool drew (#798), one undo step per edit, keeping its
|
||||
/// shape except where edited.
|
||||
#[test]
|
||||
|
||||
@@ -17,7 +17,9 @@ use vectorcraft_pathops::{Cap, Join};
|
||||
use crate::{Action, Tool, ToolContext};
|
||||
|
||||
pub use anchor::AnchorTool;
|
||||
pub use curvature::{CurvatureTool, EndCurve, curvature_close, curvature_extend, curvature_insert, curvature_move, curvature_smooth};
|
||||
pub use curvature::{
|
||||
CurvatureTool, EndCurve, curvature_close, curvature_extend, curvature_insert, curvature_move, curvature_remove, curvature_smooth,
|
||||
};
|
||||
pub use family::FamilyTool;
|
||||
pub use gesture::GestureTool;
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
//! It edits any selected path too, whatever drew it, keeping its shape except where edited
|
||||
//! (#798). A click on one of its points makes that the current point (direct-selected); a drag
|
||||
//! moves it, re-curving only the two segments at it ([`curvature_move`]); Alt-click or
|
||||
//! double-click toggles it between smooth and corner and Backspace/Delete removes it. A click on
|
||||
//! double-click toggles it between smooth and corner and Backspace/Delete removes it, the curve
|
||||
//! where it was then following the remaining points ([`curvature_remove`]). A click on
|
||||
//! a segment adds a smooth point there without changing the shape ([`curvature_insert`]), and a
|
||||
//! drag moves the new point at once. While the current point is an end of an open path, clicks go
|
||||
//! on from it ([`curvature_extend`]): the old segments keep their shape, the new ones curve
|
||||
@@ -20,7 +21,7 @@ use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{NodeId, Selection};
|
||||
use vectorcraft_geom::{Anchor, AnchorKind, BezPath, Point, SubPath, Vec2};
|
||||
|
||||
use super::{FEEDBACK, anchor_in, curvature_spline, editable_paths, end_handle, remove_anchor, segment_in, smooth_handles};
|
||||
use super::{FEEDBACK, anchor_in, curvature_spline, editable_paths, end_handle, segment_in, smooth_handles};
|
||||
use crate::guides::{DrawSnap, Leave, Targets};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
@@ -212,6 +213,106 @@ pub fn curvature_smooth(sp: &mut SubPath, i: usize) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove anchor `i` of `sp` as the Curvature tool deletes a point: the curve where it was follows
|
||||
/// the remaining points. Each neighbor that curves by the tool's rule (a smooth anchor, or an end
|
||||
/// of an open path with a handle into its segment) takes new handles from its new neighbors
|
||||
/// ([`smooth_handles`]; at an end, the one [`end_handle`] into the curve), and so does an end beyond
|
||||
/// one of them whose handle mirrors its tangent. A corner neighbor keeps its handles' directions,
|
||||
/// the one on the new segment scaled with its chord, as [`curvature_move`] scales them. With two
|
||||
/// anchors left the subpath is open, and those that curve by the tool's rule lose their handles, as
|
||||
/// [`curvature_spline`] draws two points as a line; a corner keeps its handles. On a path the tool
|
||||
/// drew, the result is the path it draws through the remaining points, except that an Alt-clicked
|
||||
/// end beside a corner without handles curves (the path stores it like any end there). False, and
|
||||
/// `sp` unchanged, when there is no anchor `i`.
|
||||
pub fn curvature_remove(sp: &mut SubPath, i: usize) -> bool {
|
||||
let n = sp.anchors.len();
|
||||
let Some(a) = sp.anchors.get(i).copied() else { return false };
|
||||
let closed = sp.closed;
|
||||
let is_end = |j: usize| !closed && (j == 0 || j + 1 == n);
|
||||
// An end without a handle beside a corner without handles is stored the same whether it was
|
||||
// Alt-clicked or not ([`curvature_spline`] gives it no handle either way), so it curves, as an
|
||||
// end the tool places does by default. Beside a tangent, a missing handle means a corner.
|
||||
let flags: Vec<bool> = sp
|
||||
.anchors
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(j, b)| {
|
||||
if !is_end(j) {
|
||||
return b.kind == AnchorKind::Smooth;
|
||||
}
|
||||
let near = sp.anchors.get(if j == 0 { 1 } else { n.saturating_sub(2) });
|
||||
b.has_in() || b.has_out() || near.is_none_or(|q| end_handle(b.p, q, j == 0).is_none())
|
||||
})
|
||||
.collect();
|
||||
let curved = |j: usize| flags.get(j).copied().unwrap_or(false);
|
||||
let [prev, next] = neighbours(sp, i);
|
||||
// A corner on both sides of the removed point: its handle on the new segment scales with it.
|
||||
if let (Some(pi), Some(ni)) = (prev, next)
|
||||
&& pi != ni
|
||||
{
|
||||
for (j, other, out) in [(pi, ni, true), (ni, pi, false)] {
|
||||
let to = sp.anchors.get(other).map(|o| o.p);
|
||||
if curved(j) {
|
||||
continue;
|
||||
}
|
||||
let Some((b, to)) = sp.anchors.get_mut(j).zip(to) else { continue };
|
||||
let old = b.p.distance(a.p);
|
||||
let s = if old > 1e-9 { b.p.distance(to) / old } else { 1.0 };
|
||||
let h = if out { &mut b.h_out } else { &mut b.h_in };
|
||||
*h = b.p + (*h - b.p) * s;
|
||||
}
|
||||
}
|
||||
let mut again: Vec<usize> = [prev, next].into_iter().flatten().filter(|&j| curved(j)).collect();
|
||||
// An end beside a smooth neighbor mirrors that neighbor's tangent, which changes.
|
||||
for j in again.clone() {
|
||||
if !is_end(j) {
|
||||
again.extend(neighbours(sp, j).into_iter().flatten().filter(|&k| k != i && is_end(k) && curved(k)));
|
||||
}
|
||||
}
|
||||
sp.anchors.remove(i);
|
||||
let mut again: Vec<usize> = again.into_iter().map(|j| if j > i { j - 1 } else { j }).collect();
|
||||
again.sort_unstable();
|
||||
again.dedup();
|
||||
let m = sp.anchors.len();
|
||||
if m < 3 {
|
||||
sp.closed = false;
|
||||
}
|
||||
if !sp.closed {
|
||||
// A new end has nothing beyond it.
|
||||
if let Some(e) = sp.anchors.first_mut() {
|
||||
e.h_in = e.p;
|
||||
}
|
||||
if let Some(e) = sp.anchors.last_mut() {
|
||||
e.h_out = e.p;
|
||||
}
|
||||
}
|
||||
let end = |j: usize| !sp.closed && (j == 0 || j + 1 == m);
|
||||
let (ends, inner): (Vec<usize>, Vec<usize>) = again.into_iter().partition(|&j| end(j));
|
||||
// The inner anchors first: an end's handle mirrors its neighbor's tangent.
|
||||
for j in inner {
|
||||
let [b, c] = neighbours(sp, j).map(|k| k.and_then(|k| sp.anchors.get(k)).map(|x| x.p));
|
||||
let Some(x) = sp.anchors.get(j).map(|x| x.p) else { continue };
|
||||
let fresh = match (b, c) {
|
||||
(Some(b), Some(c)) => smooth_handles(b, x, c).map(|(h_in, h_out)| Anchor { p: x, h_in, h_out, kind: AnchorKind::Smooth }),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(y) = sp.anchors.get_mut(j) {
|
||||
*y = fresh.unwrap_or_else(|| Anchor::corner(x));
|
||||
}
|
||||
}
|
||||
for j in ends {
|
||||
let first = j == 0;
|
||||
let near = if first { sp.anchors.get(1) } else { j.checked_sub(1).and_then(|k| sp.anchors.get(k)) }.copied();
|
||||
let Some(x) = sp.anchors.get(j).map(|x| x.p) else { continue };
|
||||
// Two points are a line.
|
||||
let h = near.filter(|_| m >= 3).and_then(|q| end_handle(x, &q, first)).unwrap_or(x);
|
||||
if let Some(y) = sp.anchors.get_mut(j) {
|
||||
*y = if first { Anchor { h_out: h, ..Anchor::corner(x) } } else { Anchor { h_in: h, ..Anchor::corner(x) } };
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Go on from an end of the open subpath `sp` (its start with `start`) to a new end at `p`; the old
|
||||
/// end bends into the new segment as `how` says. The new end has no handle on the outer side and,
|
||||
/// unless it is a `corner` (Alt-clicked), the one [`end_handle`] into the curve, so its segment
|
||||
@@ -313,6 +414,12 @@ fn select_point((id, si, ai): (NodeId, usize, usize)) -> Action {
|
||||
Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[si, ai]], "mode": "set"}))
|
||||
}
|
||||
|
||||
/// Delete anchor `(id, si, ai)` of a selected path, the curve where it was following the remaining
|
||||
/// points ([`curvature_remove`]).
|
||||
fn delete_point((id, si, ai): (NodeId, usize, usize)) -> Action {
|
||||
Action::Exec("path.removeAnchor".into(), json!({"id": id.0, "subpath": si, "anchor": ai, "curvature": true}))
|
||||
}
|
||||
|
||||
/// Toggle anchor `(id, si, ai)` of a selected path between smooth and corner. An end of an open
|
||||
/// path counts as smooth while it has a handle into its segment (the ends the tool curves are
|
||||
/// corners with one handle).
|
||||
@@ -560,7 +667,7 @@ impl Tool for CurvatureTool {
|
||||
}
|
||||
ToolKey::Backspace | ToolKey::Delete if self.claims_key(cx, key) => {
|
||||
if self.active(cx).is_none() {
|
||||
return current_point(cx).map(remove_anchor).into_iter().collect();
|
||||
return current_point(cx).map(delete_point).into_iter().collect();
|
||||
}
|
||||
let i = self.current.unwrap_or(self.pts.len() - 1).min(self.pts.len() - 1);
|
||||
self.pts.remove(i);
|
||||
|
||||
@@ -244,6 +244,62 @@ fn curvature_smooth_uses_the_tool_rule() {
|
||||
assert_eq!(fold, before);
|
||||
}
|
||||
|
||||
/// Removing a point of a path the tool drew leaves the path it draws through the remaining points,
|
||||
/// open or closed, and two points left are a straight line.
|
||||
#[test]
|
||||
fn curvature_remove_curves_through_the_remaining_points() {
|
||||
let at = |corners: &[usize]| -> Vec<(Point, bool)> {
|
||||
[(0.0, 0.0), (100.0, -80.0), (220.0, 30.0), (300.0, -60.0), (420.0, 10.0), (500.0, -40.0)]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(j, &(x, y))| (pt(x, y), corners.contains(&j)))
|
||||
.collect()
|
||||
};
|
||||
// A corner in the middle, and corners next to both ends (the ends then have no handle).
|
||||
for pts in [at(&[2]), at(&[1, 4]), at(&[])] {
|
||||
for closed in [false, true] {
|
||||
for i in 0..pts.len() {
|
||||
let mut sp = curvature_spline(&pts, closed);
|
||||
assert!(curvature_remove(&mut sp, i));
|
||||
let rest: Vec<(Point, bool)> = pts.iter().enumerate().filter(|&(j, _)| j != i).map(|(_, &q)| q).collect();
|
||||
let want = curvature_spline(&rest, closed);
|
||||
assert_eq!(sp.closed, want.closed);
|
||||
for (a, b) in sp.anchors.iter().zip(&want.anchors) {
|
||||
assert!(a.h_in.distance(b.h_in) < 1e-6 && a.h_out.distance(b.h_out) < 1e-6, "{pts:?} closed {closed}, point {i}: {a:?} != {b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let pts = at(&[2]);
|
||||
for closed in [false, true] {
|
||||
let mut sp = curvature_spline(&pts[..3], closed);
|
||||
assert!(curvature_remove(&mut sp, 1));
|
||||
assert!(!sp.closed && sp.segment_is_line(0), "two points are a line: {sp:?}");
|
||||
}
|
||||
let mut sp = curvature_spline(&pts, false);
|
||||
let before = sp.clone();
|
||||
assert!(!curvature_remove(&mut sp, 9));
|
||||
assert_eq!(sp, before);
|
||||
}
|
||||
|
||||
/// A corner neighbor with handles keeps their directions; its handle on the new segment scales
|
||||
/// with the segment's chord.
|
||||
#[test]
|
||||
fn curvature_remove_keeps_a_corner_neighbors_handle_direction() {
|
||||
let corner = Anchor { p: pt(100.0, 0.0), h_in: pt(80.0, -20.0), h_out: pt(120.0, -30.0), kind: AnchorKind::Corner };
|
||||
let mut sp = SubPath::new(vec![Anchor::corner(pt(0.0, 0.0)), corner, Anchor::corner(pt(200.0, 0.0)), Anchor::corner(pt(300.0, 0.0))], false);
|
||||
assert!(curvature_remove(&mut sp, 2));
|
||||
let c = sp.anchors[1];
|
||||
assert_eq!(c.h_in, corner.h_in);
|
||||
let (old, new) = (corner.h_out - corner.p, c.h_out - c.p);
|
||||
assert!(old.cross(new).abs() < 1e-9 && old.dot(new) > 0.0, "{c:?}");
|
||||
assert!((new.hypot() / old.hypot() - 2.0).abs() < 1e-9, "the chord doubled: {c:?}");
|
||||
// Two anchors left: the corner keeps its handle on the segment that stays.
|
||||
let mut sp = SubPath::new(vec![Anchor::corner(pt(0.0, 0.0)), corner, Anchor::corner(pt(200.0, 0.0))], false);
|
||||
assert!(curvature_remove(&mut sp, 2));
|
||||
assert_eq!((sp.anchors[1].h_in, sp.anchors[1].h_out), (corner.h_in, corner.p));
|
||||
}
|
||||
|
||||
/// A curved end counts as smooth for the toggle: Alt-click turns it into a corner.
|
||||
#[test]
|
||||
fn alt_click_on_a_curved_end_makes_it_a_corner() {
|
||||
@@ -345,7 +401,7 @@ fn alt_click_or_double_click_toggles_and_delete_removes() {
|
||||
let s = selected(id, Some(2));
|
||||
let cx = quiet(&d, &s, &p);
|
||||
assert!(t.claims_key(&cx, ToolKey::Backspace));
|
||||
assert_eq!(t.key(&cx, ToolKey::Delete, Mods::default()), vec![remove_anchor((id, 0, 2))]);
|
||||
assert_eq!(t.key(&cx, ToolKey::Delete, Mods::default()), vec![delete_point((id, 0, 2))]);
|
||||
// Esc lets go of the point.
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user