Canvas grid: turn with a rotated view, no per-frame vectors (review of #478)

The pixel grid draws through the document grid's `grid()`, which assumed an
unrotated view: with Rotate View its lines stayed upright on screen, in the
wrong places, or vanished. It now spans the box round the canvas's corners in
document space and draws each line through the view, so the pixel grid and
the document grid (lines and dots) turn with the art. The lines are iterated
rather than collected, capped, and computed by multiplication instead of
accumulating a step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-08 18:38:11 +07:00
parent aa8e77d09a
commit 7578787392
2 changed files with 29 additions and 23 deletions

View File

@ -105,7 +105,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
- A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas.
- Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close).
- Four brightness themes, persistent preferences.
- Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview. They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). About 35 stored options still do nothing yet (the rest of #394: Auto Size New Area Type, Show Font Names in English, Highlight Alternate Glyphs, Construction and Spacing Guides, performance, devices…).
- Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). About 35 stored options still do nothing yet (the rest of #394: Auto Size New Area Type, Show Font Names in English, Highlight Alternate Glyphs, Construction and Spacing Guides, performance, devices…).
- Dialogs move by dragging their heading (the band down from the top edge, as a title bar), stay on screen and keep where they were moved for the session; the form dialogs' choices are dropdowns (Offset Path's Joins, Average's and Shear's and Reflect's axis), and Offset Path lists Offset, Joins, Miter Limit and previews (#423).
- Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). Number fields in panels and dialogs step with Up/Down (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once.
- Scrubby labels (#400, beyond Illustrator, as in Photoshop): dragging a numeric field's label left or right (the ↔ cursor shows over it) steps the value one unit every 2 points (Shift: ten, Ctrl/Cmd: a tenth; counts stay whole), in the field's unit and within its limits, applied live as the art follows; the whole drag is one undo step and Escape puts the value back. A click on the label focuses the field; the text box itself never scrubs, and a field with no label doesn't. Text labels before a field (Transform, Properties, Control bar, Stroke, Transparency, form rows in dialogs) and the Character and Paragraph panels' icons act as labels. Preferences › General › Scrub Numeric Fields by Dragging (on by default) turns it off.

View File

@ -813,8 +813,10 @@ fn pref_color(hex: &str, fallback: Color32) -> Color32 {
/// subdivisions instead.
fn grid(p: &egui::Painter, xf: &Xf, spacing: f64, subdiv: u32, look: LineLook) {
let r = xf.rect;
let a = xf.to_doc(r.min);
let b = xf.to_doc(r.max);
// The view in document space: the box round the canvas's corners (the view may be rotated;
// the painter clips to the canvas).
let corners = [r.left_top(), r.right_top(), r.right_bottom(), r.left_bottom()].map(|c| xf.to_doc(c));
let view = corners.iter().fold(Rect::from_points(corners[0], corners[0]), |b, c| b.union_pt(*c));
let sub = spacing / subdiv.max(1) as f64;
let step = if sub * xf.zoom >= 6.0 { sub } else { spacing };
// Dots crowd sooner than lines.
@ -822,25 +824,16 @@ fn grid(p: &egui::Painter, xf: &Xf, spacing: f64, subdiv: u32, look: LineLook) {
return;
}
let major = |v: f64| (v / spacing - (v / spacing).round()).abs() < 1e-6;
// The lines across the view, on screen, each with whether it is a gridline.
let lines = |from: f64, to: f64, screen: &dyn Fn(f64) -> f32| {
let mut v = (from / step).floor() * step;
let mut out = vec![];
while v <= to {
out.push((screen(v), major(v)));
v += step;
}
out
// The lines across the view (where along their axis), at least 4 px apart (and capped).
let lines = |from: f64, to: f64| {
let first = (from / step).floor();
(0..10_000u32).map(move |i| (first + f64::from(i)) * step).take_while(move |v| *v <= to)
};
let xs = lines(a.x, b.x, &|x| xf.to_screen(Point::new(x, 0.0)).x);
let ys = lines(a.y, b.y, &|y| xf.to_screen(Point::new(0.0, y)).y);
if look.dots {
let mut mesh = egui::Mesh::default();
for &(x, mx) in &xs {
for &(y, my) in &ys {
if mx || my {
mesh.add_colored_rect(egui::Rect::from_center_size(pos2(x, y), vec2(1.5, 1.5)), look.color);
}
for x in lines(view.x0, view.x1) {
for y in lines(view.y0, view.y1).filter(|y| major(x) || major(*y)) {
mesh.add_colored_rect(egui::Rect::from_center_size(xf.to_screen(Point::new(x, y)), vec2(1.5, 1.5)), look.color);
}
}
p.add(Shape::mesh(mesh));
@ -848,11 +841,11 @@ fn grid(p: &egui::Painter, xf: &Xf, spacing: f64, subdiv: u32, look: LineLook) {
}
let minor = look.color.gamma_multiply(0.4);
let stroke = |major: bool| Stroke::new(1.0, if major { look.color } else { minor });
for (x, m) in xs {
p.line_segment([pos2(x, r.top()), pos2(x, r.bottom())], stroke(m));
for x in lines(view.x0, view.x1) {
p.line_segment([xf.to_screen(Point::new(x, view.y0)), xf.to_screen(Point::new(x, view.y1))], stroke(major(x)));
}
for (y, m) in ys {
p.line_segment([pos2(r.left(), y), pos2(r.right(), y)], stroke(m));
for y in lines(view.y0, view.y1) {
p.line_segment([xf.to_screen(Point::new(view.x0, y)), xf.to_screen(Point::new(view.x1, y))], stroke(major(y)));
}
}
@ -2044,6 +2037,19 @@ mod tests {
app.ui.view.pixel_preview = true;
app.session.execute("prefs.set", &json!({"key": "showPixelGrid", "value": false})).unwrap();
assert_eq!(pixel_lines(&mut app).0, 0, "the option off: none");
// A rotated view: the lines turn with the pixels, across the whole canvas.
app.session.execute("prefs.set", &json!({"key": "showPixelGrid", "value": true})).unwrap();
app.view_mut().unwrap().rotation = 30.0;
let s = shapes(&mut app, &ctx);
let turned: Vec<_> = s
.iter()
.filter_map(|s| match s {
Shape::LineSegment { points: [a, b], stroke } if stroke.color == PIXEL_GRID => Some((b.x - a.x).atan2(b.y - a.y).to_degrees().abs()),
_ => None,
})
.collect();
assert!(turned.len() >= 200, "{}", turned.len());
assert!(turned.iter().all(|a| [30.0, 60.0, 120.0, 150.0].iter().any(|t| (a - t).abs() < 0.5)), "turned 30°: {turned:?}");
}
/// Guides & Grid (#394): the grid in Grid Color, behind the art or, with Grids In Back off,