From e362cae4c75d8d0145f0f36e03ceebdfb97f6220 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 3 Oct 2026 03:21:30 -0400 Subject: [PATCH] M13.6: panel polish (log-density scopes, narrow-panel layouts) and an offscreen panels snapshot test Co-Authored-By: Claude Opus 5.5 --- crates/raster/src/scopes.rs | 9 +++-- .../ui-egui/src/panels/content_fill_panel.rs | 2 +- crates/ui-egui/src/panels/media_panels.rs | 10 +++-- crates/ui-egui/src/panels/scopes_panel.rs | 6 ++- crates/ui-egui/tests/ui_panels.rs | 40 +++++++++++++++++++ 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/crates/raster/src/scopes.rs b/crates/raster/src/scopes.rs index befb26ef..6b803a12 100644 --- a/crates/raster/src/scopes.rs +++ b/crates/raster/src/scopes.rs @@ -5,7 +5,8 @@ //! ITU-R BT.601 / BT.709 / BT.2020 coefficients, colour differences `Cb = (B − Y) / (2(1 − Kb))` //! and `Cr = (R − Y) / (2(1 − Kr))` (each in −0.5…0.5). Values are read straight (un-premultiplied) //! from the frame. Every scope is a [`Scope`]: a `width × height` grid of three density planes -//! (one per trace colour) normalised so the busiest cell is 1, ready to be tinted and drawn. +//! (one per trace colour) on a logarithmic scale where the busiest cell is 1, ready to be tinted +//! and drawn. //! //! Scales: **8-bit** maps 0…1 to the plot (0…255 graticule); **float** maps −0.25…1.25 so //! super-white and sub-black values stay visible. **Clamp** limits the signal to 0…1 first. @@ -178,10 +179,12 @@ impl Scope { self } fn normalise(&mut self) { - // Square-root density so faint traces stay visible next to dense ones. + // Logarithmic density (like a phosphor trace) so faint traces stay visible next to + // dense ones. let max = self.counts.iter().flat_map(|p| p.iter()).copied().max().unwrap_or(0).max(1) as f32; + let k = 1.0 / (1.0 + max).ln(); for c in 0..3 { - self.planes[c] = self.counts[c].iter().map(|&n| (n as f32 / max).sqrt()).collect(); + self.planes[c] = self.counts[c].iter().map(|&n| if n == 0 { 0.0 } else { (0.25 + 0.75 * (1.0 + n as f32).ln() * k).min(1.0) }).collect(); } } /// Total count of plane `c`. diff --git a/crates/ui-egui/src/panels/content_fill_panel.rs b/crates/ui-egui/src/panels/content_fill_panel.rs index 5d534e59..1937b4cc 100644 --- a/crates/ui-egui/src/panels/content_fill_panel.rs +++ b/crates/ui-egui/src/panels/content_fill_panel.rs @@ -45,7 +45,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { .unwrap_or(0); format!("{} — {masks} subtracting mask(s)", l.name) } - None => "Select a layer with transparent areas (e.g. a Subtract mask)".into(), + None => "Select a layer with a Subtract mask".into(), }; p.text(pos2(x0, y + 9.0), Align2::LEFT_CENTER, &target, Tokens::ui(11.5), if layer.is_some() { t.text_dim } else { t.text_faint }); app.auto.add("contentFill.target", Rect::from_min_size(pos2(x0, y), vec2(rect.width() - 24.0, 18.0)), &target); diff --git a/crates/ui-egui/src/panels/media_panels.rs b/crates/ui-egui/src/panels/media_panels.rs index b1a3230e..4f6fd0ae 100644 --- a/crates/ui-egui/src/panels/media_panels.rs +++ b/crates/ui-egui/src/panels/media_panels.rs @@ -104,11 +104,13 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { if only != st.importable_only { kit::exec(app, "mediaBrowser.go", json!({"path": dir, "importableOnly": only})); } - x += 100.0; - p.text(pos2(x, y + 10.0), Align2::LEFT_CENTER, &dir, Tokens::ui(11.5), t.text_dim); - app.auto.add("mediaBrowser.path", Rect::from_min_max(pos2(x, y), pos2(rect.max.x - 8.0, y + 20.0)), &dir); + // The path on its own row. + let _ = x; + let py = y + 24.0; + p.text(pos2(rect.min.x + 10.0, py + 9.0), Align2::LEFT_CENTER, &dir, Tokens::ui(11.5), t.text_dim); + app.auto.add("mediaBrowser.path", Rect::from_min_max(pos2(rect.min.x + 8.0, py), pos2(rect.max.x - 8.0, py + 18.0)), &dir); // Left column: favourites. - let side = Rect::from_min_max(pos2(rect.min.x, rect.min.y + 32.0), pos2(rect.min.x + 160.0_f32.min(rect.width() * 0.3), rect.max.y)); + let side = Rect::from_min_max(pos2(rect.min.x, rect.min.y + 54.0), pos2(rect.min.x + 160.0_f32.min(rect.width() * 0.3), rect.max.y)); p.line_segment([side.right_top(), side.right_bottom()], Stroke::new(1.0, t.separator)); p.text(pos2(side.min.x + 10.0, side.min.y + 10.0), Align2::LEFT_CENTER, "Favorites", Tokens::semibold(11.5), t.text_dim); let mut fy = side.min.y + 22.0; diff --git a/crates/ui-egui/src/panels/scopes_panel.rs b/crates/ui-egui/src/panels/scopes_panel.rs index d220213b..db5279d8 100644 --- a/crates/ui-egui/src/panels/scopes_panel.rs +++ b/crates/ui-egui/src/panels/scopes_panel.rs @@ -62,11 +62,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { st.float = i == 1; } x += 78.0; - st.clamp = kit::checkbox(app, ui, pos2(x, y + 2.0), "scopes.clamp", "Clamp Signal", st.clamp); + // Narrow panels: Clamp Signal on a second row. + let (cx, cy, top) = if x + 110.0 > rect.max.x { (rect.min.x + 8.0, y + 26.0, 60.0) } else { (x, y, 34.0) }; + st.clamp = kit::checkbox(app, ui, pos2(cx, cy + 2.0), "scopes.clamp", "Clamp Signal", st.clamp); if st != app.ui.scopes { app.ui.scopes = st.clone(); } - let area = Rect::from_min_max(pos2(rect.min.x + 8.0, rect.min.y + 34.0), pos2(rect.max.x - 8.0, rect.max.y - 8.0)); + let area = Rect::from_min_max(pos2(rect.min.x + 8.0, rect.min.y + top), pos2(rect.max.x - 8.0, rect.max.y - 8.0)); if area.width() < 40.0 || area.height() < 40.0 { return; } diff --git a/crates/ui-egui/tests/ui_panels.rs b/crates/ui-egui/tests/ui_panels.rs index 67d3db92..ee027daa 100644 --- a/crates/ui-egui/tests/ui_panels.rs +++ b/crates/ui-egui/tests/ui_panels.rs @@ -102,3 +102,43 @@ fn progress_panel_cancels_a_job() { assert!(h.state().session.jobs().is_empty()); assert_eq!(h.state().session.job_log.last().unwrap().status, "cancelled"); } + +/// Headless look at the panels (wgpu offscreen; needs a GPU adapter). Run with +/// `PANELS_SNAPSHOT=/abs/dir cargo test -p effectcraft-ui-egui --test ui_panels -- --ignored`. +#[test] +#[ignore] +fn panels_snapshot() { + let mut s = Session::default(); + s.execute("file.openDemoProject", json!({})).unwrap(); + let solid = s.project.items.values().find(|i| matches!(i.kind, effectcraft_engine::project::ItemKind::Solid(_))).map(|i| i.id.0); + let mut app = Some(EffectcraftApp::new(s)); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app.take().expect("app")); + h.run_steps(3); + let ctx = h.ctx.clone(); + effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "window.workspace", json!({"name": "All Panels"})).unwrap(); + h.run_steps(3); + let dir = std::env::var("PANELS_SNAPSHOT").unwrap_or_else(|_| concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/test-out").into()); + std::fs::create_dir_all(&dir).unwrap(); + if let Some(id) = solid { + h.state_mut().session.execute("footage.open", json!({"item": id})).unwrap(); + } + for (panel, scope) in [ + ("lumetriScopes", "waveformRgb"), + ("lumetriScopes", "vectorscopeYuv"), + ("lumetriScopes", "histogram"), + ("lumetriScopes", "paradeRgb"), + ("contentAwareFill", ""), + ("metadata", ""), + ("progress", ""), + ("mediaBrowser", ""), + ("footage", ""), + ] { + if !scope.is_empty() { + h.state_mut().ui.scopes.scope = scope.into(); + } + open(&mut h, panel); + h.run_steps(3); + let img = h.render().expect("render"); + img.save(format!("{dir}/{panel}{scope}.png")).unwrap(); + } +}