Interpret Footage: the Footage panel, thumbnails and tracking show Cineon Settings

The Footage panel, the Project panel thumbnail and the native-size source
frames that tracking, mask tracking, scene detection and auto-trace read took
DPX and Cineon frames straight from the file, so they showed the log code
values while a composition showed the converted ones. Each now applies the
footage's Cineon Settings, as the composition renderer does.
This commit is contained in:
dotCooCoo
2026-10-24 11:48:41 -07:00
parent 9de2c5ee8a
commit 7efbe7d85c
5 changed files with 40 additions and 4 deletions
@@ -56,6 +56,12 @@ pub fn timecode(item: Option<&effectcraft_project::Item>, fr: effectcraft_time::
}
}
/// The frame the Footage panel shows: footage `f` at source time `t`, with its Cineon Settings
/// applied.
pub fn frame(s: &Session, item: ItemId, f: &effectcraft_project::Footage, t: Tick) -> Option<std::sync::Arc<effectcraft_raster::Image>> {
s.footage.frame(item, f, t).map(|img| effectcraft_render::cineon_frame(img, f))
}
fn source_time(s: &Session, p: &Value, item: ItemId) -> Option<Tick> {
let (_, fr) = source_timing(s, item);
if let Some(t) = f_p(p, "time") {
+1 -1
View File
@@ -1131,7 +1131,7 @@ impl Session {
effectcraft_project::ItemKind::Footage(f) if f.has_video => {
let f = f.clone();
Some(Box::new(move || {
let img = footage.frame(item, &f, Tick(0))?;
let img = effectcraft_render::cineon_frame(footage.frame(item, &f, Tick(0))?, &f);
// Point-sample down to at most `max_side` on the long side.
let step = (img.width.max(img.height) as f32 / max_side.max(1) as f32).ceil().max(1.0) as u32;
let (w, h) = (img.width.div_ceil(step), img.height.div_ceil(step));
+29
View File
@@ -385,3 +385,32 @@ fn cineon_settings_convert_dpx_code_values() {
s.execute_checked("file.interpretProxy", json!({"items": [inner.0], "logConversion": true, "highlightRolloff": 0})).unwrap();
assert!(near(proxied(&s, 0), 0.0) && near(proxied(&s, 1), 1.0), "{} {}", proxied(&s, 0), proxied(&s, 1));
}
/// The Footage panel, the Project panel thumbnail and the tracker's source frames show DPX
/// footage with its Cineon Settings, as a composition renders it.
#[test]
fn cineon_settings_apply_to_footage_panel_thumbnail_and_tracking() {
let mut s = session();
let d = import(&mut s, "/m/scan.dpx");
s.execute("comp.new", json!({"name": "C", "width": 4, "height": 4, "frameRate": 10, "duration": 1})).unwrap();
let c = s.active_comp_id().unwrap();
s.execute("layer.addItem", json!({"item": d.0})).unwrap();
s.execute("file.interpretFootage", json!({"item": d.0, "logConversion": true, "highlightRolloff": 0})).unwrap();
let near = |a: f32, b: f32| (a - b).abs() < 1.5 / 255.0;
let rendered = px(&s, c, 0.0, RenderOpts::default(), 2, 0)[0];
assert!(near(rendered, 0.26190), "code 400 at gamma 1.7: {rendered}");
let f = match &s.project.item(d).unwrap().kind {
ItemKind::Footage(f) => f.clone(),
_ => panic!("not footage"),
};
let panel = crate::commands::footage_panel::frame(&s, d, &f, Tick::ZERO).unwrap().get(2, 0)[0];
assert!(near(panel, rendered), "Footage panel: {panel}");
let (w, _, rgba) = (s.thumbnail_job(d, Tick::ZERO, 64).unwrap())().unwrap();
assert_eq!(w, 4);
let thumb = rgba[8] as f32 / 255.0;
assert!(near(thumb, rendered), "Project panel thumbnail: {thumb}");
let comp = s.project.comp(c).unwrap();
let r = crate::render::Renderer::new(&s.project, s.footage.as_ref(), RenderOpts::default());
let (img, _) = crate::tracking::source_frame(&r, &s.project, c, comp, &comp.layers[0], Tick::ZERO, None).unwrap();
assert!(near(img.get(2, 0)[0], rendered), "tracker source frame: {}", img.get(2, 0)[0]);
}
+3 -2
View File
@@ -197,7 +197,8 @@ pub(crate) struct Work {
}
/// The layer's source frame at comp time `t` at 100 % (layer pixels + offset = image pixels).
/// Footage at its native size is used straight from the footage source (no copy).
/// Footage at its native size is used straight from the footage source (no copy unless its
/// Cineon Settings convert it).
pub(crate) fn source_frame(
r: &Renderer,
project: &Project,
@@ -215,7 +216,7 @@ pub(crate) fn source_frame(
&& img.width == f.width
&& img.height == f.height
{
return Some((img, [0.0; 2]));
return Some((effectcraft_render::cineon_frame(img, f), [0.0; 2]));
}
let buf = r.layer_source(&ctx, layer)?;
if (buf.scale - 1.0).abs() > 1e-9 && buf.scale > 0.0 {
+1 -1
View File
@@ -62,7 +62,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let cached: Option<((u64, i64, u64), egui::TextureHandle, [u32; 2])> = ctx.data(|d| d.get_temp(tid));
let tex = match cached {
Some((k, tex, size)) if k == key => Some((tex, size)),
_ => app.session.footage.frame(view.item, f, view.time).map(|img| {
_ => effectcraft_engine::commands::footage_panel::frame(&app.session, view.item, f, view.time).map(|img| {
let ci = egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &img.to_rgba8());
let tex = crate::frames::load_fitted(&ctx, "footage-panel", ci, egui::TextureOptions::LINEAR);
ctx.data_mut(|d| d.insert_temp(tid, (key, tex.clone(), [img.width, img.height])));