diff --git a/crates/ui-egui/src/panels/camera_tracker_ui.rs b/crates/ui-egui/src/panels/camera_tracker_ui.rs index 80932f490..780a48981 100644 --- a/crates/ui-egui/src/panels/camera_tracker_ui.rs +++ b/crates/ui-egui/src/panels/camera_tracker_ui.rs @@ -9,7 +9,7 @@ //! and Camera, Create Multiple …, Delete Selected Points; Delete removes the selection; //! - the "Analyzing in background (step 1 of 2)" / "Solving camera" banner; //! - the Effect Controls block with Analyze / Cancel / Create Camera, Method Used and Average -//! Error. +//! Error, and how many frames a partial solve solved. //! //! Everything is dispatched to the `camera.*` commands. @@ -425,12 +425,14 @@ pub fn editor( } let params = effectcraft_engine::camera_track::static_params(g); let solve = ct::solve(¶ms); + let partial = solve.as_deref().and_then(partial_frames); + let dy = if partial.is_some() { PARTIAL_LINE } else { 0.0 }; if solve.is_some() { if widgets::text_button(ui, b3, crate::i18n::tr("Create Camera"), false, &t, egui::Id::unique(("ct-camera", g.uid))).clicked() { actions.push(("camera.create".into(), me.clone())); } app.auto.add(&format!("effectControls.cameraTracker.{}.createCamera", g.uid), b3, "Create Camera"); - let export = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 70.0), vec2(218.0, 22.0)); + let export = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 70.0 + dy), vec2(218.0, 22.0)); if widgets::text_button(ui, export, tr("Export Scene to Blender..."), false, &t, egui::Id::unique(("ct-blender", g.uid))).clicked() && !busy { actions.push(("camera.exportBlender".into(), me.clone())); } @@ -449,10 +451,19 @@ pub fn editor( }, }; p.text(pos2(r.min.x + 24.0, r.min.y + 38.0), Align2::LEFT_CENTER, status, Tokens::ui(11.5), t.text_dim); + if let Some((solved, frames)) = partial { + let text = tr_args("Partial solve: {} of {} frames solved", &[&solved, &frames]); + p.text(pos2(r.min.x + 24.0, r.min.y + 56.0), Align2::LEFT_CENTER, &text, Tokens::ui(11.5), t.warning); + app.auto.add( + &format!("effectControls.cameraTracker.{}.partialSolve", g.uid), + Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 48.0), vec2(r.width() - 24.0, 16.0)), + &text, + ); + } if let Some(sv) = &solve { let f = sv.focal(); p.text( - pos2(r.min.x + 24.0, r.min.y + 56.0), + pos2(r.min.x + 24.0, r.min.y + 56.0 + dy), Align2::LEFT_CENTER, format!("{} solved points · Horizontal Angle of View {:.1}°", sv.points.len(), sv.hfov(f)), Tokens::ui(11.5), @@ -461,5 +472,19 @@ pub fn editor( } } -/// Height of [`editor`]. -pub const EDITOR_HEIGHT: f32 = 100.0; +/// Height of [`editor`] without the partial-solve line. +const EDITOR_HEIGHT: f32 = 100.0; +const PARTIAL_LINE: f32 = 18.0; + +/// Height of [`editor`] for tracker `g`. +pub fn editor_height(g: &PropGroup) -> f32 { + let partial = ct::solve(&effectcraft_engine::camera_track::static_params(g)).is_some_and(|sv| partial_frames(&sv).is_some()); + if partial { EDITOR_HEIGHT + PARTIAL_LINE } else { EDITOR_HEIGHT } +} + +/// `(solved, all)` frames of a solve with unsolved frames, where the solver interpolates the +/// camera; `None` when every frame is solved. +fn partial_frames(sv: &CameraSolve) -> Option<(usize, usize)> { + let solved = sv.frames.iter().filter(|c| c.solved).count(); + (solved < sv.frames.len()).then_some((solved, sv.frames.len())) +} diff --git a/crates/ui-egui/src/panels/effect_controls/editors.rs b/crates/ui-egui/src/panels/effect_controls/editors.rs index bd566cb7f..bcc0dfd88 100644 --- a/crates/ui-egui/src/panels/effect_controls/editors.rs +++ b/crates/ui-egui/src/panels/effect_controls/editors.rs @@ -114,7 +114,7 @@ pub(super) fn editor_height(effect: &str, g: &PropGroup, width: f32) -> f32 { } "ec.color.curves" => 34.0 + curves_size(width) + 26.0, effectcraft_engine::effects::warp_stab::ID => 50.0, - effectcraft_engine::effects::camera_tracker::ID => crate::panels::camera_tracker_ui::EDITOR_HEIGHT, + effectcraft_engine::effects::camera_tracker::ID => crate::panels::camera_tracker_ui::editor_height(g), effectcraft_engine::effects::roto::ID | effectcraft_engine::effects::object_matte::ID => 50.0, "ec.color.levels" | "ec.color.levelsic" => 34.0 + 80.0 + 58.0, "ec.color.autolevels" | "ec.color.autocontrast" | "ec.color.autocolor" => 24.0 + 80.0 + 12.0, diff --git a/crates/ui-egui/tests/it/ui_camera_tracker.rs b/crates/ui-egui/tests/it/ui_camera_tracker.rs index 619d7c7c5..579e92d07 100644 --- a/crates/ui-egui/tests/it/ui_camera_tracker.rs +++ b/crates/ui-egui/tests/it/ui_camera_tracker.rs @@ -202,6 +202,33 @@ fn blender_export_button_opens_python_dialog_and_writes_scene() { assert_eq!(h.state().session.active_comp().unwrap().layers.len(), 1, "export does not create a camera in the source project"); } +/// A solve with unsolved (interpolated) frames says so in Effect Controls, with the count, on a +/// line of its own that pushes the Export button down. +#[test] +fn effect_controls_show_how_many_frames_a_partial_solve_solved() { + let (a, plate, uid) = app(); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| a); + h.state_mut().show_panel(PanelKind::EffectControls); + h.run_steps(4); + let line = format!("effectControls.cameraTracker.{uid}.partialSolve"); + let export = format!("effectControls.cameraTracker.{uid}.exportBlender"); + assert!(!ids(&h).contains(&line), "a full solve has no partial-solve line"); + let before = rect(&h, &export); + + let g = h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(plate)).unwrap().props.find_group(uid).unwrap(); + let mut solve = ct::solve(&effectcraft_engine::camera_track::static_params(g)).unwrap().as_ref().clone(); + for f in solve.frames.iter_mut().skip(24) { + f.solved = false; + } + let s = &mut h.state_mut().session; + s.execute("prop.set", json!({"layer": plate, "path": "effects/#1/solve", "value": solve.to_json()})).unwrap(); + s.camera_pending.clear(); + h.run_steps(4); + let e = h.state().auto.find(&line).expect("the partial-solve line is drawn"); + assert_eq!(e.label, "Partial solve: 24 of 60 frames solved"); + assert!((rect(&h, &export).min.y - before.min.y - 18.0).abs() < 0.5, "the Export button moves down one line"); +} + /// Drag with `button` from `from` to `to` in steps, then release. fn drag_with(h: &mut Harness<'_, EffectcraftApp>, button: egui::PointerButton, from: egui::Pos2, to: egui::Pos2) { h.input_mut().events.push(egui::Event::PointerMoved(from));