mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 11:16:50 -06:00
fix(ui): Show the Pencil stroke while drawing
The Pencil tool's stroke only appeared once the mouse button was released. The tool reported the stroke so far as a path overlay, but the canvas skipped path overlays (they were never drawn), so nothing showed while drawing. The canvas now draws them: curves flattened to about a quarter of a screen pixel, dashed when asked. That also brings back the Smooth and Erase tools' dashed drag previews. The Pencil's preview was black, lost on a dark pasteboard; it is drawn in the colour of the layer the path goes on, as InDesign does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
paaspaas00
co-authored by
Claude Opus 5.5
parent
e862e0a613
commit
9d666f3dc8
@@ -51,7 +51,7 @@ impl Tool for PencilTool {
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
let Some((_, _, pts)) = &self.stroke else { return vec![] };
|
||||
let mut path = BezPath::new();
|
||||
for (i, p) in pts.iter().enumerate() {
|
||||
@@ -61,7 +61,9 @@ impl Tool for PencilTool {
|
||||
path.line_to(*p);
|
||||
}
|
||||
}
|
||||
vec![Overlay::Path { path, color: [0, 0, 0], dashed: false }]
|
||||
// In the colour of the layer the path goes on, as InDesign draws it.
|
||||
let color = cx.doc.layer(cx.layer).map_or([60, 120, 220], |l| l.color);
|
||||
vec![Overlay::Path { path, color, dashed: false }]
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
|
||||
@@ -172,7 +172,18 @@ fn pencil_draws_a_smooth_path() {
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 100.0 + 50.0 * a.sin() + off.x, 100.0 + 50.0 * (1.0 - a.cos()) + off.y));
|
||||
}
|
||||
assert!(t.busy());
|
||||
// While drawing, the stroke so far is shown, in the colour of the layer it goes on.
|
||||
let layer = d.layer(cx.layer).unwrap().color;
|
||||
match &t.overlays(&cx)[..] {
|
||||
[crate::Overlay::Path { path, color, dashed: false }] => {
|
||||
assert_eq!(*color, layer);
|
||||
assert!(path.elements().len() > 40, "the stroke so far: {} points", path.elements().len());
|
||||
assert_eq!(path.elements()[0], designcraft_geom::PathEl::MoveTo(designcraft_geom::Point::new(100.0 + off.x, 100.0 + off.y)));
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0 + off.x, 150.0 + off.y));
|
||||
assert!(t.overlays(&cx).is_empty(), "the preview ends with the stroke");
|
||||
match &a[..] {
|
||||
[Action::Exec(cmd, p)] => {
|
||||
assert_eq!(cmd, "path.create");
|
||||
|
||||
@@ -1237,7 +1237,39 @@ fn draw_tool_overlays(app: &mut DesignApp, painter: &egui::Painter, xf: &Xf) {
|
||||
painter.rect_filled(r, 3.0, tok.measure_bg);
|
||||
painter.galley(r.min + vec2(5.0, 3.0), g, tok.measure_text);
|
||||
}
|
||||
Overlay::Path { .. } => {}
|
||||
Overlay::Path { path, color, dashed: d } => draw_path_overlay(painter, xf, &path, c32(color), d),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A path overlay (canvas coordinates; the Pencil's stroke while drawing, the Smooth and Erase
|
||||
/// drags) as screen polylines, its curves flattened to about a quarter of a screen pixel.
|
||||
fn draw_path_overlay(painter: &egui::Painter, xf: &Xf, path: &designcraft_geom::BezPath, color: Color32, dashed_line: bool) {
|
||||
use designcraft_geom::PathEl;
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let mut runs: Vec<Vec<Pos2>> = Vec::new();
|
||||
designcraft_geom::kurbo::flatten(path, 0.25 / xf.zoom.max(1e-9), |el| match el {
|
||||
PathEl::MoveTo(p) => runs.push(vec![xf.to_screen(p)]),
|
||||
PathEl::LineTo(p) => {
|
||||
if let Some(run) = runs.last_mut() {
|
||||
run.push(xf.to_screen(p));
|
||||
}
|
||||
}
|
||||
PathEl::ClosePath => {
|
||||
if let Some(run) = runs.last_mut()
|
||||
&& let Some(&first) = run.first()
|
||||
{
|
||||
run.push(first);
|
||||
}
|
||||
}
|
||||
// Flattening yields only moves, lines and closes.
|
||||
PathEl::QuadTo(..) | PathEl::CurveTo(..) => {}
|
||||
});
|
||||
for run in runs.into_iter().filter(|r| r.len() >= 2) {
|
||||
if dashed_line {
|
||||
painter.extend(egui::Shape::dashed_line(&run, stroke, 3.0, 3.0));
|
||||
} else {
|
||||
painter.add(egui::Shape::line(run, stroke));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1919,3 +1951,58 @@ mod tests {
|
||||
assert!(h.query_by_label(" Points").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pencil_preview_tests {
|
||||
use egui::{Pos2, pos2, vec2};
|
||||
|
||||
/// Lines painted in `color` (egui paths) and the points they pass through.
|
||||
fn lines_in(h: &egui_kittest::Harness<'static, crate::test_window::Window>, color: egui::Color32) -> Vec<Vec<Pos2>> {
|
||||
fn walk(s: &egui::Shape, color: egui::Color32, out: &mut Vec<Vec<Pos2>>) {
|
||||
match s {
|
||||
egui::Shape::Path(p) if p.stroke.color == egui::epaint::ColorMode::Solid(color) && p.points.len() >= 2 => out.push(p.points.clone()),
|
||||
egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, color, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
h.output().shapes.iter().for_each(|s| walk(&s.shape, color, &mut out));
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pencil_stroke_shows_while_drawing() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", serde_json::json!({})).unwrap();
|
||||
app.run("tool.select", serde_json::json!({"tool": "pencil"})).unwrap();
|
||||
let mut h = crate::test_window::open(app, vec2(1400.0, 900.0));
|
||||
let doc = &h.state().app.session.doc().unwrap().doc;
|
||||
let layer = doc.layers[0].color;
|
||||
let color = egui::Color32::from_rgb(layer[0], layer[1], layer[2]);
|
||||
let items = |h: &egui_kittest::Harness<'static, crate::test_window::Window>| h.state().app.session.doc().unwrap().doc.spreads[0].items.len();
|
||||
let before = items(&h);
|
||||
// Press on the page and drag, without letting go.
|
||||
let start = pos2(520.0, 380.0);
|
||||
h.hover_at(start);
|
||||
h.drag_at(start);
|
||||
h.run_steps(2);
|
||||
let path: Vec<Pos2> = (1..=12).map(|i| start + vec2(i as f32 * 12.0, (i as f32 * 0.6).sin() * 30.0)).collect();
|
||||
for p in &path {
|
||||
h.hover_at(*p);
|
||||
h.run_steps(1);
|
||||
}
|
||||
let drawn = lines_in(&h, color);
|
||||
let stroke = drawn.iter().max_by_key(|l| l.len()).expect("the stroke is painted while the button is down");
|
||||
let near = |a: Pos2, b: Pos2| (a - b).length() < 2.0;
|
||||
assert!(near(stroke[0], start), "it starts where the press was: {:?}", stroke[0]);
|
||||
assert!(path.iter().all(|p| stroke.iter().any(|q| near(*p, *q))), "it follows the pointer: {stroke:?}");
|
||||
assert_eq!(items(&h), before, "nothing is made before the release");
|
||||
// Letting go makes the path and ends the preview.
|
||||
h.drop_at(*path.last().unwrap());
|
||||
h.run_steps(3);
|
||||
assert_eq!(items(&h), before + 1);
|
||||
let app = &mut h.state_mut().app;
|
||||
let view = app.view_info();
|
||||
assert!(app.session.overlays(view).is_empty(), "the preview is gone");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- Style groups: paragraph and character styles shown in group folders, Move to Group (new, existing, none); `style.group` renames every use (styles, stories, table cells, footnotes, object styles).
|
||||
- Scrubby zoom: drag the Zoom tool left or right to zoom continuously around the press point.
|
||||
- Guides belong to the active layer: hidden with it (or with its Show Guides off) and locked with it.
|
||||
- Pencil tool (N): freehand strokes simplified to smooth paths (Alt closes); Smooth and Erase tools re-fit or remove the stretch you drag along (`path.smooth`, `path.erase`).
|
||||
- Pencil tool (N): freehand strokes, shown as you draw in the layer's colour, simplified to smooth paths (Alt closes); Smooth and Erase tools re-fit or remove the stretch you drag along (`path.smooth`, `path.erase`).
|
||||
- Edit › Keyboard Shortcuts: every command, click and press the new keys, conflicts shown, per-command and global reset; `window.setShortcut`.
|
||||
- Primary Text Frame (New Document) and Smart Text Reflow: pages are added while the primary story oversets and empty ones at the end removed, in the same undo step (Preferences › Type).
|
||||
- Glyphs panel: every character of any font (rendered by our renderer), search by character or U+code, click to insert, recently used.
|
||||
|
||||
Reference in New Issue
Block a user