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
@@ -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]