mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Timeline: every shape Contents item has an eye switch, as in After Effects (#959)
Groups, paths, fills, strokes and operators in a shape layer's Contents show the Video eye in the A/V column (a group's Transform doesn't). Clicking it runs prop.setGroupEnabled, which the shape renderer already honours; layer-style eyes keep layer.style.toggle. Row building and search results share eye_of. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
03f1a1b221
commit
9424c9eeb1
@@ -795,12 +795,11 @@ fn display_name(app: &EffectcraftApp, l: &Layer) -> String {
|
||||
/// layer whose names contain `q` (lowercase), each with its enclosing groups. A matching group
|
||||
/// brings its whole contents.
|
||||
fn search_rows(l: &Layer, q: &str) -> Vec<Row> {
|
||||
/// The row for a group in search results: depth, its effect's enabled flag, and the eye
|
||||
/// switch for layer-style groups (not Blending Options).
|
||||
/// The row for a group in search results: depth, its effect's enabled flag, and its eye
|
||||
/// switch ([`eye_of`]).
|
||||
fn group_row(l: &Layer, g: &PropGroup, depth: usize, parent: &PropGroup) -> Row {
|
||||
let fx = matches!(g.kind, GroupKind::Effect { .. }).then_some(g.enabled);
|
||||
let eye =
|
||||
(parent.match_id == effectcraft_engine::project::styles::GROUP && g.match_id != effectcraft_engine::project::styles::BLENDING).then_some(g.enabled);
|
||||
let eye = eye_of(parent, g);
|
||||
Row { layer: l.id, depth, kind: RowKind::Group { uid: g.uid, name: g.name.clone(), open: true, has_children: !g.children.is_empty(), fx, eye } }
|
||||
}
|
||||
/// Every visible child of `g` at `depth`, groups recursed into whole — a matching group
|
||||
@@ -1235,6 +1234,14 @@ fn collect_props(g: &PropGroup, out: &mut Vec<u64>, f: &dyn Fn(&Property) -> boo
|
||||
/// in `open`, text animators list their selectors and properties directly (as in AE), and a
|
||||
/// shape group's inner Contents (AE's elided `ADBE Vectors Group`) is skipped so its items
|
||||
/// list directly under the group.
|
||||
/// The eye switch of group `g` inside `parent`, as in After Effects: layer styles (not Blending
|
||||
/// Options) and every item in a shape layer's Contents (#959), which the renderer skips when off.
|
||||
fn eye_of(parent: &PropGroup, g: &PropGroup) -> Option<bool> {
|
||||
let style = parent.match_id == effectcraft_engine::project::styles::GROUP && g.match_id != effectcraft_engine::project::styles::BLENDING;
|
||||
let shape_item = parent.match_id == "contents" && g.match_id != "transform";
|
||||
(style || shape_item).then_some(g.enabled)
|
||||
}
|
||||
|
||||
fn push_group(rows: &mut Vec<Row>, l: &Layer, g: &PropGroup, depth: usize, open: &std::collections::BTreeSet<u64>) {
|
||||
for c in &g.children {
|
||||
match c {
|
||||
@@ -1250,9 +1257,7 @@ fn push_group(rows: &mut Vec<Row>, l: &Layer, g: &PropGroup, depth: usize, open:
|
||||
Node::Group(sg) => {
|
||||
let o = open.contains(&sg.uid);
|
||||
let fx = matches!(sg.kind, GroupKind::Effect { .. }).then_some(sg.enabled);
|
||||
// Layer style groups have eye switches (not Blending Options).
|
||||
let eye = (g.match_id == effectcraft_engine::project::styles::GROUP && sg.match_id != effectcraft_engine::project::styles::BLENDING)
|
||||
.then_some(sg.enabled);
|
||||
let eye = eye_of(g, sg);
|
||||
rows.push(Row {
|
||||
layer: l.id,
|
||||
depth,
|
||||
@@ -3388,11 +3393,16 @@ fn group_switches(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc: &
|
||||
app.auto.add(&format!("timeline.group.{uid}.fx"), fr_, name);
|
||||
}
|
||||
if let Some(en) = eye {
|
||||
// Layer style eye switch in the A/V column, like AE.
|
||||
// A layer style's or shape item's eye switch in the A/V column, like AE.
|
||||
let er = Rect::from_center_size(pos2(cw.av + 10.0, cy), vec2(15.0, 15.0));
|
||||
lp.rect_stroke(er.shrink(1.5), 2.0, Stroke::new(1.0, t.separator), StrokeKind::Inside);
|
||||
if widgets::icon_toggle(ui, er, Icon::Eye, *en, &t, egui::Id::unique(("geye", uid)), Sense::click()).clicked() {
|
||||
actions.push(("layer.style.toggle".into(), json!({"layer": layer.id.0, "style": uid})));
|
||||
let style = layer.props.sub(effectcraft_engine::project::styles::GROUP).is_some_and(|s| s.find_group(*uid).is_some());
|
||||
actions.push(if style {
|
||||
("layer.style.toggle".into(), json!({"layer": layer.id.0, "style": uid}))
|
||||
} else {
|
||||
("prop.setGroupEnabled".into(), json!({"layer": layer.id.0, "prop": uid, "value": !*en}))
|
||||
});
|
||||
}
|
||||
app.auto.add(&format!("timeline.group.{uid}.eye"), er, name);
|
||||
}
|
||||
|
||||
@@ -237,6 +237,33 @@ fn contents_names(h: &Harness<'_, EffectcraftApp>, l: u64) -> Vec<String> {
|
||||
h.state().session.active_comp().unwrap().layer(LayerId(l)).unwrap().props.sub("contents").unwrap().groups().map(|g| g.name.clone()).collect()
|
||||
}
|
||||
|
||||
/// #959: every item in a shape layer's Contents has an eye switch, as in After Effects; it turns
|
||||
/// the item off (the renderer skips it), undoably. The group's Transform has none.
|
||||
#[test]
|
||||
fn shape_contents_items_have_eye_switches() {
|
||||
let (mut h, a, _, rect) = two_shape_layers();
|
||||
let enabled = |h: &Harness<'_, EffectcraftApp>| {
|
||||
h.state().session.active_comp().unwrap().layer(LayerId(a)).unwrap().props.sub("contents").unwrap().find_group(rect).unwrap().enabled
|
||||
};
|
||||
let eye = format!("timeline.group.{rect}.eye");
|
||||
assert!(enabled(&h));
|
||||
click(&mut h, &eye);
|
||||
assert!(!enabled(&h), "the eye turns Rectangle 1 off");
|
||||
click(&mut h, &eye);
|
||||
assert!(enabled(&h), "and on again");
|
||||
click(&mut h, &eye);
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(enabled(&h), "one undo step");
|
||||
// Inside the group: its path and fill have eyes, its Transform doesn't.
|
||||
h.state_mut().ui.timeline.open_groups.insert(rect);
|
||||
h.run_steps(3);
|
||||
let group = h.state().session.active_comp().unwrap().layer(LayerId(a)).unwrap().props.find_group(rect).unwrap().clone();
|
||||
let transform = group.sub("transform").expect("a shape group has a Transform").uid;
|
||||
assert!(h.state().auto.find(&format!("timeline.group.{transform}.eye")).is_none(), "no eye on Transform");
|
||||
let items = group.sub("contents").expect("the group's items").groups().map(|g| g.uid).collect::<Vec<_>>();
|
||||
assert!(!items.is_empty() && items.iter().all(|u| h.state().auto.find(&format!("timeline.group.{u}.eye")).is_some()), "every item has an eye");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_and_ctrl_v_copy_a_shape_group_into_another_shape_layer() {
|
||||
let (mut h, a, b, rect) = two_shape_layers();
|
||||
|
||||
Reference in New Issue
Block a user