| mod frame_overlay; |
| mod screen_bounds; |
| mod selected_object; |
| |
| use avm_stats::{Frame, Plane, PlaneType, Spatial}; |
| use egui::{pos2, vec2, Color32, Mesh, PointerButton, Pos2, Rect, RichText, Rounding, Shape, Stroke, Ui, Vec2}; |
| use egui_plot::{Bar, BarChart, Plot}; |
| |
| use crate::image_manager::ImageType; |
| use crate::stream::CurrentFrame as _; |
| use crate::views::RenderView; |
| use crate::{app_state::AppState, image_manager::JET_COLORMAP}; |
| pub use frame_overlay::{FrameOverlay, REF_FRAME_COLORS}; |
| pub use screen_bounds::ScreenBounds; |
| pub use selected_object::{SelectedObject, SelectedObjectKind}; |
| |
| use super::ViewMode; |
| |
| pub struct FrameViewer; |
| |
| impl FrameViewer { |
| fn check_hovered_object( |
| &self, |
| state: &AppState, |
| frame: &Frame, |
| hover_pos_world: Pos2, |
| ) -> Option<SelectedObjectKind> { |
| let view_settings = state.settings.sharable.view_mode.view_settings(); |
| if view_settings.allow_transform_unit_selection { |
| if let Some(hovered_transform_unit) = frame |
| .iter_transform_units(state.settings.sharable.current_plane.to_plane()) |
| .find(|ctx| ctx.transform_unit.rect().contains(hover_pos_world)) |
| { |
| return Some(SelectedObjectKind::TransformUnit(hovered_transform_unit.locator)); |
| } |
| } |
| if view_settings.allow_coding_unit_selection { |
| let coding_unit_kind = frame.coding_unit_kind(state.settings.sharable.current_plane); |
| if let Some(hovered_coding_unit) = frame |
| .iter_coding_units(coding_unit_kind) |
| .find(|ctx| ctx.coding_unit.rect().contains(hover_pos_world)) |
| { |
| return Some(SelectedObjectKind::CodingUnit(hovered_coding_unit.locator)); |
| } |
| } |
| None |
| } |
| } |
| impl RenderView for FrameViewer { |
| fn title(&self) -> String { |
| "Frame View".into() |
| } |
| |
| fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> { |
| egui::warn_if_debug_build(ui); |
| |
| let Some(frame) = state.stream.current_frame() else { |
| ui.spinner(); |
| return Ok(()); |
| }; |
| |
| // Okay to unwrap here since we already have a Frame, so we must also have a Stream. |
| let stream = state.stream.as_ref().unwrap(); |
| |
| let mut world_bounds = state.settings.sharable.world_bounds; |
| let mut set_selected_object = false; |
| let mut set_selected_object_parent = false; |
| let mut hover_pos_world = None; |
| |
| if state.settings.sharable.view_mode == ViewMode::Transform |
| && matches!( |
| state.settings.sharable.current_plane, |
| PlaneType::Planar(Plane::U | Plane::V) |
| ) |
| { |
| ui.label( |
| RichText::new("WARNING: Chroma transform tree data extraction is currently buggy.").color(Color32::RED), |
| ); |
| ui.end_row(); |
| } |
| |
| let size = ui.available_size_before_wrap(); |
| let (screen_bounds, response) = ui.allocate_exact_size(size, egui::Sense::drag()); |
| let scale = world_bounds.calc_scale(screen_bounds); |
| |
| if response.dragged_by(PointerButton::Primary) { |
| let delta = response.drag_delta() / -scale; |
| world_bounds = world_bounds.translate(delta); |
| } else if response.hover_pos().is_some() && ui.input(|i| i.scroll_delta) != Vec2::ZERO { |
| let delta = ui.input(|i| i.scroll_delta); |
| |
| if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) { |
| if screen_bounds.contains(mouse_pos) { |
| let zoom = if delta.y < 0.0 { |
| f32::powf(1.001, -delta.y) |
| } else { |
| 1.0 / (f32::powf(1.001, delta.y)) |
| }; |
| let zoom_center = world_bounds.screen_pos_to_world(mouse_pos, screen_bounds); |
| world_bounds.zoom_point(zoom_center, zoom); |
| } |
| } |
| } else if response.hover_pos().is_some() { |
| if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) { |
| if response.double_clicked() && state.settings.selected_object_leaf.is_some() { |
| set_selected_object_parent = true; |
| } else if response.clicked() { |
| set_selected_object = true; |
| } |
| hover_pos_world = Some(world_bounds.screen_pos_to_world(mouse_pos, screen_bounds)); |
| } |
| } |
| |
| state.settings.sharable.world_bounds = world_bounds; |
| let width = frame.width(); |
| let height = frame.height(); |
| let view_settings = state.settings.sharable.view_mode.view_settings(); |
| |
| let image_type = ImageType::new( |
| state.settings.sharable.current_plane, |
| view_settings.pixel_type, |
| view_settings.pixel_type.is_delta() && state.settings.sharable.show_relative_delta, |
| view_settings.show_heatmap, |
| ); |
| |
| let Ok(texture_handle) = stream.images.get_or_create_image( |
| ui.ctx(), |
| &stream.pixel_data, |
| frame, |
| image_type, |
| &state.settings.sharable.heatmap_settings, |
| ) else { |
| return Ok(()); |
| }; |
| |
| if state.settings.sharable.show_yuv { |
| let mut image_mesh = Mesh::with_texture(texture_handle.id()); |
| let image_world = Rect::from_min_size(pos2(0.0, 0.0), vec2(width as f32, height as f32)); |
| let image_screen = world_bounds.world_rect_to_screen(image_world, screen_bounds); |
| let image_uv = Rect::from_min_size(pos2(0.0, 0.0), vec2(1.0, 1.0)); |
| image_mesh.add_rect_with_uv(image_screen, image_uv, Color32::WHITE); |
| ui.painter().with_clip_rect(response.rect).add(image_mesh); |
| } |
| let mut painter = ui.painter().with_clip_rect(response.rect); |
| let overlay = FrameOverlay::new(frame, &state.settings); |
| overlay.draw(&mut painter); |
| |
| let style = &state.settings.persistent.style.overlay; |
| if state.settings.sharable.show_overlay { |
| if let Some(hover_pos_world) = hover_pos_world { |
| if let Some(hovered_object) = self.check_hovered_object(state, frame, hover_pos_world) { |
| if let Some(world_rect) = hovered_object.rect(frame) { |
| let screen_rect = world_bounds.world_rect_to_screen(world_rect, screen_bounds); |
| let mut already_selected = false; |
| if let Some(selected_object_leaf) = &state.settings.selected_object_leaf { |
| if selected_object_leaf == &hovered_object { |
| already_selected = true; |
| } |
| } |
| if set_selected_object && !already_selected { |
| state.settings.sharable.pixel_viewer_bounds = |
| Rect::from_min_size(pos2(0.0, 0.0), world_rect.size()); |
| if matches!(hovered_object, SelectedObjectKind::TransformUnit(_)) { |
| state.settings.sharable.coeffs_viewer_bounds = |
| Rect::from_min_size(pos2(0.0, 0.0), world_rect.size()); |
| } |
| state.settings.selected_object_leaf = Some(hovered_object.clone()); |
| state.settings.selected_object = Some(SelectedObject::new(hovered_object)); |
| } |
| if set_selected_object_parent && already_selected { |
| if let Some(SelectedObject { |
| kind: selected_object_kind, |
| .. |
| }) = &state.settings.selected_object |
| { |
| if let Some(parent) = selected_object_kind.get_parent(frame) { |
| state.settings.selected_object = Some(SelectedObject::new(parent)); |
| } |
| } |
| } |
| painter.add(Shape::rect_stroke( |
| screen_rect, |
| Rounding::ZERO, |
| style.highlighted_object_stroke, |
| )); |
| } |
| } else if set_selected_object { |
| state.settings.selected_object = None; |
| state.settings.selected_object_leaf = None; |
| } |
| } |
| if let Some(selected_object) = &state.settings.selected_object { |
| if let Some(world_rect) = selected_object.rect(frame) { |
| let screen_rect = world_bounds.world_rect_to_screen(world_rect, screen_bounds); |
| painter.add(Shape::rect_stroke( |
| screen_rect, |
| Rounding::ZERO, |
| style.selected_object_stroke, |
| )); |
| } |
| } |
| } |
| // TODO(comc): Refactor into separate module. |
| if view_settings.show_heatmap { |
| let mut show_heatmap_legend = state.settings.sharable.show_heatmap_legend; |
| let log_scale = state.settings.sharable.heatmap_histogram_log_scale; |
| let ui_ctx = ui.ctx(); |
| egui::Window::new("Heatmap Legend") |
| .id("Heatmap Legend".into()) |
| .open(&mut show_heatmap_legend) |
| .default_width(400.0) |
| .default_height(400.0) |
| .resizable(true) |
| .collapsible(false) |
| .show(ui_ctx, |ui| { |
| Plot::new("heatmap_legend") |
| .show_background(false) |
| .show_axes([true, true]) |
| .clamp_grid(true) |
| .show_grid(false) |
| .allow_boxed_zoom(false) |
| .allow_drag(false) |
| .allow_zoom(false) |
| .allow_scroll(false) |
| .show_x(false) |
| .show_y(false) |
| .x_axis_label("Bits / pixel") |
| .y_axis_formatter(move |value, _num_chars, _range| { |
| if log_scale { |
| (10_f64).powf(value).to_string() |
| } else { |
| value.to_string() |
| } |
| }) |
| .show(ui, |plot_ui| { |
| if let Ok(heatmap) = stream.images.get_or_create_heatmap( |
| ui_ctx, |
| frame, |
| image_type, |
| &state.settings.sharable.heatmap_settings, |
| ) { |
| let histogram = heatmap.histogram; |
| plot_ui.bar_chart(BarChart::new( |
| histogram |
| .iter() |
| .enumerate() |
| .map(|(index, &value)| { |
| let mut value = value as f64; |
| if log_scale && value > 0.0 { |
| value = value.log10(); |
| } |
| let bar_width = heatmap.bucket_width as f64; |
| let x = index as f64 * bar_width; |
| let colormap_index = 255.0 * (index as f64 / histogram.len() as f64); |
| let color = JET_COLORMAP[colormap_index as usize]; |
| let fill = Color32::from_rgb(color[0], color[1], color[2]); |
| Bar { |
| name: "bar".into(), |
| orientation: egui_plot::Orientation::Vertical, |
| argument: x, |
| value, |
| base_offset: None, |
| bar_width: heatmap.bucket_width as f64, |
| stroke: Stroke::new(1.0, Color32::BLACK), |
| fill, |
| } |
| }) |
| .collect(), |
| )); |
| } |
| }); |
| }); |
| state.settings.sharable.show_heatmap_legend = show_heatmap_legend; |
| } |
| Ok(()) |
| } |
| } |