Merge pull request #948 from Themolx/solo-outside-active-range

Solo: exclude other layers even when the soloed layer is outside its time range
This commit is contained in:
BFlat
2026-10-16 18:10:06 +07:00
committed by GitHub
8 changed files with 31 additions and 9 deletions
+1 -1
View File
@@ -362,7 +362,7 @@ const MAX_COLLAPSED_DEPTH: usize = 8;
fn visible_layers<'a>(ctx: &EvalCtx<'a>, exclude: &'a [LayerId]) -> impl Iterator<Item = &'a Layer> {
let comp = ctx.comp;
let time = ctx.time;
let any_solo = comp.layers.iter().any(|l| l.switches.solo && l.source.is_av() && l.is_active_at(time));
let any_solo = comp.layers.iter().any(|l| l.switches.solo && l.source.is_av());
comp.layers.iter().filter(move |l| l.is_active_at(time) && l.switches.video && (!any_solo || l.switches.solo) && !exclude.contains(&l.id))
}
+1 -1
View File
@@ -2094,7 +2094,7 @@ impl<'a> Renderer<'a> {
/// applied).
pub fn visible_layers(&self, ctx: &EvalCtx<'a>) -> Vec<&'a Layer> {
let (comp, t) = (ctx.comp, ctx.time);
let any_solo = comp.layers.iter().any(|l| l.switches.solo && l.source.is_av() && l.is_active_at(t));
let any_solo = comp.layers.iter().any(|l| l.switches.solo && l.source.is_av());
comp.layers
.iter()
.rev()
+18
View File
@@ -953,3 +953,21 @@ fn shape_content_bounds_are_the_visual_edges() {
assert!(b.iter().zip([-half, -half, half, half]).all(|(a, e)| (a - e).abs() < 1e-6), "{stroke:?}: {b:?}");
}
}
/// Solo is not time-dependent (as in After Effects): while any layer is soloed, the others stay
/// excluded even when the playhead is outside the soloed layer's in-out range.
#[test]
fn solo_excludes_other_layers_outside_the_soloed_layers_range() {
let (mut p, cid, comp) = setup();
let a = solid(&mut p, &comp, [1.0, 0.0, 0.0], 200, 100);
let mut b = solid(&mut p, &comp, [0.0, 0.0, 1.0], 200, 100);
b.in_point = Tick::from_seconds_f64(1.0);
b.switches.solo = true;
let c = p.comp_mut(cid).unwrap();
c.layers.push(b);
c.layers.push(a);
let first = render_frame(&p, cid, Tick::from_seconds_f64(0.5), 1.0);
assert_eq!(first.get(100, 50), [0.0, 0.0, 0.0, 0.0], "neither A nor B before B's in point");
let second = render_frame(&p, cid, Tick::from_seconds_f64(1.5), 1.0);
assert_eq!(second.get(100, 50), [0.0, 0.0, 1.0, 1.0], "only B inside its range");
}
+1 -1
View File
@@ -873,7 +873,7 @@ fn collapsed_layer<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &Layer, item:
let saved = b.outer;
b.outer = saved * ctx.world_matrix(layer);
let t = nctx.time;
let any_solo = nctx.comp.layers.iter().any(|l| l.switches.solo && l.source.is_av() && l.is_active_at(t));
let any_solo = nctx.comp.layers.iter().any(|l| l.switches.solo && l.source.is_av());
for l in nctx.comp.layers.iter().rev() {
if !l.is_active_at(t)
|| l.is_light()
+1 -1
View File
@@ -497,7 +497,7 @@ fn collapsed_items<'a>(
let op = ctx.opacity(layer) as f32 * r.opacity_mul();
let Some((sub, nctx)) = r.collapse_into(ctx, layer, item, op) else { return vec![] };
let t = nctx.time;
let any_solo = nctx.comp.layers.iter().any(|l| l.switches.solo && l.source.is_av() && l.is_active_at(t));
let any_solo = nctx.comp.layers.iter().any(|l| l.switches.solo && l.source.is_av());
let nested: Vec<&'a Layer> = nctx
.comp
.layers
+3 -2
View File
@@ -2283,9 +2283,10 @@ fn pen_press(
}
/// Visible, unlocked layers eligible for viewer hit tests, in front-to-back order.
/// Solo scope matches the renderer: only active AV layers activate it, even when hidden or locked.
/// Solo scope matches the renderer: any soloed AV layer activates it at every time (as in After
/// Effects), even when hidden, locked or outside its in-out range.
pub(crate) fn selectable_layers(comp: &Comp, time: Tick) -> impl Iterator<Item = &Layer> {
let any_solo = comp.layers.iter().any(|l| l.switches.solo && l.source.is_av() && l.is_active_at(time));
let any_solo = comp.layers.iter().any(|l| l.switches.solo && l.source.is_av());
comp.layers
.iter()
.filter(move |l| l.is_active_at(time) && l.switches.video && !l.switches.locked && !l.is_camera() && !l.is_light() && (!any_solo || l.switches.solo))
+4 -2
View File
@@ -121,8 +121,10 @@ fn viewer_click_does_not_pick_through_hidden_or_locked_solo_layers() {
}
}
/// Solo is not time-dependent (as in After Effects): outside the soloed layer's in-out range the
/// viewer shows nothing, so neither the soloed layer nor the others can be picked.
#[test]
fn viewer_click_ignores_inactive_solo_layers() {
fn viewer_click_picks_nothing_while_the_solo_layer_is_inactive() {
let mut h = selection_harness();
let comp = h.state().session.active_comp().unwrap();
let (front, back) = (comp.layers[0].id, comp.layers[1].id);
@@ -133,7 +135,7 @@ fn viewer_click_ignores_inactive_solo_layers() {
h.run_steps(3);
let at = screen(&h, [320.0, 180.0]);
click(&mut h, at);
assert_eq!(h.state().session.state.selected_layers, vec![back]);
assert!(h.state().session.state.selected_layers.is_empty(), "{back:?} stays excluded by solo");
}
/// A selected layer under the pointer takes the press before the layers in front of it, as in
+2 -1
View File
@@ -117,7 +117,8 @@ The most important missing piece: it turns every other estimate here into a meas
- Regression evidence for [#66](https://github.com/storytold/effectcraft/issues/66): scripted
pointer tests in `ui_viewer` and `ui_text_edit`, checked headlessly on Linux, cover viewer clicks,
marquee selection and text picking when layers are hidden or excluded by solo. They also check
hidden/locked solo layers and solo layers outside their active time range.
hidden/locked solo layers, and that solo still excludes the other layers while the soloed
layer is outside its active time range (as in After Effects, so nothing is picked there).
- Regression evidence for [#67](https://github.com/storytold/effectcraft/issues/67): scripted
input tests in `ui_project_delete`, checked headlessly on Linux, cover Delete/Backspace on
Project items, multiple selection and undo. They check that an unrelated Timeline layer stays,