1use crate::svg;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Hatch {
13 SolidWhite,
14 SolidBlack,
15 Diagonal,
16 DiagonalDense,
17 AntiDiagonal,
18 Horizontal,
19 Vertical,
20 Crosshatch,
21 Grid,
22 DotsSparse,
23 DotsDense,
24 Brick,
25 Zigzag,
26 DashedH,
27 Checker,
28}
29
30impl Hatch {
31 pub const CYCLE: [Hatch; 15] = [
34 Hatch::SolidWhite,
35 Hatch::Diagonal,
36 Hatch::AntiDiagonal,
37 Hatch::Horizontal,
38 Hatch::Vertical,
39 Hatch::Crosshatch,
40 Hatch::DotsSparse,
41 Hatch::Grid,
42 Hatch::Brick,
43 Hatch::DotsDense,
44 Hatch::Zigzag,
45 Hatch::DashedH,
46 Hatch::DiagonalDense,
47 Hatch::Checker,
48 Hatch::SolidBlack,
49 ];
50
51 pub fn cycle(index: usize) -> Hatch {
52 Self::CYCLE[index % Self::CYCLE.len()]
53 }
54
55 pub const DEFAULT_RAMP: [Hatch; 7] = [
57 Hatch::SolidWhite,
58 Hatch::DotsSparse,
59 Hatch::Diagonal,
60 Hatch::Crosshatch,
61 Hatch::DiagonalDense,
62 Hatch::DotsDense,
63 Hatch::SolidBlack,
64 ];
65
66 pub fn fill_value(self) -> String {
68 match self {
69 Hatch::SolidWhite => "white".to_string(),
70 Hatch::SolidBlack => "black".to_string(),
71 other => format!("url(#{})", other.dom_id()),
72 }
73 }
74
75 pub fn dom_id(self) -> &'static str {
77 match self {
78 Hatch::SolidWhite => "bland-pattern-solid-white",
79 Hatch::SolidBlack => "bland-pattern-solid-black",
80 Hatch::Diagonal => "bland-pattern-diagonal",
81 Hatch::DiagonalDense => "bland-pattern-diagonal-dense",
82 Hatch::AntiDiagonal => "bland-pattern-anti-diagonal",
83 Hatch::Horizontal => "bland-pattern-horizontal",
84 Hatch::Vertical => "bland-pattern-vertical",
85 Hatch::Crosshatch => "bland-pattern-crosshatch",
86 Hatch::Grid => "bland-pattern-grid",
87 Hatch::DotsSparse => "bland-pattern-dots-sparse",
88 Hatch::DotsDense => "bland-pattern-dots-dense",
89 Hatch::Brick => "bland-pattern-brick",
90 Hatch::Zigzag => "bland-pattern-zigzag",
91 Hatch::DashedH => "bland-pattern-dashed-h",
92 Hatch::Checker => "bland-pattern-checker",
93 }
94 }
95
96 pub fn needs_def(self) -> bool {
97 !matches!(self, Hatch::SolidWhite | Hatch::SolidBlack)
98 }
99}
100
101pub fn write_defs(buf: &mut String, used: &[Hatch]) {
104 let mut seen = [false; 15];
105 for hatch in used.iter().copied() {
106 if !hatch.needs_def() {
107 continue;
108 }
109 let idx = hatch as usize;
110 if seen[idx] {
111 continue;
112 }
113 seen[idx] = true;
114 write_def(buf, hatch);
115 }
116}
117
118fn write_def(buf: &mut String, hatch: Hatch) {
119 let id = hatch.dom_id();
120 match hatch {
121 Hatch::Diagonal => pattern(buf, id, 8.0, 8.0, Some(45.0), &|b| line(b, 0, 0, 0, 8)),
122 Hatch::DiagonalDense => {
123 pattern(buf, id, 4.0, 4.0, Some(45.0), &|b| line(b, 0, 0, 0, 4))
124 }
125 Hatch::AntiDiagonal => pattern(buf, id, 8.0, 8.0, Some(-45.0), &|b| line(b, 0, 0, 0, 8)),
126 Hatch::Horizontal => pattern(buf, id, 6.0, 6.0, None, &|b| line(b, 0, 3, 6, 3)),
127 Hatch::Vertical => pattern(buf, id, 6.0, 6.0, None, &|b| line(b, 3, 0, 3, 6)),
128 Hatch::Crosshatch => pattern(buf, id, 10.0, 10.0, None, &|b| {
129 line(b, 0, 0, 10, 10);
130 line(b, 10, 0, 0, 10);
131 }),
132 Hatch::Grid => pattern(buf, id, 8.0, 8.0, None, &|b| {
133 line(b, 0, 0, 8, 0);
134 line(b, 0, 0, 0, 8);
135 }),
136 Hatch::DotsSparse => pattern(buf, id, 10.0, 10.0, None, &|b| dot(b, 5.0, 5.0, 1.2)),
137 Hatch::DotsDense => pattern(buf, id, 5.0, 5.0, None, &|b| dot(b, 2.5, 2.5, 1.0)),
138 Hatch::Brick => pattern(buf, id, 16.0, 8.0, None, &|b| {
139 line(b, 0, 8, 16, 8);
140 line(b, 0, 4, 0, 8);
141 line(b, 16, 4, 16, 8);
142 line(b, 0, 0, 16, 0);
143 line(b, 0, 4, 16, 4);
144 line(b, 8, 0, 8, 4);
145 }),
146 Hatch::Zigzag => pattern(buf, id, 12.0, 8.0, None, &|b| {
147 b.push_str(
148 "<path d=\"M0 6 L3 2 L6 6 L9 2 L12 6\" fill=\"none\" stroke=\"black\" stroke-width=\"1\"/>",
149 );
150 }),
151 Hatch::DashedH => pattern(buf, id, 10.0, 6.0, None, &|b| line(b, 0, 3, 5, 3)),
152 Hatch::Checker => pattern(buf, id, 8.0, 8.0, None, &|b| {
153 b.push_str("<rect x=\"0\" y=\"0\" width=\"4\" height=\"4\" fill=\"black\"/>");
154 b.push_str("<rect x=\"4\" y=\"4\" width=\"4\" height=\"4\" fill=\"black\"/>");
155 }),
156 Hatch::SolidWhite | Hatch::SolidBlack => {}
157 }
158}
159
160fn pattern(buf: &mut String, id: &str, w: f64, h: f64, rotate: Option<f64>, body: &dyn Fn(&mut String)) {
161 buf.push_str("<pattern id=\"");
162 buf.push_str(id);
163 buf.push_str("\" patternUnits=\"userSpaceOnUse\" width=\"");
164 svg::num_into(buf, w);
165 buf.push_str("\" height=\"");
166 svg::num_into(buf, h);
167 buf.push('"');
168 if let Some(deg) = rotate {
169 buf.push_str(" patternTransform=\"rotate(");
170 svg::num_into(buf, deg);
171 buf.push_str(")\"");
172 }
173 buf.push('>');
174 body(buf);
175 buf.push_str("</pattern>");
176}
177
178fn line(buf: &mut String, x1: i32, y1: i32, x2: i32, y2: i32) {
179 use std::fmt::Write;
180 let _ = write!(
181 buf,
182 "<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"black\" stroke-width=\"1\"/>",
183 x1, y1, x2, y2
184 );
185}
186
187fn dot(buf: &mut String, cx: f64, cy: f64, r: f64) {
188 buf.push_str("<circle cx=\"");
189 svg::num_into(buf, cx);
190 buf.push_str("\" cy=\"");
191 svg::num_into(buf, cy);
192 buf.push_str("\" r=\"");
193 svg::num_into(buf, r);
194 buf.push_str("\" fill=\"black\"/>");
195}
196
197pub fn quantize(value: f64, lo: f64, hi: f64, n_levels: usize) -> usize {
200 if lo == hi {
201 return n_levels.saturating_sub(1) / 2;
202 }
203 if value <= lo {
204 return 0;
205 }
206 if value >= hi {
207 return n_levels - 1;
208 }
209 let scaled = ((value - lo) / (hi - lo) * n_levels as f64) as usize;
210 scaled.min(n_levels - 1)
211}
212
213pub fn extent(grid: &[Vec<f64>]) -> (f64, f64) {
215 let mut iter = grid.iter().flat_map(|row| row.iter()).copied().filter(|v| v.is_finite());
216 let first = match iter.next() {
217 Some(v) => v,
218 None => return (0.0, 1.0),
219 };
220 let mut lo = first;
221 let mut hi = first;
222 for v in iter {
223 if v < lo {
224 lo = v;
225 }
226 if v > hi {
227 hi = v;
228 }
229 }
230 (lo, hi)
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn quantize_clamps() {
239 assert_eq!(quantize(-1.0, 0.0, 1.0, 4), 0);
240 assert_eq!(quantize(2.0, 0.0, 1.0, 4), 3);
241 }
242
243 #[test]
244 fn quantize_midpoint_is_in_middle() {
245 assert_eq!(quantize(0.5, 0.0, 1.0, 4), 2);
246 }
247
248 #[test]
249 fn extent_finds_min_max() {
250 let grid = vec![vec![1.0, 4.0], vec![-1.0, 7.0]];
251 assert_eq!(extent(&grid), (-1.0, 7.0));
252 }
253}