ass_renderer/debug/inspector/
mod.rs1use crate::Frame;
2
3#[cfg(feature = "nostd")]
4use alloc::vec::Vec;
5
6mod info;
7
8pub use info::{
9 BoundsInfo, ColorHistogram, LineScanSegment, PixelComparison, PixelInfo, RegionInfo,
10};
11
12pub struct FrameInspector {
14 frame: Option<Frame>,
15 cursor_x: u32,
16 cursor_y: u32,
17}
18
19impl Default for FrameInspector {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl FrameInspector {
26 pub fn new() -> Self {
27 Self {
28 frame: None,
29 cursor_x: 0,
30 cursor_y: 0,
31 }
32 }
33
34 pub fn load_frame(&mut self, frame: Frame) {
35 self.cursor_x = frame.width() / 2;
36 self.cursor_y = frame.height() / 2;
37 self.frame = Some(frame);
38 }
39
40 pub fn move_cursor(&mut self, dx: i32, dy: i32) {
41 if let Some(ref frame) = self.frame {
42 let new_x = (self.cursor_x as i32 + dx)
43 .max(0)
44 .min(frame.width() as i32 - 1) as u32;
45 let new_y = (self.cursor_y as i32 + dy)
46 .max(0)
47 .min(frame.height() as i32 - 1) as u32;
48 self.cursor_x = new_x;
49 self.cursor_y = new_y;
50 }
51 }
52
53 pub fn set_cursor(&mut self, x: u32, y: u32) {
54 if let Some(ref frame) = self.frame {
55 self.cursor_x = x.min(frame.width() - 1);
56 self.cursor_y = y.min(frame.height() - 1);
57 }
58 }
59
60 pub fn get_pixel_at_cursor(&self) -> Option<PixelInfo> {
61 self.get_pixel_at(self.cursor_x, self.cursor_y)
62 }
63
64 pub fn get_pixel_at(&self, x: u32, y: u32) -> Option<PixelInfo> {
65 let frame = self.frame.as_ref()?;
66
67 if x >= frame.width() || y >= frame.height() {
68 return None;
69 }
70
71 let idx = ((y * frame.width() + x) * 4) as usize;
72 let pixels = frame.pixels();
73
74 if idx + 3 >= pixels.len() {
75 return None;
76 }
77
78 Some(PixelInfo {
79 x,
80 y,
81 r: pixels[idx],
82 g: pixels[idx + 1],
83 b: pixels[idx + 2],
84 a: pixels[idx + 3],
85 })
86 }
87
88 pub fn get_region(&self, x: u32, y: u32, width: u32, height: u32) -> RegionInfo {
89 let frame = match self.frame.as_ref() {
90 Some(f) => f,
91 None => return RegionInfo::empty(),
92 };
93
94 let mut pixels = Vec::new();
95 let mut histogram = ColorHistogram::new();
96
97 let x_end = (x + width).min(frame.width());
98 let y_end = (y + height).min(frame.height());
99
100 for py in y..y_end {
101 for px in x..x_end {
102 if let Some(pixel) = self.get_pixel_at(px, py) {
103 histogram.add_pixel(&pixel);
104 pixels.push(pixel);
105 }
106 }
107 }
108
109 RegionInfo {
110 x,
111 y,
112 width: x_end - x,
113 height: y_end - y,
114 pixels,
115 histogram,
116 }
117 }
118
119 pub fn find_non_transparent_bounds(&self) -> Option<BoundsInfo> {
120 let frame = self.frame.as_ref()?;
121 let pixels = frame.pixels();
122
123 let mut min_x = frame.width();
124 let mut min_y = frame.height();
125 let mut max_x = 0u32;
126 let mut max_y = 0u32;
127 let mut found = false;
128
129 for y in 0..frame.height() {
130 for x in 0..frame.width() {
131 let idx = ((y * frame.width() + x) * 4) as usize;
132 if idx + 3 < pixels.len() && pixels[idx + 3] > 0 {
133 found = true;
134 min_x = min_x.min(x);
135 min_y = min_y.min(y);
136 max_x = max_x.max(x);
137 max_y = max_y.max(y);
138 }
139 }
140 }
141
142 if !found {
143 return None;
144 }
145
146 Some(BoundsInfo {
147 min_x,
148 min_y,
149 max_x,
150 max_y,
151 width: max_x - min_x + 1,
152 height: max_y - min_y + 1,
153 })
154 }
155
156 pub fn scan_line(&self, y: u32) -> Vec<LineScanSegment> {
157 let frame = match self.frame.as_ref() {
158 Some(f) => f,
159 None => return Vec::new(),
160 };
161
162 if y >= frame.height() {
163 return Vec::new();
164 }
165
166 let mut segments = Vec::new();
167 let mut current_segment: Option<LineScanSegment> = None;
168
169 for x in 0..frame.width() {
170 if let Some(pixel) = self.get_pixel_at(x, y) {
171 if pixel.a > 0 {
172 match current_segment.as_mut() {
174 Some(seg) if seg.is_similar(&pixel) => {
175 seg.end_x = x;
176 seg.pixels.push(pixel);
177 }
178 _ => {
179 if let Some(seg) = current_segment.take() {
180 segments.push(seg);
181 }
182 current_segment = Some(LineScanSegment {
183 y,
184 start_x: x,
185 end_x: x,
186 pixels: vec![pixel],
187 avg_color: [pixel.r, pixel.g, pixel.b, pixel.a],
188 });
189 }
190 }
191 } else if let Some(seg) = current_segment.take() {
192 segments.push(seg);
193 }
194 }
195 }
196
197 if let Some(mut seg) = current_segment {
198 seg.calculate_average();
199 segments.push(seg);
200 }
201
202 segments
203 }
204
205 pub fn compare_pixels(&self, x1: u32, y1: u32, x2: u32, y2: u32) -> Option<PixelComparison> {
206 let pixel1 = self.get_pixel_at(x1, y1)?;
207 let pixel2 = self.get_pixel_at(x2, y2)?;
208
209 Some(PixelComparison {
210 pixel1,
211 pixel2,
212 color_distance: pixel1.color_distance(&pixel2),
213 is_similar: pixel1.is_similar(&pixel2, 30),
214 })
215 }
216
217 pub fn print_cursor_info(&self) {
218 if let Some(pixel) = self.get_pixel_at_cursor() {
219 pixel.print_detailed();
220 } else {
221 println!("No frame loaded or cursor out of bounds");
222 }
223 }
224
225 pub fn print_ascii_preview(&self, width: u32, height: u32) {
226 let frame = match self.frame.as_ref() {
227 Some(f) => f,
228 None => {
229 println!("No frame loaded");
230 return;
231 }
232 };
233
234 let scale_x = frame.width() as f32 / width as f32;
235 let scale_y = frame.height() as f32 / height as f32;
236
237 println!(
238 "ASCII Preview ({}x{} -> {}x{}):",
239 frame.width(),
240 frame.height(),
241 width,
242 height
243 );
244 let border = "─".repeat(width as usize);
245 println!("┌{border}┐");
246
247 for y in 0..height {
248 print!("│");
249 for x in 0..width {
250 let sample_x = (x as f32 * scale_x) as u32;
251 let sample_y = (y as f32 * scale_y) as u32;
252
253 if let Some(pixel) = self.get_pixel_at(sample_x, sample_y) {
254 print!("{}", pixel.to_ascii());
255 } else {
256 print!(" ");
257 }
258 }
259 println!("│");
260 }
261
262 let border = "─".repeat(width as usize);
263 println!("└{border}┘");
264 println!("Legend: █=opaque ▓=semi ░=faint ·=trace =transparent");
265 }
266}