From 581e31b43213b9dee236d87937f344b02cfc5ac9 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 30 Sep 2026 10:02:36 -0400 Subject: [PATCH] M0.12: xtask layers/wasm/ci gates (layer table, same-layer allow-list, UI crates barred below L5); cargo alias Co-Authored-By: Claude Opus 5.5 --- .cargo/config.toml | 2 + crates/engine/src/commands.rs | 4 +- crates/project/src/effect.rs | 72 +++++----- crates/render/src/effects.rs | 16 ++- crates/ui-egui/src/panels/lumetri.rs | 123 +++++++++++++---- crates/ui-egui/src/panels/mod.rs | 2 +- xtask/src/main.rs | 194 ++++++++++++++++++++++++++- 7 files changed, 354 insertions(+), 59 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..9741f113 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --quiet --package xtask --" diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index d6cd43e2..86388bc1 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -1773,7 +1773,9 @@ fn json_to_param(template: &ParamValue, v: &Value) -> Option { ParamValue::Bool(_) => ParamValue::Bool(v.as_bool()?), ParamValue::Choice(_) => ParamValue::Choice(v.as_u64()? as u32), ParamValue::Text(_) => ParamValue::Text(v.as_str()?.to_string()), - ParamValue::Curve(_) => ParamValue::Curve(v.as_array()?.iter().filter_map(|p| Some([p.get(0)?.as_f64()? as f32, p.get(1)?.as_f64()? as f32])).collect()), + ParamValue::Curve(_) => { + ParamValue::Curve(v.as_array()?.iter().filter_map(|p| Some([p.get(0)?.as_f64()? as f32, p.get(1)?.as_f64()? as f32])).collect()) + } }) } diff --git a/crates/project/src/effect.rs b/crates/project/src/effect.rs index aaa8abc2..80f69e00 100644 --- a/crates/project/src/effect.rs +++ b/crates/project/src/effect.rs @@ -38,7 +38,9 @@ pub enum ParamKind { Angle, Text, /// Tone curve (x in, y out); `hue` curves use a rainbow baseline with y = 0.5 as neutral. - Curve { hue: bool }, + Curve { + hue: bool, + }, /// Colour wheel offset (x, y in −1..1). Wheel, } @@ -402,42 +404,50 @@ fn build_effects() -> Vec { grp(f("whites", "Whites", 0.0, -100.0, 100.0, ""), "Basic Correction"), grp(f("blacks", "Blacks", 0.0, -100.0, 100.0, ""), "Basic Correction"), grp(fs("saturation", "Saturation", 100.0, (0.0, 200.0), (0.0, 200.0), "", 1), "Basic Correction"), - grp(ch("look", "Look", &["None", "Teal & Orange", "Warm Film", "Cool Blue", "Bleach Bypass", "Faded Matte", "Monochrome", "Golden Hour", "Night"], 0), "Creative"), - grp(fs("look_intensity", "Intensity", 100.0, (0.0, 200.0), (0.0, 200.0), "", 1), "Creative"), - grp(f("faded_film", "Faded Film", 0.0, 0.0, 100.0, ""), "Creative"), + grp( + ch( + "look", + "Look", + &["None", "Teal & Orange", "Warm Film", "Cool Blue", "Bleach Bypass", "Faded Matte", "Monochrome", "Golden Hour", "Night"], + 0, + ), + "Creative", + ), + grp(fs("look_intensity", "Intensity", 100.0, (0.0, 200.0), (0.0, 200.0), "", 1), "Creative"), + grp(f("faded_film", "Faded Film", 0.0, 0.0, 100.0, ""), "Creative"), grp(f("sharpen", "Sharpen", 0.0, -100.0, 100.0, ""), "Creative"), grp(f("vibrance", "Vibrance", 0.0, -100.0, 100.0, ""), "Creative"), grp(fs("creative_sat", "Saturation", 100.0, (0.0, 200.0), (0.0, 200.0), "", 1), "Creative"), grp(col("shadow_tint", "Shadow Tint", [0.5, 0.5, 0.5, 1.0]), "Creative"), grp(col("highlight_tint", "Highlight Tint", [0.5, 0.5, 0.5, 1.0]), "Creative"), grp(curve("curve_luma", "Luma Curve", false), "Curves"), - grp(curve("curve_red", "Red Curve", false), "Curves"), - grp(curve("curve_green", "Green Curve", false), "Curves"), - grp(curve("curve_blue", "Blue Curve", false), "Curves"), - grp(curve("hue_vs_sat", "Hue vs Sat", true), "Curves"), - grp(curve("hue_vs_hue", "Hue vs Hue", true), "Curves"), - grp(curve("hue_vs_luma", "Hue vs Luma", true), "Curves"), - grp(curve("luma_vs_sat", "Luma vs Sat", true), "Curves"), - grp(curve("sat_vs_sat", "Sat vs Sat", true), "Curves"), - grp(wheel("wheel_shadows", "Shadows"), "Color Wheels & Match"), - grp(f("wheel_shadows_l", "Shadows Lightness", 0.0, -100.0, 100.0, ""), "Color Wheels & Match"), - grp(wheel("wheel_midtones", "Midtones"), "Color Wheels & Match"), - grp(f("wheel_midtones_l", "Midtones Lightness", 0.0, -100.0, 100.0, ""), "Color Wheels & Match"), - grp(wheel("wheel_highlights", "Highlights"), "Color Wheels & Match"), - grp(f("wheel_highlights_l", "Highlights Lightness", 0.0, -100.0, 100.0, ""), "Color Wheels & Match"), - grp(b("hsl_on", "Enable HSL Secondary", false), "HSL Secondary"), - grp(fs("hsl_hue", "Hue Center", 0.0, (0.0, 360.0), (0.0, 360.0), "°", 0), "HSL Secondary"), - grp(fs("hsl_hue_range", "Hue Range", 30.0, (1.0, 180.0), (1.0, 180.0), "°", 0), "HSL Secondary"), - grp(fs("hsl_sat_min", "Saturation Min", 10.0, (0.0, 100.0), (0.0, 100.0), "", 0), "HSL Secondary"), - grp(fs("hsl_luma_min", "Luma Min", 5.0, (0.0, 100.0), (0.0, 100.0), "", 0), "HSL Secondary"), - grp(fs("hsl_luma_max", "Luma Max", 95.0, (0.0, 100.0), (0.0, 100.0), "", 0), "HSL Secondary"), - grp(f("hsl_soft", "Soften", 20.0, 0.0, 100.0, ""), "HSL Secondary"), - grp(ch("hsl_show_mask", "Show Mask", &["Off", "Color/Gray", "Color/Black", "White/Black"], 0), "HSL Secondary"), - grp(f("hsl_temp", "Temperature", 0.0, -100.0, 100.0, ""), "HSL Secondary"), - grp(f("hsl_tint", "Tint", 0.0, -100.0, 100.0, ""), "HSL Secondary"), - grp(fs("hsl_sat", "Saturation", 100.0, (0.0, 200.0), (0.0, 200.0), "", 1), "HSL Secondary"), - grp(f("hsl_hue_shift", "Hue Shift", 0.0, -180.0, 180.0, "°"), "HSL Secondary"), - grp(f("vignette_amount", "Amount", 0.0, -5.0, 5.0, ""), "Vignette"), + grp(curve("curve_red", "Red Curve", false), "Curves"), + grp(curve("curve_green", "Green Curve", false), "Curves"), + grp(curve("curve_blue", "Blue Curve", false), "Curves"), + grp(curve("hue_vs_sat", "Hue vs Sat", true), "Curves"), + grp(curve("hue_vs_hue", "Hue vs Hue", true), "Curves"), + grp(curve("hue_vs_luma", "Hue vs Luma", true), "Curves"), + grp(curve("luma_vs_sat", "Luma vs Sat", true), "Curves"), + grp(curve("sat_vs_sat", "Sat vs Sat", true), "Curves"), + grp(wheel("wheel_shadows", "Shadows"), "Color Wheels & Match"), + grp(f("wheel_shadows_l", "Shadows Lightness", 0.0, -100.0, 100.0, ""), "Color Wheels & Match"), + grp(wheel("wheel_midtones", "Midtones"), "Color Wheels & Match"), + grp(f("wheel_midtones_l", "Midtones Lightness", 0.0, -100.0, 100.0, ""), "Color Wheels & Match"), + grp(wheel("wheel_highlights", "Highlights"), "Color Wheels & Match"), + grp(f("wheel_highlights_l", "Highlights Lightness", 0.0, -100.0, 100.0, ""), "Color Wheels & Match"), + grp(b("hsl_on", "Enable HSL Secondary", false), "HSL Secondary"), + grp(fs("hsl_hue", "Hue Center", 0.0, (0.0, 360.0), (0.0, 360.0), "°", 0), "HSL Secondary"), + grp(fs("hsl_hue_range", "Hue Range", 30.0, (1.0, 180.0), (1.0, 180.0), "°", 0), "HSL Secondary"), + grp(fs("hsl_sat_min", "Saturation Min", 10.0, (0.0, 100.0), (0.0, 100.0), "", 0), "HSL Secondary"), + grp(fs("hsl_luma_min", "Luma Min", 5.0, (0.0, 100.0), (0.0, 100.0), "", 0), "HSL Secondary"), + grp(fs("hsl_luma_max", "Luma Max", 95.0, (0.0, 100.0), (0.0, 100.0), "", 0), "HSL Secondary"), + grp(f("hsl_soft", "Soften", 20.0, 0.0, 100.0, ""), "HSL Secondary"), + grp(ch("hsl_show_mask", "Show Mask", &["Off", "Color/Gray", "Color/Black", "White/Black"], 0), "HSL Secondary"), + grp(f("hsl_temp", "Temperature", 0.0, -100.0, 100.0, ""), "HSL Secondary"), + grp(f("hsl_tint", "Tint", 0.0, -100.0, 100.0, ""), "HSL Secondary"), + grp(fs("hsl_sat", "Saturation", 100.0, (0.0, 200.0), (0.0, 200.0), "", 1), "HSL Secondary"), + grp(f("hsl_hue_shift", "Hue Shift", 0.0, -180.0, 180.0, "°"), "HSL Secondary"), + grp(f("vignette_amount", "Amount", 0.0, -5.0, 5.0, ""), "Vignette"), grp(f("vignette_midpoint", "Midpoint", 50.0, 0.0, 100.0, ""), "Vignette"), grp(f("vignette_roundness", "Roundness", 0.0, -100.0, 100.0, ""), "Vignette"), grp(f("vignette_feather", "Feather", 50.0, 0.0, 100.0, ""), "Vignette"), diff --git a/crates/render/src/effects.rs b/crates/render/src/effects.rs index 35f3cd45..2136fda7 100644 --- a/crates/render/src/effects.rs +++ b/crates/render/src/effects.rs @@ -1295,7 +1295,18 @@ fn lumetri_advanced(img: &mut Image, e: &EffectInstance, cx: &FxCtx) { let (ls, lm, lh) = (f(e, "wheel_shadows_l", cx) / 100.0, f(e, "wheel_midtones_l", cx) / 100.0, f(e, "wheel_highlights_l", cx) / 100.0); let wheels = ws.iter().chain(&wm).chain(&wh).any(|v| v.abs() > 1e-5) || ls.abs() + lm.abs() + lh.abs() > 1e-5; let hsl_on = b(e, "hsl_on"); - let any = luma_c.is_some() || red_c.is_some() || green_c.is_some() || blue_c.is_some() || hvs.is_some() || hvh.is_some() || hvl.is_some() || lvs.is_some() || svs.is_some() || look > 0 || wheels || hsl_on; + let any = luma_c.is_some() + || red_c.is_some() + || green_c.is_some() + || blue_c.is_some() + || hvs.is_some() + || hvh.is_some() + || hvl.is_some() + || lvs.is_some() + || svs.is_some() + || look > 0 + || wheels + || hsl_on; if !any { return; } @@ -1303,7 +1314,8 @@ fn lumetri_advanced(img: &mut Image, e: &EffectInstance, cx: &FxCtx) { let (smin, lmin, lmax) = (f(e, "hsl_sat_min", cx) / 100.0, f(e, "hsl_luma_min", cx) / 100.0, f(e, "hsl_luma_max", cx) / 100.0); let soft = (f(e, "hsl_soft", cx) / 100.0 * 0.3).max(0.01); let show_mask = choice(e, "hsl_show_mask"); - let (htemp, htint, hsat, hshift) = (f(e, "hsl_temp", cx) / 100.0, f(e, "hsl_tint", cx) / 100.0, f(e, "hsl_sat", cx) / 100.0, f(e, "hsl_hue_shift", cx) / 360.0); + let (htemp, htint, hsat, hshift) = + (f(e, "hsl_temp", cx) / 100.0, f(e, "hsl_tint", cx) / 100.0, f(e, "hsl_sat", cx) / 100.0, f(e, "hsl_hue_shift", cx) / 360.0); let sample = |l: &Vec, x: f32| { let p = x.clamp(0.0, 1.0) * (N - 1) as f32; let i = p as usize; diff --git a/crates/ui-egui/src/panels/lumetri.rs b/crates/ui-egui/src/panels/lumetri.rs index 3896b17e..5032e5c8 100644 --- a/crates/ui-egui/src/panels/lumetri.rs +++ b/crates/ui-egui/src/panels/lumetri.rs @@ -58,7 +58,14 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { slider(ui, &e, mt, "tint", "Tint", Some(Gradient::Tint), &t, &mut actions, clip, idx); slider(ui, &e, mt, "saturation", "Saturation", None, &t, &mut actions, clip, idx); sub(ui, &t, "Light"); - for (id, label) in [("exposure", "Exposure"), ("contrast", "Contrast"), ("highlights", "Highlights"), ("shadows", "Shadows"), ("whites", "Whites"), ("blacks", "Blacks")] { + for (id, label) in [ + ("exposure", "Exposure"), + ("contrast", "Contrast"), + ("highlights", "Highlights"), + ("shadows", "Shadows"), + ("whites", "Whites"), + ("blacks", "Blacks"), + ] { slider(ui, &e, mt, id, label, None, &t, &mut actions, clip, idx); } } @@ -67,17 +74,23 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { Some(ParamValue::Choice(c)) => *c as usize, _ => 0, }; - let opts = filmcraft_project::find_effect("lumetri").and_then(|d| d.param("look")).and_then(|p| if let filmcraft_project::ParamKind::Choice(o) = p.kind { Some(o) } else { None }).unwrap_or(&[]); + let opts = filmcraft_project::find_effect("lumetri") + .and_then(|d| d.param("look")) + .and_then(|p| if let filmcraft_project::ParamKind::Choice(o) = p.kind { Some(o) } else { None }) + .unwrap_or(&[]); ui.horizontal(|ui| { ui.add_sized(vec2(110.0, 20.0), egui::Label::new(egui::RichText::new("Look").color(t.text_dim))); let mut sel = look; - egui::ComboBox::from_id_salt("lumetri-look").selected_text(opts.get(look).copied().unwrap_or("None")).width(ui.available_width() - 8.0).show_ui(ui, |ui| { - for (i, o) in opts.iter().enumerate() { - if ui.selectable_value(&mut sel, i, *o).changed() { - set(&mut actions, "look", json!(i)); + egui::ComboBox::from_id_salt("lumetri-look") + .selected_text(opts.get(look).copied().unwrap_or("None")) + .width(ui.available_width() - 8.0) + .show_ui(ui, |ui| { + for (i, o) in opts.iter().enumerate() { + if ui.selectable_value(&mut sel, i, *o).changed() { + set(&mut actions, "look", json!(i)); + } } - } - }); + }); }); slider(ui, &e, mt, "look_intensity", "Intensity", None, &t, &mut actions, clip, idx); sub(ui, &t, "Adjustments"); @@ -90,7 +103,9 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { let key = egui::Id::new("lumetri-curve-channel"); let mut ch: usize = ui.data(|d| d.get_temp(key)).unwrap_or(0); ui.horizontal(|ui| { - for (i, c) in [Color32::WHITE, Color32::from_rgb(230, 70, 70), Color32::from_rgb(80, 200, 90), Color32::from_rgb(80, 130, 250)].iter().enumerate() { + for (i, c) in + [Color32::WHITE, Color32::from_rgb(230, 70, 70), Color32::from_rgb(80, 200, 90), Color32::from_rgb(80, 130, 250)].iter().enumerate() + { let (r, resp) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::click()); ui.painter().circle_filled(r.center(), 6.0, *c); if ch == i { @@ -102,10 +117,21 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { } }); ui.data_mut(|d| d.insert_temp(key, ch)); - let (id, col) = [("curve_luma", Color32::WHITE), ("curve_red", Color32::from_rgb(230, 70, 70)), ("curve_green", Color32::from_rgb(80, 200, 90)), ("curve_blue", Color32::from_rgb(80, 130, 250))][ch]; + let (id, col) = [ + ("curve_luma", Color32::WHITE), + ("curve_red", Color32::from_rgb(230, 70, 70)), + ("curve_green", Color32::from_rgb(80, 200, 90)), + ("curve_blue", Color32::from_rgb(80, 130, 250)), + ][ch]; curve_editor(ui, &e, id, false, col, &t, &mut actions, clip, idx); sub(ui, &t, "Hue Saturation Curves"); - for (id, label) in [("hue_vs_sat", "Hue vs Sat"), ("hue_vs_hue", "Hue vs Hue"), ("hue_vs_luma", "Hue vs Luma"), ("luma_vs_sat", "Luma vs Sat"), ("sat_vs_sat", "Sat vs Sat")] { + for (id, label) in [ + ("hue_vs_sat", "Hue vs Sat"), + ("hue_vs_hue", "Hue vs Hue"), + ("hue_vs_luma", "Hue vs Luma"), + ("luma_vs_sat", "Luma vs Sat"), + ("sat_vs_sat", "Sat vs Sat"), + ] { ui.label(egui::RichText::new(label).color(t.text_dim)); curve_editor(ui, &e, id, true, Color32::WHITE, &t, &mut actions, clip, idx); } @@ -113,7 +139,11 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { if section(ui, app, "Color Wheels & Match") { let w = ((ui.available_width() - 24.0) / 3.0).min(120.0); ui.horizontal(|ui| { - for (id, lid, label) in [("wheel_shadows", "wheel_shadows_l", "Shadows"), ("wheel_midtones", "wheel_midtones_l", "Midtones"), ("wheel_highlights", "wheel_highlights_l", "Highlights")] { + for (id, lid, label) in [ + ("wheel_shadows", "wheel_shadows_l", "Shadows"), + ("wheel_midtones", "wheel_midtones_l", "Midtones"), + ("wheel_highlights", "wheel_highlights_l", "Highlights"), + ] { ui.vertical(|ui| { ui.label(egui::RichText::new(label).color(t.text_dim)); color_wheel(ui, &e, mt, id, lid, w, &t, &mut actions, clip, idx); @@ -129,7 +159,13 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { } sub(ui, &t, "Key"); slider(ui, &e, mt, "hsl_hue", "Hue", Some(Gradient::Hue), &t, &mut actions, clip, idx); - for (id, label) in [("hsl_hue_range", "Hue Range"), ("hsl_sat_min", "Saturation Min"), ("hsl_luma_min", "Luma Min"), ("hsl_luma_max", "Luma Max"), ("hsl_soft", "Soften")] { + for (id, label) in [ + ("hsl_hue_range", "Hue Range"), + ("hsl_sat_min", "Saturation Min"), + ("hsl_luma_min", "Luma Min"), + ("hsl_luma_max", "Luma Max"), + ("hsl_soft", "Soften"), + ] { slider(ui, &e, mt, id, label, None, &t, &mut actions, clip, idx); } let mask = match e.param("hsl_show_mask").map(|p| &p.value) { @@ -154,7 +190,9 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { slider(ui, &e, mt, "hsl_hue_shift", "Hue Shift", Some(Gradient::Hue), &t, &mut actions, clip, idx); } if section(ui, app, "Vignette") { - for (id, label) in [("vignette_amount", "Amount"), ("vignette_midpoint", "Midpoint"), ("vignette_roundness", "Roundness"), ("vignette_feather", "Feather")] { + for (id, label) in + [("vignette_amount", "Amount"), ("vignette_midpoint", "Midpoint"), ("vignette_roundness", "Roundness"), ("vignette_feather", "Feather")] + { slider(ui, &e, mt, id, label, None, &t, &mut actions, clip, idx); } } @@ -174,7 +212,12 @@ fn section(ui: &mut egui::Ui, app: &mut FilmcraftApp, name: &str) -> bool { ui.add_space(2.0); let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 40.0), Sense::click()); ui.painter().line_segment([r.left_top(), r.right_top()], Stroke::new(1.0, t.separator)); - icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 6.0, r.center().y), vec2(10.0, 10.0)), if open { Icon::ChevronDown } else { Icon::ChevronRight }, t.text_dim); + icons::paint( + ui.painter(), + Rect::from_center_size(pos2(r.min.x + 6.0, r.center().y), vec2(10.0, 10.0)), + if open { Icon::ChevronDown } else { Icon::ChevronRight }, + t.text_dim, + ); ui.painter().text(pos2(r.min.x + 20.0, r.center().y), Align2::LEFT_CENTER, name, Tokens::semibold(13.0), t.text); // toggle switch (visual; section bypass lands with per-section enables) let sw = Rect::from_center_size(pos2(r.max.x - 18.0, r.center().y), vec2(26.0, 14.0)); @@ -207,7 +250,18 @@ enum Gradient { /// Spectrum slider: 1 pt track (or gradient), hollow ring knob, scrubby value at the right. #[allow(clippy::too_many_arguments)] -fn slider(ui: &mut egui::Ui, e: &EffectInstance, mt: Tick, id: &str, label: &str, grad: Option, t: &Tokens, actions: &mut Actions, clip: ClipId, idx: usize) { +fn slider( + ui: &mut egui::Ui, + e: &EffectInstance, + mt: Tick, + id: &str, + label: &str, + grad: Option, + t: &Tokens, + actions: &mut Actions, + clip: ClipId, + idx: usize, +) { let Some(pd) = e.def().and_then(|d| d.param(id)) else { return }; let filmcraft_project::ParamKind::Float { min, max, soft_min, soft_max, decimals, .. } = pd.kind else { return }; let v = e.param(id).map(|p| p.f64_at(mt)).unwrap_or(pd.default.as_f64().unwrap_or(0.0)); @@ -240,7 +294,11 @@ fn slider(ui: &mut egui::Ui, e: &EffectInstance, mt: Tick, id: &str, label: &str let hit = Rect::from_min_max(pos2(track.min.x - 6.0, row.min.y), pos2(track.max.x + 6.0, row.max.y)); let resp = ui.interact(hit, egui::Id::new(("lslider", clip.0, id)), Sense::click_and_drag()); ui.painter().circle_filled(pos2(kx, track.center().y), 5.0, t.panel_bg); - ui.painter().circle_stroke(pos2(kx, track.center().y), 5.0, Stroke::new(1.5, if resp.dragged() { t.hot_text } else { Color32::from_rgb(0xb0, 0xb0, 0xb0) })); + ui.painter().circle_stroke( + pos2(kx, track.center().y), + 5.0, + Stroke::new(1.5, if resp.dragged() { t.hot_text } else { Color32::from_rgb(0xb0, 0xb0, 0xb0) }), + ); let mut out = None; if (resp.dragged() || resp.clicked()) && let Some(p) = resp.interact_pointer_pos() @@ -251,8 +309,13 @@ fn slider(ui: &mut egui::Ui, e: &EffectInstance, mt: Tick, id: &str, label: &str if resp.double_clicked() { out = pd.default.as_f64(); } - let mut vui = ui.new_child(egui::UiBuilder::new().max_rect(Rect::from_min_max(pos2(row.max.x - 52.0, row.min.y + 3.0), pos2(row.max.x, row.max.y - 3.0))).layout(egui::Layout::right_to_left(egui::Align::Center))); - let (_, nv) = crate::widgets::hot_number(&mut vui, egui::Id::new(("lval", clip.0, id)), v, (soft_max - soft_min) / 300.0, (min, max), decimals as usize, "", t); + let mut vui = ui.new_child( + egui::UiBuilder::new() + .max_rect(Rect::from_min_max(pos2(row.max.x - 52.0, row.min.y + 3.0), pos2(row.max.x, row.max.y - 3.0))) + .layout(egui::Layout::right_to_left(egui::Align::Center)), + ); + let (_, nv) = + crate::widgets::hot_number(&mut vui, egui::Id::new(("lval", clip.0, id)), v, (soft_max - soft_min) / 300.0, (min, max), decimals as usize, "", t); if let Some(nv) = out.or(nv) { actions.push(json!({"clip": clip.0, "effect": idx, "param": id, "value": (nv * 10f64.powi(decimals as i32)).round() / 10f64.powi(decimals as i32)})); } @@ -274,8 +337,14 @@ fn curve_editor(ui: &mut egui::Ui, e: &EffectInstance, id: &str, hue: bool, col: p.rect_filled(area, 2.0, Color32::from_rgb(0x16, 0x16, 0x16)); for i in 1..4 { let f = i as f32 / 4.0; - p.line_segment([pos2(area.min.x + area.width() * f, area.min.y), pos2(area.min.x + area.width() * f, area.max.y)], Stroke::new(1.0, Color32::from_rgb(0x2a, 0x2a, 0x2a))); - p.line_segment([pos2(area.min.x, area.min.y + area.height() * f), pos2(area.max.x, area.min.y + area.height() * f)], Stroke::new(1.0, Color32::from_rgb(0x2a, 0x2a, 0x2a))); + p.line_segment( + [pos2(area.min.x + area.width() * f, area.min.y), pos2(area.min.x + area.width() * f, area.max.y)], + Stroke::new(1.0, Color32::from_rgb(0x2a, 0x2a, 0x2a)), + ); + p.line_segment( + [pos2(area.min.x, area.min.y + area.height() * f), pos2(area.max.x, area.min.y + area.height() * f)], + Stroke::new(1.0, Color32::from_rgb(0x2a, 0x2a, 0x2a)), + ); } if hue { let n = 36; @@ -283,7 +352,14 @@ fn curve_editor(ui: &mut egui::Ui, e: &EffectInstance, id: &str, hue: bool, col: let f0 = i as f32 / n as f32; let rgb = filmcraft_color::hsl_to_rgb(f0, 0.8, 0.5); let c = Color32::from_rgb((rgb[0] * 255.0) as u8, (rgb[1] * 255.0) as u8, (rgb[2] * 255.0) as u8); - p.rect_filled(Rect::from_min_max(pos2(area.min.x + area.width() * f0, area.max.y - 6.0), pos2(area.min.x + area.width() * (f0 + 1.0 / n as f32) + 0.5, area.max.y)), 0.0, c); + p.rect_filled( + Rect::from_min_max( + pos2(area.min.x + area.width() * f0, area.max.y - 6.0), + pos2(area.min.x + area.width() * (f0 + 1.0 / n as f32) + 0.5, area.max.y), + ), + 0.0, + c, + ); } } let to_screen = |q: [f32; 2]| pos2(area.min.x + q[0] * area.width(), area.max.y - q[1] * area.height()); @@ -315,7 +391,8 @@ fn curve_editor(ui: &mut egui::Ui, e: &EffectInstance, id: &str, hue: bool, col: if resp.drag_started() && let Some(pos) = resp.interact_pointer_pos() { - let near = pts.iter().enumerate().map(|(i, q)| (i, to_screen(*q).distance(pos))).filter(|(_, d)| *d < 9.0).min_by(|a, b| a.1.total_cmp(&b.1)).map(|x| x.0); + let near = + pts.iter().enumerate().map(|(i, q)| (i, to_screen(*q).distance(pos))).filter(|(_, d)| *d < 9.0).min_by(|a, b| a.1.total_cmp(&b.1)).map(|x| x.0); dragging = near.or_else(|| { new_pts.push(from_screen(pos)); changed = true; diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 399fd0eb..ca9cca98 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -4,9 +4,9 @@ pub mod dialogs; pub mod effect_controls; pub mod effects; -pub mod lumetri; pub mod export_mode; pub mod import_mode; +pub mod lumetri; pub mod meters; pub mod misc; pub mod monitor; diff --git a/xtask/src/main.rs b/xtask/src/main.rs index f328e4d9..9adee93f 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1 +1,193 @@ -fn main() {} +//! Workspace automation: `cargo xtask `. +//! +//! - `layers`: enforces the dependency layering of `plan/architecture.md` §3 (downward-only edges, +//! listed same-layer edges, L0 codec crates depend on `bitstream` only, no UI/OS crates below L5). +//! - `wasm`: `cargo check --target wasm32-unknown-unknown` for every crate in L0–L4. +//! - `ci`: fmt check, clippy -D warnings, tests, layers, wasm. + +use std::process::{Command, ExitCode}; + +use serde_json::Value; + +/// (crate name without the `filmcraft-` prefix, layer). +const LAYERS: &[(&str, u8)] = &[ + ("time", 0), + ("geom", 0), + ("color", 0), + ("bitstream", 0), + ("isobmff", 0), + ("matroska", 0), + ("riff", 0), + ("h264", 0), + ("h264enc", 0), + ("hevc", 0), + ("prores", 0), + ("mjpeg", 0), + ("dnx", 0), + ("aac", 0), + ("frame", 1), + ("media", 1), + ("project", 1), + ("audio-dsp", 1), + ("edit", 2), + ("codecs", 2), + ("keyframe", 2), + ("effects", 2), + ("text", 2), + ("audio", 2), + ("captions", 2), + ("interchange", 2), + ("render", 3), + ("gpu", 3), + ("scopes", 3), + ("playback", 3), + ("export", 3), + ("format", 3), + ("engine", 4), + ("platform", 5), + ("ui-egui", 5), + ("automation", 5), + ("filmcraft", 6), + ("cli", 6), +]; + +/// L0 crates that are not codecs/containers (no `bitstream`-only restriction). +const L0_FOUNDATION: &[&str] = &["time", "geom", "color", "bitstream"]; + +/// Allowed same-layer edges (from, to). +const SAME_LAYER: &[(&str, &str)] = &[ + ("media", "frame"), + ("project", "media"), + ("project", "frame"), + ("effects", "keyframe"), + ("audio", "keyframe"), + ("playback", "render"), + ("playback", "gpu"), + ("export", "render"), + ("scopes", "gpu"), + ("gpu", "render"), + ("cli", "filmcraft"), +]; + +/// Crates that must not appear below L5 (UI toolkits, windowing, OS audio/menus). +const UI_ONLY: &[&str] = &["egui", "eframe", "egui-wgpu", "winit", "rfd", "cpal", "muda"]; + +fn short(name: &str) -> &str { + name.strip_prefix("filmcraft-").unwrap_or(name) +} + +fn layer_of(name: &str) -> Option { + LAYERS.iter().find(|(n, _)| *n == short(name)).map(|(_, l)| *l) +} + +fn metadata() -> Result { + let out = Command::new(env!("CARGO")).args(["metadata", "--format-version", "1", "--no-deps"]).output().map_err(|e| e.to_string())?; + if !out.status.success() { + return Err(String::from_utf8_lossy(&out.stderr).into()); + } + serde_json::from_slice(&out.stdout).map_err(|e| e.to_string()) +} + +fn workspace_crates(md: &Value) -> Vec<(String, Vec)> { + md["packages"] + .as_array() + .into_iter() + .flatten() + .filter(|p| p["name"] != "xtask") + .map(|p| { + let deps = p["dependencies"] + .as_array() + .into_iter() + .flatten() + .filter(|d| d["kind"].is_null() || d["kind"] == "build") + .filter_map(|d| d["name"].as_str().map(str::to_string)) + .collect(); + (p["name"].as_str().unwrap_or_default().to_string(), deps) + }) + .collect() +} + +fn layers() -> Result<(), String> { + let md = metadata()?; + let mut errors = Vec::new(); + for (name, deps) in workspace_crates(&md) { + let Some(l) = layer_of(&name) else { + errors.push(format!("{name}: not assigned a layer (add it to xtask LAYERS and plan/architecture.md §3)")); + continue; + }; + for d in &deps { + if l < 5 && UI_ONLY.contains(&d.as_str()) { + errors.push(format!("{name} (L{l}) depends on UI/OS crate `{d}`")); + } + if !d.starts_with("filmcraft-") { + continue; + } + let Some(dl) = layer_of(d) else { continue }; + let (a, b) = (short(&name), short(d)); + if l == 0 && !L0_FOUNDATION.contains(&a) && b != "bitstream" { + errors.push(format!("{a} (L0 codec/container) may depend only on bitstream, found {b}")); + } else if dl > l { + errors.push(format!("{a} (L{l}) depends upward on {b} (L{dl})")); + } else if dl == l && l > 0 && !SAME_LAYER.contains(&(a, b)) { + errors.push(format!("{a} → {b}: same-layer edge (L{l}) not in the allowed list")); + } + } + } + if errors.is_empty() { + println!("layers: ok"); + Ok(()) + } else { + Err(errors.join("\n")) + } +} + +fn run(cmd: &mut Command) -> Result<(), String> { + eprintln!("$ {cmd:?}"); + let st = cmd.status().map_err(|e| e.to_string())?; + if st.success() { Ok(()) } else { Err(format!("failed: {cmd:?}")) } +} + +fn wasm() -> Result<(), String> { + let md = metadata()?; + let mut cmd = Command::new(env!("CARGO")); + cmd.args(["check", "--target", "wasm32-unknown-unknown"]); + let mut n = 0; + for (name, _) in workspace_crates(&md) { + if layer_of(&name).is_some_and(|l| l <= 4) { + cmd.args(["-p", &name]); + n += 1; + } + } + if n == 0 { + return Ok(()); + } + run(&mut cmd)?; + println!("wasm: ok ({n} crates)"); + Ok(()) +} + +fn ci() -> Result<(), String> { + let cargo = env!("CARGO"); + run(Command::new(cargo).args(["fmt", "--check"]))?; + run(Command::new(cargo).args(["clippy", "--workspace", "--all-targets", "--release", "--", "-D", "warnings"]))?; + run(Command::new(cargo).args(["test", "--workspace", "--release"]))?; + layers()?; + wasm() +} + +fn main() -> ExitCode { + let task = std::env::args().nth(1).unwrap_or_default(); + let r = match task.as_str() { + "layers" => layers(), + "wasm" => wasm(), + "ci" => ci(), + _ => Err("usage: cargo xtask ".into()), + }; + match r { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("{e}"); + ExitCode::FAILURE + } + } +}