mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
Tools: only the freehand tools get every move of a frame (#4227 review)
Every pointer move of a frame went to whatever tool was active, so with a fast mouse (or a pen) a tool whose drag preview is costly (the Selection tool moving a large selection, Free Distort, the Width tool) redid it for each, up to 64 times a frame. Tool::wants_samples (default false) says a tool builds a stroke from the drag's samples: the Pencil, Paintbrush, Blob Brush, Eraser, Knife, Smooth and Path Eraser answer true (the Paintbrush through its wrapper), the Join tool doesn't. The canvas still reads the frame's moves every frame, so the macOS feed's pressures stay paired, but replays them only to such a tool; every other tool gets one drag a frame at the latest position. A frame whose moves end where the last frame's did still reaches the tool. Tests: only_the_freehand_tools_get_every_move_of_a_frame (Pencil, Paintbrush and Blob Brush get the 19 moves before the last, Selection, Direct Selection and Width none), a_mouse_stroke_keeps_a_frame_that_ends_where_it_started, the_freehand_tools_want_every_sample, and a_mouse_stroke_keeps_every_move_of_a_frame now checks every position in order, not only the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
954d3f3116
commit
21a9e854e1
@@ -415,6 +415,12 @@ impl Session {
|
||||
self.tool.wants_ticks()
|
||||
}
|
||||
|
||||
/// Does the active tool build a stroke from every position the pointer passed
|
||||
/// ([`vectorcraft_tools::Tool::wants_samples`])?
|
||||
pub fn tool_wants_samples(&self) -> bool {
|
||||
self.tool.wants_samples()
|
||||
}
|
||||
|
||||
/// Is the active tool waiting for work it runs on another thread ([`Tool::waiting`])?
|
||||
pub fn tool_waiting(&self) -> bool {
|
||||
self.tool.waiting()
|
||||
|
||||
@@ -533,6 +533,11 @@ impl Tool for GestureTool {
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
/// The freehand tools build their stroke from every position the pointer passed; the Join tool
|
||||
/// only needs where its scribble crosses the ends.
|
||||
fn wants_samples(&self) -> bool {
|
||||
matches!(self.id, "pencil" | "paintbrush" | "blobBrush" | "eraser" | "knife" | "smooth" | "pathEraser")
|
||||
}
|
||||
/// `[` and `]` are the Blob Brush's and the Eraser's: their size (#1661).
|
||||
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
|
||||
matches!(key, ToolKey::BracketLeft | ToolKey::BracketRight) && matches!(self.id, "blobBrush" | "eraser")
|
||||
@@ -874,6 +879,16 @@ mod tests {
|
||||
/// Holding Alt shows the Paintbrush's closing cursor before the press, and the Pencil's and the
|
||||
/// Paintbrush's while drawing (before a press, Alt gives the Pencil its straight-line cursor); a release
|
||||
/// with Alt still held closes the path, and Alt let go before the release leaves it open.
|
||||
/// The freehand tools take every position the pointer passed (`wants_samples`); the Join tool
|
||||
/// doesn't need them.
|
||||
#[test]
|
||||
fn the_freehand_tools_want_every_sample() {
|
||||
for id in ["pencil", "paintbrush", "blobBrush", "eraser", "knife", "smooth", "pathEraser"] {
|
||||
assert!(GestureTool::new(id).wants_samples(), "{id}");
|
||||
}
|
||||
assert!(!GestureTool::new("join").wants_samples());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_shows_the_closing_cursor_and_a_release_with_alt_closes() {
|
||||
let (d, _) = doc_with_rect();
|
||||
|
||||
@@ -923,6 +923,14 @@ pub trait Tool: Send {
|
||||
fn wants_ticks(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Does the tool build a stroke from the drag's samples (the Pencil and the other freehand
|
||||
/// tools)? The host then hands it every position the pointer passed between two frames, not
|
||||
/// only the last, so a fast stroke keeps its curve. Every other tool gets one drag a frame, so
|
||||
/// one whose drag preview is costly (moving a large selection, Free Distort, the Width tool)
|
||||
/// doesn't redo it for each sample.
|
||||
fn wants_samples(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Is the tool waiting for work it runs on another thread (the planar map of a large selection
|
||||
/// for the Shape Builder and the Live Paint Bucket)? It highlights no region under the pointer
|
||||
/// until the work is done. While this holds, the desktop app calls [`Tool::poll`] each frame,
|
||||
|
||||
@@ -261,6 +261,9 @@ impl Tool for BrushPaintTool {
|
||||
self.inner.set_option(key, v);
|
||||
}
|
||||
}
|
||||
fn wants_samples(&self) -> bool {
|
||||
self.inner.wants_samples()
|
||||
}
|
||||
fn busy(&self) -> bool {
|
||||
self.inner.busy()
|
||||
}
|
||||
|
||||
@@ -233,6 +233,15 @@ fn frame_moves(ui: &Ui, stylus: Option<&StylusFeed>) -> Vec<(Pos2, Option<f32>)>
|
||||
})
|
||||
}
|
||||
|
||||
/// The moves of this frame ([`frame_moves`]) the active tool gets before the pointer's position:
|
||||
/// all of them for a tool that builds a stroke from them ([`vectorcraft_engine::Session::tool_wants_samples`]), none
|
||||
/// for the others, which get one drag a frame. The moves are read either way, so the host's feed
|
||||
/// pairs the next frame's with its own.
|
||||
fn tool_frame_moves(ui: &Ui, app: &VectorcraftApp) -> Vec<(Pos2, Option<f32>)> {
|
||||
let moves = frame_moves(ui, app.services.stylus.as_ref());
|
||||
if app.session.tool_wants_samples() { moves } else { vec![] }
|
||||
}
|
||||
|
||||
pub fn mods(m: egui::Modifiers, space: bool) -> Mods {
|
||||
Mods { shift: m.shift, alt: m.alt, cmd: m.command, ctrl: m.ctrl, space }
|
||||
}
|
||||
@@ -1187,13 +1196,16 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
ui.data_mut(|dd| dd.insert_temp(drag_id(), Drag::Art));
|
||||
}
|
||||
Drag::Tool => {
|
||||
if tool_drag_moved(ui, &xf, p, &pointer) {
|
||||
// Every position the pen or mouse passed this frame, not just the last: a
|
||||
// fast stroke keeps its curve (a pen or a mouse reports far more often than
|
||||
// frames come). Touch events (a pen on Windows or the web, a finger) and a
|
||||
// macOS pen's moves carry their own pressure; a mouse's, a pen's on X11
|
||||
// among them, take the host's latest.
|
||||
for (at, force) in frame_moves(ui, app.services.stylus.as_ref()) {
|
||||
// Every position the pen or mouse passed this frame, not just the last, for a
|
||||
// tool that builds a stroke from them: a fast stroke keeps its curve (a pen or a
|
||||
// mouse reports far more often than frames come). Touch events (a pen on Windows
|
||||
// or the web, a finger) and a macOS pen's moves carry their own pressure; a
|
||||
// mouse's, a pen's on X11 among them, take the host's latest. Other tools get
|
||||
// the last position only, so a costly drag preview runs once a frame.
|
||||
let moves = tool_frame_moves(ui, app);
|
||||
// A frame whose pointer ends where the last one did still moved in between.
|
||||
if tool_drag_moved(ui, &xf, p, &pointer) || !moves.is_empty() {
|
||||
for (at, force) in moves {
|
||||
let pressure = force.unwrap_or_else(|| pen_pressure(ui, app, false));
|
||||
dispatch(app, &PointerEvent { kind: PointerKind::Drag, pos: xf.to_doc(at), mods: mods(m, space), pressure }, view);
|
||||
}
|
||||
@@ -9572,7 +9584,59 @@ mod tests {
|
||||
let (cmd, p) = app.session.journal.last().unwrap().clone();
|
||||
assert_eq!(cmd, "path.freehand");
|
||||
let pts = p["points"].as_array().unwrap();
|
||||
assert!(pts.len() >= 20, "the stroke keeps the frame's moves: {} points", pts.len());
|
||||
let expected: Vec<_> = std::iter::once(start)
|
||||
.chain(zigzag.iter().copied())
|
||||
.map(|p| {
|
||||
let p = xf.to_doc(p);
|
||||
json!([p.x, p.y])
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(pts, expected.as_slice(), "the stroke keeps every move of the frame, in order");
|
||||
}
|
||||
|
||||
/// A frame whose moves end where the last frame's did still reaches the Pencil: the stroke went
|
||||
/// out and came back in between.
|
||||
#[test]
|
||||
fn a_mouse_stroke_keeps_a_frame_that_ends_where_it_started() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.select_tool("pencil");
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let at = |x: f64, y: f64| xf.to_screen(Point::new(x, y));
|
||||
let button =
|
||||
|p, pressed| egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
|
||||
let (start, a) = (at(100.0, 100.0), at(160.0, 100.0));
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(start), button(start, true)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(a)]);
|
||||
// Out to a bump and back to `a`, all in one frame.
|
||||
let bump = [at(170.0, 80.0), at(180.0, 70.0), at(170.0, 80.0), a];
|
||||
frame(&mut app, &ctx, bump.iter().map(|p| egui::Event::PointerMoved(*p)).collect());
|
||||
frame(&mut app, &ctx, vec![button(a, false)]);
|
||||
let (_, p) = app.session.journal.last().unwrap().clone();
|
||||
let n = p["points"].as_array().unwrap().len();
|
||||
assert!(n >= 5, "the bump's moves are kept: {n} points");
|
||||
}
|
||||
|
||||
/// Only a tool that builds a stroke from the drag's samples gets every move of a frame (#4227):
|
||||
/// the Selection tool and the other tools get one drag a frame, at the pointer's position, so a
|
||||
/// costly drag preview runs once.
|
||||
#[test]
|
||||
fn only_the_freehand_tools_get_every_move_of_a_frame() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let events: Vec<_> = (0..20).map(|i| egui::Event::PointerMoved(pos2(100.0 + i as f32 * 5.0, 100.0))).collect();
|
||||
for (tool, every) in
|
||||
[("pencil", true), ("paintbrush", true), ("blobBrush", true), ("selection", false), ("directSelection", false), ("width", false)]
|
||||
{
|
||||
app.select_tool(tool);
|
||||
let mut moves = vec![];
|
||||
let mut out = ctx.run_ui(egui::RawInput { events: events.clone(), ..Default::default() }, |ui| moves = tool_frame_moves(ui, &app));
|
||||
out.textures_delta.clear();
|
||||
assert_eq!(moves.len(), if every { 19 } else { 0 }, "{tool}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #491: a pen tap whose press and lift come in one frame selects what it tapped, and the
|
||||
|
||||
Reference in New Issue
Block a user