collective

Browse / github.com/storytold/photocraft / patch

perf(ui): cache Adaptive Wide Angle preview mesh for constraint overlays

perf(ui): cache Adaptive Wide Angle preview mesh for constraint overlays ## Problem Inside `WideAngleDialog::show()`, the constraint overlay path previously ran: ```rust let mesh = (!show_src && !d.params.constraints.is_empty()).then(|| wideangle::solve(&d.proxy_params(), pf)); for c in &d.params.constraints { ... } ``` Because `show()` runs on every frame tick, mouse movement, and hover event in egui, `wideangle::solve` was executed repeatedly on the UI thread at 60 Hz whenever constraints were present, even when no parameters or image contents had changed. ## Change - Cache `mesh: Option<WideMesh>` directly in `WideAngleDialog`. - Compute the proxy mesh once i

Status
open
Score
0.00
Adopters
0 other operators
Author
wanikhawar (imported from pull request #9872, open)
Base
07bd794686ea4db0b557c978d58173a62760e4ca
Size
+66 −3 in 1 file
Review
pending
Shared
2026-10-28
Id
a626faa90da100bb982511d2

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/dialogs/wide_angle_ui.rs

diff --git a/crates/ui-egui/src/dialogs/wide_angle_ui.rs b/crates/ui-egui/src/dialogs/wide_angle_ui.rs--- a/crates/ui-egui/src/dialogs/wide_angle_ui.rs+++ b/crates/ui-egui/src/dialogs/wide_angle_ui.rs@@ -11,7 +11,7 @@  use egui::{Align2, Color32, FontId, Pos2, Rect as ERect, Sense, Stroke, TextureHandle, pos2, vec2}; use photocraft_algo::transform::Interp;-use photocraft_algo::wideangle::{self, Camera, Constraint, Orientation, WideAngle, WideModel};+use photocraft_algo::wideangle::{self, Camera, Constraint, Orientation, WideAngle, WideMesh, WideModel}; use photocraft_color::PixelFormat; use photocraft_doc::LayerId; use photocraft_geom::Rect;@@ -38,6 +38,8 @@ pub struct WideAngleDialog {     ph: usize,     src_tex: Option<TextureHandle>,     out_tex: Option<TextureHandle>,+    /// The proxy warp used by both the rendered preview and its constraint overlays.+    mesh: Option<WideMesh>,     dirty: bool,     pub preview: bool,     drag: Option<([f64; 2], [f64; 2])>,@@ -86,6 +88,7 @@ impl WideAngleDialog {         if self.src_tex.is_none() {             self.src_tex = Some(ctx.load_texture("awa-src", tex_image(&self.proxy, self.pw, self.ph), egui::TextureOptions::LINEAR));         }+        self.mesh = Some(mesh);         self.render_ms = crate::gpu_canvas::now_ms() - t0;         self.dirty = false;     }@@ -143,6 +146,7 @@ pub fn open(app: &mut PhotocraftApp, ctx: &egui::Context) -> Result<(), String>         ph,         src_tex: None,         out_tex: None,+        mesh: None,         dirty: true,         preview: true,         drag: None,@@ -334,9 +338,9 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {             Orientation::Vertical => t.danger,         };         let pf = Rect::new(0, 0, d.pw as i32, d.ph as i32);-        let mesh = (!show_src && !d.params.constraints.is_empty()).then(|| wideangle::solve(&d.proxy_params(), pf));+        let mesh = if show_src { None } else { d.mesh.as_ref() };         for c in &d.params.constraints {-            let pts: Vec<Pos2> = match &mesh {+            let pts: Vec<Pos2> = match mesh {                 Some(m) => cam                     .arc(c.a, c.b, 24)                     .iter()@@ -508,6 +512,65 @@ mod tests {         menu(app, ctx, "filter.adaptiveWideAngle", &json!({"ui": {"add": {"a": [20, 50], "b": [140, 50]}}})).unwrap().unwrap();     } +    #[test]+    fn constraint_overlay_reuses_the_preview_mesh_on_redraw() {+        fn constraint_line(shape: &egui::Shape) -> Option<&[Pos2]> {+            match shape {+                egui::Shape::Path(p) if p.points.len() == 25 => Some(&p.points),+                egui::Shape::Vec(shapes) => shapes.iter().find_map(constraint_line),+                _ => None,+            }+        }+        let mut app = grey_layer();+        let ctx = egui::Context::default();+        PhotocraftApp::setup_context(&ctx, Default::default());+        open_with_a_constraint(&mut app, &ctx);+        let input = || egui::RawInput { screen_rect: Some(ERect::from_min_size(Pos2::ZERO, vec2(1000.0, 600.0))), ..Default::default() };+        ctx.run_ui(input(), |ui| show(&mut app, ui.ctx())).textures_delta.clear();+        // Move only the cached warp. A fresh solve would draw the original line instead.+        let d = app.wide_angle.as_mut().unwrap();+        for (out, _) in &mut d.mesh.as_mut().unwrap().verts {+            out[0] += 10.0;+        }+        let verts = d.mesh.as_ref().unwrap().verts.clone();+        let render_ms = d.render_ms;+        for (preview, dragging, shifted) in [(true, false, true), (false, false, false), (true, true, false), (true, false, true)] {+            let d = app.wide_angle.as_mut().unwrap();+            d.preview = preview;+            d.drag = dragging.then_some(([20.0, 30.0], [140.0, 30.0]));+            let mut output = ctx.run_ui(input(), |ui| show(&mut app, ui.ctx()));+            output.textures_delta.clear();+            let line = output.shapes.iter().find_map(|s| constraint_line(&s.shape)).expect("painted constraint line");+            // The 160×100 image fits the 668-point-wide viewport. The source line is centred;+            // the altered cached warp moves its centre ten proxy pixels to the right.+            let centre_x = (line.first().unwrap().x + line.last().unwrap().x) * 0.5;+            let expected_x = 350.0 + if shifted { 10.0 * (668.0 / 160.0) } else { 0.0 };+            assert!((centre_x - expected_x).abs() < 0.1, "preview={preview}, dragging={dragging}: centre={centre_x}, expected={expected_x}");+            let d = app.wide_angle.as_ref().unwrap();+            assert_eq!(d.mesh.as_ref().unwrap().verts, verts, "redraw retains the cached warp");+            assert_eq!(d.render_ms, render_ms, "redraw does not render the preview again");+        }+    }++    #[test]+    fn preview_mesh_refreshes_after_edits_and_history_steps() {+        let mut app = grey_layer();+        let ctx = egui::Context::default();+        open_with_a_constraint(&mut app, &ctx);+        let original = app.wide_angle.as_ref().unwrap().mesh.as_ref().unwrap().verts.clone();+        menu(&mut app, &ctx, "filter.adaptiveWideAngle", &json!({"ui": {"set": {"scale": 120.0}}})).unwrap().unwrap();+        let edited = app.wide_angle.as_ref().unwrap().mesh.as_ref().unwrap().verts.clone();+        assert_ne!(edited, original);+        for (step, expected) in [(Step::Undo, &original), (Step::Redo, &edited)] {+            let d = app.wide_angle.as_mut().unwrap();+            d.step(step);+            assert!(d.dirty);+            d.render(&ctx);+            assert!(!d.dirty);+            assert_eq!(&d.mesh.as_ref().unwrap().verts, expected);+        }+    }+     /// `orient` sets a constraint's orientation, as the right-click menu does; a bad index is     /// an error that changes nothing.     #[test]