1use crate::{MathFunction, Glyph, RenderLayer};
7use crate::glyph::GlyphId;
8use crate::ProofEngine;
9use glam::{Vec2, Vec3, Vec4};
10
11const BAR_CHARS: &[char] = &[' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
13
14pub struct MathGraph {
16 pub function: MathFunction,
17 pub time_range: (f32, f32),
19 pub columns: usize,
20 pub rows: usize,
21 pub graph_color: Vec4,
22 pub axis_color: Vec4,
23 pub bg_color: Vec4,
24 pub label: Option<String>,
25}
26
27impl MathGraph {
28 pub fn new(function: MathFunction, time_range: (f32, f32)) -> Self {
29 Self {
30 function,
31 time_range,
32 columns: 24,
33 rows: 6,
34 graph_color: Vec4::new(0.3, 1.0, 0.5, 0.9),
35 axis_color: Vec4::new(0.5, 0.5, 0.5, 0.6),
36 bg_color: Vec4::new(0.05, 0.05, 0.1, 0.7),
37 label: None,
38 }
39 }
40
41 pub fn with_label(mut self, label: impl Into<String>) -> Self {
42 self.label = Some(label.into());
43 self
44 }
45
46 pub fn with_dimensions(mut self, columns: usize, rows: usize) -> Self {
47 self.columns = columns;
48 self.rows = rows;
49 self
50 }
51
52 pub fn render(
54 &self,
55 engine: &mut ProofEngine,
56 origin: Vec3,
57 char_w: f32,
58 char_h: f32,
59 time: f32,
60 ) -> Vec<GlyphId> {
61 let mut ids = Vec::new();
62 let cols = self.columns.max(4);
63 let rows = self.rows.max(2);
64 let char_w_s = char_w / cols as f32;
65 let char_h_s = char_h / rows as f32;
66
67 let t_start = time + self.time_range.0;
69 let t_end = time + self.time_range.1;
70 let samples: Vec<f32> = (0..cols).map(|i| {
71 let t = t_start + (i as f32 / (cols - 1) as f32) * (t_end - t_start);
72 self.function.evaluate(t, 0.0)
73 }).collect();
74
75 let s_min = samples.iter().cloned().fold(f32::MAX, f32::min);
77 let s_max = samples.iter().cloned().fold(f32::MIN, f32::max);
78 let range = (s_max - s_min).max(f32::EPSILON);
79
80 for row in 0..rows {
82 for col in 0..cols {
83 let pos = origin + Vec3::new(col as f32 * char_w_s, -(row as f32 * char_h_s), 0.0);
84 let is_mid_row = row == rows / 2;
85 let ch = if is_mid_row { '─' } else { '·' };
86 let color = if is_mid_row { self.axis_color } else { self.bg_color };
87 let id = engine.scene.spawn_glyph(Glyph {
88 character: ch,
89 position: pos,
90 color,
91 emission: 0.0,
92 glow_color: Vec3::ZERO,
93 glow_radius: 0.0,
94 scale: Vec2::splat(char_w_s * 0.9),
95 layer: RenderLayer::UI,
96 ..Default::default()
97 });
98 ids.push(id);
99 }
100 }
101
102 for (col, &sample) in samples.iter().enumerate() {
104 let norm = ((sample - s_min) / range).clamp(0.0, 1.0);
105 let bar_height = (norm * rows as f32) as usize;
106
107 for row in 0..rows {
108 let row_flip = rows - 1 - row; let pos = origin + Vec3::new(col as f32 * char_w_s, -(row as f32 * char_h_s), 0.1);
110
111 if row_flip < bar_height {
112 let ch = '█';
114 let t = row_flip as f32 / rows.max(1) as f32;
115 let color = Vec4::new(
116 self.graph_color.x * (0.5 + t * 0.5),
117 self.graph_color.y,
118 self.graph_color.z * (1.0 - t * 0.3),
119 self.graph_color.w,
120 );
121 let id = engine.scene.spawn_glyph(Glyph {
122 character: ch,
123 position: pos,
124 color,
125 emission: 0.4 + t * 0.4,
126 glow_color: Vec3::new(color.x, color.y, color.z),
127 glow_radius: 0.5,
128 scale: Vec2::splat(char_w_s * 0.85),
129 layer: RenderLayer::UI,
130 ..Default::default()
131 });
132 ids.push(id);
133 } else if row_flip == bar_height {
134 let frac_height = (norm * rows as f32).fract();
136 let bar_idx = (frac_height * BAR_CHARS.len() as f32) as usize;
137 let ch = BAR_CHARS[bar_idx.min(BAR_CHARS.len() - 1)];
138 let id = engine.scene.spawn_glyph(Glyph {
139 character: ch,
140 position: pos,
141 color: self.graph_color,
142 emission: 0.8,
143 glow_color: Vec3::new(self.graph_color.x, self.graph_color.y, self.graph_color.z),
144 glow_radius: 0.8,
145 scale: Vec2::splat(char_w_s * 0.85),
146 layer: RenderLayer::UI,
147 ..Default::default()
148 });
149 ids.push(id);
150 }
151 }
152 }
153
154 if let Some(ref label) = self.label {
156 for (i, ch) in label.chars().enumerate() {
157 let pos = origin + Vec3::new(
158 i as f32 * char_w_s,
159 -(rows as f32 * char_h_s + 0.3),
160 0.1,
161 );
162 let id = engine.scene.spawn_glyph(Glyph {
163 character: ch,
164 position: pos,
165 color: Vec4::new(0.7, 0.7, 0.7, 0.8),
166 scale: Vec2::splat(char_w_s * 0.8),
167 layer: RenderLayer::UI,
168 ..Default::default()
169 });
170 ids.push(id);
171 }
172 }
173
174 let current = self.function.evaluate(time, 0.0);
176 let val_str = format!("{:+.2}", current);
177 for (i, ch) in val_str.chars().enumerate() {
178 let pos = origin + Vec3::new(
179 (cols as f32 + 0.5 + i as f32) * char_w_s,
180 -(rows as f32 * 0.5 * char_h_s),
181 0.1,
182 );
183 let id = engine.scene.spawn_glyph(Glyph {
184 character: ch,
185 position: pos,
186 color: self.graph_color,
187 emission: 0.5,
188 glow_color: Vec3::new(self.graph_color.x, self.graph_color.y, self.graph_color.z),
189 scale: Vec2::splat(char_w_s),
190 layer: RenderLayer::UI,
191 ..Default::default()
192 });
193 ids.push(id);
194 }
195
196 ids
197 }
198}