mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 07:45:38 +00:00
Merge pull request #6718 from dotCooCoo/fix/issue-6634
Fix #6634: Pen closing drag no longer gives the first anchor symmetric handles
This commit is contained in:
@@ -285,3 +285,54 @@ fn the_tool_draws_a_path_one_step_per_anchor_and_closes_it() {
|
||||
assert_eq!(path(&s, id).subpaths[0].anchors.len(), 3);
|
||||
assert_eq!(steps(&s), n + 8);
|
||||
}
|
||||
|
||||
/// #6634: the tool's closing drag on the first anchor sets that anchor's incoming handle to the
|
||||
/// mirror of the pointer and does not give it symmetric handles: a corner keeps its outgoing
|
||||
/// handle, a smooth anchor turns it to stay in line at its own length, and Alt pressed mid-drag
|
||||
/// keeps it where the drag had turned it.
|
||||
#[test]
|
||||
fn the_tools_closing_drag_does_not_make_the_first_anchor_symmetric() {
|
||||
// An open path whose first anchor, at (50, 50), has the handles `first`, drawn on from its last
|
||||
// anchor; the closing gesture is then up to the caller.
|
||||
let draw = |first: Value| {
|
||||
let mut s = session();
|
||||
let mut a = json!({"x": 50, "y": 50});
|
||||
a.as_object_mut().unwrap().extend(first.as_object().unwrap().clone());
|
||||
let anchors = json!([a, {"x": 150, "y": 50}, {"x": 150, "y": 150}]);
|
||||
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
s.select_tool("smartPen", view()).unwrap();
|
||||
gesture(&mut s, &[(150.0, 150.0)], Mods::default());
|
||||
(s, id)
|
||||
};
|
||||
let first = |s: &Session, id: NodeId| {
|
||||
let sp = &path(s, id).subpaths[0];
|
||||
assert!(sp.closed, "{sp:?}");
|
||||
sp.anchors[0]
|
||||
};
|
||||
let near = |a: Point, b: Point| a.distance(b) < 1e-6;
|
||||
|
||||
// A corner: the first segment keeps its shape.
|
||||
let (mut s, id) = draw(json!({"out": [80, 30]}));
|
||||
gesture(&mut s, &[(50.0, 50.0), (50.0, 100.0)], Mods::default());
|
||||
let a = first(&s, id);
|
||||
assert_eq!((a.h_in, a.h_out, a.kind), (Point::new(50.0, 0.0), Point::new(80.0, 30.0), AnchorKind::Corner), "{a:?}");
|
||||
|
||||
// A smooth anchor: the outgoing handle turns opposite the incoming one, keeping its length 30.
|
||||
let (mut s, id) = draw(json!({"in": [20, 50], "out": [80, 50]}));
|
||||
assert_eq!(path(&s, id).subpaths[0].anchors[0].kind, AnchorKind::Smooth);
|
||||
gesture(&mut s, &[(50.0, 50.0), (50.0, 100.0)], Mods::default());
|
||||
let a = first(&s, id);
|
||||
assert_eq!((a.h_in, a.kind), (Point::new(50.0, 0.0), AnchorKind::Smooth), "{a:?}");
|
||||
assert!(near(a.h_out, Point::new(50.0, 80.0)), "{a:?}");
|
||||
|
||||
// Alt pressed mid-drag: a corner, its outgoing handle where the drag had turned it.
|
||||
let (mut s, id) = draw(json!({"in": [20, 50], "out": [80, 50]}));
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 50.0, 50.0), view()).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 50.0, 100.0), view()).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 80.0, 100.0).with_mods(alt), view()).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 80.0, 100.0).with_mods(alt), view()).unwrap();
|
||||
let a = first(&s, id);
|
||||
assert_eq!((a.h_in, a.kind), (Point::new(20.0, 0.0), AnchorKind::Corner), "{a:?}");
|
||||
assert!(near(a.h_out, Point::new(50.0, 80.0)), "{a:?}");
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Close Path",
|
||||
[],
|
||||
None,
|
||||
"{id, subpath?: usize (the open subpath to close, 0 included; default: the last open one), in?: [x,y] (handle into the first anchor), independent?: bool}",
|
||||
"{id, subpath?: usize (the open subpath to close, 0 included; default: the last open one), in?: [x,y] (handle into the first anchor, placed as path.setHandle places it: a smooth first anchor keeps its outgoing handle in line at its own length, a corner keeps it where it is), out?: [x,y] (the first anchor's outgoing handle, placed after `in`; the anchor becomes a corner), independent?: bool (`in` makes the first anchor a corner and leaves its outgoing handle)}",
|
||||
has_doc,
|
||||
close
|
||||
),
|
||||
@@ -202,14 +202,18 @@ fn append_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
/// Close the active open subpath of `id`, optionally setting the incoming handle of the first anchor.
|
||||
/// Close the active open subpath of `id`, optionally setting the handles of the first anchor. The
|
||||
/// incoming handle moves as Move Direction Handle moves it: the outgoing handle turns with it only
|
||||
/// on a smooth anchor, so the first segment of a path that starts at a corner keeps its shape.
|
||||
fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id").ok_or_else(|| bad("path.close", "missing id"))?;
|
||||
let subpath = p.get("subpath").and_then(Value::as_u64).map(|u| u as usize);
|
||||
let h_in = match p.get("in").filter(|h| !h.is_null()) {
|
||||
None => None,
|
||||
Some(h) => Some(finite_numbers(h).map(|[x, y]| Point::new(x, y)).ok_or_else(|| bad("path.close", "`in` must be [x, y]"))?),
|
||||
let handle = |key: &str| match p.get(key).filter(|h| !h.is_null()) {
|
||||
None => Ok(None),
|
||||
Some(h) => finite_numbers(h).map(|[x, y]| Some(Point::new(x, y))).ok_or_else(|| bad("path.close", format!("`{key}` must be [x, y]"))),
|
||||
};
|
||||
let h_in = handle("in")?;
|
||||
let h_out = handle("out")?;
|
||||
let independent = switch_or(p, "independent", false, "path.close")?;
|
||||
s.edit("Close Path", |d, sel| {
|
||||
let path = path_mut(d, id)?;
|
||||
@@ -218,11 +222,13 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => path.subpaths.iter_mut().rev().find(|s| !s.closed).ok_or_else(|| EngineError::Other("path is already closed".into()))?,
|
||||
};
|
||||
sp.closed = true;
|
||||
if let (Some(h), Some(first)) = (h_in, sp.anchors.first_mut()) {
|
||||
first.h_in = h;
|
||||
if !independent {
|
||||
first.h_out = first.p - (h - first.p);
|
||||
first.kind = AnchorKind::Smooth;
|
||||
if let Some(first) = sp.anchors.first_mut() {
|
||||
if let Some(h) = h_in {
|
||||
first.set_handle(false, h, independent);
|
||||
}
|
||||
if let Some(h) = h_out {
|
||||
first.h_out = h;
|
||||
first.kind = AnchorKind::Corner;
|
||||
}
|
||||
}
|
||||
let top = compound_or_self(d, id);
|
||||
|
||||
@@ -3146,7 +3146,7 @@ fn shift_reshapes_the_closing_segment_of_a_rotated_square_and_undoes() {
|
||||
fn pen_drag_from_the_first_anchor_closes_the_path() {
|
||||
// #501: closing a path by dragging out of its first anchor flickered between a close and a new
|
||||
// path, and released at the wrong moment left the shape open. The press decides: the drag
|
||||
// shapes the closing curve, symmetric, and the release closes the path wherever it ends.
|
||||
// shapes the closing curve, and the release closes the path wherever it ends.
|
||||
let none = Mods::default();
|
||||
let v = view();
|
||||
let mut s = session();
|
||||
@@ -3167,13 +3167,119 @@ fn pen_drag_from_the_first_anchor_closes_the_path() {
|
||||
let sp = &path(&s, id).subpaths[0];
|
||||
assert!(sp.closed && sp.anchors.len() == 3, "{sp:?}");
|
||||
let a = sp.anchors[0];
|
||||
assert_eq!((a.h_in, a.h_out), (Point::new(40.0, 348.0), Point::new(160.0, 252.0)), "symmetric");
|
||||
assert_eq!((a.h_in, a.h_out), (Point::new(40.0, 348.0), Point::new(100.0, 300.0)), "only the closing segment curves");
|
||||
assert_eq!(paths(&s).len(), 1);
|
||||
// The next click starts a new path.
|
||||
gesture(&mut s, &[(500.0, 500.0)], none);
|
||||
assert_eq!(paths(&s).len(), 2);
|
||||
}
|
||||
|
||||
/// #6634: closing a path by dragging on its first anchor does not give that anchor symmetric
|
||||
/// handles. Its incoming handle mirrors the pointer; its outgoing handle stays where it is on a
|
||||
/// corner, so the first segment keeps its shape, and on a smooth anchor turns to stay in line at
|
||||
/// its own length. Alt keeps the outgoing handle where it was when Alt went down, Shift keeps the
|
||||
/// drag to 45° steps, and a click leaves the anchor a corner with the closing segment straight
|
||||
/// into it. Each close is one undo step.
|
||||
#[test]
|
||||
fn pen_closing_drag_does_not_make_the_first_anchor_symmetric() {
|
||||
use vectorcraft_geom::AnchorKind;
|
||||
let (none, alt, shift) = (Mods::default(), Mods { alt: true, ..Mods::default() }, Mods { shift: true, ..Mods::default() });
|
||||
let v = view();
|
||||
// Draws a path from (100, 300), dragged out to `start` when given (a smooth anchor), through
|
||||
// (200, 200) and (300, 300), and returns it with its first anchor as drawn.
|
||||
let draw = |s: &mut Session, start: Option<(f64, f64)>| {
|
||||
s.select_tool("pen", v).unwrap();
|
||||
let first = match start {
|
||||
Some(out) => vec![(100.0, 300.0), out],
|
||||
None => vec![(100.0, 300.0)],
|
||||
};
|
||||
gesture(s, &first, none);
|
||||
gesture(s, &[(200.0, 200.0)], none);
|
||||
gesture(s, &[(300.0, 300.0)], none);
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
let anchor = path(s, id).subpaths[0].anchors[0];
|
||||
(id, anchor)
|
||||
};
|
||||
let first = |s: &Session, id: NodeId| {
|
||||
let sp = &path(s, id).subpaths[0];
|
||||
assert!(sp.closed, "{sp:?}");
|
||||
sp.anchors[0]
|
||||
};
|
||||
|
||||
// A corner: the first segment stays straight however the drag turns.
|
||||
let mut s = session();
|
||||
let (id, before) = draw(&mut s, None);
|
||||
assert_eq!(gesture(&mut s, &[(100.0, 300.0), (80.0, 250.0), (60.0, 200.0)], none), 1, "one undo step");
|
||||
let a = first(&s, id);
|
||||
assert_eq!((a.h_in, a.h_out, a.kind), (Point::new(140.0, 400.0), before.h_out, AnchorKind::Corner), "{a:?}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(!path(&s, id).subpaths[0].closed, "the undo opens the path again");
|
||||
|
||||
// A smooth anchor: the outgoing handle turns opposite the incoming one, keeping its length 50.
|
||||
let mut s = session();
|
||||
let (id, before) = draw(&mut s, Some((150.0, 300.0)));
|
||||
assert_eq!((before.h_out, before.kind), (Point::new(150.0, 300.0), AnchorKind::Smooth));
|
||||
gesture(&mut s, &[(100.0, 300.0), (100.0, 250.0), (100.0, 200.0)], none);
|
||||
let a = first(&s, id);
|
||||
assert_eq!((a.h_in, a.kind), (Point::new(100.0, 400.0), AnchorKind::Smooth), "{a:?}");
|
||||
assert!(near(a.h_out, Point::new(100.0, 250.0)), "in line, at its own length: {a:?}");
|
||||
|
||||
// Alt held from the press: a corner, its outgoing handle where it was.
|
||||
let mut s = session();
|
||||
let (id, before) = draw(&mut s, Some((150.0, 300.0)));
|
||||
gesture(&mut s, &[(100.0, 300.0), (100.0, 250.0), (100.0, 200.0)], alt);
|
||||
let a = first(&s, id);
|
||||
assert_eq!((a.h_in, a.h_out, a.kind), (Point::new(100.0, 400.0), before.h_out, AnchorKind::Corner), "{a:?}");
|
||||
|
||||
// Alt pressed mid-drag: the outgoing handle stays where the drag had turned it.
|
||||
let mut s = session();
|
||||
let (id, _) = draw(&mut s, Some((150.0, 300.0)));
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 300.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 100.0, 200.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 130.0, 200.0).with_mods(alt), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 130.0, 200.0).with_mods(alt), v).unwrap();
|
||||
let a = first(&s, id);
|
||||
assert_eq!((a.h_in, a.kind), (Point::new(70.0, 400.0), AnchorKind::Corner), "{a:?}");
|
||||
assert!(near(a.h_out, Point::new(100.0, 250.0)), "{a:?}");
|
||||
|
||||
// Shift: the drag keeps to 45° steps (straight up here), at the pointer's distance from the
|
||||
// anchor, mirrored into the incoming handle.
|
||||
let mut s = session();
|
||||
let (id, before) = draw(&mut s, None);
|
||||
gesture(&mut s, &[(100.0, 300.0), (95.0, 250.0), (90.0, 200.0)], shift);
|
||||
let a = first(&s, id);
|
||||
assert!(near(a.h_in, Point::new(100.0, 300.0 + 10f64.hypot(100.0))) && a.h_out == before.h_out, "{a:?}");
|
||||
|
||||
// A click: the closing segment comes straight in, and a smooth anchor keeps its outgoing
|
||||
// handle and becomes a corner.
|
||||
let mut s = session();
|
||||
let (id, before) = draw(&mut s, Some((150.0, 300.0)));
|
||||
assert_eq!(gesture(&mut s, &[(100.0, 300.0)], none), 1);
|
||||
let a = first(&s, id);
|
||||
assert_eq!((a.h_in, a.h_out, a.kind), (Point::new(100.0, 300.0), before.h_out, AnchorKind::Corner), "{a:?}");
|
||||
}
|
||||
|
||||
/// Close Path's `in` moves the first anchor's incoming handle as Move Direction Handle does, and
|
||||
/// `out` places its outgoing handle, making it a corner; `out` that is not [x, y] is refused.
|
||||
#[test]
|
||||
fn close_path_places_the_first_anchors_handles() {
|
||||
use vectorcraft_geom::AnchorKind;
|
||||
let mut s = session();
|
||||
let id = curve(&mut s);
|
||||
let a = path(&s, id).subpaths[0].anchors[0];
|
||||
assert!(a.has_out() && a.kind == AnchorKind::Corner, "{a:?}");
|
||||
for h in [json!("1, 2"), json!([1]), json!([1, "2"])] {
|
||||
refuses_with(&mut s, "path.close", &json!({"id": id.0, "out": h}), "`out` must be [x, y]");
|
||||
}
|
||||
s.execute("path.close", &json!({"id": id.0, "in": [60.0, 140.0]})).unwrap();
|
||||
let b = path(&s, id).subpaths[0].anchors[0];
|
||||
assert_eq!((b.h_in, b.h_out, b.kind), (Point::new(60.0, 140.0), a.h_out, AnchorKind::Corner), "a corner keeps its outgoing handle");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("path.close", &json!({"id": id.0, "in": [60.0, 140.0], "out": [120.0, 60.0]})).unwrap();
|
||||
let b = path(&s, id).subpaths[0].anchors[0];
|
||||
assert_eq!((b.h_in, b.h_out, b.kind), (Point::new(60.0, 140.0), Point::new(120.0, 60.0), AnchorKind::Corner));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pen_with_alt_converts_the_anchors_of_the_path_being_drawn() {
|
||||
// #504: while drawing, Alt-click on a smooth anchor of the path makes it a corner and Alt-drag
|
||||
|
||||
@@ -46,7 +46,8 @@ fn cmd_with_the_pen_edits_the_path_being_drawn() {
|
||||
assert_eq!(paths(&h).iter().map(Vec::len).collect::<Vec<_>>(), [3], "the same path, three anchors");
|
||||
}
|
||||
|
||||
/// #501: a drag out of the first anchor closes the path, shaping the closing curve.
|
||||
/// #501: a drag out of the first anchor closes the path, shaping the closing curve. #6634: the first
|
||||
/// anchor was placed by a click, so it stays a corner and the first segment keeps its shape.
|
||||
#[test]
|
||||
fn a_drag_from_the_first_anchor_closes_the_path() {
|
||||
let mut h = Headless::new();
|
||||
@@ -62,5 +63,6 @@ fn a_drag_from_the_first_anchor_closes_the_path() {
|
||||
d.walk(|n| closed.extend(n.path_data().map(|p| p.is_closed())));
|
||||
assert_eq!(closed, [true], "one path, closed");
|
||||
let first = paths(&h)[0][0];
|
||||
assert_eq!((first.h_in, first.h_out), (vectorcraft_geom::Point::new(40.0, 348.0), vectorcraft_geom::Point::new(160.0, 252.0)));
|
||||
assert_eq!((first.h_in, first.h_out), (vectorcraft_geom::Point::new(40.0, 348.0), vectorcraft_geom::Point::new(100.0, 300.0)));
|
||||
assert_eq!(first.kind, vectorcraft_geom::AnchorKind::Corner);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! tangent); a drag makes a smooth anchor (Alt held breaks the outgoing handle off, Shift keeps it
|
||||
//! to 45° steps, Space moves the anchor); Tab switches the segment into the anchor being placed, or
|
||||
//! the last one drawn, between straight and curved. A rubber band shows the next segment. A click
|
||||
//! on the first anchor closes the path, on an end of another open path joins the two, and away
|
||||
//! on the first anchor closes the path (a drag from it shapes the closing segment as the Pen's
|
||||
//! closing drag does), on an end of another open path joins the two, and away
|
||||
//! from paths, with no path being drawn, starts a new one. Enter or Esc ends the path.
|
||||
//!
|
||||
//! Editing the shown paths' points: a click selects a point (an end of an open path is then drawn
|
||||
@@ -30,7 +31,7 @@ use vectorcraft_pathops::addons::pen::{leave_handle, straighten_segment};
|
||||
use crate::direct::hit_handle;
|
||||
use crate::draw2::{anchor_in, hit_segment, segment_in, shown_paths};
|
||||
use crate::guides::{DrawSnap, Leave};
|
||||
use crate::pen::{active_path, continue_from, open_end_at};
|
||||
use crate::pen::{active_path, close_drag, continue_from, first_anchor, open_end_at};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
/// The tool's id.
|
||||
@@ -82,11 +83,13 @@ struct Place {
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Target {
|
||||
Place(Place),
|
||||
/// Closing the path being drawn on its first anchor, at `first`.
|
||||
/// Closing the path being drawn on its first anchor, at `first`; `anchor` holds that anchor as
|
||||
/// the press found it and its outgoing handle as the drag last left it.
|
||||
Close {
|
||||
id: NodeId,
|
||||
si: usize,
|
||||
first: Point,
|
||||
anchor: Option<(Anchor, Point)>,
|
||||
},
|
||||
/// An anchor of a shown path at `p`; `n` anchors in its subpath, `closed` or not.
|
||||
Point {
|
||||
@@ -285,7 +288,8 @@ impl SmartPenTool {
|
||||
let press = |what| Press { at: ev.pos, mods: ev.mods, dragged: false, begun: false, what };
|
||||
match self.hit(cx, ev.pos) {
|
||||
Hit::Close(id, si, first) => {
|
||||
self.press = Some(Press { begun: true, ..press(Target::Close { id, si, first }) });
|
||||
let anchor = first_anchor(cx, id, si).map(|a| (a, a.h_out));
|
||||
self.press = Some(Press { begun: true, ..press(Target::Close { id, si, first, anchor }) });
|
||||
let seg = if self.curved { "curved" } else { "straight" };
|
||||
vec![
|
||||
Action::Begin("Close Path".into()),
|
||||
@@ -362,10 +366,11 @@ impl SmartPenTool {
|
||||
pr.what = Target::Place(pl);
|
||||
("", self.place_preview(&pl, true))
|
||||
}
|
||||
Target::Close { id, si, first } => {
|
||||
Target::Close { id, si, first, mut anchor } => {
|
||||
let out = Self::handle_to(cx, first, ev.pos, first, shift, false);
|
||||
let h_in = first - (out - first);
|
||||
("", Action::Preview("path.close".into(), json!({"id": id.0, "subpath": si, "in": xy(h_in), "independent": alt})))
|
||||
let act = close_drag(id, Some(si), first - (out - first), alt, anchor.as_mut());
|
||||
pr.what = Target::Close { id, si, first, anchor };
|
||||
("", act)
|
||||
}
|
||||
Target::Point { id, si, ai, p, .. } if pr.mods.alt => {
|
||||
let h = Self::handle_to(cx, p, ev.pos, p, shift, false);
|
||||
|
||||
@@ -157,7 +157,13 @@ fn a_press_on_the_first_anchor_closes_the_path() {
|
||||
// A drag shapes the closing curve.
|
||||
let (c, params) = preview(&t.pointer(&cx, &ev(PointerKind::Drag, 130.0, 100.0, NO)));
|
||||
assert_eq!((c.as_str(), params["in"].clone()), ("path.close", json!([70.0, 100.0])));
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 130.0, 100.0, NO)), vec![Action::Commit]);
|
||||
// Alt pressed mid-drag makes the anchor a corner and keeps its outgoing handle where it was.
|
||||
let (_, params) = preview(&t.pointer(&cx, &ev(PointerKind::Drag, 130.0, 120.0, ALT)));
|
||||
assert_eq!(
|
||||
(params["in"].clone(), params["out"].clone(), params["independent"].clone()),
|
||||
(json!([70.0, 80.0]), json!([100.0, 100.0]), json!(true))
|
||||
);
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 130.0, 120.0, ALT)), vec![Action::Commit]);
|
||||
assert!(!t.drawing, "the path is finished");
|
||||
}
|
||||
|
||||
|
||||
+51
-13
@@ -3,7 +3,11 @@
|
||||
//! Click adds a corner anchor; click-drag adds a smooth anchor with symmetric handles (Alt breaks
|
||||
//! them: the incoming handle stays where it was when Alt went down and only the outgoing one
|
||||
//! follows the pointer; Space held moves the anchor, handles and all); Shift constrains to 45°.
|
||||
//! A press on the first anchor closes the path, the drag shaping the closing curve. Clicking the
|
||||
//! A press on the first anchor closes the path, the drag shaping the closing curve: the anchor's
|
||||
//! incoming handle mirrors the pointer (a click leaves it retracted and the anchor a corner, so the
|
||||
//! closing segment comes in straight). Its outgoing handle stays where it is on a corner, so the first segment keeps its
|
||||
//! shape, and on a smooth anchor turns to stay in line at its own length; Alt makes the anchor a
|
||||
//! corner, leaving the outgoing handle where it was when Alt went down. Clicking the
|
||||
//! last one retracts its outgoing handle, so the next segment leaves it as a corner; dragging from
|
||||
//! it pulls a new one out on its own.
|
||||
//! Alt held over a handle end, an anchor or a segment of a selected path (the one being drawn too)
|
||||
@@ -27,7 +31,7 @@
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{NodeId, NodeKind};
|
||||
use vectorcraft_geom::{BezPath, Point, Vec2};
|
||||
use vectorcraft_geom::{Anchor, BezPath, Point, Vec2};
|
||||
|
||||
use crate::direct::hit_handle;
|
||||
use crate::draw2::{AnchorTool, editable_paths, shown_paths};
|
||||
@@ -47,6 +51,9 @@ pub struct PenTool {
|
||||
hover: Option<Point>,
|
||||
/// The incoming handle of the anchor being dragged out, as last previewed.
|
||||
in_h: Point,
|
||||
/// The first anchor, as it was when a press on it began closing the path, and its outgoing
|
||||
/// handle as the closing drag last left it: Alt pressed mid-drag keeps the handle there.
|
||||
first: Option<(Anchor, Point)>,
|
||||
/// The pointer at the last button-down or drag: Space held moves the anchor as far as it moves.
|
||||
last: Point,
|
||||
/// Where the button went down: the drag threshold is measured from it.
|
||||
@@ -289,9 +296,7 @@ impl Tool for PenTool {
|
||||
// Pick at the raw pointer before snapping or constraining a new point.
|
||||
// A one-anchor path doesn't close on itself: its anchor is the last one too.
|
||||
if picked == Some(Endpoint::First) {
|
||||
self.drag = Some(Place { at: first, path: Some(id), subpath: Some(si), closing: true });
|
||||
let params = json!({"id": id.0, "subpath": si});
|
||||
return vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), params)];
|
||||
return self.begin_close(cx, id, si, first);
|
||||
}
|
||||
// Alt uses Anchor Point behavior at the terminal anchor too.
|
||||
if let Some(acts) = self.alt_convert(cx, ev) {
|
||||
@@ -307,9 +312,7 @@ impl Tool for PenTool {
|
||||
if raw_other.is_none() && p != ev.pos {
|
||||
let snapped = pick_endpoint(closing_end, last, p, tol, Endpoint::First);
|
||||
if snapped == Some(Endpoint::First) {
|
||||
self.drag = Some(Place { at: first, path: Some(id), subpath: Some(si), closing: true });
|
||||
let params = json!({"id": id.0, "subpath": si});
|
||||
return vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), params)];
|
||||
return self.begin_close(cx, id, si, first);
|
||||
}
|
||||
if snapped == Some(Endpoint::Last)
|
||||
&& let Some((si, ai)) = end
|
||||
@@ -410,11 +413,7 @@ impl Tool for PenTool {
|
||||
)];
|
||||
};
|
||||
if closing {
|
||||
let mut params = json!({"id": id.0, "in": [2.0 * a.x - out_h.x, 2.0 * a.y - out_h.y], "independent": alt});
|
||||
if let Some(si) = subpath {
|
||||
params["subpath"] = json!(si);
|
||||
}
|
||||
return vec![Action::Preview("path.close".into(), params)];
|
||||
return vec![close_drag(id, subpath, a - (out_h - a), alt, self.first.as_mut())];
|
||||
}
|
||||
// Alt pressed mid-drag keeps the curve already shaped into the anchor: only the
|
||||
// outgoing handle follows. Held from the start, the incoming handle stays retracted.
|
||||
@@ -436,6 +435,7 @@ impl Tool for PenTool {
|
||||
}
|
||||
let Some(place) = self.drag.take() else { return vec![] };
|
||||
if place.closing {
|
||||
self.first = None;
|
||||
self.stop();
|
||||
}
|
||||
vec![Action::Commit]
|
||||
@@ -453,6 +453,7 @@ impl Tool for PenTool {
|
||||
/// close it was making: the path being drawn goes on with the next click.
|
||||
fn drop_press(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.drag = None;
|
||||
self.first = None;
|
||||
self.handle = None;
|
||||
self.convert = None;
|
||||
self.snap.clear();
|
||||
@@ -464,6 +465,7 @@ impl Tool for PenTool {
|
||||
self.stop();
|
||||
self.snap.clear();
|
||||
let placing = self.drag.take().is_some();
|
||||
self.first = None;
|
||||
let handling = self.handle.take().is_some();
|
||||
// Finish the preview as it stands; clearing state alone leaves its transaction open.
|
||||
let mut acts = self.convert.take().map(|mut t| t.deactivate(cx)).unwrap_or_default();
|
||||
@@ -577,6 +579,16 @@ impl PenTool {
|
||||
(self.drawing, self.path, self.subpath) = (false, None, None);
|
||||
}
|
||||
|
||||
/// A press on the first anchor (at `at`) of subpath `si` of the path being drawn: close it, the
|
||||
/// anchor a corner with its incoming handle retracted until a drag pulls it out, as a click on
|
||||
/// the last anchor retracts its outgoing one ([`set_out_handle`]).
|
||||
fn begin_close(&mut self, cx: &ToolContext, id: NodeId, si: usize, at: Point) -> Vec<Action> {
|
||||
self.drag = Some(Place { at, path: Some(id), subpath: Some(si), closing: true });
|
||||
self.first = first_anchor(cx, id, si).map(|a| (a, a.h_out));
|
||||
let params = json!({"id": id.0, "subpath": si, "in": [at.x, at.y], "independent": true});
|
||||
vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), params)]
|
||||
}
|
||||
|
||||
/// Hand an Alt press on a selected path's handle, anchor or segment to the Anchor Point tool.
|
||||
fn alt_convert(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Option<Vec<Action>> {
|
||||
if !alt_converts(cx, ev.pos, ev.mods) {
|
||||
@@ -592,6 +604,32 @@ impl PenTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The first anchor of subpath `si` of path `id`.
|
||||
pub(crate) fn first_anchor(cx: &ToolContext, id: NodeId, si: usize) -> Option<Anchor> {
|
||||
cx.doc.node(id)?.path_data()?.subpaths.get(si)?.anchors.first().copied()
|
||||
}
|
||||
|
||||
/// The preview of a closing drag that puts the first anchor's incoming handle at `in_h`. `first`
|
||||
/// holds that anchor as it was when the press began closing the path, and its outgoing handle as
|
||||
/// the drag last left it: a smooth anchor turns that handle to stay in line, and Alt (`alt`), which
|
||||
/// makes the anchor a corner, keeps it there.
|
||||
pub(crate) fn close_drag(id: NodeId, subpath: Option<usize>, in_h: Point, alt: bool, first: Option<&mut (Anchor, Point)>) -> Action {
|
||||
let mut params = json!({"id": id.0, "in": [in_h.x, in_h.y], "independent": alt});
|
||||
if let Some((anchor, out)) = first {
|
||||
if alt {
|
||||
params["out"] = json!([out.x, out.y]);
|
||||
} else {
|
||||
let mut a = *anchor;
|
||||
a.set_handle(false, in_h, false);
|
||||
*out = a.h_out;
|
||||
}
|
||||
}
|
||||
if let Some(si) = subpath {
|
||||
params["subpath"] = json!(si);
|
||||
}
|
||||
Action::Preview("path.close".into(), params)
|
||||
}
|
||||
|
||||
/// Go on drawing subpath `si` of open path `id` from its last anchor, or from its first
|
||||
/// (`at_first`, the subpath reversed so that it is the last): the path (its compound path) is
|
||||
/// selected.
|
||||
|
||||
@@ -2,6 +2,13 @@ use super::*;
|
||||
use crate::testutil::*;
|
||||
use vectorcraft_doc::Selection;
|
||||
|
||||
/// What a press on the first anchor, at `first`, of subpath 0 of `id` does: the path closes with
|
||||
/// the anchor a corner with its incoming handle retracted.
|
||||
fn close_press(id: NodeId, first: Point) -> Vec<Action> {
|
||||
let params = json!({"id": id.0, "subpath": 0, "in": [first.x, first.y], "independent": true});
|
||||
vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), params)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn press_inside_drag_threshold_places_a_corner() {
|
||||
let (d, s) = (vectorcraft_doc::Document::new(500.0, 500.0), Selection::default());
|
||||
@@ -131,10 +138,7 @@ fn snapped_first_endpoint_closes_active_path_after_raw_miss() {
|
||||
assert!(open_end_at(&cx, raw, tol, Some((id, 0))).is_none());
|
||||
assert!(open_end_at(&cx, first, tol, Some((id, 0))).is_none());
|
||||
|
||||
assert_eq!(
|
||||
pen.pointer(&cx, &PointerEvent::new(PointerKind::Down, raw.x, raw.y)),
|
||||
vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), json!({"id": id.0, "subpath": 0}))]
|
||||
);
|
||||
assert_eq!(pen.pointer(&cx, &PointerEvent::new(PointerKind::Down, raw.x, raw.y)), close_press(id, first));
|
||||
let place = pen.drag.unwrap();
|
||||
assert!(place.closing);
|
||||
assert_eq!((place.path, place.at), (Some(id), first));
|
||||
@@ -349,10 +353,7 @@ fn terminal_anchor_conversion_precedes_grid_snapping() {
|
||||
assert_eq!(t.cursor(&cx, Point::new(63.0, 303.0), Mods::default()), Cursor::PenContinue);
|
||||
assert!(click(&mut t, &cx, 63.0, 303.0, Mods::default()).is_empty());
|
||||
assert_eq!(t.active(&cx).map(|a| a.0), Some(id));
|
||||
assert_eq!(
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 13.0, 303.0)),
|
||||
vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), json!({"id": id.0, "subpath": 0}))]
|
||||
);
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 13.0, 303.0)), close_press(id, Point::new(13.0, 303.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -401,7 +402,7 @@ fn overlapping_endpoint_tolerances_pick_the_nearest_end() {
|
||||
let cursor = if first_end { Cursor::PenClose } else { Cursor::PenConvert };
|
||||
let actual_cursor = pen.cursor(&cx, at, mods);
|
||||
let expected = if first_end {
|
||||
vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), json!({"id": id.0, "subpath": 0}))]
|
||||
close_press(id, first)
|
||||
} else {
|
||||
vec![Action::Begin("Convert Anchor Point".into()), set_out(id, 1, last.x, last.y)]
|
||||
};
|
||||
@@ -459,10 +460,7 @@ fn coincident_endpoints_keep_the_prior_tie_choice() {
|
||||
let cx = ToolContext { zoom, smart_guides: false, snap_to_point: false, ..cx(&d, &selection, &paint) };
|
||||
let mut drawing = PenTool { drawing: true, ..PenTool::default() };
|
||||
assert_eq!(drawing.cursor(&cx, at, Mods::default()), Cursor::PenClose);
|
||||
assert_eq!(
|
||||
click(&mut drawing, &cx, at.x, at.y, Mods::default()),
|
||||
vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), json!({"id": id.0, "subpath": 0}))]
|
||||
);
|
||||
assert_eq!(click(&mut drawing, &cx, at.x, at.y, Mods::default()), close_press(id, at));
|
||||
let mut resuming = PenTool::default();
|
||||
assert_eq!(resuming.cursor(&cx, at, Mods::default()), Cursor::PenContinue);
|
||||
assert!(click(&mut resuming, &cx, at.x, at.y, Mods::default()).is_empty());
|
||||
@@ -516,10 +514,7 @@ fn enlarged_anchor_radius_is_used_for_endpoints_and_alt_conversion() {
|
||||
let mut closing = PenTool { drawing: true, ..PenTool::default() };
|
||||
let first = Point::new(10.0 - 6.0 / zoom, 300.0);
|
||||
assert_eq!(closing.cursor(&cx, first, Mods::default()), Cursor::PenClose);
|
||||
assert_eq!(
|
||||
click(&mut closing, &cx, first.x, first.y, Mods::default()),
|
||||
vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), json!({"id": id.0, "subpath": 0}))]
|
||||
);
|
||||
assert_eq!(click(&mut closing, &cx, first.x, first.y, Mods::default()), close_press(id, Point::new(10.0, 300.0)));
|
||||
let mut alt_pen = PenTool { drawing: true, ..PenTool::default() };
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
assert!(alt_pen.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(alt)).is_empty());
|
||||
@@ -577,7 +572,8 @@ fn ending_during_a_pen_gesture_commits_and_clears_it() {
|
||||
|
||||
/// #501: a press on the first anchor closes the path, however far the drag then goes: the
|
||||
/// closing preview leaves the path closed (so not one the pen extends), and each move still
|
||||
/// previews the close with symmetric handles (Alt breaks them), released as a close.
|
||||
/// previews the close with the incoming handle mirroring the pointer (Alt keeps the outgoing one
|
||||
/// where it is), released as a close.
|
||||
#[test]
|
||||
fn a_drag_from_the_first_anchor_always_closes() {
|
||||
let pts = [Point::new(100.0, 300.0), Point::new(200.0, 300.0), Point::new(200.0, 400.0)];
|
||||
@@ -585,22 +581,53 @@ fn a_drag_from_the_first_anchor_always_closes() {
|
||||
let p = paint();
|
||||
let mut t = PenTool { drawing: true, ..PenTool::default() };
|
||||
let a = t.pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::Down, 101.0, 301.0));
|
||||
assert_eq!(a, vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), json!({"id": id.0, "subpath": 0}))]);
|
||||
assert_eq!(a, close_press(id, pts[0]));
|
||||
// What the tool sees once the preview applied: the path closed.
|
||||
d.node_mut(id).unwrap().path_data_mut().unwrap().subpaths[0].closed = true;
|
||||
let c = cx(&d, &s, &p);
|
||||
let close = |x: f64, y: f64, alt: bool| {
|
||||
vec![Action::Preview("path.close".into(), json!({"id": id.0, "in": [200.0 - x, 600.0 - y], "independent": alt, "subpath": 0}))]
|
||||
let close = |x: f64, y: f64| {
|
||||
vec![Action::Preview("path.close".into(), json!({"id": id.0, "in": [200.0 - x, 600.0 - y], "independent": false, "subpath": 0}))]
|
||||
};
|
||||
for (x, y) in [(105.0, 290.0), (130.0, 260.0), (160.0, 240.0)] {
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Drag, x, y)), close(x, y, false), "at ({x}, {y})");
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Drag, x, y)), close(x, y), "at ({x}, {y})");
|
||||
}
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 170.0, 250.0).with_mods(alt)), close(170.0, 250.0, true));
|
||||
let alt_close = json!({"id": id.0, "in": [30.0, 350.0], "out": [100.0, 300.0], "independent": true, "subpath": 0});
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 170.0, 250.0).with_mods(alt)),
|
||||
vec![Action::Preview("path.close".into(), alt_close)]
|
||||
);
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 170.0, 250.0)), vec![Action::Commit]);
|
||||
assert!(!t.busy() && t.active(&c).is_none(), "the path is done");
|
||||
}
|
||||
|
||||
/// #6634: on a smooth first anchor the closing drag turns the outgoing handle in line with the
|
||||
/// incoming one; Alt pressed mid-drag sends the outgoing handle where the drag had turned it.
|
||||
#[test]
|
||||
fn alt_mid_close_keeps_the_turned_outgoing_handle() {
|
||||
use vectorcraft_geom::{Anchor, SubPath};
|
||||
let anchors = vec![Anchor::smooth(Point::new(100.0, 300.0), Point::new(150.0, 300.0)), Anchor::corner(Point::new(200.0, 200.0))];
|
||||
let (mut d, id, s) = drawing(SubPath { anchors, closed: false });
|
||||
let p = paint();
|
||||
let mut t = PenTool { drawing: true, ..PenTool::default() };
|
||||
assert_eq!(t.pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::Down, 100.0, 300.0)), close_press(id, Point::new(100.0, 300.0)));
|
||||
d.node_mut(id).unwrap().path_data_mut().unwrap().subpaths[0].closed = true;
|
||||
let c = cx(&d, &s, &p);
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 100.0, 200.0)),
|
||||
vec![Action::Preview("path.close".into(), json!({"id": id.0, "in": [100.0, 400.0], "independent": false, "subpath": 0}))]
|
||||
);
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 130.0, 200.0).with_mods(alt)),
|
||||
vec![Action::Preview(
|
||||
"path.close".into(),
|
||||
json!({"id": id.0, "in": [70.0, 400.0], "out": [100.0, 250.0], "independent": true, "subpath": 0})
|
||||
)]
|
||||
);
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 130.0, 200.0)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_one_anchor_path_does_not_close_on_itself() {
|
||||
let (d, id, s) = drawing(vectorcraft_geom::SubPath::polyline(&[Point::new(10.0, 300.0)], false));
|
||||
|
||||
+9
-2
@@ -4007,6 +4007,13 @@ an `anchor` or `segment` in the same way. The document does not change and no un
|
||||
after the new anchor on its subpath are numbered one higher, and a selected segment that the new anchor splits stays
|
||||
selected as its two halves, with the new anchor as an end of both that moves with them. Undo restores the path and
|
||||
the selection in one step.
|
||||
`path.close {id, subpath?, in?, out?, independent?}` closes an open subpath (by default the last open one). `in` places
|
||||
the first anchor's incoming handle as `path.setHandle` places a handle: a smooth anchor turns its outgoing handle to
|
||||
stay in line and keeps that handle's length, and a corner keeps its outgoing handle where it is. With
|
||||
`independent: true` the anchor becomes a corner first, so its outgoing handle does not move. `out`, placed after `in`,
|
||||
is the first anchor's outgoing handle, and makes the anchor a corner. A Pen press on the first anchor sends `in` at
|
||||
the anchor with `independent: true`. The Pen's and the Smart Pen's closing drags send the mirror of the pointer as
|
||||
`in`, and once Alt is held they also send `out` at the outgoing handle's position when Alt went down.
|
||||
|
||||
`path.reshape {id, x, y, dx, dy, tol?, focal?, radius?}` (a Reshape tool drag) grabs the path at `x`, `y`, adding an
|
||||
anchor there unless one lies within `tol` (3 by default), and moves that point by `dx`, `dy`.
|
||||
@@ -4055,8 +4062,8 @@ an `x` without a `y`, or a `y` without an `x`, with "x and y must be given toget
|
||||
`path.blob` refuse a point in `points` whose x or y is not a number, such as `["10", 10]` or a point written as a
|
||||
string, with "x and y must both be numbers". All of them except `path.curvature` refuse a pen pressure, tilt or
|
||||
rotation that is neither a number nor null with `` `pressure` must be a number, not "0.5" ``, naming the key
|
||||
(`pressure`, `tiltX`, `tiltY` or `rotation`). `path.close` refuses an `in` that is not `[x, y]`
|
||||
with `` `in` must be [x, y] ``.
|
||||
(`pressure`, `tiltX`, `tiltY` or `rotation`). `path.close` refuses an `in` or `out` that is not `[x, y]`
|
||||
with `` `in` must be [x, y] ``, naming the key.
|
||||
|
||||
These commands refuse a string, a number or another value that is not true or false with
|
||||
`` `closed` must be true or false ``, naming the key; the document does not change and no undo step is recorded:
|
||||
|
||||
@@ -959,7 +959,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Align, Distribute, Distribute Spacing, Reflect and Shear return an error for any `horizontal`, `vertical`, `to`, `axis`, `bounds` or `spacing` value they don't take, listing the choices, and Align returns one when a call gives neither `horizontal` nor `vertical`; nothing moves and no undo step is recorded. Align and Distribute act on the anchor points selected with Direct Selection: the points of one path align to each other, not to the artboard, and Distribute spaces them (#1641, fixes #1636; aligning to a key anchor is still missing).
|
||||
Transform Each opens on the scale, move, rotate, reflect, Random, reference point, Transform Objects and Preview it was last confirmed with (kept with the UI state; Copy never is), and its Copy button, left of Cancel, transforms a copy of each object as the [Scale multiple objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/scale-multiple-objects.html) help page has it; the Copy checkbox is gone (#1732; the remembered values follow a maintainer comment on the issue, not the help page). Its Scale section has the Relative and Absolute radio buttons the same page describes: percentages of each object's size, or the width and height it becomes (`object.transformEach` `relative`, which Transform Again repeats); Absolute measures each object's own box, and the scales act along its axes about that box's reference point, so a turned shape keeps its shape and objects of different sizes come out alike; the fields show the width and height the selected objects share and read "Mixed" when they differ or a side is 0 (a line's), each object measured by its own box, rotated with it, as the Transform panel and the Control bar show it (sizes they show alike count as the same), and a size typed into them sets every object to it (a side left Mixed keeps its size), our choices where the page leaves them open; a live Repeat scales its spacing by the geometric mean of the two factors, so its Absolute size is approximate. The link beside the two scales keeps them in proportion, as the shape dialogs' Width and Height link does (`__constrain`, kept with the dialog's values; where it sits is our choice, the public pages don't establish it). The panel lists Scale, Move and Rotate, then the Options in two columns (Transform Objects, Transform Patterns, Scale Strokes & Effects, Scale Corners; Reflect X, Reflect Y, Random), the Reference Point, and Preview at the left of the button row, level with Copy, Cancel and OK.
|
||||
Transform Again uses the Copy, Transform Patterns, Scale Strokes & Effects and Scale Corners choices of the transform it repeats; after a bounding-box handle drag on area type, it resizes the type area again and the type keeps its size (`object.transformAgain`). With anchors selected with Direct Selection, Move, Rotate, Scale, Reflect, Shear and Transform Again (the commands, their dialogs and the four tools' drags) transform only those anchors, their handles with them, about the centre of the box around them unless a point was clicked; a copy still copies the whole object (#1408; behaviour reference: [adjust path segments](https://helpx.adobe.com/illustrator/using/adjust-path-segments.html); the default reference point and the copy are our choices). The Move, Rotate, Scale, Reflect and Shear dialogs have a Preview checkbox, off when they open: while it is on the canvas follows the values, Cancel rolls it back and OK is still one undo step; Up/Down step the Rotate and Shear angles and the Scale percentages by 1, Shift by 10 (#1412). The five dialogs and Transform Each have Transform Objects beside Transform Patterns: off, the art stays and only its pattern tiles transform (`objects` on the transform commands), and Transform Again repeats the choice (#1448; behaviour reference: [scale objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/scale-objects.html); the checkboxes are always enabled and the Scale page's pair is applied to all of them, our choices). Reflect's axis is Horizontal, Vertical or an angle (#1460; [reflect objects along an axis](https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/reflect-objects-along-an-axis.html)).
|
||||
- **Pen:** Alt pressed in the middle of dragging out an anchor splits its handles and keeps the curve already shaped into it (only the outgoing handle follows the pointer); Space held while dragging out an anchor moves it, handles and all. A press on the first anchor of the path being drawn closes it whatever the drag then does: the drag shapes the closing curve (symmetric handles, Alt breaks them) and the release closes the path wherever it ends (#501). Clicking the last anchor of the path being drawn retracts its outgoing handle (convert cursor), and dragging from it pulls out a new one on its own. Auto Add/Delete: over a selected path, a click on a segment adds an anchor and a click on an anchor deletes it (add and delete cursors); Shift held, or Preferences › General › Disable Auto Add/Delete, starts a new path instead. Alt held over a handle end or an anchor of a selected path (the one being drawn too) works as the Anchor Point tool, with its convert cursor: dragging a handle moves it alone, clicking a smooth anchor makes it a corner, dragging an anchor pulls out new symmetric handles, dragging a segment reshapes it; drawing then goes on. Cmd (Ctrl) held drags with the selection tool chosen last, Direct Selection until one is (handles, anchors and segments of the path being drawn or another), and the Pen then goes on drawing its path while that stays selected; the pointer shows that tool's while Cmd is held. The same holds for agents (`pointer_gesture` with `mods.cmd`), as the borrowing now lives in the engine (#494). A drag's preview (the Pen's, the Anchor Point tool's, Direct Selection's handles and anchors) follows every position the pointer is seen at (#834): the canvas sends the tool a drag whenever the pointer's document position differs from the last one it sent, not only when egui's frame delta is non-zero (zero when a compositor coalesces motion and in the frame the pointer comes back into the window after leaving it), and a move that arrives in the release's own frame shapes the preview before the release commits it, once the press has dragged. With the Pen, a press that moves under 3 screen pixels is a click (a corner without handles), and a Precise Cursors command toggles the existing preference and can take a shortcut (#1663).
|
||||
- **Pen:** Alt pressed in the middle of dragging out an anchor splits its handles and keeps the curve already shaped into it (only the outgoing handle follows the pointer); Space held while dragging out an anchor moves it, handles and all. A press on the first anchor of the path being drawn closes it whatever the drag then does: the drag shapes the closing curve and the release closes the path wherever it ends (#501). The first anchor's incoming handle mirrors the pointer, and a click leaves it retracted and the anchor a corner. Its outgoing handle stays where it is on a corner, so the first segment keeps its shape, and on a smooth anchor it turns to stay in line at its own length; Alt makes the anchor a corner and leaves the outgoing handle where it was when Alt went down. The Smart Pen's closing drag does the same (fixes #6634; `path.close`'s `in` and `out`; behavior reference: [draw with the Pen, Curvature, or Pencil tool](https://web.archive.org/web/20220614224656/https://helpx.adobe.com/illustrator/using/drawing-pen-curvature-or-pencil.html), where a click or drag on the first anchor closes the path and Alt breaks the pairing of the closing anchor's handles; which handles the drag sets are our choices). Moving the closing anchor with Space held, which that page also describes, is still missing. Clicking the last anchor of the path being drawn retracts its outgoing handle (convert cursor), and dragging from it pulls out a new one on its own. Auto Add/Delete: over a selected path, a click on a segment adds an anchor and a click on an anchor deletes it (add and delete cursors); Shift held, or Preferences › General › Disable Auto Add/Delete, starts a new path instead. Alt held over a handle end or an anchor of a selected path (the one being drawn too) works as the Anchor Point tool, with its convert cursor: dragging a handle moves it alone, clicking a smooth anchor makes it a corner, dragging an anchor pulls out new symmetric handles, dragging a segment reshapes it; drawing then goes on. Cmd (Ctrl) held drags with the selection tool chosen last, Direct Selection until one is (handles, anchors and segments of the path being drawn or another), and the Pen then goes on drawing its path while that stays selected; the pointer shows that tool's while Cmd is held. The same holds for agents (`pointer_gesture` with `mods.cmd`), as the borrowing now lives in the engine (#494). A drag's preview (the Pen's, the Anchor Point tool's, Direct Selection's handles and anchors) follows every position the pointer is seen at (#834): the canvas sends the tool a drag whenever the pointer's document position differs from the last one it sent, not only when egui's frame delta is non-zero (zero when a compositor coalesces motion and in the frame the pointer comes back into the window after leaving it), and a move that arrives in the release's own frame shapes the preview before the release commits it, once the press has dragged. With the Pen, a press that moves under 3 screen pixels is a click (a corner without handles), and a Precise Cursors command toggles the existing preference and can take a shortcut (#1663).
|
||||
- **Direct Selection (#1278):** clicking a segment picks its connection, with hollow endpoint anchors and only that curve's direction handles. Delete leaves the neighboring curves and appearance intact, opens/splits contours in one undo step, and another Delete removes the remaining selected path. A whole-path drag and deliberate anchor selection keep their existing behavior; egui frames and native control/art checked, OS mouse/visual QA pending. Where anchors of overlapping objects coincide, a click or drag takes the anchor of the selected object, also when another object lies over it (#1509, fixes #1508). Delete removes every selected anchor of a closed path (it used to remove the wrong ones), removes objects selected whole along with the selected anchors of others, and keeps the remainder selected so a second Delete removes it (#1674). In isolation mode Direct Selection and Group Selection pick only inside the isolated object (#1666). Escape cancels a Direct Selection drag (#1803, #1743). Double-clicking type (including type nested inside a group) switches to the Type tool and puts the caret at the clicked character.
|
||||
With Direct Selection and Group Selection, a drag that starts on an object while the selection also holds direct-selected anchors or segments moves those anchors and the objects selected whole in one undo step (`path.moveAnchors`), as a drag of a selected anchor does, and the other anchors of a partly selected path stay in place. While Alt is held during the drag, it copies the selected objects instead (`object.transform`). When direct-selected anchors are moved or transformed together with objects selected whole (such a drag, an anchor drag, an arrow key, or Move, Rotate, Scale, Reflect and Shear without a copy), a path inside a selected group is transformed once, with the group.
|
||||
A path segment with a control point whose x or y is infinite or larger in size than `f64::MAX / 16` is measured by its control points, so its bounds hold the whole curve and the canvas check refuses an edit that leaves such a path selected (`path.create` included).
|
||||
|
||||
Reference in New Issue
Block a user