Merge pull request #517 from storytold/fix/issue-494
Fix #494: Pen + Cmd edits the path being drawn; Pen + Alt reshapes segments; handle drags fixed and fast
This commit is contained in:
commit
3c91ca8a1d
File diff suppressed because one or more lines are too long
|
|
@ -70,6 +70,49 @@ fn load_average() -> Option<f64> {
|
|||
text.split_whitespace().map(|t| t.trim_matches(|c| c == '{' || c == '}')).find_map(|t| t.parse().ok())
|
||||
}
|
||||
|
||||
/// Milliseconds per pointer move of a Direct Selection drag of a handle, then of a segment, of a
|
||||
/// 5,000-anchor wave of smooth anchors added on top of the synthetic `n`-path document.
|
||||
fn edit_drags(n: usize) -> Result<(f64, f64), String> {
|
||||
use vectorcraft_engine::tools::{Mods, PointerEvent, PointerKind};
|
||||
use vectorcraft_geom::{Anchor, PathData, SubPath, Vec2};
|
||||
let mut doc = synthetic(n)?;
|
||||
let l = doc.layers.first().map(|l| l.id);
|
||||
let id = doc.alloc_id();
|
||||
// Anchors 2 pt apart, up and down by 20 pt, their handles 0.6 pt long.
|
||||
let anchor = |i: usize| {
|
||||
let p = Point::new(i as f64 * 2.0, if i.is_multiple_of(2) { 500.0 } else { 520.0 });
|
||||
Anchor::smooth(p, p + Vec2::new(0.6, 0.0))
|
||||
};
|
||||
let path = PathData::single(SubPath::new((0..5000).map(anchor).collect(), false));
|
||||
doc.insert(l, usize::MAX, Node::path(id, path, Appearance::default_art())).map_err(|e| e.to_string())?;
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.add_document(doc, None);
|
||||
let view = vectorcraft_engine::ViewInfo { zoom: 8.0, ..Default::default() };
|
||||
let err = |e: vectorcraft_engine::EngineError| e.to_string();
|
||||
let mut drag = |down: Point| -> Result<f64, String> {
|
||||
let ev = |kind, y: f64| PointerEvent { kind, pos: down + Vec2::new(0.0, y), mods: Mods::default(), pressure: 1.0 };
|
||||
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 400]]})).map_err(err)?;
|
||||
s.select_tool("directSelection", view).map_err(err)?;
|
||||
let undo = s.doc().map_err(err)?.history.undo.len();
|
||||
s.pointer(&ev(PointerKind::Down, 0.0), view).map_err(err)?;
|
||||
let mut y = 0.0;
|
||||
let ms = median_ms(9, || {
|
||||
y += 3.0;
|
||||
// A failed move shows as the missing undo step below.
|
||||
let _ = s.pointer(&ev(PointerKind::Drag, y), view);
|
||||
});
|
||||
s.pointer(&ev(PointerKind::Up, y), view).map_err(err)?;
|
||||
if s.doc().map_err(err)?.history.undo.len() != undo + 1 {
|
||||
return Err(format!("the drag from {down:?} made no undo step"));
|
||||
}
|
||||
Ok(ms)
|
||||
};
|
||||
// Anchor 400 is at (800, 500): its outgoing handle's end, then its segment to anchor 401.
|
||||
let handle = drag(Point::new(800.6, 500.0))?;
|
||||
let segment = drag(Point::new(801.0, 510.0))?;
|
||||
Ok((handle, segment))
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let mut n = 50_000usize;
|
||||
let mut it = args.iter();
|
||||
|
|
@ -145,6 +188,15 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
|||
}
|
||||
});
|
||||
rows.push(("Pathfinder Unite, 1,000 paths", unite, 150.0));
|
||||
|
||||
// Path editing: one pointer move of a Direct Selection drag (the tool event and the preview it
|
||||
// applies), on a 5,000-anchor path among the synthetic paths.
|
||||
let (handle, segment) = edit_drags(n).unwrap_or_else(|e| {
|
||||
failed = Some(e);
|
||||
(0.0, 0.0)
|
||||
});
|
||||
rows.push(("handle drag, 5,000-anchor path (per move)", handle, 8.0));
|
||||
rows.push(("segment drag, 5,000-anchor path (per move)", segment, 8.0));
|
||||
if let Some(e) = failed {
|
||||
return Err(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -701,13 +701,16 @@ fn reshape_segment(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
return Err(EngineError::Other("no such segment".into()));
|
||||
}
|
||||
let n = sp.anchors.len();
|
||||
// Moving both inner control points by v moves B(t) by 3t(1-t)·v.
|
||||
// Moving both inner control points by v moves B(t) by 3t(1-t)·v. A smooth anchor stays
|
||||
// smooth: its other handle turns with the moved one.
|
||||
let v = dv / (3.0 * t * (1.0 - t));
|
||||
let (i0, i1) = (seg % n, (seg + 1) % n);
|
||||
let a = sp.anchors[i0];
|
||||
sp.anchors[i0] = Anchor::with_handles(a.p, a.h_in, a.h_out + v);
|
||||
let b = sp.anchors[i1];
|
||||
sp.anchors[i1] = Anchor::with_handles(b.p, b.h_in + v, b.h_out);
|
||||
if let Some(a) = sp.anchors.get_mut(i0) {
|
||||
a.set_handle(true, a.h_out + v, false);
|
||||
}
|
||||
if let Some(b) = sp.anchors.get_mut(i1) {
|
||||
b.set_handle(false, b.h_in + v, false);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
|
|
|
|||
|
|
@ -721,6 +721,12 @@ pub struct Session {
|
|||
/// Executed commands (for actions and debugging).
|
||||
pub journal: Vec<(String, Value)>,
|
||||
pub(crate) tool: Box<dyn Tool>,
|
||||
/// While Cmd lends `tool` (a selection tool) for a drag: the tool it was lent to, which comes
|
||||
/// back as it was at the release ([`Session::pointer`]).
|
||||
pub(crate) lender: Option<Box<dyn Tool>>,
|
||||
/// The selection tool chosen last (Selection, Direct Selection or Group Selection): the one Cmd
|
||||
/// lends the other tools. None until one is chosen.
|
||||
pub(crate) last_selection_tool: Option<&'static str>,
|
||||
pub(crate) last_view: ViewInfo,
|
||||
depth: u32,
|
||||
/// Set when the active tool panicked (see [`guard`]); reported by the next tool event.
|
||||
|
|
@ -800,6 +806,8 @@ impl Session {
|
|||
clipboard: Clipboard::default(),
|
||||
journal: vec![],
|
||||
tool: vectorcraft_tools::create("selection"),
|
||||
lender: None,
|
||||
last_selection_tool: None,
|
||||
last_view: ViewInfo::default(),
|
||||
depth: 0,
|
||||
tool_panic: None,
|
||||
|
|
@ -858,6 +866,8 @@ impl Session {
|
|||
return;
|
||||
}
|
||||
let view = self.last_view;
|
||||
// The switch goes on whatever the lent tool's last actions did, as for the deactivation.
|
||||
let _ = self.give_back_tool(view);
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx));
|
||||
let _ = self.apply_actions(acts);
|
||||
// A batch leaves its interaction open in the document it leaves, to keep or roll back with
|
||||
|
|
|
|||
|
|
@ -57,6 +57,20 @@ fn click(s: &mut Session, tool: &str, x: f64, y: f64) -> Vec<UiRequest> {
|
|||
drag(s, tool, &[(x, y)], Mods::default())
|
||||
}
|
||||
|
||||
/// A gesture of the active tool, `m` held throughout: press at the first point, drag through the
|
||||
/// others, release at the last. The undo steps it made.
|
||||
fn gesture(s: &mut Session, pts: &[(f64, f64)], m: Mods) -> usize {
|
||||
let v = view();
|
||||
let undo = undo_steps(s);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, pts[0].0, pts[0].1).with_mods(m), v).unwrap();
|
||||
for &(x, y) in &pts[1..] {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, x, y).with_mods(m), v).unwrap();
|
||||
}
|
||||
let l = pts[pts.len() - 1];
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, l.0, l.1).with_mods(m), v).unwrap();
|
||||
undo_steps(s) - undo
|
||||
}
|
||||
|
||||
/// A wavy stroke sampled every 2 pt.
|
||||
fn wave(x0: f64, x1: f64, y: f64) -> Vec<(f64, f64)> {
|
||||
let n = ((x1 - x0) / 2.0) as usize;
|
||||
|
|
@ -622,16 +636,6 @@ fn pen_with_alt_moves_one_handle_and_converts_anchors() {
|
|||
let anchors = json!([{"x": 100, "y": 200}, {"x": 200, "y": 100, "in": [150, 100], "out": [250, 100]}, {"x": 300, "y": 200}]);
|
||||
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
s.select_tool("pen", v).unwrap();
|
||||
let gesture = |s: &mut Session, pts: &[(f64, f64)], m: Mods| {
|
||||
let undo = undo_steps(s);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, pts[0].0, pts[0].1).with_mods(m), v).unwrap();
|
||||
for &(x, y) in &pts[1..] {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, x, y).with_mods(m), v).unwrap();
|
||||
}
|
||||
let l = pts[pts.len() - 1];
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, l.0, l.1).with_mods(m), v).unwrap();
|
||||
undo_steps(s) - undo
|
||||
};
|
||||
// Alt-drag the out handle: it moves alone, the in handle stays.
|
||||
assert_eq!(gesture(&mut s, &[(250.0, 100.0), (260.0, 130.0), (275.0, 165.0)], alt), 1);
|
||||
assert!(!s.in_interaction());
|
||||
|
|
@ -659,3 +663,74 @@ fn pen_with_alt_moves_one_handle_and_converts_anchors() {
|
|||
assert_eq!(path(&s, new).subpaths[0].anchors.len(), 3);
|
||||
assert_eq!(paths(&s).len(), 2);
|
||||
}
|
||||
|
||||
/// The anchor's handles point opposite ways.
|
||||
fn smooth(a: vectorcraft_geom::Anchor) -> bool {
|
||||
let (i, o) = (a.h_in - a.p, a.h_out - a.p);
|
||||
(i.x * o.y - i.y * o.x).abs() < 1e-6 && i.x * o.x + i.y * o.y < 0.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pen_with_cmd_borrows_direct_selection_and_goes_on_drawing() {
|
||||
// #494: Cmd (Ctrl) held with the Pen drags with the selection tool used last (Direct Selection
|
||||
// until one is chosen): handles, segments and anchors of the path being drawn; released, the
|
||||
// Pen goes on drawing that path. Through pointer events, so agents reach it too.
|
||||
let (none, cmd) = (Mods::default(), Mods { cmd: true, ..Mods::default() });
|
||||
let v = view();
|
||||
let mut s = session();
|
||||
s.select_tool("pen", v).unwrap();
|
||||
gesture(&mut s, &[(100.0, 300.0)], none);
|
||||
gesture(&mut s, &[(200.0, 300.0), (250.0, 300.0)], none);
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
assert_eq!(s.cursor(Point::new(250.0, 300.0), none, v), vectorcraft_tools::Cursor::Pen);
|
||||
assert_eq!(s.cursor(Point::new(250.0, 300.0), cmd, v), vectorcraft_tools::Cursor::ArrowHollow, "Direct Selection's");
|
||||
// The new anchor's outgoing handle: it moves, its incoming one turning with it.
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 250.0, 300.0).with_mods(cmd), v).unwrap();
|
||||
assert_eq!(s.tool_id(), "directSelection");
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 250.0, 260.0).with_mods(cmd), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 250.0, 260.0).with_mods(cmd), v).unwrap();
|
||||
assert_eq!(s.tool_id(), "pen");
|
||||
let a = path(&s, id).subpaths[0].anchors[1];
|
||||
assert!(a.h_out == Point::new(250.0, 260.0) && smooth(a), "{a:?}");
|
||||
// The segment between the two anchors bends, the smooth anchor staying smooth.
|
||||
let before = path(&s, id).subpaths[0].anchors[1].h_in;
|
||||
gesture(&mut s, &[(120.0, 300.0), (120.0, 270.0)], cmd);
|
||||
let a = path(&s, id).subpaths[0].anchors[1];
|
||||
assert!(a.h_in != before && smooth(a), "{a:?}");
|
||||
assert_eq!(s.tool_id(), "pen");
|
||||
// A plain click goes on drawing the same path.
|
||||
gesture(&mut s, &[(300.0, 350.0)], none);
|
||||
assert_eq!((paths(&s).len(), path(&s, id).subpaths[0].anchors.len()), (1, 3));
|
||||
// A Cmd-click away from the art deselects: the path is done, the next click starts another.
|
||||
gesture(&mut s, &[(600.0, 100.0)], cmd);
|
||||
gesture(&mut s, &[(400.0, 500.0)], none);
|
||||
assert_eq!(paths(&s).len(), 2);
|
||||
assert_eq!(path(&s, id).subpaths[0].anchors.len(), 3);
|
||||
// With the Selection tool chosen last, Cmd lends it.
|
||||
s.select_tool("selection", v).unwrap();
|
||||
s.select_tool("pen", v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 600.0, 100.0).with_mods(cmd), v).unwrap();
|
||||
assert_eq!(s.tool_id(), "selection");
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 600.0, 100.0).with_mods(cmd), v).unwrap();
|
||||
assert_eq!(s.tool_id(), "pen");
|
||||
// Choosing a tool mid-gesture keeps it: the Pen doesn't come back over it.
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 600.0, 100.0).with_mods(cmd), v).unwrap();
|
||||
s.select_tool("rectangle", v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 600.0, 100.0), v).unwrap();
|
||||
assert_eq!(s.tool_id(), "rectangle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reshaping_a_segment_keeps_smooth_anchors_smooth() {
|
||||
// #494: the anchors at the ends of a dragged segment keep their kind: a smooth one's other
|
||||
// handle turns with the moved one (at its length), a corner's stays.
|
||||
let mut s = session();
|
||||
let anchors = json!([{"x": 100, "y": 300}, {"x": 200, "y": 300, "in": [160, 300], "out": [240, 300]}, {"x": 300, "y": 300}]);
|
||||
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("path.reshapeSegment", &json!({"id": id.0, "segment": 0, "t": 0.5, "dx": 0, "dy": -30})).unwrap();
|
||||
let sp = &path(&s, id).subpaths[0];
|
||||
let a = sp.anchors[1];
|
||||
assert!(near(a.h_in, Point::new(160.0, 260.0)) && smooth(a), "{a:?}");
|
||||
assert!((a.h_out.distance(a.p) - 40.0).abs() < 1e-9, "its length kept");
|
||||
assert!(near(sp.anchors[0].h_out, Point::new(100.0, 260.0)) && !sp.anchors[0].has_in(), "the corner end: {:?}", sp.anchors[0]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ impl Session {
|
|||
/// Switch to tool `id` without choosing it afresh (back from a temporary tool: a hidden
|
||||
/// perspective grid stays hidden), finishing any pending tool work first.
|
||||
pub fn switch_tool(&mut self, id: &str, view: ViewInfo) -> Result<()> {
|
||||
self.give_back_tool(view)?;
|
||||
if self.tool.id() == id {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -89,9 +90,47 @@ impl Session {
|
|||
self.apply_actions(acts)?;
|
||||
self.keep_tool_settings();
|
||||
self.tool = self.make_tool(id);
|
||||
if vectorcraft_tools::catalog::is_selection_tool(id) {
|
||||
self.last_selection_tool = Some(self.tool.id());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The selection tool Cmd lends tool `id` for a drag: the one chosen last; until one is,
|
||||
/// Direct Selection to the tools that edit anchors (the Pen) and Selection to the others.
|
||||
/// None for the selection tools themselves and the view tools.
|
||||
fn lent_tool(&self, id: &str) -> Option<&'static str> {
|
||||
use vectorcraft_tools::catalog::{edits_anchors, is_selection_tool};
|
||||
if is_selection_tool(id) || matches!(id, "hand" | "zoom" | "rotateView") {
|
||||
return None;
|
||||
}
|
||||
Some(self.last_selection_tool.unwrap_or(if edits_anchors(id) { "directSelection" } else { "selection" }))
|
||||
}
|
||||
|
||||
/// A press with Cmd held (Ctrl elsewhere) and any other tool than a selection tool: the
|
||||
/// selection tool [`Self::lent_tool`] takes the gesture, pressing without Cmd (a plain click,
|
||||
/// not Select Behind). The tool lent to stays as it was, unaware, until the release.
|
||||
fn lend_selection_tool(&mut self, ev: &PointerEvent) -> PointerEvent {
|
||||
if ev.kind != PointerKind::Down || !ev.mods.cmd || self.lender.is_some() {
|
||||
return *ev;
|
||||
}
|
||||
let Some(id) = self.lent_tool(self.tool.id()) else { return *ev };
|
||||
let lent = self.make_tool(id);
|
||||
self.lender = Some(std::mem::replace(&mut self.tool, lent));
|
||||
PointerEvent { mods: Mods { cmd: false, ..ev.mods }, ..*ev }
|
||||
}
|
||||
|
||||
/// The lent selection tool's gesture is over: the tool it was lent to is back as it was (the
|
||||
/// Pen goes on drawing its path).
|
||||
pub(crate) fn give_back_tool(&mut self, view: ViewInfo) -> Result<()> {
|
||||
let Some(lender) = self.lender.take() else { return Ok(()) };
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx));
|
||||
let r = self.apply_actions(acts);
|
||||
self.keep_tool_settings();
|
||||
self.tool = lender;
|
||||
r.map(drop)
|
||||
}
|
||||
|
||||
/// A fresh `id` tool with the options it keeps ([`settings`]): its last values, from this
|
||||
/// session or a saved one.
|
||||
pub(crate) fn make_tool(&self, id: &str) -> Box<dyn Tool> {
|
||||
|
|
@ -212,13 +251,19 @@ impl Session {
|
|||
if let Some(r) = self.plane_widget_pointer(ev, view) {
|
||||
return r;
|
||||
}
|
||||
let ev = &self.lend_selection_tool(ev);
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.pointer(cx, ev));
|
||||
self.take_tool_panic()?;
|
||||
// A gesture may change options (Alt-drag sizes a Liquify brush): keep them.
|
||||
let r = self.take_tool_panic().and_then(|()| {
|
||||
// A gesture may change options (Alt-drag sizes a Liquify brush): keep them.
|
||||
if ev.kind == PointerKind::Up {
|
||||
self.keep_tool_settings();
|
||||
}
|
||||
self.apply_actions(acts)
|
||||
});
|
||||
if ev.kind == PointerKind::Up {
|
||||
self.keep_tool_settings();
|
||||
self.give_back_tool(view)?;
|
||||
}
|
||||
self.apply_actions(acts)
|
||||
r
|
||||
}
|
||||
|
||||
/// A guide dragged out of a ruler, whatever the tool: a vertical one out of the left ruler, a
|
||||
|
|
@ -325,9 +370,20 @@ impl Session {
|
|||
}
|
||||
|
||||
/// The pointer the active tool shows at `p` (General › Use Precise Cursors makes the drawing
|
||||
/// tools' a crosshair, [`Cursor::precise`]).
|
||||
/// tools' a crosshair, [`Cursor::precise`]). With Cmd held, the pointer of the selection tool a
|
||||
/// press would borrow ([`Self::lend_selection_tool`]).
|
||||
pub fn cursor(&mut self, p: Point, mods: Mods, view: ViewInfo) -> Cursor {
|
||||
let c = self.with_tool_cx(view, |t, cx| t.cursor(cx, p, mods));
|
||||
let lent = if mods.cmd && self.lender.is_none() { self.lent_tool(self.tool.id()) } else { None };
|
||||
let c = match lent {
|
||||
Some(id) => {
|
||||
let lent = self.make_tool(id);
|
||||
let own = std::mem::replace(&mut self.tool, lent);
|
||||
let c = self.with_tool_cx(view, |t, cx| t.cursor(cx, p, Mods { cmd: false, ..mods }));
|
||||
self.tool = own;
|
||||
c
|
||||
}
|
||||
None => self.with_tool_cx(view, |t, cx| t.cursor(cx, p, mods)),
|
||||
};
|
||||
if self.prefs.use_precise_cursors { c.precise() } else { c }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ mod tests_links;
|
|||
#[cfg(test)]
|
||||
mod tests_liquify;
|
||||
#[cfg(test)]
|
||||
mod tests_pen;
|
||||
#[cfg(test)]
|
||||
mod tests_persp;
|
||||
#[cfg(test)]
|
||||
mod tests_place;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
//! The Pen over MCP, headless: `pointer_gesture` with the modifiers held as on the desktop.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{Backend, Headless, call_tool};
|
||||
|
||||
fn text(r: &crate::ToolResult) -> Value {
|
||||
assert!(!r.is_error, "{r:?}");
|
||||
serde_json::from_str(r.content[0]["text"].as_str().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// A press at the first point, drags through the others, the release at the last.
|
||||
fn gesture(pts: &[(f64, f64)]) -> Value {
|
||||
let (first, last) = (pts[0], pts[pts.len() - 1]);
|
||||
let mut events = vec![json!({"kind": "down", "x": first.0, "y": first.1})];
|
||||
events.extend(pts[1..].iter().map(|(x, y)| json!({"kind": "drag", "x": x, "y": y})));
|
||||
events.push(json!({"kind": "up", "x": last.0, "y": last.1}));
|
||||
json!({ "events": events })
|
||||
}
|
||||
|
||||
/// The anchors of each path, in paint order.
|
||||
fn paths(h: &Headless) -> Vec<Vec<vectorcraft_geom::Anchor>> {
|
||||
let mut v = vec![];
|
||||
h.session.doc().unwrap().doc.walk(|n| {
|
||||
if let Some(p) = n.path_data() {
|
||||
v.push(p.subpaths.iter().flat_map(|s| s.anchors.clone()).collect());
|
||||
}
|
||||
});
|
||||
v
|
||||
}
|
||||
|
||||
/// #494: Cmd held at the press borrows Direct Selection from the Pen; the path goes on afterwards.
|
||||
#[test]
|
||||
fn cmd_with_the_pen_edits_the_path_being_drawn() {
|
||||
let mut h = Headless::new();
|
||||
h.call("engine.execute", json!({"command": "file.new", "params": {"width": 800, "height": 600}})).unwrap();
|
||||
text(&call_tool(&mut h, "select_tool", &json!({"tool": "pen"})));
|
||||
text(&call_tool(&mut h, "pointer_gesture", &gesture(&[(100.0, 300.0)])));
|
||||
text(&call_tool(&mut h, "pointer_gesture", &gesture(&[(200.0, 300.0), (250.0, 300.0)])));
|
||||
let mut drag = gesture(&[(250.0, 300.0), (250.0, 280.0), (250.0, 263.0)]);
|
||||
drag["mods"] = json!({"cmd": true});
|
||||
let r = text(&call_tool(&mut h, "pointer_gesture", &drag));
|
||||
assert_eq!(r["tool"], "pen", "{r}");
|
||||
assert_eq!(paths(&h)[0][1].h_out, vectorcraft_geom::Point::new(250.0, 263.0));
|
||||
text(&call_tool(&mut h, "pointer_gesture", &gesture(&[(300.0, 350.0)])));
|
||||
assert_eq!(paths(&h).iter().map(Vec::len).collect::<Vec<_>>(), [3], "the same path, three anchors");
|
||||
}
|
||||
|
|
@ -154,6 +154,17 @@ pub fn group_of(id: &str) -> Option<usize> {
|
|||
TOOL_GROUPS.iter().position(|g| g.iter().any(|t| t.id == id))
|
||||
}
|
||||
|
||||
/// Selection, Direct Selection and Group Selection: the tools Cmd held lends the others for a drag.
|
||||
pub fn is_selection_tool(id: &str) -> bool {
|
||||
matches!(id, "selection" | "directSelection" | "groupSelection")
|
||||
}
|
||||
|
||||
/// The tools that show a selected path's anchors and handles as Direct Selection does, and drag
|
||||
/// them: a Cmd drag with one of them is a Direct Selection drag until a selection tool is chosen.
|
||||
pub fn edits_anchors(id: &str) -> bool {
|
||||
matches!(id, "directSelection" | "pen" | "addAnchor" | "deleteAnchor" | "anchorPoint" | "curvature")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -178,5 +189,7 @@ mod tests {
|
|||
assert_eq!(tool_for_shortcut("V").unwrap().id, "selection");
|
||||
assert_eq!(tool_for_shortcut("Shift+M").unwrap().id, "shapeBuilder");
|
||||
assert_eq!(group_of("star"), group_of("rectangle"));
|
||||
assert!(is_selection_tool("groupSelection") && !is_selection_tool("pen"));
|
||||
assert!(edits_anchors("pen") && !edits_anchors("rectangle"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@ use std::borrow::Cow;
|
|||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::hit::{HitKind, hit_test};
|
||||
use vectorcraft_doc::{AnchorRef, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{PathData, Point, Rect};
|
||||
use vectorcraft_geom::{Anchor, PathData, Point, Rect};
|
||||
|
||||
use crate::bbox::move_delta;
|
||||
use crate::corners::{self, CornerDrag, over_widget};
|
||||
use crate::guides::{PointSnap, Targets};
|
||||
use crate::guides::{HandleSnap, PointSnap, Targets};
|
||||
use crate::meshedit::MeshEdit;
|
||||
use crate::pathtype::{self, BracketDrag, over_bracket};
|
||||
use crate::rulerguide::GuideEdit;
|
||||
|
|
@ -112,6 +112,8 @@ pub struct DirectSelectionTool {
|
|||
guides: Vec<Overlay>,
|
||||
/// What the dragged anchors snap to, gathered when the drag begins.
|
||||
anchor_snap: Option<PointSnap>,
|
||||
/// What the dragged handle snaps to.
|
||||
handle_snap: HandleSnap,
|
||||
/// What the objects being moved snap to, gathered when the move begins.
|
||||
move_snap: Option<MoveSnap>,
|
||||
/// The anchor under the pointer (Highlight anchors on mouse over): its path, where it is.
|
||||
|
|
@ -128,6 +130,7 @@ impl DirectSelectionTool {
|
|||
mesh: MeshEdit::default(),
|
||||
guides: vec![],
|
||||
anchor_snap: None,
|
||||
handle_snap: HandleSnap::default(),
|
||||
move_snap: None,
|
||||
hover: None,
|
||||
guide: GuideEdit::default(),
|
||||
|
|
@ -217,30 +220,37 @@ fn hit_frame_edge(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, Vec<A
|
|||
})
|
||||
}
|
||||
|
||||
/// Selection & Anchor Display → Show handles when multiple anchors are selected: off, handles show
|
||||
/// (and drag) only while a single anchor is selected.
|
||||
pub fn handles_shown(selection: &vectorcraft_doc::Selection, multiple: bool) -> bool {
|
||||
multiple || selection.anchors.values().map(|set| set.len()).sum::<usize>() <= 1
|
||||
/// The anchors whose direction handles show, and drag, with the tools that edit anchors
|
||||
/// ([`crate::catalog::edits_anchors`]): the direct-selected anchors of a path, every anchor of a
|
||||
/// path selected as a whole. Selection & Anchor Display → Show handles when multiple anchors are
|
||||
/// selected off, none while more than one anchor is selected.
|
||||
fn handle_anchors(cx: &ToolContext) -> Vec<(NodeId, usize, usize, Anchor)> {
|
||||
let mut v = vec![];
|
||||
for &id in &cx.selection.objects {
|
||||
let Some(pd) = cx.doc.node(id).and_then(|n| n.path_data()) else { continue };
|
||||
let shown = cx.selection.partial(id);
|
||||
v.extend(pd.anchors().filter(|(si, ai, _)| shown.is_none_or(|set| set.contains(&(*si, *ai)))).map(|(si, ai, a)| (id, si, ai, *a)));
|
||||
}
|
||||
if !cx.handles_multiple && v.len() > 1 {
|
||||
v.clear();
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Direction handle of a partially selected anchor under `p`: (id, si, ai, is_out).
|
||||
fn hit_handle(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, usize, usize, bool)> {
|
||||
if !handles_shown(cx.selection, cx.handles_multiple) {
|
||||
return None;
|
||||
}
|
||||
for (id, set) in &cx.selection.anchors {
|
||||
let Some(pd) = cx.doc.node(*id).and_then(|n| n.path_data()) else { continue };
|
||||
for &(si, ai) in set {
|
||||
let Some(a) = pd.subpaths.get(si).and_then(|s| s.anchors.get(ai)) else { continue };
|
||||
if a.has_out() && a.h_out.distance(p) <= tol {
|
||||
return Some((*id, si, ai, true));
|
||||
}
|
||||
if a.has_in() && a.h_in.distance(p) <= tol {
|
||||
return Some((*id, si, ai, false));
|
||||
/// The end of a shown direction handle ([`handle_anchors`]) nearest to `p` within `tol`, and
|
||||
/// nearer to it than the handle's anchor (a short handle leaves its anchor to be picked): (path,
|
||||
/// subpath, anchor, is_out).
|
||||
pub(crate) fn hit_handle(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, usize, usize, bool)> {
|
||||
let mut best: Option<((NodeId, usize, usize, bool), f64)> = None;
|
||||
for (id, si, ai, a) in handle_anchors(cx) {
|
||||
for (out, has, h) in [(true, a.has_out(), a.h_out), (false, a.has_in(), a.h_in)] {
|
||||
let d = h.distance(p);
|
||||
if has && d <= tol && d < a.p.distance(p) && best.is_none_or(|b| d < b.1) {
|
||||
best = Some(((id, si, ai, out), d));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
best.map(|b| b.0)
|
||||
}
|
||||
|
||||
fn anchors_json(v: &[AnchorRef]) -> Value {
|
||||
|
|
@ -335,6 +345,7 @@ impl Tool for DirectSelectionTool {
|
|||
self.mesh.unfocus();
|
||||
if let Some((id, si, ai, out)) = hit_handle(cx, p, tol) {
|
||||
self.state = State::Handle { id, si, ai, out };
|
||||
self.handle_snap = HandleSnap::default();
|
||||
return vec![Action::Begin("Reshape".into())];
|
||||
}
|
||||
if let Some((id, anchor, from)) = hit_spine_point(cx, p, tol) {
|
||||
|
|
@ -471,7 +482,7 @@ impl Tool for DirectSelectionTool {
|
|||
out
|
||||
}
|
||||
(PointerKind::Drag, State::Handle { id, si, ai, out }) => {
|
||||
let (q, guides) = crate::guides::snap_handle(cx, (id, si, ai), p, ev.mods.shift);
|
||||
let (q, guides) = self.handle_snap.snap(cx, (id, si, ai), p, ev.mods.shift);
|
||||
self.guides = guides;
|
||||
vec![Action::Preview(
|
||||
"path.setHandle".into(),
|
||||
|
|
@ -871,6 +882,61 @@ mod tests {
|
|||
assert!(matches!(&a[..], [Action::Begin(_), Action::Preview(c, _)] if c == "object.transform"), "{a:?}");
|
||||
}
|
||||
|
||||
/// #494: the handles shown are the handles dragged. A path selected as a whole (the one the
|
||||
/// Pen just drew) shows all its handles, and they drag; the direct-selected anchors' handles
|
||||
/// only otherwise; none with Show handles when multiple anchors are selected off and several
|
||||
/// anchors selected. A handle shorter than the tolerance leaves its anchor to be picked, and
|
||||
/// the tolerance is the preference's, in screen pixels at any zoom.
|
||||
#[test]
|
||||
fn the_handles_shown_are_the_handles_dragged() {
|
||||
let (d, arch) = arch_doc();
|
||||
let p = paint();
|
||||
// Does a press at (x, y) take a handle?
|
||||
let handle = |c: &ToolContext, x: f64, y: f64| {
|
||||
DirectSelectionTool::new(false).pointer(c, &PointerEvent::new(PointerKind::Down, x, y)) == vec![Action::Begin("Reshape".into())]
|
||||
};
|
||||
let mut whole = Selection::default();
|
||||
whole.set([arch]);
|
||||
let c = cx(&d, &whole, &p);
|
||||
assert!(handle(&c, 101.0, 251.0), "the first anchor's handle, the path selected whole");
|
||||
let mut t = DirectSelectionTool::new(false);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 101.0, 251.0));
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 120.0, 240.0));
|
||||
assert!(matches!(&a[..], [Action::Preview(c, v)] if c == "path.setHandle" && v["anchor"] == 0 && v["which"] == "out"), "{a:?}");
|
||||
// Only the second anchor direct-selected: the first one's handle is hidden.
|
||||
let second = anchors_of(arch, &[(0, 1)]);
|
||||
assert!(!handle(&cx(&d, &second, &p), 101.0, 251.0));
|
||||
assert!(handle(&cx(&d, &second, &p), 201.0, 251.0));
|
||||
// Show handles when multiple anchors are selected off: the whole path's are hidden.
|
||||
assert!(!handle(&ToolContext { handles_multiple: false, ..cx(&d, &whole, &p) }, 101.0, 251.0));
|
||||
assert!(handle(&ToolContext { handles_multiple: false, ..cx(&d, &second, &p) }, 201.0, 251.0));
|
||||
// The tolerance is the preference's, in screen pixels: 3 px is 0.75 pt at 400%.
|
||||
let zoomed = ToolContext { zoom: 4.0, ..cx(&d, &whole, &p) };
|
||||
assert!(!handle(&zoomed, 101.0, 251.0));
|
||||
assert!(handle(&zoomed, 100.5, 250.5));
|
||||
assert!(handle(&ToolContext { selection_tolerance: 8.0, ..cx(&d, &whole, &p) }, 106.0, 254.0));
|
||||
}
|
||||
|
||||
/// #494: a handle 2 pt long, inside the tolerance round its anchor, doesn't hide the anchor.
|
||||
#[test]
|
||||
fn a_short_handle_leaves_its_anchor_to_be_picked() {
|
||||
let (mut d, _) = doc_with_rect();
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let mut sp = vectorcraft_geom::SubPath::polyline(&[Point::new(100.0, 300.0), Point::new(200.0, 300.0)], false);
|
||||
sp.anchors[0].h_out = Point::new(102.0, 300.0);
|
||||
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, PathData::single(sp), vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let s = anchors_of(id, &[(0, 0)]);
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = DirectSelectionTool::new(false);
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.0, 300.0)), vec![], "the anchor, already selected");
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 90.0, 290.0));
|
||||
assert_eq!(a[0], Action::Begin("Move".into()));
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 90.0, 290.0));
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 102.5, 300.0)), vec![Action::Begin("Reshape".into())], "the handle");
|
||||
}
|
||||
|
||||
/// [`doc_with_rect`] (A, 100..200) and a second square B, 300..400: (A, B).
|
||||
fn two_squares() -> (vectorcraft_doc::Document, NodeId, NodeId) {
|
||||
let (mut d, a) = doc_with_rect();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ use vectorcraft_geom::{Anchor, AnchorKind, BezPath, Point, SubPath};
|
|||
use crate::{Action, Tool, ToolContext};
|
||||
|
||||
pub use anchor::AnchorTool;
|
||||
pub(crate) use anchor::hit_handle;
|
||||
pub use curvature::CurvatureTool;
|
||||
pub use family::FamilyTool;
|
||||
pub use gesture::GestureTool;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ use vectorcraft_doc::NodeId;
|
|||
use vectorcraft_geom::Point;
|
||||
|
||||
use super::{hit_anchor, hit_segment, insert_anchor, remove_anchor};
|
||||
use crate::direct::hit_handle;
|
||||
use crate::guides::HandleSnap;
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
|
|
@ -25,8 +27,9 @@ enum State {
|
|||
pub struct AnchorTool {
|
||||
id: &'static str,
|
||||
state: State,
|
||||
/// Smart guides of the handle being dragged.
|
||||
/// Smart guides of the handle being dragged, and what it snaps to.
|
||||
guides: Vec<Overlay>,
|
||||
snap: HandleSnap,
|
||||
}
|
||||
|
||||
impl AnchorTool {
|
||||
|
|
@ -37,36 +40,18 @@ impl AnchorTool {
|
|||
"scissors" => "scissors",
|
||||
_ => "addAnchor",
|
||||
};
|
||||
Self { id, state: State::Idle, guides: vec![] }
|
||||
Self { id, state: State::Idle, guides: vec![], snap: HandleSnap::default() }
|
||||
}
|
||||
|
||||
fn add(cx: &ToolContext, p: Point) -> Vec<Action> {
|
||||
hit_segment(cx, p, cx.tol(4.0)).map(insert_anchor).into_iter().collect()
|
||||
hit_segment(cx, p, cx.pick_tol()).map(insert_anchor).into_iter().collect()
|
||||
}
|
||||
|
||||
fn delete(cx: &ToolContext, p: Point) -> Vec<Action> {
|
||||
hit_anchor(cx, p, cx.tol(4.0)).map(remove_anchor).into_iter().collect()
|
||||
hit_anchor(cx, p, cx.pick_tol()).map(remove_anchor).into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// A direction handle of a selected path under `p`, among those shown (a path with anchors
|
||||
/// direct-selected shows theirs only): (id, subpath, anchor, is_out).
|
||||
pub(crate) fn hit_handle(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, usize, usize, bool)> {
|
||||
for id in &cx.selection.objects {
|
||||
let Some(pd) = cx.doc.node(*id).and_then(|n| n.path_data()) else { continue };
|
||||
let shown = cx.selection.partial(*id);
|
||||
for (si, ai, a) in pd.anchors().filter(|(si, ai, _)| shown.is_none_or(|set| set.contains(&(*si, *ai)))) {
|
||||
if a.has_out() && a.h_out.distance(p) <= tol {
|
||||
return Some((*id, si, ai, true));
|
||||
}
|
||||
if a.has_in() && a.h_in.distance(p) <= tol {
|
||||
return Some((*id, si, ai, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl Tool for AnchorTool {
|
||||
fn id(&self) -> &'static str {
|
||||
self.id
|
||||
|
|
@ -76,7 +61,7 @@ impl Tool for AnchorTool {
|
|||
}
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let p = ev.pos;
|
||||
let tol = cx.tol(4.0);
|
||||
let tol = cx.pick_tol();
|
||||
match (self.id, ev.kind) {
|
||||
("addAnchor", PointerKind::Down) => {
|
||||
if ev.mods.alt {
|
||||
|
|
@ -104,6 +89,7 @@ impl Tool for AnchorTool {
|
|||
("anchorPoint", PointerKind::Down) => {
|
||||
if let Some((id, si, ai, out)) = hit_handle(cx, p, tol) {
|
||||
self.state = State::Handle { id, si, ai, out, began: false };
|
||||
self.snap = HandleSnap::default();
|
||||
} else if let Some((id, si, ai)) = hit_anchor(cx, p, tol) {
|
||||
self.state = State::Convert { id, si, ai, start: p, began: false };
|
||||
} else if let Some((id, si, seg, t)) = hit_segment(cx, p, tol) {
|
||||
|
|
@ -134,7 +120,7 @@ impl Tool for AnchorTool {
|
|||
json!({"id": id.0, "subpath": si, "anchor": ai, "to": "smooth", "x": p.x, "y": p.y}),
|
||||
),
|
||||
State::Handle { id, si, ai, out, .. } => {
|
||||
let (q, guides) = crate::guides::snap_handle(cx, (id, si, ai), p, ev.mods.shift);
|
||||
let (q, guides) = self.snap.snap(cx, (id, si, ai), p, ev.mods.shift);
|
||||
self.guides = guides;
|
||||
Action::Preview(
|
||||
"path.setHandle".into(),
|
||||
|
|
@ -192,7 +178,7 @@ impl Tool for AnchorTool {
|
|||
"deleteAnchor" if !m.alt => Cursor::PenDelete,
|
||||
"deleteAnchor" => Cursor::PenAdd,
|
||||
"scissors" => {
|
||||
if hit_segment(cx, p, cx.tol(4.0)).is_some() {
|
||||
if hit_segment(cx, p, cx.pick_tol()).is_some() {
|
||||
Cursor::Crosshair
|
||||
} else {
|
||||
Cursor::NotAllowed
|
||||
|
|
@ -257,4 +243,38 @@ mod tests {
|
|||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 230.0));
|
||||
assert!(matches!(&a[1], Action::Preview(c, v) if c == "path.reshapeSegment" && v["dy"] == 30.0));
|
||||
}
|
||||
|
||||
/// #494: the Anchor Point tool picks within Selection & Anchor Display → Tolerance, and drags
|
||||
/// only the handles shown.
|
||||
#[test]
|
||||
fn anchor_point_picks_within_the_tolerance_and_drags_shown_handles() {
|
||||
let (mut d, _) = doc_with_rect();
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let mut sp = vectorcraft_geom::SubPath::polyline(&[Point::new(100.0, 300.0), Point::new(200.0, 300.0), Point::new(300.0, 300.0)], false);
|
||||
sp.anchors[1] = vectorcraft_geom::Anchor::smooth(Point::new(200.0, 300.0), Point::new(240.0, 300.0));
|
||||
let path = vectorcraft_geom::PathData::single(sp);
|
||||
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, path, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let mut s = Selection::default();
|
||||
s.set([id]);
|
||||
let p = paint();
|
||||
let press = |c: &ToolContext, x: f64, y: f64| {
|
||||
let mut t = AnchorTool::new("anchorPoint");
|
||||
t.pointer(c, &PointerEvent::new(PointerKind::Down, x, y));
|
||||
t.pointer(c, &PointerEvent::new(PointerKind::Drag, x + 10.0, y + 30.0))
|
||||
};
|
||||
let dragged = |a: Vec<Action>| match a.as_slice() {
|
||||
[Action::Begin(_), Action::Preview(c, _)] => c.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
// The segment 6 px off: picked with an 8 px tolerance, not with the default 3 px.
|
||||
assert_eq!(dragged(press(&cx(&d, &s, &p), 150.0, 306.0)), "");
|
||||
let wide = ToolContext { selection_tolerance: 8.0, ..cx(&d, &s, &p) };
|
||||
assert_eq!(dragged(press(&wide, 150.0, 306.0)), "path.reshapeSegment");
|
||||
// The middle anchor's handle drags; with Show handles when multiple anchors are selected
|
||||
// off it's hidden (three anchors selected) and the press finds the segment under it.
|
||||
assert_eq!(dragged(press(&cx(&d, &s, &p), 240.0, 301.0)), "path.setHandle");
|
||||
let single = ToolContext { handles_multiple: false, ..cx(&d, &s, &p) };
|
||||
assert_eq!(dragged(press(&single, 240.0, 301.0)), "path.reshapeSegment");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -553,25 +553,35 @@ impl PointSnap {
|
|||
}
|
||||
}
|
||||
|
||||
/// Where a dragged direction handle of anchor `ai` of subpath `si` of path `id` goes for the
|
||||
/// pointer at `p`: with Shift at a multiple of 45° (from the Constrain Angle) round its anchor,
|
||||
/// else snapped as a drawn point is (smart guides, the grid, pixels).
|
||||
pub fn snap_handle(cx: &ToolContext, (id, si, ai): (NodeId, usize, usize), p: Point, shift: bool) -> (Point, Vec<Overlay>) {
|
||||
let path = cx.doc.node(id).and_then(|n| n.path_data());
|
||||
if shift && let Some(a) = path.and_then(|pd| pd.subpaths.get(si)?.anchors.get(ai)) {
|
||||
return (a.p + vectorcraft_geom::constrain_angle_from(p - a.p, 45.0, cx.constrain_angle), vec![]);
|
||||
}
|
||||
snap_with(cx, p, || {
|
||||
// The path's bounds move with the handle, so they would chase it: its anchors (which stay
|
||||
// put) are the targets instead, the handle lining up with them too.
|
||||
let mut t = Targets::collect(cx.doc, &[id], None);
|
||||
for (_, _, a) in path.into_iter().flat_map(|pd| pd.anchors()).take(20_000) {
|
||||
t.points.push((a.p, Kind::Anchor));
|
||||
t.xs.push((a.p.x, a.p, Kind::Anchor));
|
||||
t.ys.push((a.p.y, a.p, Kind::Anchor));
|
||||
/// Snapping for a dragged direction handle: Shift keeps it at 45° steps round its anchor, else it
|
||||
/// snaps as a drawn point does. The targets are gathered on the first move that needs them and
|
||||
/// kept for the rest of the drag (gathering them takes as long as the document is big).
|
||||
#[derive(Default)]
|
||||
pub struct HandleSnap(Option<PointSnap>);
|
||||
|
||||
impl HandleSnap {
|
||||
/// Where the handle of anchor `ai` of subpath `si` of path `id` goes with the pointer at `p`,
|
||||
/// and the guides showing why.
|
||||
pub fn snap(&mut self, cx: &ToolContext, (id, si, ai): (NodeId, usize, usize), p: Point, shift: bool) -> (Point, Vec<Overlay>) {
|
||||
let path = cx.doc.node(id).and_then(|n| n.path_data());
|
||||
if shift && let Some(a) = path.and_then(|pd| pd.subpaths.get(si)?.anchors.get(ai)) {
|
||||
return (a.p + vectorcraft_geom::constrain_angle_from(p - a.p, 45.0, cx.constrain_angle), vec![]);
|
||||
}
|
||||
t
|
||||
})
|
||||
let snap = self.0.get_or_insert_with(|| {
|
||||
PointSnap::new(cx, || {
|
||||
// The path's bounds move with the handle, so they would chase it: its anchors
|
||||
// (which stay put) are the targets instead, the handle lining up with them too.
|
||||
let mut t = Targets::collect(cx.doc, &[id], None);
|
||||
for (_, _, a) in path.into_iter().flat_map(|pd| pd.anchors()).take(20_000) {
|
||||
t.points.push((a.p, Kind::Anchor));
|
||||
t.xs.push((a.p.x, a.p, Kind::Anchor));
|
||||
t.ys.push((a.p.y, a.p, Kind::Anchor));
|
||||
}
|
||||
t
|
||||
})
|
||||
});
|
||||
snap.snap(cx, p)
|
||||
}
|
||||
}
|
||||
|
||||
/// Snap a picked point (a transform tool's reference point) to the nearest anchor or centre of the
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@
|
|||
//! follows the pointer; Space held moves the anchor, handles and all); Shift constrains to 45°.
|
||||
//! Clicking the first anchor closes the path. 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 or an anchor of a selected path (the one being drawn too) works as
|
||||
//! the Anchor Point tool: dragging a handle moves it alone, clicking a smooth anchor makes it a
|
||||
//! corner and dragging an anchor pulls out new symmetric handles.
|
||||
//! Alt held over a handle end, an anchor or a segment of a selected path (the one being drawn too)
|
||||
//! works as the Anchor Point tool: dragging a handle moves it alone, clicking a smooth anchor makes
|
||||
//! it a corner, dragging an anchor pulls out new symmetric handles and dragging a segment reshapes
|
||||
//! it. Cmd held lends the selection tool used last for a drag (the engine's
|
||||
//! `Session::pointer`); the path being drawn goes on afterwards while it stays selected.
|
||||
//! Enter/Esc (or switching tools) ends the path. Clicking the end of a selected open path continues
|
||||
//! it. The rubber-band preview shows the next segment (Enable Rubber Band for Pen Tool). Auto Add/Delete: between paths, a click on a
|
||||
//! segment of a selected path adds an anchor there and a click on one of its anchors deletes it
|
||||
|
|
@ -18,13 +20,16 @@ use serde_json::json;
|
|||
use vectorcraft_doc::{NodeId, NodeKind};
|
||||
use vectorcraft_geom::{BezPath, Point};
|
||||
|
||||
use crate::draw2::{AnchorTool, hit_handle};
|
||||
use crate::direct::hit_handle;
|
||||
use crate::draw2::AnchorTool;
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PenTool {
|
||||
/// The path being drawn (set once the first anchor exists and the engine selected it).
|
||||
/// A path is being drawn...
|
||||
drawing: bool,
|
||||
/// ...this one, once the engine made it: another path selected meanwhile isn't drawn on.
|
||||
path: Option<NodeId>,
|
||||
drag: Option<(Point, bool)>,
|
||||
hover: Option<Point>,
|
||||
/// The incoming handle of the anchor being dragged out, as last previewed.
|
||||
|
|
@ -63,10 +68,14 @@ fn last_anchor(cx: &ToolContext, id: NodeId) -> Option<(usize, usize)> {
|
|||
Some((si, ai))
|
||||
}
|
||||
|
||||
/// Alt held over a handle end or an anchor of a selected path: the Anchor Point tool's gesture.
|
||||
/// Alt held over a handle end, an anchor or a segment of a selected path: the Anchor Point tool's
|
||||
/// gesture, within its tolerance.
|
||||
fn alt_converts(cx: &ToolContext, p: Point, m: Mods) -> bool {
|
||||
let tol = cx.tol(4.0);
|
||||
m.alt && (hit_handle(cx, p, tol).is_some() || crate::draw2::anchor_in(cx, editable_paths(cx), p, tol).is_some())
|
||||
let tol = cx.pick_tol();
|
||||
m.alt
|
||||
&& (hit_handle(cx, p, tol).is_some()
|
||||
|| crate::draw2::anchor_in(cx, editable_paths(cx), p, tol).is_some()
|
||||
|| crate::draw2::segment_in(cx, editable_paths(cx), p, tol).is_some())
|
||||
}
|
||||
|
||||
/// The selected paths the pen edits (not guides, nor locked or hidden ones).
|
||||
|
|
@ -97,9 +106,13 @@ impl Tool for PenTool {
|
|||
let exclude: Vec<vectorcraft_doc::NodeId> = if self.drawing { cx.selection.objects.clone() } else { vec![] };
|
||||
let (mut p, _) = if matches!(ev.kind, PointerKind::Down) { crate::guides::snap_draw(cx, ev.pos, &exclude) } else { (ev.pos, vec![]) };
|
||||
let tol = cx.tol(5.0);
|
||||
let active = if self.drawing { active_path(cx) } else { None };
|
||||
// The path a press began is the selected one by the next event.
|
||||
if self.drawing && self.path.is_none() {
|
||||
self.path = active_path(cx).map(|a| a.0);
|
||||
}
|
||||
let active = self.active(cx);
|
||||
if self.drawing && active.is_none() && ev.kind == PointerKind::Down {
|
||||
self.drawing = false;
|
||||
self.stop();
|
||||
}
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
|
|
@ -142,6 +155,7 @@ impl Tool for PenTool {
|
|||
&& (p.distance(last) <= tol || p.distance(first) <= tol)
|
||||
{
|
||||
self.drawing = true;
|
||||
self.path = active_path(cx).map(|a| a.0);
|
||||
if p.distance(first) <= tol && p.distance(last) > tol {
|
||||
return vec![Action::Exec("path.reverse".into(), json!({}))];
|
||||
}
|
||||
|
|
@ -150,7 +164,7 @@ impl Tool for PenTool {
|
|||
if let Some(act) = auto_add_delete(cx, ev.pos, ev.mods, tol) {
|
||||
return vec![act];
|
||||
}
|
||||
self.drawing = true;
|
||||
(self.drawing, self.path) = (true, None);
|
||||
self.drag = Some((p, false));
|
||||
vec![Action::Begin("Pen".into()), Action::Preview("path.create".into(), json!({"anchors": [{"x": p.x, "y": p.y}]}))]
|
||||
}
|
||||
|
|
@ -179,7 +193,7 @@ impl Tool for PenTool {
|
|||
out_h = a + vectorcraft_geom::constrain_angle(ev.pos - a, 45.0);
|
||||
}
|
||||
let alt = ev.mods.alt;
|
||||
let Some((id, ..)) = active_path(cx).or(active) else {
|
||||
let Some((id, ..)) = active else {
|
||||
// First anchor of a new path: re-issue create with handles.
|
||||
let in_h = a - (out_h - a);
|
||||
return vec![Action::Preview(
|
||||
|
|
@ -211,7 +225,7 @@ impl Tool for PenTool {
|
|||
}
|
||||
let Some((_, closing)) = self.drag.take() else { return vec![] };
|
||||
if closing {
|
||||
self.drawing = false;
|
||||
self.stop();
|
||||
}
|
||||
vec![Action::Commit]
|
||||
}
|
||||
|
|
@ -221,7 +235,7 @@ impl Tool for PenTool {
|
|||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _m: Mods) -> Vec<Action> {
|
||||
match key {
|
||||
ToolKey::Enter | ToolKey::Escape => {
|
||||
self.drawing = false;
|
||||
self.stop();
|
||||
self.drag = None;
|
||||
self.handle = None;
|
||||
// An Alt gesture under way ends as it stands.
|
||||
|
|
@ -231,17 +245,17 @@ impl Tool for PenTool {
|
|||
}
|
||||
}
|
||||
fn deactivate(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
self.drawing = false;
|
||||
self.stop();
|
||||
self.convert.take().map(|mut t| t.deactivate(cx)).unwrap_or_default()
|
||||
}
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
if let Some(t) = &self.convert {
|
||||
return t.overlays(cx);
|
||||
}
|
||||
if !cx.pen_rubber_band || !self.drawing || self.drag.is_some() || self.handle.is_some() {
|
||||
if !cx.pen_rubber_band || self.drag.is_some() || self.handle.is_some() {
|
||||
return vec![];
|
||||
}
|
||||
let (Some((id, _, last, out)), Some(h)) = (active_path(cx), self.hover) else { return vec![] };
|
||||
let (Some((id, _, last, out)), Some(h)) = (self.active(cx), self.hover) else { return vec![] };
|
||||
let mut bp = BezPath::new();
|
||||
bp.move_to(last);
|
||||
if out.distance(last) > 1e-9 {
|
||||
|
|
@ -254,10 +268,7 @@ impl Tool for PenTool {
|
|||
}
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor {
|
||||
let tol = cx.tol(5.0);
|
||||
let active = active_path(cx);
|
||||
if self.drawing
|
||||
&& let Some((id, first, last, _)) = active
|
||||
{
|
||||
if let Some((id, first, last, _)) = self.active(cx) {
|
||||
let one = last_anchor(cx, id).is_some_and(|(_, ai)| ai == 0);
|
||||
if p.distance(first) <= tol && !one {
|
||||
return Cursor::PenClose;
|
||||
|
|
@ -275,7 +286,7 @@ impl Tool for PenTool {
|
|||
Some([_]) => return Cursor::PenAdd,
|
||||
_ => {}
|
||||
}
|
||||
if active.is_some_and(|(_, first, last, _)| p.distance(last) <= tol || p.distance(first) <= tol) {
|
||||
if active_path(cx).is_some_and(|(_, first, last, _)| p.distance(last) <= tol || p.distance(first) <= tol) {
|
||||
return Cursor::PenContinue;
|
||||
}
|
||||
match auto_add_delete(cx, p, m, tol) {
|
||||
|
|
@ -289,13 +300,27 @@ impl Tool for PenTool {
|
|||
}
|
||||
|
||||
impl PenTool {
|
||||
/// Hand an Alt press on a selected path's handle or anchor to the Anchor Point tool.
|
||||
/// The open path being drawn: the one the pen made or went on with, while it is the single
|
||||
/// selected open path.
|
||||
fn active(&self, cx: &ToolContext) -> Option<(NodeId, Point, Point, Point)> {
|
||||
active_path(cx).filter(|(id, ..)| self.drawing && self.path.is_none_or(|p| p == *id))
|
||||
}
|
||||
|
||||
/// The path is finished.
|
||||
fn stop(&mut self) {
|
||||
(self.drawing, self.path) = (false, None);
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
return None;
|
||||
}
|
||||
let mut t = AnchorTool::new("anchorPoint");
|
||||
let acts = t.pointer(cx, ev);
|
||||
if !t.busy() {
|
||||
return None;
|
||||
}
|
||||
self.convert = Some(t);
|
||||
Some(acts)
|
||||
}
|
||||
|
|
@ -587,4 +612,53 @@ mod tests {
|
|||
let cx1 = cx(&d, &s, &p);
|
||||
assert_eq!(t.cursor(&cx1, Point::new(250.0, 101.0), alt), Cursor::Pen);
|
||||
}
|
||||
|
||||
/// #494: Alt held over a segment of a selected path reshapes it as the Anchor Point tool does,
|
||||
/// while drawing too; Alt away from paths still places an anchor.
|
||||
#[test]
|
||||
fn alt_over_a_segment_reshapes_it() {
|
||||
let (d, id, s) = drawing(vectorcraft_geom::SubPath::polyline(&[Point::new(100.0, 300.0), Point::new(200.0, 300.0)], false));
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let mut t = PenTool { drawing: true, ..PenTool::default() };
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 302.0), alt), Cursor::PenConvert);
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 302.0), Mods::default()), Cursor::Pen);
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 302.0).with_mods(alt)), vec![]);
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 262.0).with_mods(alt));
|
||||
assert_eq!(a[0], Action::Begin("Reshape".into()));
|
||||
assert!(
|
||||
matches!(&a[1], Action::Preview(c, v) if c == "path.reshapeSegment" && v["id"] == id.0 && v["segment"] == 0 && v["dy"] == -40.0),
|
||||
"{a:?}"
|
||||
);
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 262.0)), vec![Action::Commit]);
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 250.0, 400.0).with_mods(alt));
|
||||
assert!(matches!(&a[..], [Action::Begin(_), Action::Preview(c, _)] if c == "path.appendAnchor"), "{a:?}");
|
||||
}
|
||||
|
||||
/// #494: the Pen goes on drawing the path it drew while that path stays selected (after a Cmd
|
||||
/// drag with Direct Selection), but never another open path selected meanwhile.
|
||||
#[test]
|
||||
fn the_pen_draws_on_the_path_it_began_only() {
|
||||
let (mut d, a, s) = drawing(vectorcraft_geom::SubPath::polyline(&[Point::new(10.0, 300.0), Point::new(60.0, 300.0)], false));
|
||||
let l = d.layers[0].id;
|
||||
let b = d.alloc_id();
|
||||
let line = vectorcraft_geom::shapes::line(Point::new(10.0, 400.0), Point::new(60.0, 400.0));
|
||||
d.insert(Some(l), 2, vectorcraft_doc::Node::path(b, line, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let p = paint();
|
||||
let mut t = PenTool { drawing: true, ..PenTool::default() };
|
||||
let cx_a = cx(&d, &s, &p);
|
||||
let appended = |acts: &[Action]| match acts {
|
||||
[Action::Begin(_), Action::Preview(c, v)] if c == "path.appendAnchor" => v["id"].as_u64(),
|
||||
_ => None,
|
||||
};
|
||||
assert_eq!(appended(&click(&mut t, &cx_a, 100.0, 320.0, Mods::default())), Some(a.0));
|
||||
// Path B selected instead: the next click starts a new path.
|
||||
let mut sb = Selection::default();
|
||||
sb.set([b]);
|
||||
let cx_b = cx(&d, &sb, &p);
|
||||
assert_eq!(t.cursor(&cx_b, Point::new(150.0, 350.0), Mods::default()), Cursor::Pen);
|
||||
let acts = click(&mut t, &cx_b, 150.0, 350.0, Mods::default());
|
||||
assert!(matches!(&acts[..], [Action::Begin(_), Action::Preview(c, _)] if c == "path.create"), "{acts:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,31 +76,19 @@ enum Drag {
|
|||
start_angle: f64,
|
||||
start_rot: f64,
|
||||
},
|
||||
/// Cmd held: temporary selection tool; restore this tool on release.
|
||||
TempSelect,
|
||||
/// A Selection tool move dragged off the canvas: the panels get the art
|
||||
/// ([`widgets::PanelDrag::Art`]); `temp` restores the tool Cmd switched from on release.
|
||||
Art {
|
||||
temp: bool,
|
||||
},
|
||||
/// ([`widgets::PanelDrag::Art`]).
|
||||
Art,
|
||||
}
|
||||
|
||||
fn drag_id() -> egui::Id {
|
||||
egui::Id::new("canvas-drag")
|
||||
}
|
||||
fn temp_tool_id() -> egui::Id {
|
||||
egui::Id::new("canvas-temp-tool")
|
||||
}
|
||||
/// Where the pointer was last seen during a press on the canvas.
|
||||
fn drag_pos_id() -> egui::Id {
|
||||
egui::Id::new("canvas-drag-pos")
|
||||
}
|
||||
|
||||
/// Selection, Direct Selection and Group Selection: the tools Cmd switches to for a drag.
|
||||
pub(crate) fn is_selection_tool(id: &str) -> bool {
|
||||
matches!(id, "selection" | "directSelection" | "groupSelection")
|
||||
}
|
||||
|
||||
/// The pen pressure (0..1) of the press in progress: the force of this frame's pen or touch
|
||||
/// input, else the last one seen since the press (`pressed`: a new press, whose mouse has none
|
||||
/// until a pen reports it). A mouse presses fully (1).
|
||||
|
|
@ -531,19 +519,10 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
|||
let c = rect.center();
|
||||
Drag::RotateView { start_angle: (p.y - c.y).atan2(p.x - c.x) as f64, start_rot: v.rotation }
|
||||
} else {
|
||||
let mut kind = Drag::Tool;
|
||||
// Cmd with another tool: drag with the selection tool used last.
|
||||
if m.command && !is_selection_tool(tool) {
|
||||
ui.data_mut(|d| d.insert_temp(temp_tool_id(), tool.to_string()));
|
||||
let last = app.ui.last_selection_tool.clone();
|
||||
app.select_tool(if is_selection_tool(&last) { &last } else { "selection" });
|
||||
kind = Drag::TempSelect;
|
||||
}
|
||||
// Cmd only brought the selection tool: its click is a plain one, not Select Behind.
|
||||
let mods = Mods { cmd: m.command && kind != Drag::TempSelect, ..mods(m, space) };
|
||||
let ev = PointerEvent { kind: PointerKind::Down, pos: xf.to_doc(p), mods, pressure: pen_pressure(ui, true) };
|
||||
// Cmd with another tool drags with the selection tool used last (the session lends it).
|
||||
let ev = PointerEvent { kind: PointerKind::Down, pos: xf.to_doc(p), mods: mods(m, space), pressure: pen_pressure(ui, true) };
|
||||
dispatch(app, &ev, view);
|
||||
kind
|
||||
Drag::Tool
|
||||
};
|
||||
ui.data_mut(|dd| {
|
||||
dd.insert_temp(drag_id(), d);
|
||||
|
|
@ -580,11 +559,11 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
|||
vm.rotation = vectorcraft_geom::normalize_deg(deg);
|
||||
}
|
||||
}
|
||||
Drag::ZoomBox { .. } | Drag::Art { .. } => {}
|
||||
Drag::Tool | Drag::TempSelect if drag_art_out(app, ui, resp, p, view) => {
|
||||
ui.data_mut(|dd| dd.insert_temp(drag_id(), Drag::Art { temp: d == Drag::TempSelect }));
|
||||
Drag::ZoomBox { .. } | Drag::Art => {}
|
||||
Drag::Tool if drag_art_out(app, ui, resp, p, view) => {
|
||||
ui.data_mut(|dd| dd.insert_temp(drag_id(), Drag::Art));
|
||||
}
|
||||
Drag::Tool | Drag::TempSelect => {
|
||||
Drag::Tool => {
|
||||
if pointer.delta() != egui::Vec2::ZERO {
|
||||
let ev = PointerEvent { kind: PointerKind::Drag, pos: xf.to_doc(p), mods: mods(m, space), pressure: pen_pressure(ui, false) };
|
||||
dispatch(app, &ev, view);
|
||||
|
|
@ -620,17 +599,11 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
|||
}
|
||||
}
|
||||
}
|
||||
Drag::Tool | Drag::TempSelect | Drag::Art { .. } => {
|
||||
if !matches!(d, Drag::Art { .. }) {
|
||||
let ev = PointerEvent { kind: PointerKind::Up, pos: xf.to_doc(p), mods: mods(m, space), pressure: pen_pressure(ui, false) };
|
||||
dispatch(app, &ev, view);
|
||||
}
|
||||
if matches!(d, Drag::TempSelect | Drag::Art { temp: true })
|
||||
&& let Some(prev) = ui.data(|dd| dd.get_temp::<String>(temp_tool_id()))
|
||||
{
|
||||
app.restore_tool(&prev);
|
||||
}
|
||||
Drag::Tool => {
|
||||
let ev = PointerEvent { kind: PointerKind::Up, pos: xf.to_doc(p), mods: mods(m, space), pressure: pen_pressure(ui, false) };
|
||||
dispatch(app, &ev, view);
|
||||
}
|
||||
Drag::Art => {}
|
||||
Drag::Pan { .. } | Drag::RotateView { .. } => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1371,7 +1344,7 @@ fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
|||
let app = &*app;
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let tool = app.session.tool_id();
|
||||
let direct = matches!(tool, "directSelection" | "pen" | "addAnchor" | "deleteAnchor" | "anchorPoint" | "curvature");
|
||||
let direct = vectorcraft_tools::catalog::edits_anchors(tool);
|
||||
// Selection & Anchor Display › Size (1–7, 3 the default): anchors, handles and the bounding
|
||||
// box's handles a point bigger or smaller per step.
|
||||
let look = HandleLook::of(&app.session.prefs);
|
||||
|
|
|
|||
|
|
@ -540,19 +540,8 @@ impl VectorcraftApp {
|
|||
|
||||
/// Select a tool (also used by the toolbar and shortcuts).
|
||||
pub fn select_tool(&mut self, id: &str) {
|
||||
self.change_tool(id, true);
|
||||
}
|
||||
|
||||
/// Go back to tool `id` after a temporary one, without choosing it afresh
|
||||
/// ([`vectorcraft_engine::Session::switch_tool`]).
|
||||
pub fn restore_tool(&mut self, id: &str) {
|
||||
self.change_tool(id, false);
|
||||
}
|
||||
|
||||
fn change_tool(&mut self, id: &str, choose: bool) {
|
||||
let v = self.view_info();
|
||||
let r = if choose { self.session.select_tool(id, v) } else { self.session.switch_tool(id, v) };
|
||||
if let Err(e) = r {
|
||||
if let Err(e) = self.session.select_tool(id, v) {
|
||||
self.ui.status = e.to_string();
|
||||
}
|
||||
if let Some(g) = vectorcraft_tools::catalog::group_of(id)
|
||||
|
|
@ -560,9 +549,6 @@ impl VectorcraftApp {
|
|||
{
|
||||
*slot = id.to_string();
|
||||
}
|
||||
if canvas::is_selection_tool(id) {
|
||||
self.ui.last_selection_tool = id.to_string();
|
||||
}
|
||||
toolbar::remember(self, id);
|
||||
self.ui.flyout = None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
// Enter with a selection tool opens the Move dialog, as a double-click on its button
|
||||
// does.
|
||||
let tool = app.session.tool_id();
|
||||
if app.ui.dialog.is_none() && crate::canvas::is_selection_tool(tool) {
|
||||
if app.ui.dialog.is_none() && vectorcraft_tools::catalog::is_selection_tool(tool) {
|
||||
// Nothing selected: it fails and nothing opens (the menu item is disabled then too).
|
||||
let _ = crate::toolbar::open_options(app, tool);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,9 +283,6 @@ pub struct UiState {
|
|||
pub flyout: Option<usize>,
|
||||
/// Last tool shown for each toolbar group (flyout selection sticks).
|
||||
pub group_tool: Vec<String>,
|
||||
/// The selection tool used last (Selection, Direct Selection or Group Selection): a Cmd press
|
||||
/// with any other tool drags with it.
|
||||
pub last_selection_tool: String,
|
||||
pub status: String,
|
||||
pub palette_open: bool,
|
||||
pub palette_query: String,
|
||||
|
|
@ -496,7 +493,6 @@ impl Default for UiState {
|
|||
dialog: None,
|
||||
flyout: None,
|
||||
group_tool: vectorcraft_tools::TOOL_GROUPS.iter().map(|g| g[0].id.to_string()).collect(),
|
||||
last_selection_tool: "selection".into(),
|
||||
status: String::new(),
|
||||
palette_open: false,
|
||||
palette_query: String::new(),
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
|
|||
"hand" => app.run("view.fitArtboard", json!({})),
|
||||
"zoom" => app.run("view.actualSize", json!({})),
|
||||
"rotate" | "scale" | "reflect" | "shear" | "selection" | "directSelection" | "groupSelection" => {
|
||||
let dialog = if crate::canvas::is_selection_tool(tool) { "move" } else { tool };
|
||||
let dialog = if vectorcraft_tools::catalog::is_selection_tool(tool) { "move" } else { tool };
|
||||
let id = format!("object.{dialog}");
|
||||
if let Some(c) = vectorcraft_engine::find_command(&id) {
|
||||
(c.enabled)(&app.session)?;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ so only enable it while you use it. Transport: `apps/vectorcraft/src/control_ser
|
|||
| `ui.menu.list` / `ui.menu.invoke` | `{command, params}` | the full menu tree / invoke an item |
|
||||
| `ui.contextMenu.list` | | the canvas context menu for the current selection, flattened like `ui.menu.list` (`path` holds its submenus). `ui.click {x, y, button: "right"}` on the canvas opens it, after selecting the object there unless it is already selected |
|
||||
| `ui.tool.select` / `ui.tool.list` | `{tool}` | |
|
||||
| `ui.pointer` | `{events:[{kind: down|drag|up|move|doubleclick, x, y, space?: "doc"|"screen", mods?}]}` | drive the active tool exactly like the mouse |
|
||||
| `ui.pointer` | `{events:[{kind: down|drag|up|move|doubleclick, x, y, space?: "doc"|"screen", mods?}]}` | drive the active tool exactly like the mouse: `mods.cmd` held at a press with any tool but a selection tool drags with the selection tool chosen last (Direct Selection with the Pen until one is), and the release gives the tool back as it was |
|
||||
| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | synthetic keyboard input |
|
||||
| `ui.wheel` | `{x, y, dy?, dx?, unit?: "line"\|"point", shift?, alt?, cmd?}` | a mouse wheel turn over screen point (x, y): `dy` notches up (+) or down, `dx` sideways. Over the canvas the wheel scrolls and Cmd- or Alt-wheel (Option on the Mac) zooms about the pointer; with the `zoomWithMouseWheel` preference the wheel and Alt-wheel zoom about the pointer, Shift-wheel scrolls up and down and Cmd-wheel (Ctrl on Windows and Linux) sideways. Over a focused numeric field (click it first) each notch steps its value as Up/Down do (Shift: ten, Cmd/Ctrl: a tenth) and the panel stays put; over anything else in a panel the wheel scrolls it |
|
||||
| `ui.set` | `{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?}` | |
|
||||
|
|
|
|||
25
docs/mcp.md
25
docs/mcp.md
|
|
@ -1049,7 +1049,20 @@ on top of each other, and an open path becomes one path per piece. Each cut leav
|
|||
so `path.moveAnchors {dx, dy}` (or a Direct Selection drag) pulls the path apart there; `path.join {}` (Connect
|
||||
Selected End Points) joins the ends again. `path.convertAnchor {id, subpath?, anchor, to, x?, y?}` and
|
||||
`path.split {id, subpath?, anchor}` do the same to one anchor (the Anchor Point and Scissors tools); the Pen with Alt
|
||||
held over a selected path's handle or anchor works as the Anchor Point tool.
|
||||
held over a selected path's handle, anchor or segment works as the Anchor Point tool. `path.reshapeSegment {id,
|
||||
subpath?, segment, t, dx, dy}` (a segment dragged with Direct Selection or the Anchor Point tool) moves the segment's
|
||||
point at `t` by `dx`, `dy`: a smooth anchor at either end stays smooth, its other handle turning with the moved one.
|
||||
|
||||
A `pointer_gesture` with `mods: {cmd: true}` held at the press, with any tool but the selection tools (Command on
|
||||
macOS, Ctrl elsewhere), drags with the selection tool chosen last (`select_tool`), or Direct Selection with the Pen,
|
||||
Curvature and anchor tools until one is chosen; the release gives the tool back as it was, so the Pen goes on drawing
|
||||
the path it had begun while that stays selected. Its reply's `tool` is the tool given back.
|
||||
|
||||
```json
|
||||
{"name":"pointer_gesture","arguments":{"tool":"pen","events":[{"kind":"down","x":100,"y":300},{"kind":"up","x":100,"y":300},{"kind":"down","x":200,"y":300},{"kind":"drag","x":250,"y":300},{"kind":"up","x":250,"y":300}]}}
|
||||
{"name":"pointer_gesture","arguments":{"mods":{"cmd":true},"events":[{"kind":"down","x":250,"y":300},{"kind":"drag","x":250,"y":260},{"kind":"up","x":250,"y":260}]}}
|
||||
{"name":"pointer_gesture","arguments":{"events":[{"kind":"down","x":300,"y":350},{"kind":"up","x":300,"y":350}]}}
|
||||
```
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"select.anchors","params":{"id":12,"anchors":[[0,2]]}}}
|
||||
|
|
@ -1153,8 +1166,9 @@ Effects off a 100 pt wide rectangle with a 10 pt stroke set to `width: 220` gets
|
|||
|
||||
The Selection & Anchor Display and General preferences apply to `pointer_gesture` as they do to the mouse:
|
||||
|
||||
- `selectionTolerance` (1–8 px, 3 by default): how near a click must be to a path to pick it, and to an anchor or
|
||||
handle for Direct Selection.
|
||||
- `selectionTolerance` (1–8 px, 3 by default): how near a click must be to a path to pick it, and to an anchor,
|
||||
handle or segment for Direct Selection, the Anchor Point, Add and Delete Anchor Point and Scissors tools, and the
|
||||
Pen with Alt. A handle shorter than that leaves its anchor to be picked when pressed nearer the anchor.
|
||||
- `objectSelectionByPathOnly`: a click inside a filled path or compound path doesn't select it, one on its path does.
|
||||
- `ctrlClickSelectsBehind` (on by default): a Selection tool click with `mods: {cmd: true}` (Command on macOS, Ctrl
|
||||
elsewhere) selects the object under the selected one there, the next such click the one under that, then the
|
||||
|
|
@ -1168,8 +1182,9 @@ The Selection & Anchor Display and General preferences apply to `pointer_gesture
|
|||
hidden art with the artboard too; off (the default) it stays where it is.
|
||||
- `penRubberBand`, `curvatureRubberBand` (on by default): off, the Pen and Curvature tools draw no preview segment to
|
||||
the pointer.
|
||||
- `showHandlesMultipleAnchors` (on by default): off, Direct Selection shows and drags direction handles only while a
|
||||
single anchor is selected. `handleStyle` (`solid`, `hollow`, `large`) draws their ends (desktop app).
|
||||
- `showHandlesMultipleAnchors` (on by default): off, Direct Selection (and the Anchor Point tool, and the Pen with Alt)
|
||||
shows and drags direction handles only while a single anchor is selected. On, the handles of the direct-selected
|
||||
anchors, or of every anchor of a path selected as a whole, show and drag. `handleStyle` (`solid`, `hollow`, `large`) draws their ends (desktop app).
|
||||
- `hideCornerWidgetAbove` (177° by default): corners wider than this show no Live Corners widget (a rectangle's right
|
||||
angles hide below 90°).
|
||||
- `transformPatternTiles` (off by default): the default of the transforms' `patterns` param (`object.transform`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue