mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #869 from vangel76/fix-864-track-matte-mode
Track Matte: picking a mode without a matte layer takes the layer above (#864)
This commit is contained in:
commit
34ad8c7e9c
3 files changed
+91
-12
No files matched your search
@@ -135,6 +135,32 @@ fn track_matte_alpha() {
|
||||
assert!(img.get(150, 50)[3] < 0.01);
|
||||
}
|
||||
|
||||
/// Every matte kind with an opaque white 50×50 matte at the left of a 200×100 comp: the plain
|
||||
/// kinds keep the fill only inside the matte, the inverted kinds only outside it (#864).
|
||||
#[test]
|
||||
fn track_matte_kinds_keep_and_invert() {
|
||||
for (kind, inside) in [(MatteKind::Alpha, true), (MatteKind::AlphaInverted, false), (MatteKind::Luma, true), (MatteKind::LumaInverted, false)] {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut matte = solid(&mut p, &comp, [1.0, 1.0, 1.0], 50, 50);
|
||||
matte.props.prop_mut("transform/position").unwrap().value = Value::Vec3([50.0, 50.0, 0.0]);
|
||||
matte.switches.video = false;
|
||||
let mut fill = solid(&mut p, &comp, [0.0, 0.0, 1.0], 200, 100);
|
||||
fill.track_matte = Some(TrackMatte { layer: matte.id, kind });
|
||||
let c = p.comp_mut(cid).unwrap();
|
||||
c.layers.push(matte);
|
||||
c.layers.push(fill);
|
||||
let img = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||||
let (a_in, a_out) = (img.get(50, 50)[3], img.get(150, 50)[3]);
|
||||
if inside {
|
||||
assert!(a_in > 0.99 && a_out < 0.01, "{kind:?}: {a_in} inside, {a_out} outside");
|
||||
assert!(img.get(50, 50)[2] > 0.99, "{kind:?} keeps the fill's colour");
|
||||
} else {
|
||||
assert!(a_in < 0.01 && a_out > 0.99, "{kind:?}: {a_in} inside, {a_out} outside");
|
||||
assert!(img.get(150, 50)[2] > 0.99, "{kind:?} keeps the fill's colour");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_layer_draws_fill_and_stroke() {
|
||||
let (mut p, cid, comp) = setup();
|
||||
|
||||
@@ -1942,7 +1942,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
Some(m) => comp.layer(m.layer).map(|l| format!("{} {}", idx_of(l.id), l.name)).unwrap_or("None".into()),
|
||||
None => "No Track Matte".into(),
|
||||
};
|
||||
let tresp = widgets::dropdown(ui, mr, &label, &t, egui::Id::new(("trkmat", layer.id.0)));
|
||||
// The cell is too narrow for the mode as well: it is in the tooltip and ticked in the menu.
|
||||
let tip = layer.track_matte.map(|m| format!("{label} · {}", crate::i18n::tr(m.kind.label())));
|
||||
let tresp = widgets::dropdown_tip(ui, mr, &label, tip.as_deref(), &t, egui::Id::new(("trkmat", layer.id.0)));
|
||||
app.auto.add(&format!("timeline.layer.{}.trackMatte", layer.id.0), mr, "Track Matte");
|
||||
let pop = egui::Id::new(("trk-pop", layer.id.0));
|
||||
if tresp.clicked() {
|
||||
@@ -1962,17 +1964,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
opts.push(k.label().into());
|
||||
}
|
||||
}
|
||||
if let Some(i) = widgets::popup_menu(ui, pop, mr.left_bottom(), &opts, None) {
|
||||
if i == 0 {
|
||||
actions.push(("layer.setTrackMatte".into(), json!({"layer": layer.id.0, "matte": null})));
|
||||
} else if i >= 2 && i < 2 + candidates.len() {
|
||||
let kind = layer.track_matte.map(|m| m.kind).unwrap_or_default();
|
||||
actions
|
||||
.push(("layer.setTrackMatte".into(), json!({"layer": layer.id.0, "matte": candidates[i - 2].id.0, "kind": kind_name(kind)})));
|
||||
} else if let Some(k) = MatteKind::ALL.into_iter().find(|k| k.label() == opts[i])
|
||||
&& let Some(m) = layer.track_matte
|
||||
{
|
||||
actions.push(("layer.setTrackMatte".into(), json!({"layer": layer.id.0, "matte": m.layer.0, "kind": kind_name(k)})));
|
||||
let current = layer.track_matte.and_then(|m| opts.iter().position(|o| o == m.kind.label()));
|
||||
if let Some(i) = widgets::popup_menu(ui, pop, mr.left_bottom(), &opts, current) {
|
||||
let ids: Vec<LayerId> = candidates.iter().map(|l| l.id).collect();
|
||||
if let Some(a) = opts.get(i).and_then(|o| track_matte_pick(layer, &ids, i, o)) {
|
||||
actions.push(a);
|
||||
}
|
||||
}
|
||||
if layer.track_matte.is_some() {
|
||||
@@ -3412,6 +3408,21 @@ fn text_anim_popups(
|
||||
}
|
||||
}
|
||||
|
||||
/// The command for the Track Matte menu item `i` (`choice`) of `layer`, whose menu lists "No Track
|
||||
/// Matte", a separator, the matte `candidates`, a separator and the modes. A mode without a matte
|
||||
/// layer takes the layer above, like Layer ▸ Track Matte (#864: it used to do nothing).
|
||||
fn track_matte_pick(layer: &Layer, candidates: &[LayerId], i: usize, choice: &str) -> Option<(String, serde_json::Value)> {
|
||||
if i == 0 {
|
||||
return Some(("layer.setTrackMatte".into(), json!({"layer": layer.id.0, "matte": null})));
|
||||
}
|
||||
if let Some(m) = i.checked_sub(2).and_then(|j| candidates.get(j)) {
|
||||
let kind = layer.track_matte.map(|m| m.kind).unwrap_or_default();
|
||||
return Some(("layer.setTrackMatte".into(), json!({"layer": layer.id.0, "matte": m.0, "kind": kind_name(kind)})));
|
||||
}
|
||||
let k = MatteKind::ALL.into_iter().find(|k| k.label() == choice)?;
|
||||
Some(("layer.trackMatte".into(), json!({"layers": [layer.id.0], "op": kind_name(k)})))
|
||||
}
|
||||
|
||||
fn kind_name(k: MatteKind) -> &'static str {
|
||||
match k {
|
||||
MatteKind::Alpha => "alpha",
|
||||
@@ -3746,6 +3757,40 @@ mod tests {
|
||||
assert!(edge_scroll_speed_x(100.0, 500.0, 600.0) > edge_scroll_speed_x(100.0, 500.0, 520.0));
|
||||
}
|
||||
|
||||
/// #864: picking only a mode in a layer's Track Matte menu sets a matte (the layer above);
|
||||
/// it used to close the menu and change nothing.
|
||||
#[test]
|
||||
fn a_track_matte_mode_without_a_matte_takes_the_layer_above() {
|
||||
let mut s = effectcraft_engine::Session::default();
|
||||
s.execute("comp.new", json!({"name": "T", "width": 64, "height": 64, "duration": 1})).unwrap();
|
||||
for name in ["Chain Bottom", "Matte", "Chain Top"] {
|
||||
s.execute("layer.newSolid", json!({"name": name, "color": "#ffffff", "width": 64, "height": 64})).unwrap();
|
||||
}
|
||||
let find = |s: &effectcraft_engine::Session, n: &str| s.active_comp().unwrap().layers.iter().find(|l| l.name == n).unwrap().clone();
|
||||
let (bottom, matte, top) = (find(&s, "Chain Bottom"), find(&s, "Matte"), find(&s, "Chain Top"));
|
||||
let candidates = [top.id, matte.id];
|
||||
let pick = |s: &mut effectcraft_engine::Session, name: &str, i: usize, choice: &str| {
|
||||
let (id, params) = track_matte_pick(&find(s, name), &candidates, i, choice).expect("a menu item is an action");
|
||||
s.execute(&id, params)
|
||||
};
|
||||
pick(&mut s, "Chain Bottom", 5, "Alpha Inverted Matte").unwrap();
|
||||
let m = find(&s, "Chain Bottom").track_matte.expect("a matte");
|
||||
assert_eq!((m.layer, m.kind), (matte.id, MatteKind::AlphaInverted));
|
||||
assert!(!find(&s, "Matte").switches.video, "the matte layer is hidden");
|
||||
// A mode change keeps the matte layer; a matte change keeps the mode.
|
||||
pick(&mut s, "Chain Bottom", 6, "Luma Matte").unwrap();
|
||||
assert_eq!(find(&s, "Chain Bottom").track_matte.map(|m| (m.layer, m.kind)), Some((matte.id, MatteKind::Luma)));
|
||||
pick(&mut s, "Chain Bottom", 2, "1. Chain Top").unwrap();
|
||||
assert_eq!(find(&s, "Chain Bottom").track_matte.map(|m| (m.layer, m.kind)), Some((top.id, MatteKind::Luma)));
|
||||
assert!(s.undo());
|
||||
assert_eq!(find(&s, "Chain Bottom").track_matte.map(|m| m.layer), Some(matte.id), "one undo step per pick");
|
||||
// The top layer has nothing above it: an error for the status bar, not a silent no-op.
|
||||
assert!(pick(&mut s, "Chain Top", 5, "Alpha Matte").is_err());
|
||||
pick(&mut s, "Chain Bottom", 0, "No Track Matte").unwrap();
|
||||
assert!(find(&s, "Chain Bottom").track_matte.is_none());
|
||||
assert!(track_matte_pick(&bottom, &candidates, 1, "-").is_none(), "a separator does nothing");
|
||||
}
|
||||
|
||||
/// A comp with two solids; "Box" has an animated Position and a Gaussian Blur.
|
||||
fn app() -> EffectcraftApp {
|
||||
let mut s = effectcraft_engine::Session::default();
|
||||
|
||||
@@ -314,6 +314,11 @@ fn field(ui: &mut Ui, rect: Rect, text: &mut String, hint: &str, t: &Tokens, sea
|
||||
|
||||
/// Dropdown-looking control ("Full ▾").
|
||||
pub fn dropdown(ui: &mut Ui, rect: Rect, text: &str, t: &Tokens, id: egui::Id) -> Response {
|
||||
dropdown_tip(ui, rect, text, None, t, id)
|
||||
}
|
||||
|
||||
/// [`dropdown`] with a tooltip `tip` (shown always; it replaces the elided text's tooltip).
|
||||
pub fn dropdown_tip(ui: &mut Ui, rect: Rect, text: &str, tip: Option<&str>, t: &Tokens, id: egui::Id) -> Response {
|
||||
let resp = ui.interact(rect, id, Sense::click());
|
||||
ui.painter().rect_filled(rect, 3.0, if resp.hovered() { t.hover } else { t.field_bg });
|
||||
ui.painter().rect_stroke(rect, 3.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
@@ -335,6 +340,9 @@ pub fn dropdown(ui: &mut Ui, rect: Rect, text: &str, t: &Tokens, id: egui::Id) -
|
||||
};
|
||||
ui.painter().text(pos2(rect.min.x + 7.0, rect.center().y), Align2::LEFT_CENTER, shown, Tokens::ui(11.5), t.text);
|
||||
icons::paint(ui.painter(), Rect::from_center_size(pos2(rect.max.x - 9.0, rect.center().y), vec2(8.0, 8.0)), Icon::ChevronDown, t.text_dim);
|
||||
if let Some(tip) = tip {
|
||||
return resp.on_hover_text(tip.to_string());
|
||||
}
|
||||
if text.chars().count() > 0 && !fits(text) {
|
||||
return resp.on_hover_text(text.to_string());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user