use crate::svg;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Hatch {
SolidWhite,
SolidBlack,
Diagonal,
DiagonalDense,
AntiDiagonal,
Horizontal,
Vertical,
Crosshatch,
Grid,
DotsSparse,
DotsDense,
Brick,
Zigzag,
DashedH,
Checker,
}
impl Hatch {
pub const CYCLE: [Hatch; 15] = [
Hatch::SolidWhite,
Hatch::Diagonal,
Hatch::AntiDiagonal,
Hatch::Horizontal,
Hatch::Vertical,
Hatch::Crosshatch,
Hatch::DotsSparse,
Hatch::Grid,
Hatch::Brick,
Hatch::DotsDense,
Hatch::Zigzag,
Hatch::DashedH,
Hatch::DiagonalDense,
Hatch::Checker,
Hatch::SolidBlack,
];
pub fn cycle(index: usize) -> Hatch {
Self::CYCLE[index % Self::CYCLE.len()]
}
pub const DEFAULT_RAMP: [Hatch; 7] = [
Hatch::SolidWhite,
Hatch::DotsSparse,
Hatch::Diagonal,
Hatch::Crosshatch,
Hatch::DiagonalDense,
Hatch::DotsDense,
Hatch::SolidBlack,
];
pub fn fill_value(self) -> String {
match self {
Hatch::SolidWhite => "white".to_string(),
Hatch::SolidBlack => "black".to_string(),
other => format!("url(#{})", other.dom_id()),
}
}
pub fn dom_id(self) -> &'static str {
match self {
Hatch::SolidWhite => "bland-pattern-solid-white",
Hatch::SolidBlack => "bland-pattern-solid-black",
Hatch::Diagonal => "bland-pattern-diagonal",
Hatch::DiagonalDense => "bland-pattern-diagonal-dense",
Hatch::AntiDiagonal => "bland-pattern-anti-diagonal",
Hatch::Horizontal => "bland-pattern-horizontal",
Hatch::Vertical => "bland-pattern-vertical",
Hatch::Crosshatch => "bland-pattern-crosshatch",
Hatch::Grid => "bland-pattern-grid",
Hatch::DotsSparse => "bland-pattern-dots-sparse",
Hatch::DotsDense => "bland-pattern-dots-dense",
Hatch::Brick => "bland-pattern-brick",
Hatch::Zigzag => "bland-pattern-zigzag",
Hatch::DashedH => "bland-pattern-dashed-h",
Hatch::Checker => "bland-pattern-checker",
}
}
pub fn needs_def(self) -> bool {
!matches!(self, Hatch::SolidWhite | Hatch::SolidBlack)
}
}
pub fn write_defs(buf: &mut String, used: &[Hatch]) {
let mut seen = [false; 15];
for hatch in used.iter().copied() {
if !hatch.needs_def() {
continue;
}
let idx = hatch as usize;
if seen[idx] {
continue;
}
seen[idx] = true;
write_def(buf, hatch);
}
}
fn write_def(buf: &mut String, hatch: Hatch) {
let id = hatch.dom_id();
match hatch {
Hatch::Diagonal => pattern(buf, id, 8.0, 8.0, Some(45.0), &|b| line(b, 0, 0, 0, 8)),
Hatch::DiagonalDense => {
pattern(buf, id, 4.0, 4.0, Some(45.0), &|b| line(b, 0, 0, 0, 4))
}
Hatch::AntiDiagonal => pattern(buf, id, 8.0, 8.0, Some(-45.0), &|b| line(b, 0, 0, 0, 8)),
Hatch::Horizontal => pattern(buf, id, 6.0, 6.0, None, &|b| line(b, 0, 3, 6, 3)),
Hatch::Vertical => pattern(buf, id, 6.0, 6.0, None, &|b| line(b, 3, 0, 3, 6)),
Hatch::Crosshatch => pattern(buf, id, 10.0, 10.0, None, &|b| {
line(b, 0, 0, 10, 10);
line(b, 10, 0, 0, 10);
}),
Hatch::Grid => pattern(buf, id, 8.0, 8.0, None, &|b| {
line(b, 0, 0, 8, 0);
line(b, 0, 0, 0, 8);
}),
Hatch::DotsSparse => pattern(buf, id, 10.0, 10.0, None, &|b| dot(b, 5.0, 5.0, 1.2)),
Hatch::DotsDense => pattern(buf, id, 5.0, 5.0, None, &|b| dot(b, 2.5, 2.5, 1.0)),
Hatch::Brick => pattern(buf, id, 16.0, 8.0, None, &|b| {
line(b, 0, 8, 16, 8);
line(b, 0, 4, 0, 8);
line(b, 16, 4, 16, 8);
line(b, 0, 0, 16, 0);
line(b, 0, 4, 16, 4);
line(b, 8, 0, 8, 4);
}),
Hatch::Zigzag => pattern(buf, id, 12.0, 8.0, None, &|b| {
b.push_str(
"<path d=\"M0 6 L3 2 L6 6 L9 2 L12 6\" fill=\"none\" stroke=\"black\" stroke-width=\"1\"/>",
);
}),
Hatch::DashedH => pattern(buf, id, 10.0, 6.0, None, &|b| line(b, 0, 3, 5, 3)),
Hatch::Checker => pattern(buf, id, 8.0, 8.0, None, &|b| {
b.push_str("<rect x=\"0\" y=\"0\" width=\"4\" height=\"4\" fill=\"black\"/>");
b.push_str("<rect x=\"4\" y=\"4\" width=\"4\" height=\"4\" fill=\"black\"/>");
}),
Hatch::SolidWhite | Hatch::SolidBlack => {}
}
}
fn pattern(buf: &mut String, id: &str, w: f64, h: f64, rotate: Option<f64>, body: &dyn Fn(&mut String)) {
buf.push_str("<pattern id=\"");
buf.push_str(id);
buf.push_str("\" patternUnits=\"userSpaceOnUse\" width=\"");
svg::num_into(buf, w);
buf.push_str("\" height=\"");
svg::num_into(buf, h);
buf.push('"');
if let Some(deg) = rotate {
buf.push_str(" patternTransform=\"rotate(");
svg::num_into(buf, deg);
buf.push_str(")\"");
}
buf.push('>');
body(buf);
buf.push_str("</pattern>");
}
fn line(buf: &mut String, x1: i32, y1: i32, x2: i32, y2: i32) {
use std::fmt::Write;
let _ = write!(
buf,
"<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"black\" stroke-width=\"1\"/>",
x1, y1, x2, y2
);
}
fn dot(buf: &mut String, cx: f64, cy: f64, r: f64) {
buf.push_str("<circle cx=\"");
svg::num_into(buf, cx);
buf.push_str("\" cy=\"");
svg::num_into(buf, cy);
buf.push_str("\" r=\"");
svg::num_into(buf, r);
buf.push_str("\" fill=\"black\"/>");
}
pub fn quantize(value: f64, lo: f64, hi: f64, n_levels: usize) -> usize {
if lo == hi {
return n_levels.saturating_sub(1) / 2;
}
if value <= lo {
return 0;
}
if value >= hi {
return n_levels - 1;
}
let scaled = ((value - lo) / (hi - lo) * n_levels as f64) as usize;
scaled.min(n_levels - 1)
}
pub fn extent(grid: &[Vec<f64>]) -> (f64, f64) {
let mut iter = grid.iter().flat_map(|row| row.iter()).copied().filter(|v| v.is_finite());
let first = match iter.next() {
Some(v) => v,
None => return (0.0, 1.0),
};
let mut lo = first;
let mut hi = first;
for v in iter {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
(lo, hi)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quantize_clamps() {
assert_eq!(quantize(-1.0, 0.0, 1.0, 4), 0);
assert_eq!(quantize(2.0, 0.0, 1.0, 4), 3);
}
#[test]
fn quantize_midpoint_is_in_middle() {
assert_eq!(quantize(0.5, 0.0, 1.0, 4), 2);
}
#[test]
fn extent_finds_min_max() {
let grid = vec![vec![1.0, 4.0], vec![-1.0, 7.0]];
assert_eq!(extent(&grid), (-1.0, 7.0));
}
}