Files
designcraft/crates/render/src/grid.rs
T
aiongg 61504dae48 View: draw the document grid (#530)
Show/Hide Document Grid (View > Grids & Guides, Cmd+') draws the grid
on the positions Snap to Document Grid uses: subdivision and major
lines from each page's top-left corner, across the spread's band of the
pasteboard, behind or in front of page items as the grid settings say.
Dense lines are left out at low zoom. The Properties toggle runs the
command.
2026-10-11 20:46:38 +00:00

140 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The document grid on screen (View › Grids & Guides › Show/Hide Document Grid).
//!
//! The lines are the positions Snap to Document Grid snaps to: every `vertical / subdivisions`
//! on x and `horizontal / subdivisions` on y, counted from the top-left corner of the page that
//! owns that x (`Spread::page_at_x`: a page owns its own width, the outer pages also own the
//! pasteboard beside them).
use designcraft_doc::{DocumentGrid, Spread};
use designcraft_geom::Rect;
use vello_cpu::{RenderContext, peniko};
use crate::Frame;
/// Lines closer together than this many device pixels are left out: subdivisions first, then
/// major lines.
const MIN_GAP_PX: f64 = 6.0;
/// Most lines drawn on one axis of one page column. A denser grid draws nothing on that axis.
const MAX_LINES: f64 = 4096.0;
/// The step between drawn lines on one axis and how many steps make a major line, or `None`
/// when the spacing is unusable or every line would be closer than [`MIN_GAP_PX`].
fn axis_step(major: f64, subdivisions: u32, px: f64) -> Option<(f64, i64)> {
if !major.is_finite() || major <= 0.0 || !px.is_finite() || px <= 0.0 {
return None;
}
let sub = subdivisions.max(1);
let minor = major / f64::from(sub);
if minor.is_finite() && minor / px >= MIN_GAP_PX {
return Some((minor, i64::from(sub)));
}
(major / px >= MIN_GAP_PX).then_some((major, 1))
}
/// Lines `origin + n × step` in `[lo, hi)`, each with whether it is a major line.
fn lines(origin: f64, (step, every): (f64, i64), lo: f64, hi: f64) -> Vec<(f64, bool)> {
let n0 = ((lo - origin) / step).ceil();
let n1 = ((hi - origin) / step).ceil() - 1.0;
if !n0.is_finite() || !n1.is_finite() || n1 < n0 || n1 - n0 >= MAX_LINES {
return vec![];
}
let (n0, n1) = (n0 as i64, n1 as i64);
(n0..=n1).map(|n| (n as f64 * step + origin, n.rem_euclid(every.max(1)) == 0)).collect()
}
/// Page columns of `sp` across `[x0, x1)`: each x range with the bounds origin of its page.
fn columns(sp: &Spread, x0: f64, x1: f64) -> Vec<(f64, f64, f64, f64)> {
let mut cuts: Vec<f64> = vec![x0, x1];
for p in &sp.pages {
for x in [p.x, p.x + p.width] {
if x > x0 && x < x1 {
cuts.push(x);
}
}
}
cuts.retain(|x| x.is_finite());
cuts.sort_by(f64::total_cmp);
cuts.dedup();
cuts.windows(2)
.filter_map(|w| {
let (a, b) = (*w.first()?, *w.get(1)?);
let page = sp.pages.get(sp.page_at_x((a + b) / 2.0)?)?;
let o = page.bounds();
Some((a, b, o.x0, o.y0))
})
.collect()
}
/// Draws the grid over `area` (spread coordinates) of `sp`, clipped to the visible area.
pub(crate) fn draw(ctx: &mut RenderContext, f: &Frame, sp: &Spread, grid: &DocumentGrid, area: Rect) {
let area = area.intersect(f.visible);
if area.width() <= 0.0 || area.height() <= 0.0 {
return;
}
if let Some(xs) = axis_step(grid.vertical, grid.subdivisions, f.px) {
draw_columns(ctx, f, sp, grid, area, xs);
}
if let Some(ys) = axis_step(grid.horizontal, grid.subdivisions, f.px) {
draw_rows(ctx, f, sp, grid, area, ys);
}
}
fn paints(grid: &DocumentGrid) -> [peniko::Color; 2] {
let [r, g, b] = grid.color;
[peniko::Color::from_rgba8(r, g, b, 110), peniko::Color::from_rgba8(r, g, b, 255)]
}
/// Vertical lines.
fn draw_columns(ctx: &mut RenderContext, f: &Frame, sp: &Spread, grid: &DocumentGrid, area: Rect, xs: (f64, i64)) {
let half = f.px / 2.0;
ctx.set_transform(f.view);
let cols = columns(sp, area.x0, area.x1);
for (major, paint) in [false, true].into_iter().zip(paints(grid)) {
ctx.set_paint(paint);
for &(a, b, ox, _) in &cols {
for (x, m) in lines(ox, xs, a, b) {
if m == major {
ctx.fill_rect(&Rect::new(x - half, area.y0, x + half, area.y1));
}
}
}
}
}
/// Horizontal lines, each page column from its own page's top.
fn draw_rows(ctx: &mut RenderContext, f: &Frame, sp: &Spread, grid: &DocumentGrid, area: Rect, ys: (f64, i64)) {
let half = f.px / 2.0;
ctx.set_transform(f.view);
let cols = columns(sp, area.x0, area.x1);
for (major, paint) in [false, true].into_iter().zip(paints(grid)) {
ctx.set_paint(paint);
for &(a, b, _, oy) in &cols {
for (y, m) in lines(oy, ys, area.y0, area.y1) {
if m == major {
ctx.fill_rect(&Rect::new(a, y - half, b, y + half));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unusable_spacing_draws_nothing_and_dense_grids_thin_out() {
for bad in [0.0, -9.0, f64::NAN, f64::INFINITY] {
assert_eq!(axis_step(bad, 8, 1.0), None);
}
assert_eq!(axis_step(72.0, 8, 1.0), Some((9.0, 8)));
// 9 pt subdivisions at 2 pt per pixel are 4.5 px apart: only major lines.
assert_eq!(axis_step(72.0, 8, 2.0), Some((72.0, 1)));
assert_eq!(axis_step(72.0, 0, 1.0), Some((72.0, 1)));
assert_eq!(axis_step(72.0, 8, 100.0), None);
// A span too wide for the cap draws no lines instead of looping.
assert!(lines(0.0, (1e-9, 1), 0.0, 1e9).is_empty());
assert_eq!(lines(10.0, (9.0, 8), 0.0, 30.0), vec![(1.0, false), (10.0, true), (19.0, false), (28.0, false)]);
}
}