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
//! Scatter-point markers in the monochrome technical-drawing tradition.
//!
//! Each marker is drawn as a self-contained SVG fragment at a pixel
//! point. Presets alternate between *open* (stroke-only) and *filled*
//! shapes so overlapping series stay distinguishable without color.

use crate::svg;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Marker {
    CircleOpen,
    CircleFilled,
    SquareOpen,
    SquareFilled,
    TriangleOpen,
    TriangleFilled,
    DiamondOpen,
    DiamondFilled,
    Cross,
    Plus,
    Asterisk,
    Dot,
}

impl Marker {
    pub const CYCLE: [Marker; 12] = [
        Marker::CircleOpen,
        Marker::SquareOpen,
        Marker::TriangleOpen,
        Marker::DiamondOpen,
        Marker::Cross,
        Marker::Plus,
        Marker::CircleFilled,
        Marker::SquareFilled,
        Marker::TriangleFilled,
        Marker::DiamondFilled,
        Marker::Asterisk,
        Marker::Dot,
    ];

    pub fn cycle(index: usize) -> Marker {
        Self::CYCLE[index % Self::CYCLE.len()]
    }
}

/// Append SVG for the marker centered at `(cx, cy)` to `buf`.
///
/// `size` is a radius-like parameter in px. `stroke_width` controls open
/// markers; filled markers ignore it.
pub fn draw(buf: &mut String, marker: Marker, cx: f64, cy: f64, size: f64, stroke_width: f64) {
    match marker {
        Marker::CircleOpen => {
            let attrs = format!(
                " fill=\"none\" stroke=\"black\" stroke-width=\"{}\"",
                svg::num(stroke_width)
            );
            svg::circle(buf, cx, cy, size, &attrs);
        }
        Marker::CircleFilled => {
            svg::circle(buf, cx, cy, size, " fill=\"black\"");
        }
        Marker::SquareOpen => {
            let attrs = format!(
                " fill=\"none\" stroke=\"black\" stroke-width=\"{}\"",
                svg::num(stroke_width)
            );
            svg::rect(buf, cx - size, cy - size, 2.0 * size, 2.0 * size, &attrs);
        }
        Marker::SquareFilled => {
            svg::rect(
                buf,
                cx - size,
                cy - size,
                2.0 * size,
                2.0 * size,
                " fill=\"black\"",
            );
        }
        Marker::TriangleOpen => {
            let pts = triangle_points(cx, cy, size);
            let attrs = format!(
                " fill=\"none\" stroke=\"black\" stroke-width=\"{}\" stroke-linejoin=\"miter\"",
                svg::num(stroke_width)
            );
            svg::polygon(buf, &pts, &attrs);
        }
        Marker::TriangleFilled => {
            let pts = triangle_points(cx, cy, size);
            svg::polygon(buf, &pts, " fill=\"black\"");
        }
        Marker::DiamondOpen => {
            let pts = diamond_points(cx, cy, size);
            let attrs = format!(
                " fill=\"none\" stroke=\"black\" stroke-width=\"{}\"",
                svg::num(stroke_width)
            );
            svg::polygon(buf, &pts, &attrs);
        }
        Marker::DiamondFilled => {
            let pts = diamond_points(cx, cy, size);
            svg::polygon(buf, &pts, " fill=\"black\"");
        }
        Marker::Cross => {
            let attrs = format!(
                " stroke=\"black\" stroke-width=\"{}\"",
                svg::num(stroke_width)
            );
            svg::line(buf, cx - size, cy - size, cx + size, cy + size, &attrs);
            svg::line(buf, cx - size, cy + size, cx + size, cy - size, &attrs);
        }
        Marker::Plus => {
            let attrs = format!(
                " stroke=\"black\" stroke-width=\"{}\"",
                svg::num(stroke_width)
            );
            svg::line(buf, cx - size, cy, cx + size, cy, &attrs);
            svg::line(buf, cx, cy - size, cx, cy + size, &attrs);
        }
        Marker::Asterisk => {
            let attrs = format!(
                " stroke=\"black\" stroke-width=\"{}\"",
                svg::num(stroke_width)
            );
            svg::line(buf, cx - size, cy, cx + size, cy, &attrs);
            svg::line(
                buf,
                cx - size * 0.5,
                cy - size * 0.866,
                cx + size * 0.5,
                cy + size * 0.866,
                &attrs,
            );
            svg::line(
                buf,
                cx - size * 0.5,
                cy + size * 0.866,
                cx + size * 0.5,
                cy - size * 0.866,
                &attrs,
            );
        }
        Marker::Dot => {
            svg::circle(buf, cx, cy, 0.8, " fill=\"black\"");
        }
    }
}

fn triangle_points(cx: f64, cy: f64, s: f64) -> [(f64, f64); 3] {
    [
        (cx, cy - s),
        (cx + s * 0.866, cy + s * 0.5),
        (cx - s * 0.866, cy + s * 0.5),
    ]
}

fn diamond_points(cx: f64, cy: f64, s: f64) -> [(f64, f64); 4] {
    [(cx, cy - s), (cx + s, cy), (cx, cy + s), (cx - s, cy)]
}