From 68cd71a754a0943d482a0062a88774bb844c0b1d Mon Sep 17 00:00:00 2001 From: Michael Grupp Date: Thu, 26 Mar 2026 23:09:38 +0100 Subject: [PATCH] Improve naming of transformed texture requests --- crates/maps/src/grid.rs | 24 ++-- crates/maps/src/lens.rs | 2 +- crates/maps/tests/snapshots/fixed_lens.png | 4 +- .../tests/snapshots/fixed_lens_background.png | 4 +- .../tests/snapshots/reload_session_config.png | 4 +- crates/maps_rendering/src/lib.rs | 2 +- crates/maps_rendering/src/rect_helpers.rs | 8 +- crates/maps_rendering/src/texture_request.rs | 127 ++++++++++-------- crates/maps_rendering/src/texture_state.rs | 66 ++++----- 9 files changed, 123 insertions(+), 118 deletions(-) diff --git a/crates/maps/src/grid.rs b/crates/maps/src/grid.rs index 531ea15..19ddcc6 100644 --- a/crates/maps/src/grid.rs +++ b/crates/maps/src/grid.rs @@ -8,7 +8,7 @@ use crate::grid_options::{GridLineDimension, GridOptions, LineType}; use crate::map_state::MapState; use crate::movable::Draggable; use maps_io_ros::MapPose; -use maps_rendering::{ImagePlacement, RotatedCropRequest, TextureRequest}; +use maps_rendering::{ImagePlacement, TextureRequest, TransformedTextureRequest}; /// Grid area for displaying metric objects in screen space (points). pub struct Grid { @@ -143,15 +143,12 @@ impl Grid { let relation = GridMapRelation::new(self, map); - let rect = egui::Rect::from_min_size( - self.origin_in_points + relation.ulc_to_origin_in_points, - relation.scaled_size, - ); + let scaled_rect = egui::Rect::from_min_size(egui::Pos2::ZERO, relation.scaled_size); let pose_rotation = map.pose.rot2().inverse(); // RHS to LHS let origin_rotation = map.meta.origin_theta.inverse(); - let uncropped = TextureRequest::new(map_name.to_string(), rect) + let base_request = TextureRequest::new(map_name.to_string(), scaled_rect) .with_tint(map.tint) .with_color_to_alpha(map.color_to_alpha) .with_thresholding(map.get_value_interpretation()) @@ -159,23 +156,22 @@ impl Grid { let placement = ImagePlacement { rotation: pose_rotation * origin_rotation, - translation: relation.ulc_to_origin_in_points_translated - - relation.ulc_to_origin_in_points, + translation: self.origin_in_points.to_vec2() + + relation.ulc_to_origin_in_points_translated, rotation_center: relation.ulc_to_origin_in_points, - points_per_pixel: relation.points_per_cell, + points_per_texel: relation.points_per_cell, original_image_size: map.image_pyramid.original_size, }; - let request = RotatedCropRequest::from_visible( - ui, - &self.painter.clip_rect(), - uncropped, + let transformed_request = TransformedTextureRequest::from_visible( + &self.painter, + base_request, &placement, self.texture_crop_threshold, ); map.get_or_create_texture_state(self.name.as_str()) - .crop_and_put(ui, &request); + .transform_and_put(ui, &transformed_request); if options.marker_visibility.maps_visible() { self.draw_axes(options, Some(&map.pose)); diff --git a/crates/maps/src/lens.rs b/crates/maps/src/lens.rs index 127f4b8..b8ba430 100644 --- a/crates/maps/src/lens.rs +++ b/crates/maps/src/lens.rs @@ -92,7 +92,7 @@ impl<'a> Lens<'a> { ); // When partially visible, we deal with a UV rect inside an UV rect. - let texture_uv = texture_state.desired_uv; + let texture_uv = texture_state.desired_crop_uv; let original_image = &texture_state.image_pyramid.original; let original_width = original_image.width() as f32; diff --git a/crates/maps/tests/snapshots/fixed_lens.png b/crates/maps/tests/snapshots/fixed_lens.png index 5cd10dc..c13ccf5 100644 --- a/crates/maps/tests/snapshots/fixed_lens.png +++ b/crates/maps/tests/snapshots/fixed_lens.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c968879c2ae21f02995089872bd7ee4e40fb2a2dfa04193c34bbbbbc2b056b1 -size 123216 +oid sha256:6406fc857ab286f3862c2de9c90e81ab27681c3cbf166772c1afccfa1e99da64 +size 123313 diff --git a/crates/maps/tests/snapshots/fixed_lens_background.png b/crates/maps/tests/snapshots/fixed_lens_background.png index 1198d17..a16d37f 100644 --- a/crates/maps/tests/snapshots/fixed_lens_background.png +++ b/crates/maps/tests/snapshots/fixed_lens_background.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54dc7da482a205cd87943d75b996f6d45ec926bca6516700a57a87c832b67dc3 -size 84992 +oid sha256:f29b54bbca4e539881c91ac10622aff6390f7f9a347fdf4fa04942ef859e4d1a +size 85023 diff --git a/crates/maps/tests/snapshots/reload_session_config.png b/crates/maps/tests/snapshots/reload_session_config.png index 5b27ef8..eb85bb8 100644 --- a/crates/maps/tests/snapshots/reload_session_config.png +++ b/crates/maps/tests/snapshots/reload_session_config.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21e2c107f83f0b6e43f4ce37c482b1f685cfa10df991e33566aafc34be5283c7 -size 261949 +oid sha256:403e89e4b14069f77e034608c45a6b821563195b718c930f48c95f9814a4f8ea +size 261859 diff --git a/crates/maps_rendering/src/lib.rs b/crates/maps_rendering/src/lib.rs index 03618bd..932e736 100644 --- a/crates/maps_rendering/src/lib.rs +++ b/crates/maps_rendering/src/lib.rs @@ -14,5 +14,5 @@ pub mod texture_state; // Re-export commonly used structs and types. pub use image_pyramid::ImagePyramid; pub use render_options::TextureFilter; -pub use texture_request::{ImagePlacement, NO_TINT, RotatedCropRequest, TextureRequest}; +pub use texture_request::{ImagePlacement, NO_TINT, TextureRequest, TransformedTextureRequest}; pub use texture_state::TextureState; diff --git a/crates/maps_rendering/src/rect_helpers.rs b/crates/maps_rendering/src/rect_helpers.rs index a38cda1..069d884 100644 --- a/crates/maps_rendering/src/rect_helpers.rs +++ b/crates/maps_rendering/src/rect_helpers.rs @@ -1,8 +1,8 @@ use eframe::egui; use log::{Level, log_enabled}; -/// Rotates a rectangle around an origin point and returns the bounding rectangle. -pub fn rotate(rect: &egui::Rect, rot: egui::emath::Rot2, origin: egui::Vec2) -> egui::Rect { +/// Rotates a rectangle around an origin point and returns the axis-aligned bounding rectangle. +pub fn rotate_aabb(rect: &egui::Rect, rot: egui::emath::Rot2, origin: egui::Vec2) -> egui::Rect { let a = origin + rot * (rect.left_top() - origin.to_pos2()); let b = origin + rot * (rect.right_top() - origin.to_pos2()); let c = origin + rot * (rect.left_bottom() - origin.to_pos2()); @@ -54,9 +54,9 @@ pub fn quantized_intersection( } /// Paints a rectangle with a color when trace logging is enabled. -pub fn debug_paint(ui: &egui::Ui, rect: egui::Rect, color: egui::Color32, label: &str) { +pub fn debug_paint(painter: &egui::Painter, rect: egui::Rect, color: egui::Color32, label: &str) { if !log_enabled!(Level::Trace) { return; } - ui.painter().debug_rect(rect, color, label); + painter.debug_rect(rect, color, label); } diff --git a/crates/maps_rendering/src/texture_request.rs b/crates/maps_rendering/src/texture_request.rs index 893a5bd..36674ca 100644 --- a/crates/maps_rendering/src/texture_request.rs +++ b/crates/maps_rendering/src/texture_request.rs @@ -1,7 +1,7 @@ use eframe::egui; use eframe::emath::GuiRounding as _; -use crate::rect_helpers::{debug_paint, quantized_intersection, rotate}; +use crate::rect_helpers::{debug_paint, quantized_intersection, rotate_aabb}; use maps_io_ros::ValueInterpretation; pub const NO_TINT: egui::Color32 = egui::Color32::WHITE; @@ -78,21 +78,22 @@ impl TextureRequest { /// Extended request for rendering scaled textures with arbitrary rotated pose /// and support for cropping (e.g. to viewport). #[derive(Debug)] -pub struct RotatedCropRequest { - /// Base texture request for the bare unrotated & uncropped image rect. - pub uncropped: TextureRequest, - /// Bounding box rectangle of the visible area of the final rotated & cropped texture. - pub visible_rect: egui::Rect, +pub struct TransformedTextureRequest { + /// Base texture request for the bare unrotated & untransformed image rect. + pub base_request: TextureRequest, + /// Rectangle in the scaled image coordinate space defining the + /// potentially cropped region to extract before applying transformations. + pub crop_rect: egui::Rect, /// Image crop specified in UV image coordinates. - pub uv: [egui::Pos2; 2], + pub crop_uv: [egui::Pos2; 2], /// Desired rotation of the texture. pub rotation: eframe::emath::Rot2, /// Desired translation of the texture. pub translation: egui::Vec2, /// Rotation center of the image in UV image coordinates. pub rotation_center_in_uv: egui::Vec2, - /// Scale of the texture, i.e. desired screen points per pixel. - pub points_per_pixel: f32, + /// Scale of the texture, i.e. desired screen points per texel. + pub points_per_texel: f32, } /// Information needed for placement of an image as a scaled texture at a 2D pose. @@ -103,66 +104,76 @@ pub struct ImagePlacement { pub translation: egui::Vec2, /// Position of the image's rotation center in points relative to the viewport. pub rotation_center: egui::Vec2, - /// Amount of points occupied by a pixel of the image, for scaling. - pub points_per_pixel: f32, + /// Amount of points occupied by a texel of the image, for scaling. + pub points_per_texel: f32, /// Size of the unscaled, uncropped source image in pixels. pub original_image_size: egui::Vec2, } -impl RotatedCropRequest { +impl TransformedTextureRequest { /// Returns true if this request represents a full texture (not a crop). pub fn is_full_texture(&self) -> bool { - self.uv[0] == egui::Pos2::ZERO && self.uv[1] == egui::pos2(1.0, 1.0) + self.crop_uv[0] == egui::Pos2::ZERO && self.crop_uv[1] == egui::pos2(1.0, 1.0) } /// Pre-calculate the minimal, unrotated crop that is needed to show the rotated surface in the viewport. /// I.e. neither clipping too much nor making the texture unnecessarily large / inefficient. /// Enable trace log level to see what is going on (I spent too much time figuring this out). fn min_crop( - ui: &egui::Ui, - viewport_clip_rect: &egui::Rect, - image_rect: &egui::Rect, + paint_context: &egui::Painter, + scaled_rect: &egui::Rect, rotation: eframe::emath::Rot2, translation: egui::Vec2, rotation_center_in_points: egui::Vec2, - points_per_pixel: f32, + points_per_texel: f32, ) -> egui::Rect { - let origin_in_points = (image_rect.min - rotation_center_in_points).to_vec2(); + let origin_in_points = (scaled_rect.min - rotation_center_in_points).to_vec2(); - let rotated = rotate(image_rect, rotation, origin_in_points); - let transformed = rotated.translate(translation); - debug_paint(ui, transformed, egui::Color32::RED, "transformed"); + let transformed_aabb = + rotate_aabb(scaled_rect, rotation, origin_in_points).translate(translation); + debug_paint( + paint_context, + transformed_aabb, + egui::Color32::RED, + "transformed_aabb", + ); - let transformed_visible = transformed.intersect(*viewport_clip_rect); + let transformed_aabb_visible = transformed_aabb.intersect(paint_context.clip_rect()); debug_paint( - ui, - transformed_visible, + paint_context, + transformed_aabb_visible, egui::Color32::GOLD, - "transformed_visible", + "transformed_aabb_visible", ); - let min_crop = rotate( - &transformed_visible.translate(-translation), + let min_crop = rotate_aabb( + &transformed_aabb_visible.translate(-translation), rotation.inverse(), origin_in_points, ); - debug_paint(ui, min_crop, egui::Color32::BLUE, "min_crop"); + let ui_offset = paint_context.clip_rect().min.to_vec2(); + debug_paint( + paint_context, + min_crop.translate(ui_offset), + egui::Color32::BLUE, + "min_crop", + ); // The minimal rectangle is the instersection of crop rectangle and image rectangle. // The image cropping happens in pixel space, so we have to also quantize the rectangle // to the next best multiple of the scaled pixel size. // Otherwise the texture size/placement is not exact, especially at high zoom levels. - let visible_rect = quantized_intersection(image_rect, &min_crop, points_per_pixel); - // Round visible_rect matching egui 0.32's "pixel-perfect" paint_at behavior. + let crop_rect = quantized_intersection(scaled_rect, &min_crop, points_per_texel); + // Round crop_rect matching egui 0.32's "pixel-perfect" paint_at behavior. // See also: https://github.com/emilk/egui/pull/7078 - let visible_rect = visible_rect.round_to_pixels(ui.pixels_per_point()); + let crop_rect = crop_rect.round_to_pixels(paint_context.ctx().pixels_per_point()); debug_paint( - ui, - visible_rect, + paint_context, + crop_rect.translate(ui_offset), egui::Color32::GREEN, - "visible_rect_quantized", + "crop_rect_quantized", ); - visible_rect + crop_rect } /// Creates a request for displaying an image with the desired `placement` @@ -171,53 +182,51 @@ impl RotatedCropRequest { /// cropped to the viewport. Use this to support displaying large images /// at high zoom levels as cropped textures to avoid texture buffer size limits. pub fn from_visible( - ui: &egui::Ui, - viewport_clip_rect: &egui::Rect, - uncropped: TextureRequest, + paint_context: &egui::Painter, + base_request: TextureRequest, placement: &ImagePlacement, crop_threshold: u32, - ) -> RotatedCropRequest { - let image_rect = uncropped.desired_rect; - let visible_rect = if uncropped.desired_rect.size().max_elem() as u32 <= crop_threshold + ) -> TransformedTextureRequest { + let scaled_rect = base_request.desired_rect; + let crop_rect = if scaled_rect.size().max_elem() as u32 <= crop_threshold || placement.original_image_size.max_elem() as u32 <= crop_threshold { // Desired texture is small enough to not need cropping. - image_rect + scaled_rect } else { // Desired texture is large, crop to the viewport. Self::min_crop( - ui, - viewport_clip_rect, - &image_rect, + paint_context, + &scaled_rect, placement.rotation, placement.translation, placement.rotation_center, - placement.points_per_pixel, + placement.points_per_texel, ) }; - RotatedCropRequest { - uncropped, - visible_rect, - uv: [ + TransformedTextureRequest { + base_request, + crop_rect, + crop_uv: [ egui::Pos2::new( - (visible_rect.min.x - image_rect.min.x) / image_rect.width(), - (visible_rect.min.y - image_rect.min.y) / image_rect.height(), + (crop_rect.min.x - scaled_rect.min.x) / scaled_rect.width(), + (crop_rect.min.y - scaled_rect.min.y) / scaled_rect.height(), ), egui::Pos2::new( - (visible_rect.max.x - image_rect.min.x) / image_rect.width(), - (visible_rect.max.y - image_rect.min.y) / image_rect.height(), + (crop_rect.max.x - scaled_rect.min.x) / scaled_rect.width(), + (crop_rect.max.y - scaled_rect.min.y) / scaled_rect.height(), ), ], rotation: placement.rotation, translation: placement.translation, rotation_center_in_uv: egui::Vec2::new( - -(placement.rotation_center.x + (visible_rect.min.x - image_rect.min.x)) - / visible_rect.width(), - -(placement.rotation_center.y + (visible_rect.min.y - image_rect.min.y)) - / visible_rect.height(), + -(placement.rotation_center.x + (crop_rect.min.x - scaled_rect.min.x)) + / crop_rect.width(), + -(placement.rotation_center.y + (crop_rect.min.y - scaled_rect.min.y)) + / crop_rect.height(), ), - points_per_pixel: placement.points_per_pixel, + points_per_texel: placement.points_per_texel, } } } diff --git a/crates/maps_rendering/src/texture_state.rs b/crates/maps_rendering/src/texture_state.rs index 402fdb2..de78977 100644 --- a/crates/maps_rendering/src/texture_state.rs +++ b/crates/maps_rendering/src/texture_state.rs @@ -6,7 +6,7 @@ use log::trace; use crate::image::{color_to_alpha, fit_image, to_egui_image}; use crate::image_pyramid::ImagePyramid; use crate::texture_cache::TextureCache; -use crate::texture_request::{RotatedCropRequest, TextureRequest}; +use crate::texture_request::{TextureRequest, TransformedTextureRequest}; use maps_io_ros::ValueInterpretation; /// Manages the state of a texture across its lifetime. @@ -23,7 +23,7 @@ pub struct TextureState { /// Currently active texture (reference to one in the cache). pub texture_handle: Option, pub desired_size: egui::Vec2, - pub desired_uv: [egui::Pos2; 2], + pub desired_crop_uv: [egui::Pos2; 2], pub desired_color_to_alpha: Option, pub desired_thresholding: Option, pub used_level: u32, @@ -59,7 +59,7 @@ impl TextureState { self.texture_handle = None; } self.desired_size = request.desired_rect.size(); - self.desired_uv = [egui::Pos2::ZERO, egui::pos2(1., 1.)]; + self.desired_crop_uv = [egui::Pos2::ZERO, egui::pos2(1., 1.)]; self.desired_color_to_alpha = request.color_to_alpha; self.desired_thresholding = request.thresholding; self.texture_options = request.texture_options.unwrap_or_default(); @@ -100,15 +100,15 @@ impl TextureState { } /// Returns true if the request changes the image cropping. - fn changed_crop(&self, request: &RotatedCropRequest) -> bool { - self.desired_uv != request.uv + fn changed_crop(&self, request: &TransformedTextureRequest) -> bool { + self.desired_crop_uv != request.crop_uv } /// Tries to find and use a cached texture for full (non-cropped) textures. /// Returns true if a cached texture was found and applied, false otherwise. fn try_use_cached_texture( &mut self, - request: &RotatedCropRequest, + request: &TransformedTextureRequest, desired_size: egui::Vec2, ) -> bool { // Only cache full textures (not crops). @@ -122,16 +122,16 @@ impl TextureState { // Check if we have a cached texture that matches appearance. if let Some(texture_handle) = self.texture_cache - .query(&request.uncropped.client, level, &request.uncropped) + .query(&request.base_request.client, level, &request.base_request) { // Reuse cached texture and update state. self.texture_handle = Some(texture_handle); self.used_level = level; self.desired_size = desired_size; - self.desired_uv = request.uv; - self.desired_color_to_alpha = request.uncropped.color_to_alpha; - self.desired_thresholding = request.uncropped.thresholding; - self.texture_options = request.uncropped.texture_options.unwrap_or_default(); + self.desired_crop_uv = request.crop_uv; + self.desired_color_to_alpha = request.base_request.color_to_alpha; + self.desired_thresholding = request.base_request.thresholding; + self.texture_options = request.base_request.texture_options.unwrap_or_default(); return true; } @@ -146,8 +146,8 @@ impl TextureState { /// 1. Try to reuse cached texture for full textures. /// 2. Check if any changes require creating a new texture. /// 3. Create and optionally cache the new texture. - fn update_crop(&mut self, ui: &mut egui::Ui, request: &RotatedCropRequest) { - let desired_size = request.uncropped.desired_rect.size(); + fn maybe_update_crop(&mut self, ui: &mut egui::Ui, request: &TransformedTextureRequest) { + let desired_size = request.base_request.desired_rect.size(); // Try to use cached texture for full textures. if self.try_use_cached_texture(request, desired_size) { @@ -155,21 +155,21 @@ impl TextureState { } // Check if we need to create a new texture. - let changed_uncropped = self.changed(&request.uncropped); + let changed_base_request = self.changed(&request.base_request); let changed_crop = self.changed_crop(request); - let changed_appearance = self.changed_appearance(&request.uncropped); + let changed_appearance = self.changed_appearance(&request.base_request); - if !(changed_uncropped || changed_crop || changed_appearance) { + if !(changed_base_request || changed_crop || changed_appearance) { return; } self.desired_size = desired_size; - self.desired_uv = request.uv; - self.desired_color_to_alpha = request.uncropped.color_to_alpha; - self.desired_thresholding = request.uncropped.thresholding; - self.texture_options = request.uncropped.texture_options.unwrap_or_default(); + self.desired_crop_uv = request.crop_uv; + self.desired_color_to_alpha = request.base_request.color_to_alpha; + self.desired_thresholding = request.base_request.thresholding; + self.texture_options = request.base_request.texture_options.unwrap_or_default(); - if request.visible_rect.is_negative() || request.uv[0] == request.uv[1] { + if request.crop_rect.is_negative() || request.crop_uv[0] == request.crop_uv[1] { self.texture_handle = None; return; } @@ -179,8 +179,8 @@ impl TextureState { self.used_level = level; trace!("Cropping and reloading texture for {request:?}"); - let uv_min = request.uv[0]; - let uv_max = request.uv[1]; + let uv_min = request.crop_uv[0]; + let uv_max = request.crop_uv[1]; let min_x = (uv_min.x * uncropped.width() as f32).round() as u32; let min_y = (uv_min.y * uncropped.height() as f32).round() as u32; let max_x = (uv_max.x * uncropped.width() as f32).round() as u32; @@ -191,13 +191,13 @@ impl TextureState { self.texture_handle = None; return; } - color_to_alpha(&mut cropped_image, request.uncropped.color_to_alpha); - if let Some(thresholding) = &request.uncropped.thresholding { + color_to_alpha(&mut cropped_image, request.base_request.color_to_alpha); + if let Some(thresholding) = &request.base_request.thresholding { thresholding.apply(&mut cropped_image, self.image_pyramid.original_has_alpha); } let texture_handle = ui.ctx().load_texture( - format!("{}_{}", request.uncropped.client, level), + format!("{}_{}", request.base_request.client, level), to_egui_image(&cropped_image), self.texture_options, ); @@ -205,10 +205,10 @@ impl TextureState { // Cache full textures for zoom performance. if request.is_full_texture() { self.texture_cache.store( - &request.uncropped.client, + &request.base_request.client, level, texture_handle.clone(), - &request.uncropped, + &request.base_request, ); } @@ -216,8 +216,8 @@ impl TextureState { } /// Updates the state and puts the texture into the UI according to the request. - pub fn crop_and_put(&mut self, ui: &mut egui::Ui, request: &RotatedCropRequest) { - self.update_crop(ui, request); + pub fn transform_and_put(&mut self, ui: &mut egui::Ui, request: &TransformedTextureRequest) { + self.maybe_update_crop(ui, request); if let Some(texture) = &self.texture_handle { // Manually paint and get response. @@ -225,9 +225,9 @@ impl TextureState { let image = egui::Image::new(texture) .rotate(request.rotation.angle(), request.rotation_center_in_uv) .maintain_aspect_ratio(false) - .fit_to_exact_size(request.visible_rect.size()) - .tint(request.uncropped.tint); - image.paint_at(ui, request.visible_rect.translate(request.translation)); + .fit_to_exact_size(request.crop_rect.size()) + .tint(request.base_request.tint); + image.paint_at(ui, request.crop_rect.translate(request.translation)); // We can't get a proper image response from a rotated/translated manual paint, // and also don't need one (grid interaction is handled elsewhere). self.image_response = None;