bland 0.1.0

Pure-Rust library for paper-ready, monochrome, hatch-patterned technical plots in the visual tradition of 1960s-80s engineering reports.
Documentation
//! Marching-squares contour extraction for 2D scalar grids.
//!
//! Given a `rows × cols` grid, [`segments`] returns a list of line
//! segments in data space where the field equals each requested level.
//! The renderer draws those segments as iso-lines.

use crate::series::Origin;

pub type Point = (f64, f64);
pub type Segment = (Point, Point);

/// Extracts iso-line segments at every level in `levels`.
pub fn segments(
    grid: &[Vec<f64>],
    x_edges: &[f64],
    y_edges: &[f64],
    levels: &[f64],
    origin: Origin,
) -> Vec<(f64, Vec<Segment>)> {
    let rows = grid.len();
    if rows < 2 {
        return Vec::new();
    }
    let cols = grid[0].len();
    if cols < 2 {
        return Vec::new();
    }

    let y_idx = |r: usize| match origin {
        Origin::TopLeft => rows - 1 - r,
        Origin::BottomLeft => r,
    };

    levels
        .iter()
        .map(|&level| {
            let mut segs = Vec::new();
            for row in 0..(rows - 1) {
                for col in 0..(cols - 1) {
                    cell_segments(
                        grid, x_edges, y_edges, &y_idx, row, col, level, &mut segs,
                    );
                }
            }
            (level, segs)
        })
        .collect()
}

fn cell_segments<F>(
    grid: &[Vec<f64>],
    x_edges: &[f64],
    y_edges: &[f64],
    y_idx: &F,
    row: usize,
    col: usize,
    level: f64,
    out: &mut Vec<Segment>,
) where
    F: Fn(usize) -> usize,
{
    let x_l = x_edges[col];
    let x_r = x_edges[col + 1];

    let y_row = y_idx(row);
    let y_row_next = y_idx(row + 1);
    let y_b = y_edges[y_row.min(y_row_next)];
    let y_t = y_edges[y_row.max(y_row_next)];

    let (row_b, row_t) = if y_row < y_row_next {
        (row, row + 1)
    } else {
        (row + 1, row)
    };

    let bl = grid[row_b][col];
    let br = grid[row_b][col + 1];
    let tr = grid[row_t][col + 1];
    let tl = grid[row_t][col];

    let mut idx = 0u8;
    if bl >= level {
        idx |= 1;
    }
    if br >= level {
        idx |= 2;
    }
    if tr >= level {
        idx |= 4;
    }
    if tl >= level {
        idx |= 8;
    }

    let bottom = || (interp(x_l, x_r, bl, br, level), y_b);
    let right = || (x_r, interp(y_b, y_t, br, tr, level));
    let top = || (interp(x_r, x_l, tr, tl, level), y_t);
    let left = || (x_l, interp(y_t, y_b, tl, bl, level));

    match idx {
        0 | 15 => {}
        1 => out.push((left(), bottom())),
        2 => out.push((bottom(), right())),
        3 => out.push((left(), right())),
        4 => out.push((right(), top())),
        6 => out.push((bottom(), top())),
        7 => out.push((left(), top())),
        8 => out.push((top(), left())),
        9 => out.push((top(), bottom())),
        11 => out.push((top(), right())),
        12 => out.push((right(), left())),
        13 => out.push((right(), bottom())),
        14 => out.push((bottom(), left())),
        5 => {
            // Saddle case — disambiguate with the cell-center mean.
            if (bl + br + tr + tl) / 4.0 >= level {
                out.push((left(), top()));
                out.push((right(), bottom()));
            } else {
                out.push((left(), bottom()));
                out.push((right(), top()));
            }
        }
        10 => {
            if (bl + br + tr + tl) / 4.0 >= level {
                out.push((bottom(), left()));
                out.push((top(), right()));
            } else {
                out.push((bottom(), right()));
                out.push((top(), left()));
            }
        }
        _ => {}
    }
}

fn interp(x1: f64, x2: f64, v1: f64, v2: f64, level: f64) -> f64 {
    if v1 == v2 {
        return (x1 + x2) / 2.0;
    }
    let t = (level - v1) / (v2 - v1);
    x1 + t * (x2 - x1)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn flat_grid_produces_no_segments_off_level() {
        let grid = vec![vec![0.0, 0.0], vec![0.0, 0.0]];
        let edges = vec![0.0, 1.0];
        let result = segments(&grid, &edges, &edges, &[0.5], Origin::BottomLeft);
        assert_eq!(result[0].1.len(), 0);
    }

    #[test]
    fn single_cell_with_one_corner_above_emits_segment() {
        let grid = vec![vec![0.0, 0.0], vec![1.0, 0.0]];
        let edges = vec![0.0, 1.0];
        let result = segments(&grid, &edges, &edges, &[0.5], Origin::BottomLeft);
        assert_eq!(result[0].1.len(), 1);
    }
}