M13.6: panel polish (log-density scopes, narrow-panel layouts) and an offscreen panels snapshot test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b29d833b2d
commit
e362cae4c7
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue