diff --git a/apps/effectcraft-cli/src/main.rs b/apps/effectcraft-cli/src/main.rs index 6e5b8d6cc..073153c09 100644 --- a/apps/effectcraft-cli/src/main.rs +++ b/apps/effectcraft-cli/src/main.rs @@ -1189,7 +1189,7 @@ fn adv3d_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option Result { let ids: Vec = proj.items.keys().copied().filter(|c| *c != comp).collect(); for cid in ids { if let Some(c) = proj.comp_mut(cid) { - for l in c.layers.iter_mut().filter(|l| is_user(l)) { + for l in c.layers.iter_mut_where(|l| is_user(l)) { l.source = LayerSource::Comp { item: comp }; n += 1; } diff --git a/crates/engine/src/cmd/comp.rs b/crates/engine/src/cmd/comp.rs index 6648a7426..8bc0931f8 100644 --- a/crates/engine/src/cmd/comp.rs +++ b/crates/engine/src/cmd/comp.rs @@ -35,7 +35,7 @@ fn apply_settings(c: &mut Comp, p: &Value) { let dx = (c.width as f64 - ow as f64) * (a % 3) as f64 / 2.0; let dy = (c.height as f64 - oh as f64) * (a / 3) as f64 / 2.0; if dx != 0.0 || dy != 0.0 { - for l in c.layers.iter_mut().filter(|l| l.parent.is_none()) { + for l in c.layers.iter_mut_where(|l| l.parent.is_none()) { if let Some(tr) = l.transform_mut() { super::layer::offset_position(tr, dx, dy); } diff --git a/crates/engine/src/cmd/comp_more.rs b/crates/engine/src/cmd/comp_more.rs index d6de320ec..3982bc2d9 100644 --- a/crates/engine/src/cmd/comp_more.rs +++ b/crates/engine/src/cmd/comp_more.rs @@ -25,7 +25,7 @@ fn crop(s: &mut Session, cid: effectcraft_project::ItemId, r: [f64; 4], label: & let (old_w, old_h) = (c.width, c.height); c.width = w; c.height = h; - for l in c.layers.iter_mut().filter(|l| l.parent.is_none()) { + for l in c.layers.iter_mut_where(|l| l.parent.is_none()) { if let Some(tr) = l.transform_mut() { super::layer::offset_position(tr, -x, -y); } diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index d0dbabb5b..f10a3706a 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -90,7 +90,7 @@ fn deselect_all(s: &mut Session, _: &Value) -> Result { fn relink_copies(comp: &mut Comp, map: &[(LayerId, LayerId)]) { let present: Vec = comp.layers.iter().map(|l| l.id).collect(); let link = |id: LayerId| map.iter().find(|(o, _)| *o == id).map(|(_, c)| *c).or(present.contains(&id).then_some(id)); - for l in comp.layers.iter_mut().filter(|l| map.iter().any(|(_, c)| *c == l.id)) { + for l in comp.layers.iter_mut_where(|l| map.iter().any(|(_, c)| *c == l.id)) { l.parent = l.parent.and_then(link); l.track_matte = l.track_matte.and_then(|m| link(m.layer).map(|layer| TrackMatte { layer, ..m })); } @@ -675,7 +675,7 @@ fn label(s: &mut Session, p: &Value) -> Result { } } if let Some(comp) = proj.comp_mut(cid) { - for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in comp.layers.iter_mut_where(|l| ids.contains(&l.id)) { l.label = lab; } } diff --git a/crates/engine/src/cmd/layer/parent.rs b/crates/engine/src/cmd/layer/parent.rs index fbaec14e4..f2b68ba7d 100644 --- a/crates/engine/src/cmd/layer/parent.rs +++ b/crates/engine/src/cmd/layer/parent.rs @@ -323,7 +323,7 @@ pub(super) fn set_parent(s: &mut Session, p: &Value) -> Result { let fixes = if compensate { parent_fixes(s, cid, &ids, parent) } else { vec![] }; s.edit("Parent", None, |proj, _| { let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; - for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in comp.layers.iter_mut_where(|l| ids.contains(&l.id)) { l.parent = parent; if let Some(f) = fixes.iter().find(|f| f.layer == l.id) { f.apply(l); diff --git a/crates/engine/src/cmd/layer/switches.rs b/crates/engine/src/cmd/layer/switches.rs index 38689afbb..6e2a41892 100644 --- a/crates/engine/src/cmd/layer/switches.rs +++ b/crates/engine/src/cmd/layer/switches.rs @@ -19,7 +19,7 @@ pub(super) fn set_switch(s: &mut Session, p: &Value) -> Result { let first = comp.layers.iter().find(|l| ids.contains(&l.id)).map(|l| switch_value(l, &name)).unwrap_or(Some(false)); let Some(first) = first else { return Err(bad("layer.setSwitch", format!("unknown switch `{name}`"))) }; let target = v.unwrap_or(!first); - for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in comp.layers.iter_mut_where(|l| ids.contains(&l.id)) { set_switch_value(l, &name, target); last = target; } @@ -55,7 +55,7 @@ fn set_switch_others(s: &mut Session, cid: ItemId, ids: &[LayerId], name: &str, alone &= on == (ids.contains(&l.id) == active); } let others = value.unwrap_or(alone == active); - for l in comp.layers.iter_mut().filter(|l| applies(l)) { + for l in comp.layers.iter_mut_where(|l| applies(l)) { let v = if ids.contains(&l.id) { active } else { others }; set_switch_value(l, name, v); } @@ -152,7 +152,7 @@ pub(super) fn set_comment(s: &mut Session, p: &Value) -> Result { let text = str_p(p, "comment").ok_or_else(|| bad("layer.setComment", "missing `comment`"))?.to_string(); s.edit("Layer Comment", None, |proj, _| { let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; - for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in comp.layers.iter_mut_where(|l| ids.contains(&l.id)) { l.comment = text.clone(); } Ok(()) @@ -169,7 +169,7 @@ pub(super) fn blend(s: &mut Session, p: &Value) -> Result { } s.edit("Blending Mode", None, |proj, _| { let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; - for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in comp.layers.iter_mut_where(|l| ids.contains(&l.id)) { l.blend_mode = match mode { Some(m) => m, None if step > 0 => l.blend_mode.next(), diff --git a/crates/engine/src/cmd/layer/timing.rs b/crates/engine/src/cmd/layer/timing.rs index 711eefee2..38ecf3a61 100644 --- a/crates/engine/src/cmd/layer/timing.rs +++ b/crates/engine/src/cmd/layer/timing.rs @@ -18,7 +18,7 @@ pub(super) fn timing(s: &mut Session, p: &Value) -> Result { let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; let fd = comp.frame_duration(); let comp_dur = comp.duration; - for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in comp.layers.iter_mut_where(|l| ids.contains(&l.id)) { let shift = |l: &mut Layer, d: Tick| { l.start_time += d; l.in_point += d; @@ -159,14 +159,14 @@ pub(super) fn arrange(s: &mut Session, p: &Value) -> Result { for i in 1..n { if picked(&comp.layers[i]) && !picked(&comp.layers[i - 1]) { let run_end = (i..n).find(|j| !picked(&comp.layers[*j])).unwrap_or(n); - comp.layers[i - 1..run_end].rotate_left(1); + comp.layers.rotate_range_left(i - 1..run_end, 1); } } } else { for i in (0..n.saturating_sub(1)).rev() { if picked(&comp.layers[i]) && !picked(&comp.layers[i + 1]) { let run_start = (0..=i).rev().find(|j| !picked(&comp.layers[*j])).map_or(0, |j| j + 1); - comp.layers[run_start..=i + 1].rotate_right(1); + comp.layers.rotate_range_right(run_start..=i + 1, 1); } } } diff --git a/crates/engine/src/cmd/layer_menu.rs b/crates/engine/src/cmd/layer_menu.rs index 33d17bb4f..be11e6b74 100644 --- a/crates/engine/src/cmd/layer_menu.rs +++ b/crates/engine/src/cmd/layer_menu.rs @@ -61,7 +61,7 @@ fn quality(s: &mut Session, p: &Value) -> Result { q => return Err(bad("layer.quality", format!("quality must be best|draft|wireframe, not `{q}`"))), }; s.edit("Layer Quality", None, |proj, _| { - for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut_where(|l| ids.contains(&l.id)) { l.switches.quality = q; } Ok(()) @@ -84,7 +84,7 @@ fn sampling(s: &mut Session, p: &Value) -> Result { q => q, }; s.edit("Layer Sampling", None, |proj, _| { - for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut_where(|l| ids.contains(&l.id)) { l.switches.sampling = q; } Ok(()) @@ -101,7 +101,7 @@ fn frame_blending(s: &mut Session, p: &Value) -> Result { m => return Err(bad("layer.frameBlending", format!("mode must be off|frameMix|pixelMotion, not `{m}`"))), }; s.edit("Frame Blending", None, |proj, _| { - for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut_where(|l| ids.contains(&l.id)) { l.switches.frame_blend = m; } Ok(()) @@ -116,7 +116,7 @@ fn hide_other_video(s: &mut Session, p: &Value) -> Result { s.edit("Hide Other Video", None, |proj, _| { // Every layer has a Video switch (nulls, cameras and lights included, as the Timeline's // eye cell shows): the selection stays shown. - for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut().filter(|l| !ids.contains(&l.id)) { + for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut_where(|l| !ids.contains(&l.id)) { l.switches.video = false; } Ok(()) @@ -130,7 +130,7 @@ fn show_all_video(s: &mut Session, p: &Value) -> Result { let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; // Track mattes stay hidden (their video switch is off by design). let mattes: Vec = c.layers.iter().filter_map(|l| l.track_matte.map(|m| m.layer)).collect(); - for l in c.layers.iter_mut().filter(|l| !mattes.contains(&l.id)) { + for l in c.layers.iter_mut_where(|l| !mattes.contains(&l.id)) { l.switches.video = true; } Ok(()) @@ -154,7 +154,7 @@ fn expressions(s: &mut Session, p: &Value) -> Result { let on = b_p(p, "enabled").unwrap_or(true); let n = s.edit(if on { "Enable Expressions" } else { "Disable Expressions" }, None, |proj, _| { let mut n = 0; - for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut_where(|l| ids.contains(&l.id)) { l.props.walk_mut(&mut |pr| { if let Some(e) = &mut pr.expr { e.enabled = on; @@ -586,7 +586,7 @@ fn markers_lock(s: &mut Session, p: &Value) -> Result { let r = s.edit("Lock Markers", None, |proj, _| { let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; let target = v.unwrap_or_else(|| !c.layers.iter().find(|l| ids.contains(&l.id)).is_some_and(|l| l.markers_locked)); - for l in c.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in c.layers.iter_mut_where(|l| ids.contains(&l.id)) { l.markers_locked = target; } Ok(target) @@ -597,7 +597,7 @@ fn markers_lock(s: &mut Session, p: &Value) -> Result { fn delete_all_markers(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; s.edit("Delete All Markers", None, |proj, _| { - for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut().filter(|l| ids.contains(&l.id) && !l.markers_locked) { + for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut_where(|l| ids.contains(&l.id) && !l.markers_locked) { l.markers.clear(); } Ok(()) diff --git a/crates/engine/src/cmd/layer_time.rs b/crates/engine/src/cmd/layer_time.rs index d98bfdea7..370745e4e 100644 --- a/crates/engine/src/cmd/layer_time.rs +++ b/crates/engine/src/cmd/layer_time.rs @@ -34,7 +34,7 @@ fn time_stretch(s: &mut Session, p: &Value) -> Result { let fd = comp_rate.frame_duration(); s.edit(if reverse { "Time-Reverse Layer" } else { "Time Stretch" }, None, |proj, _| { let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; - for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in comp.layers.iter_mut_where(|l| ids.contains(&l.id)) { let old = l.stretch; if reverse { // Keep the in/out points; play the visible span backwards. @@ -109,7 +109,7 @@ fn enable_time_remap(s: &mut Session, p: &Value) -> Result { let mut next = proj.next_id; let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; let mut on = false; - for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id) && remappable(l)) { + for l in comp.layers.iter_mut_where(|l| ids.contains(&l.id) && remappable(l)) { let has = l.props.get("timeRemap").is_some(); let target = want.unwrap_or(!has); on |= target; diff --git a/crates/engine/src/cmd/rig3d.rs b/crates/engine/src/cmd/rig3d.rs index f034fa641..8b64aa1e9 100644 --- a/crates/engine/src/cmd/rig3d.rs +++ b/crates/engine/src/cmd/rig3d.rs @@ -268,7 +268,7 @@ fn stereo_rig(s: &mut Session, p: &Value) -> Result { set_expr(&mut cam, "cameraOptions/zoom", ze); cam.auto_orient = AutoOrient::TowardsPointOfInterest; let comp = proj.comp_mut(eid).ok_or(EngineError::NoComp)?; - comp.layers = vec![cam, nest]; + comp.layers = vec![cam, nest].into(); eye_comps.push(eid); } // The output comp: both eyes through 3D Glasses. @@ -291,7 +291,7 @@ fn stereo_rig(s: &mut Session, p: &Value) -> Result { fxg.children.push(g.into()); } let (lid, rid) = (left.id, right.id); - proj.comp_mut(oid).ok_or(EngineError::NoComp)?.layers = vec![left, right]; + proj.comp_mut(oid).ok_or(EngineError::NoComp)?.layers = vec![left, right].into(); st.project_selection = vec![oid]; Ok(json!({ "controls": ctl_id.0, diff --git a/crates/engine/src/cmd/three_d.rs b/crates/engine/src/cmd/three_d.rs index e143690d3..846b13536 100644 --- a/crates/engine/src/cmd/three_d.rs +++ b/crates/engine/src/cmd/three_d.rs @@ -532,7 +532,7 @@ fn auto_orient(s: &mut Session, p: &Value) -> Result { }; s.edit("Auto-Orient", None, |proj, _| { let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; - for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) { + for l in comp.layers.iter_mut_where(|l| ids.contains(&l.id)) { let rig = l.is_camera() || l.is_light(); let ok = match mode { AutoOrient::Off | AutoOrient::AlongPath => true, diff --git a/crates/engine/src/home/demo.rs b/crates/engine/src/home/demo.rs index 656bd7708..7503178fa 100644 --- a/crates/engine/src/home/demo.rs +++ b/crates/engine/src/home/demo.rs @@ -121,7 +121,7 @@ fn lower_third(p: &mut Project) -> Comp { [440.0, 914.0], ); anim(&mut title, "transform/opacity", keys(&[(0.4, Value::Scalar(0.0)), (0.9, Value::Scalar(100.0))])); - c.layers = vec![title, bar]; + c.layers = vec![title, bar].into(); c } @@ -270,7 +270,7 @@ fn showcase_3d(p: &mut Project, solids: ItemId) -> Comp { ); anim(&mut caption, "transform/opacity", keys(&[(0.3, Value::Scalar(0.0)), (1.2, Value::Scalar(100.0))])); - c.layers = vec![caption, cam, key, fill, title, left, right, x1, x2, floor]; + c.layers = vec![caption, cam, key, fill, title, left, right, x1, x2, floor].into(); c } @@ -463,7 +463,7 @@ pub fn demo_project() -> Project { lower.out_point = t(9.5); // Stack: top first. - comp.layers = vec![lower, title, sub, line, burst, rings, null, bg]; + comp.layers = vec![lower, title, sub, line, burst, rings, null, bg].into(); for l in &mut comp.layers { if l.name == "Controller" { l.switches.video = false; diff --git a/crates/engine/src/render_queue.rs b/crates/engine/src/render_queue.rs index 27e9a7dce..05e4eef23 100644 --- a/crates/engine/src/render_queue.rs +++ b/crates/engine/src/render_queue.rs @@ -676,7 +676,7 @@ impl Session { continue; } if let Some(c) = proj.comp_mut(cid) { - for l in c.layers.iter_mut().filter(|l| matches!(l.source, LayerSource::Comp { item } if item == comp)) { + for l in c.layers.iter_mut_where(|l| matches!(l.source, LayerSource::Comp { item } if item == comp)) { l.source = LayerSource::Footage { item: fid }; } } diff --git a/crates/engine/src/system/perf.rs b/crates/engine/src/system/perf.rs index ab1723d9f..929726c95 100644 --- a/crates/engine/src/system/perf.rs +++ b/crates/engine/src/system/perf.rs @@ -180,7 +180,7 @@ pub fn large_project(spec: &LargeSpec) -> Project { *n += 1; layers.push(l); } - comp.layers = layers; + comp.layers = layers.into(); }; let others = spec.comps.saturating_sub(1); let depth = spec.nest_depth.min(others); @@ -215,7 +215,7 @@ pub fn large_project(spec: &LargeSpec) -> Project { n += 1; layers.push(l); } - main.layers = layers; + main.layers = layers.into(); p.add_item("Main", crate::color::Label::None, None, ItemKind::Comp(Arc::new(main))); // `Main` must be the first comp `replace_project` opens: give it the smallest id. let main_id = p.items.values().find(|i| i.name == "Main").map(|i| i.id).expect("main"); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 579d3d30d..8601a46a1 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -17,6 +17,7 @@ mod history; mod import_project; mod key_metadata_order; mod key_time; +mod shared_layers; mod keylight; mod limits; mod lottie; diff --git a/crates/engine/src/tests/effects.rs b/crates/engine/src/tests/effects.rs index 1ea57f4cf..966c35fae 100644 --- a/crates/engine/src/tests/effects.rs +++ b/crates/engine/src/tests/effects.rs @@ -550,7 +550,7 @@ fn opening_an_old_project_upgrades_effect_instances() { effectcraft_project::ItemKind::Comp(c) => c, _ => panic!("not a composition"), }); - let l = comp.layers.iter_mut().find(|l| l.id == LayerId(a)).unwrap(); + let l = comp.layers.find_mut(|l| l.id == LayerId(a)).unwrap(); for n in &mut l.props.sub_mut("effects").unwrap().children { let Node::Group(g) = n else { continue }; if g.match_id == "ec.blur.gaussian" { diff --git a/crates/engine/src/tests/session.rs b/crates/engine/src/tests/session.rs index b3f0602b3..756333e34 100644 --- a/crates/engine/src/tests/session.rs +++ b/crates/engine/src/tests/session.rs @@ -86,7 +86,7 @@ fn imported_files_go_into_the_comp_where_they_were_dropped() { s.execute("file.import", params).unwrap(); let comp = s.active_comp().unwrap(); assert_eq!(comp.layers.iter().map(|l| l.name.as_str()).collect::>(), ["a.png", "b.png", "Bg"]); - for l in &comp.layers[..2] { + for l in comp.layers.iter().take(2) { assert_eq!(l.props.prop("transform/position").map(|p| p.value.clone()), Some(effectcraft_keyframe::Value::Vec3([40.0, 50.0, 0.0]))); assert_eq!(l.in_point, effectcraft_time::Tick::from_seconds_f64(1.0)); } diff --git a/crates/engine/src/tests/shared_layers.rs b/crates/engine/src/tests/shared_layers.rs new file mode 100644 index 000000000..275fdd624 --- /dev/null +++ b/crates/engine/src/tests/shared_layers.rs @@ -0,0 +1,42 @@ +//! An edit copies only the layer it changes: the others stay shared with the undo state, so an +//! edit's cost doesn't grow with the size of the comp (scripts setting many keys went quadratic). + +use std::sync::Arc; + +use serde_json::json; + +use crate::Session; + +#[test] +fn a_key_on_one_layer_leaves_the_other_layers_shared_with_undo() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Big", "duration": 10, "frameRate": 30})).unwrap(); + let ids: Vec = (0..20).map(|i| s.execute("layer.newNull", json!({"name": format!("N{i}")})).unwrap()["layer"].as_u64().unwrap()).collect(); + let cid = s.active_comp_id().unwrap(); + let before = s.project.clone(); + s.execute("prop.addKey", json!({"layer": ids[7], "path": "transform/position", "time": 1.0, "value": [5, 5]})).unwrap(); + let (old, new) = (before.comp(cid).unwrap(), s.project.comp(cid).unwrap()); + assert_eq!(old.layers.len(), new.layers.len()); + let edited = new.layers.iter().position(|l| l.id.0 == ids[7]).unwrap(); + for i in 0..new.layers.len() { + assert_eq!(new.layers.same_at(&old.layers, i), i != edited, "layer {i} shared: {}", new.layers.same_at(&old.layers, i)); + } + // Undo brings back the very same state, and the edited layer is the only one that differed. + s.execute("edit.undo", json!({})).unwrap(); + assert!(Arc::ptr_eq(&s.project, &before)); +} + +#[test] +fn layers_save_and_load_as_a_plain_list() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Round trip", "duration": 5, "frameRate": 25})).unwrap(); + for i in 0..3 { + s.execute("layer.newNull", json!({"name": format!("N{i}")})).unwrap(); + } + let cid = s.active_comp_id().unwrap(); + let comp = s.project.comp(cid).unwrap(); + let text = serde_json::to_value(&comp.layers).unwrap(); + assert_eq!(text.as_array().map(Vec::len), Some(3), "a JSON array of layers, as before"); + let back: effectcraft_project::Layers = serde_json::from_value(text).unwrap(); + assert_eq!(&back, &comp.layers); +} diff --git a/crates/export/tests/it/movie_bytes.rs b/crates/export/tests/it/movie_bytes.rs index 5f1f8eb6b..fd54c1ac4 100644 --- a/crates/export/tests/it/movie_bytes.rs +++ b/crates/export/tests/it/movie_bytes.rs @@ -61,7 +61,7 @@ fn project() -> (Project, ItemId) { } box_.blend_mode = BlendMode::Screen; - comp.layers = vec![box_, ring, solid]; + comp.layers = vec![box_, ring, solid].into(); let cid = p.add_item("Comp 1", Label::Sandstone, None, ItemKind::Comp(comp.into())); (p, cid) } diff --git a/crates/expr/src/tests.rs b/crates/expr/src/tests.rs index 7d2790255..8b12a6437 100644 --- a/crates/expr/src/tests.rs +++ b/crates/expr/src/tests.rs @@ -76,7 +76,7 @@ fn fx() -> Fx { let ids = (a.id, b.id, title.id, shape.id); let c = p.comp_mut(cid).unwrap(); - c.layers = vec![a, b, title, shape]; + c.layers = vec![a, b, title, shape].into(); c.markers.push(Marker { time: secs(1.5), comment: "comp marker".into(), ..Default::default() }); let other = Comp::new(64, 32, FrameRate::FPS_24, Tick::from_seconds_f64(3.0)); diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index fa318ef79..5b9c4a99d 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -498,7 +498,7 @@ fn layer_styles_collapsed_precomps_and_cpu_fallbacks() { b.blend_mode = BlendMode::Difference; (a, b) }; - s.p.comp_mut(inner_id).unwrap().layers = vec![b, a]; + s.p.comp_mut(inner_id).unwrap().layers = vec![b, a].into(); let bg = s.footage(97, 61); s.push(bg); let mut pre = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None); diff --git a/crates/gpu/src/tests/adjust.rs b/crates/gpu/src/tests/adjust.rs index 5635e08e1..39438d43e 100644 --- a/crates/gpu/src/tests/adjust.rs +++ b/crates/gpu/src/tests/adjust.rs @@ -83,7 +83,7 @@ fn track_mattes_limit_gpu_adjustments_like_the_cpu() { set(&mut matte, "transform/rotation", Value::Scalar(10.0)); let mid = s.push(matte); let c = s.p.comp_mut(s.cid).unwrap(); - let adj = c.layers.iter_mut().find(|l| l.switches.adjustment).unwrap(); + let adj = c.layers.find_mut(|l| l.switches.adjustment).unwrap(); adj.track_matte = Some(TrackMatte { layer: mid, kind }); check(&format!("matted adjustment {kind:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.0); } diff --git a/crates/gpu/src/tests/adv3d.rs b/crates/gpu/src/tests/adv3d.rs index 21422aa6b..87e32426d 100644 --- a/crates/gpu/src/tests/adv3d.rs +++ b/crates/gpu/src/tests/adv3d.rs @@ -181,7 +181,7 @@ fn gpu_matches_cpu_wide_penumbrae() { set(&mut comp.layers[0], "lightOptions/shadowDiffusion", Value::Scalar(30.0)); let s = scene(&p, cid); let (cpu, gt) = (adv::raster::render(&s), g.raster_3d(&s).expect("gpu raster")); - for l in &mut p.comp_mut(cid).unwrap().layers[..2] { + for l in p.comp_mut(cid).unwrap().layers.iter_mut().take(2) { set(l, "lightOptions/castsShadows", Value::Bool(false)); } let unshadowed = adv::raster::render(&scene(&p, cid)); @@ -251,7 +251,7 @@ pub(crate) fn dof_mb_project() -> (Project, ItemId) { c.enable_motion_blur = true; c.motion_blur_samples = 6; // The sphere moves 40 px per frame. - let sphere = c.layers.iter_mut().find(|l| matches!(l.source, LayerSource::Primitive { kind: PrimitiveKind::Sphere })).unwrap(); + let sphere = c.layers.find_mut(|l| matches!(l.source, LayerSource::Primitive { kind: PrimitiveKind::Sphere })).unwrap(); sphere.switches.motion_blur = true; sphere.props.prop_mut("transform/position").unwrap().keys = vec![ effectcraft_keyframe::Keyframe::new(Tick::ZERO, Value::Vec3([80.0, 95.0, 0.0])), @@ -411,7 +411,7 @@ fn gpu_compositor_draws_advanced_3d_runs_on_the_device() { eprintln!("walk: {:.3} % beyond 1/255 (max {max:.4})", share * 100.0); assert!(share <= 0.01, "{:.3} % of pixels differ", share * 100.0); // A blend mode in the run takes the 2D compositing path (`SplitRun`, also on the GPU). - p.comp_mut(cid).unwrap().layers.iter_mut().find(|l| l.switches.three_d && !l.is_light() && l.source.is_av()).unwrap().blend_mode = + p.comp_mut(cid).unwrap().layers.find_mut(|l| l.switches.three_d && !l.is_light() && l.source.is_av()).unwrap().blend_mode = effectcraft_color::BlendMode::Screen; let (ctx, run) = run_of(&p, cid); assert!(Renderer::new(&p, &NoFootage, RenderOpts::default()).prepare_adv_run(&ctx, &run, (240, 160)).is_none()); @@ -532,12 +532,12 @@ fn gpu_compositor_draws_split_advanced_3d_runs_and_skies() { }); } let c = p.comp_mut(cid).unwrap(); - let env = c.layers.iter_mut().find(|l| l.environment).unwrap(); + let env = c.layers.find_mut(|l| l.environment).unwrap(); env.props.sub_mut("effects").unwrap().children.push(ramp.into()); let ids: Vec<_> = c.layers.iter().filter(|l| l.switches.three_d && !l.is_light() && !l.environment && l.source.is_av()).map(|l| l.id).collect(); let top = c.layers[0].id; fn by_id(c: &mut Comp, id: effectcraft_project::LayerId) -> &mut effectcraft_project::Layer { - c.layers.iter_mut().find(|l| l.id == id).unwrap() + c.layers.find_mut(|l| l.id == id).unwrap() } match case { 0 => by_id(c, ids[1]).blend_mode = effectcraft_color::BlendMode::Screen, @@ -555,7 +555,7 @@ fn gpu_compositor_draws_split_advanced_3d_runs_and_skies() { } _ => { // An environment background behind the run, and the 2D track matte. - c.layers.iter_mut().find(|l| l.environment).unwrap().environment_background = true; + c.layers.find_mut(|l| l.environment).unwrap().environment_background = true; by_id(c, ids[0]).track_matte = Some(effectcraft_project::TrackMatte { layer: top, kind: effectcraft_project::MatteKind::Alpha }); } } diff --git a/crates/gpu/src/tests/fx_noise.rs b/crates/gpu/src/tests/fx_noise.rs index 38a30aca5..7d13004ee 100644 --- a/crates/gpu/src/tests/fx_noise.rs +++ b/crates/gpu/src/tests/fx_noise.rs @@ -208,7 +208,7 @@ fn echo_and_posterize_time() { pos.keys = vec![Keyframe::new(Tick::ZERO, crate::tests::v3(10.0, 12.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::v3(50.0, 30.0))]; let rot = a.props.prop_mut("transform/rotation").unwrap(); rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(90.0))]; - s.p.comp_mut(inner_id).unwrap().layers = vec![a]; + s.p.comp_mut(inner_id).unwrap().layers = vec![a].into(); let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None); s.effect(&mut l, id, &vals); set(&mut l, "transform/rotation", Value::Scalar(8.0)); diff --git a/crates/gpu/src/tests/fx_time.rs b/crates/gpu/src/tests/fx_time.rs index 0c94bb413..c71355736 100644 --- a/crates/gpu/src/tests/fx_time.rs +++ b/crates/gpu/src/tests/fx_time.rs @@ -40,7 +40,7 @@ fn moving(id: &str, vals: &[(&str, Value)], map: Option<&str>) { pos.keys = vec![Keyframe::new(Tick::ZERO, v3(10.0, 12.0)), Keyframe::new(Tick::from_seconds_f64(1.0), v3(50.0, 30.0))]; let rot = a.props.prop_mut("transform/rotation").unwrap(); rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(90.0))]; - s.p.comp_mut(inner_id).unwrap().layers = vec![a]; + s.p.comp_mut(inner_id).unwrap().layers = vec![a].into(); let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None); s.effect(&mut l, id, &vals); set(&mut l, "transform/rotation", Value::Scalar(8.0)); diff --git a/crates/gpu/src/tests/fx_tone.rs b/crates/gpu/src/tests/fx_tone.rs index 08212a12a..40bc9483f 100644 --- a/crates/gpu/src/tests/fx_tone.rs +++ b/crates/gpu/src/tests/fx_tone.rs @@ -164,7 +164,7 @@ fn color_stabilizer() { pos.keys = vec![Keyframe::new(Tick::ZERO, crate::tests::v3(22.0, 18.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::v3(38.0, 24.0))]; let op = a.props.prop_mut("transform/opacity").unwrap(); op.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(100.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(40.0))]; - s.p.comp_mut(inner_id).unwrap().layers = vec![a]; + s.p.comp_mut(inner_id).unwrap().layers = vec![a].into(); let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None); let vals = [ ("stabilize", e(mode)), diff --git a/crates/gpu/src/tests/three_d.rs b/crates/gpu/src/tests/three_d.rs index bd9455b9e..8073681e1 100644 --- a/crates/gpu/src/tests/three_d.rs +++ b/crates/gpu/src/tests/three_d.rs @@ -161,7 +161,7 @@ fn mattes_preserve_transparency_precomps_and_adjustment_splits() { let mid = s.push(m); let layers = &mut s.p.comp_mut(s.cid).unwrap().layers; layers[1].track_matte = Some(TrackMatte { layer: mid, kind: MatteKind::Luma }); - if let Some(far) = layers.iter_mut().find(|l| l.switches.three_d && l.name == "Pattern") { + if let Some(far) = layers.find_mut(|l| l.switches.three_d && l.name == "Pattern") { far.preserve_transparency = true; } // Move the matte below the bottom so the 3D run stays one run. @@ -187,7 +187,7 @@ fn mattes_preserve_transparency_precomps_and_adjustment_splits() { a.switches.three_d = true; set(&mut a, "transform/rotationX", Value::Scalar(40.0)); let b = s.solid([0.2, 0.8, 0.4], 30, 20); - s.p.comp_mut(inner_id).unwrap().layers = vec![b, a]; + s.p.comp_mut(inner_id).unwrap().layers = vec![b, a].into(); let mut pre = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (80, 60), None); pre.switches.collapse = true; pre.switches.three_d = true; @@ -277,7 +277,7 @@ fn two_d_layer_with_a_three_d_track_matte() { let layers = &mut s.p.comp_mut(s.cid).unwrap().layers; let fi = layers.iter().position(|l| l.id == fid).unwrap(); layers[fi].track_matte = Some(TrackMatte { layer: mid, kind }); - if let Some(ml) = layers.iter_mut().find(|l| l.id == mid) { + if let Some(ml) = layers.find_mut(|l| l.id == mid) { ml.switches.video = false; } check(&format!("3d matte {kind:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.005); diff --git a/crates/host/tests/it/footage_reload.rs b/crates/host/tests/it/footage_reload.rs index faf2b09e3..3d47d0783 100644 --- a/crates/host/tests/it/footage_reload.rs +++ b/crates/host/tests/it/footage_reload.rs @@ -24,7 +24,7 @@ const BLUE: [f32; 4] = [0.0, 0.0, 1.0, 1.0]; /// A one-colour document; `extra` adds a hidden layer, so the file's size changes too. fn flat(rgba: [f32; 4], extra: bool) -> Vec { let mut d = WDoc::new(64, 48); - d.layers = vec![WLayer::solid("Background", Rect::new(0, 0, 64, 48), rgba)]; + d.layers = vec![WLayer::solid("Background", Rect::new(0, 0, 64, 48), rgba)].into(); if extra { let mut hidden = WLayer::solid("Hidden", Rect::new(0, 0, 8, 8), RED); hidden.hidden = true; @@ -86,7 +86,7 @@ fn reload_footage_shows_the_new_pixels() { fn reload_keeps_a_photoshop_layer_item_on_its_layer() { let path = tmp("layers.psd"); let mut d = WDoc::new(64, 48); - d.layers = vec![WLayer::solid("Background", Rect::new(0, 0, 64, 48), RED), WLayer::solid("Patch", Rect::new(0, 0, 32, 48), BLUE)]; + d.layers = vec![WLayer::solid("Background", Rect::new(0, 0, 64, 48), RED), WLayer::solid("Patch", Rect::new(0, 0, 32, 48), BLUE)].into(); std::fs::write(&path, write(&d)).unwrap(); let mut s = effectcraft_host::session(); let r = s.execute_checked("file.import", json!({"paths": [path], "importAs": "compositionLayerSizes"})).unwrap(); @@ -100,7 +100,7 @@ fn reload_keeps_a_photoshop_layer_item_on_its_layer() { WLayer::solid("Background", Rect::new(0, 0, 64, 48), RED), WLayer::solid("Added", Rect::new(40, 0, 8, 8), RED), WLayer::solid("Patch", Rect::new(0, 0, 24, 48), GREEN), - ]; + ].into(); save(&path, &write(&d)); let footage_items: Vec = items.iter().filter(|i| matches!(s.project.item(**i).map(|x| &x.kind), Some(ItemKind::Footage(_)))).map(|i| i.0).collect(); s.execute_checked("file.reloadFootage", json!({"items": footage_items})).unwrap(); diff --git a/crates/host/tests/it/vector_import.rs b/crates/host/tests/it/vector_import.rs index 36d83dd1a..badd60b66 100644 --- a/crates/host/tests/it/vector_import.rs +++ b/crates/host/tests/it/vector_import.rs @@ -509,7 +509,7 @@ fn shapes_from_vector_layer_keeps_images_as_footage_layers() { assert_eq!(names, ["images.pdf Outlines", "Image", "Image", "images.pdf Outlines 1", "images.pdf"]); assert_eq!(c.layers[0].id.0, sid); assert_eq!(r["layers"].as_array().unwrap().len(), 2); - for l in &c.layers[1..3] { + for l in c.layers.iter().skip(1).take(2) { assert_eq!(l.parent.map(|p| p.0), Some(sid), "{}", l.name); assert!(matches!(l.source, effectcraft_project::LayerSource::Footage { .. })); } diff --git a/crates/interchange/src/tests.rs b/crates/interchange/src/tests.rs index 3ad7fcaae..76fd542fc 100644 --- a/crates/interchange/src/tests.rs +++ b/crates/interchange/src/tests.rs @@ -411,7 +411,7 @@ fn ae_project() -> (Project, ItemId) { let mut ls = effectcraft_project::build::layer(&mut p, &c, "tone", LayerSource::Footage { item: s }, (0, 0), None); ls.out_point = t(3.0); ls.props.prop_mut("audio/levels").unwrap().value = Value::Vec2([-3.0, -3.0]); - c.layers = vec![lt, lp, la, ls]; + c.layers = vec![lt, lp, la, ls].into(); let cid = comp_item(&mut p, "Main", c); (p, cid) } diff --git a/crates/project/src/comp.rs b/crates/project/src/comp.rs index 5a3c8650e..4b620b192 100644 --- a/crates/project/src/comp.rs +++ b/crates/project/src/comp.rs @@ -87,8 +87,8 @@ pub struct Comp { pub display_start: Tick, pub background: [f32; 3], pub work_area: (Tick, Tick), - /// Layer #1 first (top of the stack). - pub layers: Vec, + /// Layer #1 first (top of the stack). Each layer is shared between undo states until edited. + pub layers: super::Layers, #[serde(default)] pub markers: Vec, pub shutter_angle: f64, @@ -188,7 +188,7 @@ impl Comp { display_start: Tick::ZERO, background: [0.0, 0.0, 0.0], work_area: (Tick::ZERO, duration), - layers: vec![], + layers: super::Layers::new(), markers: vec![], shutter_angle: 180.0, shutter_phase: -90.0, @@ -239,7 +239,7 @@ impl Comp { } pub fn layer_mut(&mut self, id: LayerId) -> Option<&mut Layer> { - self.layers.iter_mut().find(|l| l.id == id) + self.layers.find_mut(|l| l.id == id) } /// 1-based layer number (#) of a layer. diff --git a/crates/project/src/layers.rs b/crates/project/src/layers.rs new file mode 100644 index 000000000..6119a5753 --- /dev/null +++ b/crates/project/src/layers.rs @@ -0,0 +1,209 @@ +//! A comp's layer stack with structural sharing. +//! +//! Every undoable edit keeps the previous project for undo, so the comp it touches is shared and +//! `Arc::make_mut` copies it. With the layers stored inline that copy is the whole stack, every +//! layer and every keyframe, so an edit costs time in proportion to the comp's size and a script +//! that sets many keys in a big comp goes quadratic. Here each layer sits behind its own `Arc`: +//! copying the stack copies pointers, and only the layer an edit actually changes is copied. +//! +//! The API mirrors the `Vec` it replaces. Serialized form is identical (a list of layers). + +use std::ops::{Index, IndexMut}; +use std::sync::Arc; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use super::layer::Layer; + +/// Layer #1 first (top of the stack). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Layers(Vec>); + +impl Layers { + pub const fn new() -> Self { + Self(Vec::new()) + } + pub fn with_capacity(n: usize) -> Self { + Self(Vec::with_capacity(n)) + } + pub fn len(&self) -> usize { + self.0.len() + } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + pub fn iter(&self) -> impl DoubleEndedIterator + ExactSizeIterator + Clone { + self.0.iter().map(|l| &**l) + } + /// Mutable access to every layer: each one that is still shared with an undo state is copied, + /// so prefer [`Layers::get_mut`] (or `iter` to find, then `get_mut`) when only some change. + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator + ExactSizeIterator { + self.0.iter_mut().map(Arc::make_mut) + } + /// Mutable access to just the layers `keep` accepts. `keep` reads each layer without copying + /// it; only the matching ones are copied (if shared). Use this, not `iter_mut().filter(..)`. + pub fn iter_mut_where<'a>(&'a mut self, mut keep: impl FnMut(&Layer) -> bool + 'a) -> impl Iterator + 'a { + self.0.iter_mut().filter(move |l| keep(l)).map(Arc::make_mut) + } + /// The first layer `pred` accepts, copied only if it is that one. Use this, not `iter_mut().find(..)`. + pub fn find_mut(&mut self, mut pred: impl FnMut(&Layer) -> bool) -> Option<&mut Layer> { + self.0.iter_mut().find(|l| pred(l)).map(Arc::make_mut) + } + pub fn get(&self, i: usize) -> Option<&Layer> { + self.0.get(i).map(|l| &**l) + } + pub fn get_mut(&mut self, i: usize) -> Option<&mut Layer> { + self.0.get_mut(i).map(Arc::make_mut) + } + pub fn first(&self) -> Option<&Layer> { + self.get(0) + } + pub fn last(&self) -> Option<&Layer> { + self.0.last().map(|l| &**l) + } + pub fn first_mut(&mut self) -> Option<&mut Layer> { + self.get_mut(0) + } + pub fn last_mut(&mut self) -> Option<&mut Layer> { + self.0.last_mut().map(Arc::make_mut) + } + pub fn push(&mut self, l: Layer) { + self.0.push(Arc::new(l)); + } + pub fn insert(&mut self, i: usize, l: Layer) { + self.0.insert(i, Arc::new(l)); + } + pub fn remove(&mut self, i: usize) -> Layer { + Arc::unwrap_or_clone(self.0.remove(i)) + } + pub fn pop(&mut self) -> Option { + self.0.pop().map(Arc::unwrap_or_clone) + } + pub fn retain(&mut self, mut f: impl FnMut(&Layer) -> bool) { + self.0.retain(|l| f(l)); + } + pub fn swap(&mut self, a: usize, b: usize) { + self.0.swap(a, b); + } + pub fn truncate(&mut self, n: usize) { + self.0.truncate(n); + } + pub fn clear(&mut self) { + self.0.clear(); + } + pub fn reverse(&mut self) { + self.0.reverse(); + } + pub fn sort_by_key(&mut self, mut f: impl FnMut(&Layer) -> K) { + self.0.sort_by_key(|l| f(l)); + } + pub fn sort_by(&mut self, mut f: impl FnMut(&Layer, &Layer) -> std::cmp::Ordering) { + self.0.sort_by(|a, b| f(a, b)); + } + pub fn rotate_left(&mut self, n: usize) { + self.0.rotate_left(n); + } + pub fn rotate_right(&mut self, n: usize) { + self.0.rotate_right(n); + } + /// `layers[range].rotate_left(n)` (a slice of `Arc`s can't be handed out as a `&mut [Layer]`). + pub fn rotate_range_left(&mut self, r: impl std::slice::SliceIndex<[Arc], Output = [Arc]>, n: usize) { + if let Some(s) = self.0.get_mut(r) { + s.rotate_left(n); + } + } + pub fn rotate_range_right(&mut self, r: impl std::slice::SliceIndex<[Arc], Output = [Arc]>, n: usize) { + if let Some(s) = self.0.get_mut(r) { + s.rotate_right(n); + } + } + pub fn split_off(&mut self, at: usize) -> Layers { + Layers(self.0.split_off(at)) + } + pub fn append(&mut self, other: &mut Layers) { + self.0.append(&mut other.0); + } + /// Remove a range of layers and return them (owned). + pub fn drain(&mut self, r: impl std::ops::RangeBounds) -> std::vec::IntoIter { + self.0.drain(r).map(Arc::unwrap_or_clone).collect::>().into_iter() + } + /// Whether `self` and `other` hold the very same (unchanged) layer at `i`: a cheap "did this + /// layer change" test between two project states. + pub fn same_at(&self, other: &Layers, i: usize) -> bool { + matches!((self.0.get(i), other.0.get(i)), (Some(a), Some(b)) if Arc::ptr_eq(a, b)) + } + /// The shared layers themselves, for parallel iteration (`rayon` over `&[Arc]`). + pub fn as_arcs(&self) -> &[Arc] { + &self.0 + } + pub fn to_vec(&self) -> Vec { + self.iter().cloned().collect() + } +} + +impl Index for Layers { + type Output = Layer; + fn index(&self, i: usize) -> &Layer { + &self.0[i] + } +} + +impl IndexMut for Layers { + fn index_mut(&mut self, i: usize) -> &mut Layer { + Arc::make_mut(&mut self.0[i]) + } +} + +impl From> for Layers { + fn from(v: Vec) -> Self { + Self(v.into_iter().map(Arc::new).collect()) + } +} + +impl FromIterator for Layers { + fn from_iter>(it: I) -> Self { + Self(it.into_iter().map(Arc::new).collect()) + } +} + +impl Extend for Layers { + fn extend>(&mut self, it: I) { + self.0.extend(it.into_iter().map(Arc::new)); + } +} + +impl IntoIterator for Layers { + type Item = Layer; + type IntoIter = std::iter::Map>, fn(Arc) -> Layer>; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter().map(Arc::unwrap_or_clone as fn(Arc) -> Layer) + } +} + +impl<'a> IntoIterator for &'a Layers { + type Item = &'a Layer; + type IntoIter = std::iter::Map>, fn(&'a Arc) -> &'a Layer>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter().map(|l| &**l) + } +} + +impl<'a> IntoIterator for &'a mut Layers { + type Item = &'a mut Layer; + type IntoIter = std::iter::Map>, fn(&'a mut Arc) -> &'a mut Layer>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut().map(Arc::make_mut) + } +} + +impl Serialize for Layers { + fn serialize(&self, s: S) -> Result { + s.collect_seq(self.iter()) + } +} + +impl<'de> Deserialize<'de> for Layers { + fn deserialize>(d: D) -> Result { + Vec::::deserialize(d).map(Layers::from) + } +} diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index 8e1409c37..2ad56d366 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -1,8 +1,8 @@ //! The EffectCraft document model. //! //! `Project` → items (folders, compositions, footage, solids) → `Comp` → `Layer` → property tree -//! ([`props`]). Compositions sit behind `Arc` so undo snapshots share everything that did not -//! change. +//! ([`props`]). Compositions, and each layer within them ([`Layers`]), sit behind `Arc` so undo +//! snapshots share everything that did not change. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] @@ -33,11 +33,13 @@ mod comp; mod footage; mod item; mod layer; +mod layers; mod settings; pub use comp::{Comp, Guide, Renderer, Resolution}; pub use footage::{AlphaMode, FieldOrder, Footage, FootageKind, Proxy, Solid, SourceLayer}; pub use item::{Item, ItemId, ItemKind, LayerId}; +pub use layers::Layers; pub use layer::{AutoOrient, FrameBlend, Layer, LayerSource, LightKind, MatteKind, PrimitiveKind, Quality, Sampling, Switches, TrackMatte}; pub use settings::{ APP_VERSION, BitDepth, ColorEngine, CuePoint, HdrMode, Marker, ProjectError, ProjectSettings, SCHEMA_EFFECT_SPACE, SCHEMA_VERSION, SavedView, diff --git a/crates/project/src/styles.rs b/crates/project/src/styles.rs index 83db5e581..2b2bdf92c 100644 --- a/crates/project/src/styles.rs +++ b/crates/project/src/styles.rs @@ -470,7 +470,7 @@ mod tests { let mut ids = Ids(&mut p.next_id); add_style(&mut a, &mut ids, "dropShadow", &gl, true); add_style(&mut b, &mut ids, "bevelEmboss", &gl, true); - comp.layers = vec![a, b]; + comp.layers = vec![a, b].into(); comp.layers[1].props.sub_mut(GROUP).unwrap().sub_mut(BLENDING).unwrap().get_mut("globalLightAngle").unwrap().value = Value::Scalar(45.0); assert!(needs_sync(&comp)); sync_comp(&mut comp); diff --git a/crates/psd/src/memory_tests.rs b/crates/psd/src/memory_tests.rs index 108046b66..b864aa42b 100644 --- a/crates/psd/src/memory_tests.rs +++ b/crates/psd/src/memory_tests.rs @@ -255,7 +255,7 @@ fn doc(depth: u16, mode: ColorMode, rle: bool) -> WDoc { let mut masked = WLayer::pixels("masked", Rect::new(10, 8, 12, 12), grad(12, 12)); masked.mask = Some(WMask { rect: Rect::new(12, 10, 12, 12), data: (0..144).map(|i| (i % 12) as f32 / 11.0).collect(), default_color: 255, disabled: false }); - d.layers = vec![bg, off, solid, masked]; + d.layers = vec![bg, off, solid, masked].into(); d } diff --git a/crates/psd/src/parse.rs b/crates/psd/src/parse.rs index d221b9044..6a83bb30b 100644 --- a/crates/psd/src/parse.rs +++ b/crates/psd/src/parse.rs @@ -184,7 +184,7 @@ impl Psd { r.skip(c.len)?; } } - self.layers = layers; + self.layers = layers.into(); Ok(()) } diff --git a/crates/psd/src/tests.rs b/crates/psd/src/tests.rs index 074976510..054aca2ed 100644 --- a/crates/psd/src/tests.rs +++ b/crates/psd/src/tests.rs @@ -25,7 +25,7 @@ fn doc_with_layers(depth: u16, mode: ColorMode, rle: bool) -> WDoc { // Mask: left half opaque, right half transparent; default (outside) black. let mrect = Rect::new(20, 10, 12, 12); masked.mask = Some(WMask { rect: mrect, data: (0..144).map(|i| if i % 12 < 6 { 1.0 } else { 0.0 }).collect(), default_color: 0, disabled: false }); - d.layers = vec![bg, red, masked]; + d.layers = vec![bg, red, masked].into(); d } diff --git a/crates/psd/src/tests/opt.rs b/crates/psd/src/tests/opt.rs index 908026d07..c0c07eeb6 100644 --- a/crates/psd/src/tests/opt.rs +++ b/crates/psd/src/tests/opt.rs @@ -407,7 +407,7 @@ fn base() -> Psd { let mut masked = WLayer::solid("Masked", Rect::new(20, 10, 12, 12), [0.0, 0.5, 1.0, 1.0]); let data = (0..144).map(|i| if i % 12 < 6 { 1.0 } else { 0.25 }).collect(); masked.mask = Some(WMask { rect: Rect::new(20, 10, 12, 12), data, default_color: 0, disabled: false }); - d.layers = vec![bg, red, masked]; + d.layers = vec![bg, red, masked].into(); Psd::parse(write(&d)).unwrap() } diff --git a/crates/render/src/audio.rs b/crates/render/src/audio.rs index 66b54b6d0..839a71ca7 100644 --- a/crates/render/src/audio.rs +++ b/crates/render/src/audio.rs @@ -347,7 +347,7 @@ mod tests { pr.value = effectcraft_project::Value::Vec2([-6.0206, -96.0]); } let mut comp = comp; - comp.layers = vec![l1, l2]; + comp.layers = vec![l1, l2].into(); let cid = p.add_item("C", effectcraft_color::Label::Sandstone, None, ItemKind::Comp(comp.into())); assert!(comp_has_audio(&p, cid)); let rate = 1000; @@ -393,11 +393,11 @@ mod tests { let base = Comp::new(64, 64, FrameRate::new(25, 1), Tick::from_seconds_f64(2.0)); let tone = build::layer(&mut p, &base, "tone", LayerSource::Footage { item: fid }, (0, 0), None); let mut inner = base.clone(); - inner.layers = vec![tone]; + inner.layers = vec![tone].into(); let inner_id = p.add_item("Inner", effectcraft_color::Label::Sandstone, None, ItemKind::Comp(inner.into())); let pre = build::layer(&mut p, &base, "pre", LayerSource::Comp { item: inner_id }, (64, 64), None); let mut outer = base.clone(); - outer.layers = vec![pre]; + outer.layers = vec![pre].into(); let outer_id = p.add_item("Outer", effectcraft_color::Label::Sandstone, None, ItemKind::Comp(outer.into())); let rate = 8000; let t = Tick::from_seconds_f64(0.5); @@ -424,7 +424,7 @@ mod tests { let mut tone = build::layer(&mut p, &base, "tone", LayerSource::Footage { item: fid }, (0, 0), None); tone.stretch = stretch; let mut comp = base.clone(); - comp.layers = vec![tone]; + comp.layers = vec![tone].into(); let cid = p.add_item("C", effectcraft_color::Label::Sandstone, None, ItemKind::Comp(comp.into())); let n = 2000usize; let t = Tick::from_seconds_f64(0.5); diff --git a/crates/render/src/disk_cache.rs b/crates/render/src/disk_cache.rs index 4fa2e1cb3..9c9e61f59 100644 --- a/crates/render/src/disk_cache.rs +++ b/crates/render/src/disk_cache.rs @@ -1113,7 +1113,7 @@ mod tests { let f = Footage { path: "/virtual.mov".into(), width: 16, height: 9, duration: Tick::from_seconds_f64(1.0), has_video: true, ..Default::default() }; let fid = p.add_item("virtual.mov", effectcraft_color::Label::None, None, ItemKind::Footage(f)); let mut comp = Comp::new(16, 9, FrameRate::FPS_30, Tick::from_seconds_f64(1.0)); - comp.layers = vec![build::layer(&mut p, &comp, "clip", LayerSource::Footage { item: fid }, (16, 9), None)]; + comp.layers = vec![build::layer(&mut p, &comp, "clip", LayerSource::Footage { item: fid }, (16, 9), None)].into(); let cid = p.add_item("C", effectcraft_color::Label::None, None, ItemKind::Comp(comp.into())); let id = |mark: u64| move |path: &str| (path == "/virtual.mov").then_some((8, mark)); diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 079274499..3caf20c30 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -313,7 +313,7 @@ fn particle_world_scene(pan: f64) -> (Project, ItemId) { let Value::Vec3(v) = pr.value else { panic!("{path}: {:?}", pr.value) }; pr.value = Value::Vec3([v[0] + pan, v[1], v[2]]); } - p.comp_mut(cid).unwrap().layers = vec![cam, l]; + p.comp_mut(cid).unwrap().layers = vec![cam, l].into(); (p, cid) } @@ -411,7 +411,7 @@ fn cache_scene() -> (Project, ItemId) { vec![Keyframe::new(Tick::ZERO, Value::Vec3([20.0, 70.0, 0.0])), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Vec3([120.0, 70.0, 0.0]))]; add_effect_200(&mut p, &mut text, "ec.blur.gaussian", &[("blurriness", Value::Scalar(6.0))]); let c = p.comp_mut(cid).unwrap(); - c.layers = vec![text, shape, bg]; + c.layers = vec![text, shape, bg].into(); (p, cid) } @@ -651,7 +651,7 @@ fn edge_pinning_displaces_all_of_a_centred_shape_layer() { p.comp_mut(cid).unwrap().layers.push(l.clone()); let plain = render_frame(&p, cid, Tick::ZERO, 1.0); add_effect_200(&mut p, &mut l, id, &vals); - p.comp_mut(cid).unwrap().layers = vec![l]; + p.comp_mut(cid).unwrap().layers = vec![l].into(); let warped = render_frame(&p, cid, Tick::ZERO, 1.0); // Alpha change per comp quadrant (TL, TR, BL, BR): the rectangle's edges move in each. let mut q = [0.0f32; 4]; @@ -685,7 +685,7 @@ fn default_effect_points_sit_at_the_centre_of_a_shape_layer() { p.comp_mut(cid).unwrap().layers.push(l.clone()); let plain = render_frame(&p, cid, Tick::ZERO, 1.0); add_effect_200(&mut p, &mut l, id, &vals); - p.comp_mut(cid).unwrap().layers = vec![l]; + p.comp_mut(cid).unwrap().layers = vec![l].into(); let warped = render_frame(&p, cid, Tick::ZERO, 1.0); // Alpha change per comp quadrant (TL, TR, BL, BR): the top and bottom edges move in each. let mut q = [0.0f32; 4]; @@ -714,7 +714,7 @@ fn effects_on_text_reach_past_the_text_bounds() { let plain = render_frame(&p, cid, Tick::ZERO, 1.0); let painted = plain.data.iter().filter(|px| px[3] > 0.5).count(); add_effect_200(&mut p, &mut l, "ec.distort.cctiler", &[("scale", Value::Scalar(25.0))]); - p.comp_mut(cid).unwrap().layers = vec![l]; + p.comp_mut(cid).unwrap().layers = vec![l].into(); let tiled = render_frame(&p, cid, Tick::ZERO, 1.0); let (mut lo, mut hi) = (u32::MAX, 0u32); for y in 0..100 { @@ -748,7 +748,7 @@ fn cc_smear_trails_text_past_its_bounds() { "ec.distort.ccsmear", &[("from", Value::Vec2([110.0, 45.0])), ("to", Value::Vec2([185.0, 45.0])), ("reach", Value::Scalar(100.0)), ("radius", Value::Scalar(40.0))], ); - p.comp_mut(cid).unwrap().layers = vec![l]; + p.comp_mut(cid).unwrap().layers = vec![l].into(); let smeared = right(&render_frame(&p, cid, Tick::ZERO, 1.0)); assert!(plain > 40 && smeared > plain + 25, "CC Smear trails the glyphs past the text bounds: right edge {plain} → {smeared}"); } @@ -771,7 +771,7 @@ fn layer_parameters_see_a_whole_shape_layer() { let matte_id = shape.id; let mut s = solid(&mut p, &comp, [0.0, 0.0, 1.0], 200, 100); add_effect_200(&mut p, &mut s, "ec.channel.setmatte", &[("takeMatteFromLayer", Value::Layer(Some(matte_id.0)))]); - p.comp_mut(cid).unwrap().layers = vec![s, shape]; + p.comp_mut(cid).unwrap().layers = vec![s, shape].into(); let f = render_frame(&p, cid, Tick::ZERO, 1.0); // The matte covers x 50..150, y 25..75: kept inside (all four quadrants), gone outside. for (x, y) in [(60, 30), (140, 30), (60, 70), (140, 70)] { @@ -861,7 +861,7 @@ fn self_inputs_share_source_semantics_across_effects_and_times() { "ec.channel.blend", &[("blendWithLayer", Value::Layer(Some(id))), ("blendWithLayerSource", Value::Enum(0)), ("blendWithOriginal", Value::Scalar(0.0))], ); - p.comp_mut(cid).unwrap().layers = vec![l]; + p.comp_mut(cid).unwrap().layers = vec![l].into(); let c = p.comp(cid).unwrap(); let ctx = crate::eval::EvalCtx::new(&p, cid, c, Tick::ZERO); assert!(crate::cache::input_key(&ctx, &c.layers[0], 1.0, false, false, 1).is_some()); @@ -879,7 +879,7 @@ fn timed_source_reads_preserve_other_layer_masks() { let mask = build::mask(&mut Ids(&mut next), "Left", ShapePath::rect([50.0, 50.0], 80.0, 80.0), MaskMode::Add, [255, 255, 0]); p.next_id = next; other.props.sub_mut("masks").unwrap().children.push(mask.into()); - p.comp_mut(cid).unwrap().layers = vec![owner.clone(), other.clone()]; + p.comp_mut(cid).unwrap().layers = vec![owner.clone(), other.clone()].into(); let ctx = crate::EvalCtx::new(&p, cid, p.comp(cid).unwrap(), Tick::ZERO); let renderer = crate::Renderer::new(&p, &crate::NoFootage, crate::RenderOpts::default()); let host = crate::FxHost { r: &renderer, ctx: &ctx, layer: &owner, origin: [0.0; 2], index: Default::default(), original: None }; @@ -900,7 +900,7 @@ fn layer_dependencies_remain_cacheable_and_invalidate_only_related_edits() { let unrelated = solid(&mut p, &comp, [0.2, 0.3, 0.4], 200, 100); add_effect_200(&mut p, &mut source, "ec.channel.blend", &[("blendWithLayer", Value::Layer(Some(third.id.0))), ("blendWithOriginal", Value::Scalar(0.0))]); add_effect_200(&mut p, &mut owner, "ec.channel.blend", &[("blendWithLayer", Value::Layer(Some(source.id.0))), ("blendWithOriginal", Value::Scalar(0.0))]); - p.comp_mut(cid).unwrap().layers = vec![owner, source, third, unrelated]; + p.comp_mut(cid).unwrap().layers = vec![owner, source, third, unrelated].into(); let key = |p: &Project, time: f64, limit| { let comp = p.comp(cid).unwrap(); let ctx = crate::EvalCtx::new(p, cid, comp, Tick::from_seconds_f64(time)); @@ -941,12 +941,12 @@ fn precomp_dependencies_hash_nested_sources_and_evaluated_animation() { child.props.prop_mut("transform/opacity").unwrap().keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(100.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(50.0))]; let mut nested = comp.clone(); - nested.layers = vec![child]; + nested.layers = vec![child].into(); let nested_id = p.add_item("Nested", Label::Sandstone, None, ItemKind::Comp(nested.into())); let pre = build::layer(&mut p, &comp, "Precomp", LayerSource::Comp { item: nested_id }, (200, 100), None); let mut owner = solid(&mut p, &comp, [1.0, 0.0, 0.0], 200, 100); add_effect_200(&mut p, &mut owner, "ec.channel.blend", &[("blendWithLayer", Value::Layer(Some(pre.id.0))), ("blendWithOriginal", Value::Scalar(0.0))]); - p.comp_mut(cid).unwrap().layers = vec![owner, pre]; + p.comp_mut(cid).unwrap().layers = vec![owner, pre].into(); let key = |p: &Project, time| { let c = p.comp(cid).unwrap(); crate::cache::layer_key(&crate::EvalCtx::new(p, cid, c, time), &c.layers[0], 1.0, false, false).unwrap() @@ -1021,7 +1021,7 @@ fn scene_reader_dependency(pan: f64) -> (Project, ItemId) { let Value::Vec3(v) = pr.value else { panic!("{path}: {:?}", pr.value) }; pr.value = Value::Vec3([v[0] + pan, v[1], v[2]]); } - p.comp_mut(cid).unwrap().layers = vec![cam, owner, source]; + p.comp_mut(cid).unwrap().layers = vec![cam, owner, source].into(); (p, cid) } diff --git a/crates/render/src/tests/audio_fx.rs b/crates/render/src/tests/audio_fx.rs index aced8208e..0f74b1f7c 100644 --- a/crates/render/src/tests/audio_fx.rs +++ b/crates/render/src/tests/audio_fx.rs @@ -78,7 +78,7 @@ fn comp_with(id: Option<&str>, vals: &[(&str, Value)], solid: bool) -> (Project, l.props.sub_mut("effects").unwrap().children.push(g.into()); } let mut comp = comp; - comp.layers = vec![l]; + comp.layers = vec![l].into(); let cid = p.add_item("C", effectcraft_color::Label::Sandstone, None, ItemKind::Comp(comp.into())); (p, cid) } @@ -236,7 +236,7 @@ fn audio_spectrum_follows_time_through_the_layer_cache() { g.prop_mut("audioLayer").unwrap().value = Value::Layer(Some(a.id.0)); s.props.sub_mut("effects").unwrap().children.push(g.into()); let mut comp = comp; - comp.layers = vec![s, a]; + comp.layers = vec![s, a].into(); let cid = p.add_item("C", effectcraft_color::Label::Sandstone, None, ItemKind::Comp(comp.into())); let src = Signal(late_tone); diff --git a/crates/render/src/tests/collapse.rs b/crates/render/src/tests/collapse.rs index 797960033..6f2ad8441 100644 --- a/crates/render/src/tests/collapse.rs +++ b/crates/render/src/tests/collapse.rs @@ -53,17 +53,17 @@ fn scene(collapse: bool, flatten: bool) -> (Project, ItemId) { a.parent = Some(null.id); b.parent = Some(null.id); let mut c = outer.clone(); - c.layers = vec![null, b, a, bg]; + c.layers = vec![null, b, a, bg].into(); p.add_item("Outer", Label::Sandstone, None, ItemKind::Comp(c.into())) } else { let mut inner = inner; - inner.layers = vec![b, a]; + inner.layers = vec![b, a].into(); let iid = p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into())); let mut pre = build::layer(&mut p, &outer, "Inner", LayerSource::Comp { item: iid }, (100, 100), None); xf(&mut pre); pre.switches.collapse = collapse; let mut c = outer.clone(); - c.layers = vec![pre, bg]; + c.layers = vec![pre, bg].into(); p.add_item("Outer", Label::Sandstone, None, ItemKind::Comp(c.into())) }; (p, cid) @@ -122,7 +122,7 @@ fn collapsed_3d_layers_join_the_parent_scene() { let mut blue = solid(&mut p, &inner, [0.0, 0.0, 1.0], (60, 60), [100.0, 100.0, -200.0]); blue.switches.three_d = true; let mut inner = inner; - inner.layers = vec![blue]; + inner.layers = vec![blue].into(); let iid = p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into())); let mut pre = build::layer(&mut p, &outer, "Inner", LayerSource::Comp { item: iid }, (200, 200), None); pre.switches.three_d = true; @@ -130,7 +130,7 @@ fn collapsed_3d_layers_join_the_parent_scene() { let mut red = solid(&mut p, &outer, [1.0, 0.0, 0.0], (100, 100), [100.0, 100.0, 0.0]); red.switches.three_d = true; let mut c = outer; - c.layers = vec![red, pre]; + c.layers = vec![red, pre].into(); let cid = p.add_item("Outer", Label::Sandstone, None, ItemKind::Comp(c.into())); render_frame(&p, cid, Tick::ZERO, 1.0).get(100, 100) }; @@ -168,11 +168,11 @@ fn scaled_square(cr: bool, via_parent: bool) -> Image { set(&mut null, "transform/scale", Value::Vec3([800.0, 800.0, 100.0])); set(&mut l, "transform/position", Value::Vec3([0.0, 0.0, 0.0])); l.parent = Some(null.id); - c.layers = vec![l, null]; + c.layers = vec![l, null].into(); } else { set(&mut l, "transform/position", Value::Vec3([100.3, 100.3, 0.0])); set(&mut l, "transform/scale", Value::Vec3([800.0, 800.0, 100.0])); - c.layers = vec![l]; + c.layers = vec![l].into(); } let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(c.into())); render_frame(&p, cid, Tick::ZERO, 1.0) @@ -201,7 +201,7 @@ fn wireframe_quality_draws_the_layer_bounds() { let mut l = solid(&mut p, &comp, [1.0, 0.0, 0.0], (40, 40), [50.0, 50.0, 0.0]); l.switches.quality = Quality::Wireframe; let mut c = comp; - c.layers = vec![l]; + c.layers = vec![l].into(); let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(c.into())); let img = render_frame(&p, cid, Tick::ZERO, 1.0); assert_eq!(img.get(50, 50)[3], 0.0, "no fill"); @@ -212,7 +212,7 @@ fn wireframe_quality_draws_the_layer_bounds() { let mut l = solid(&mut p, &comp, [1.0, 1.0, 1.0], (40, 40), [50.5, 50.0, 0.0]); l.switches.quality = Quality::Draft; let mut c = comp; - c.layers = vec![l]; + c.layers = vec![l].into(); let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(c.into())); let img = render_frame(&p, cid, Tick::ZERO, 1.0); assert!(img.data.iter().all(|q| q[3] == 0.0 || q[3] == 1.0)); @@ -230,13 +230,13 @@ fn collapsed_precomp_of_another_size_renders_through_the_parent_camera() { let mut blue = solid(&mut p, &inner, [0.0, 0.0, 1.0], (40, 40), [500.0, 300.0, 0.0]); blue.switches.three_d = true; let mut inner = inner; - inner.layers = vec![blue]; + inner.layers = vec![blue].into(); let iid = p.add_item("Scene", Label::Sandstone, None, ItemKind::Comp(inner.into())); let mut pre = build::layer(&mut p, &outer, "Scene", LayerSource::Comp { item: iid }, (1000, 600), None); pre.switches.three_d = three_d_precomp; pre.switches.collapse = true; let mut c = outer.clone(); - c.layers = vec![pre]; + c.layers = vec![pre].into(); if camera { let cam = build::layer(&mut p, &outer, "Camera", LayerSource::Camera, (300, 200), None); c.layers.insert(0, cam); @@ -267,7 +267,7 @@ fn text_scaled_up_stays_sharp_without_the_switch() { set(&mut l, "transform/scale", Value::Vec3([scale, scale, 100.0])); assert!(!l.switches.collapse); let mut c = comp; - c.layers = vec![l]; + c.layers = vec![l].into(); let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(c.into())); render_frame(&p, cid, Tick::ZERO, 1.0) }; @@ -291,7 +291,7 @@ fn unscaled_text_rasterises_at_the_output_scale() { let mut comp = Comp::new(240, 160, FrameRate::FPS_30, Tick::from_seconds_f64(2.0)); let mut l = build::layer(&mut p, &comp, "Text", LayerSource::Text, (240, 160), None); l.props.prop_mut("text/sourceText").unwrap().value = Value::Text(Box::new(TextDoc { text: "HE".into(), size: 30.0, ..Default::default() })); - comp.layers = vec![l]; + comp.layers = vec![l].into(); let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.into())); for i in 0..500 { let pos = [3.1 + i as f64 * 0.37, 7.3 + i as f64 * 0.29, 0.0]; @@ -327,7 +327,7 @@ fn effects_on_a_scaled_shape_follow_its_scale() { set(&mut l, "transform/position", Value::Vec3([100.0, 100.0, 0.0])); set(&mut l, "transform/scale", Value::Vec3([scale, scale, 100.0])); let mut c = comp; - c.layers = vec![l]; + c.layers = vec![l].into(); let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(c.into())); render_frame(&p, cid, Tick::ZERO, 1.0) }; diff --git a/crates/render/src/tests/guides.rs b/crates/render/src/tests/guides.rs index cb9851250..44f3fc7b0 100644 --- a/crates/render/src/tests/guides.rs +++ b/crates/render/src/tests/guides.rs @@ -40,7 +40,7 @@ fn scene(nest: Nest) -> (Project, ItemId, ItemId) { guide.switches.guide = true; let bg = solid(&mut p, &inner, [1.0, 0.0, 0.0], (100, 100), three_d); let mut inner = inner; - inner.layers = vec![guide, bg]; + inner.layers = vec![guide, bg].into(); let iid = p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into())); let mut outer = Comp::new(100, 100, FrameRate::FPS_30, Tick::from_seconds_f64(2.0)); if matches!(nest, Nest::CollapsedAdvanced) { @@ -49,7 +49,7 @@ fn scene(nest: Nest) -> (Project, ItemId, ItemId) { let mut pre = build::layer(&mut p, &outer, "Inner", LayerSource::Comp { item: iid }, (100, 100), None); pre.switches.collapse = !matches!(nest, Nest::Precomp); pre.switches.three_d = three_d; - outer.layers = vec![pre]; + outer.layers = vec![pre].into(); let oid = p.add_item("Outer", Label::Sandstone, None, ItemKind::Comp(outer.into())); (p, iid, oid) } diff --git a/crates/render/src/tests/motion_tile.rs b/crates/render/src/tests/motion_tile.rs index 7d1645a02..28faca73c 100644 --- a/crates/render/src/tests/motion_tile.rs +++ b/crates/render/src/tests/motion_tile.rs @@ -58,7 +58,7 @@ fn adjustment_scene(out: f64, scale: f64) -> (Project, ItemId) { tile_then_scale(&mut p, &mut adj, out, scale); let mut layers = vec![adj]; layers.extend(picture(&mut p, &comp)); - p.comp_mut(cid).unwrap().layers = layers; + p.comp_mut(cid).unwrap().layers = layers.into(); (p, cid) } @@ -68,7 +68,7 @@ fn layer_scene(out: f64, scale: f64) -> (Project, ItemId) { let inner = Comp::new(W, H, FrameRate::FPS_30, Tick::from_seconds_f64(1.0)); let iid = p.add_item("Picture", Label::Sandstone, None, ItemKind::Comp(inner.clone().into())); let layers = picture(&mut p, &inner); - p.comp_mut(iid).unwrap().layers = layers; + p.comp_mut(iid).unwrap().layers = layers.into(); let mut pre = build::layer(&mut p, &comp, "Picture", LayerSource::Comp { item: iid }, (W, H), None); tile_then_scale(&mut p, &mut pre, out, scale); p.comp_mut(cid).unwrap().layers.push(pre); @@ -151,7 +151,7 @@ fn adjustment_motion_tile_matches_the_after_effects_frames() { } adj.props.sub_mut("effects").unwrap().children.push(g.into()); } - p.comp_mut(cid).unwrap().layers = vec![adj, red]; + p.comp_mut(cid).unwrap().layers = vec![adj, red].into(); // Quarter resolution: comp pixel (x, y) is output pixel (x / 4, y / 4). let img = render_frame(&p, cid, Tick::ZERO, 0.25); let at = |x: f64, y: f64| img.get((x / 4.0) as i64, (y / 4.0) as i64); diff --git a/crates/render/src/tests/roi.rs b/crates/render/src/tests/roi.rs index 9182817cf..a9892d9c7 100644 --- a/crates/render/src/tests/roi.rs +++ b/crates/render/src/tests/roi.rs @@ -29,7 +29,7 @@ fn project(three_d: bool) -> (Project, effectcraft_project::ItemId) { } layers.push(l); } - p.comp_mut(cid).unwrap().layers = layers; + p.comp_mut(cid).unwrap().layers = layers.into(); (p, cid) } diff --git a/crates/render/src/tests/styles.rs b/crates/render/src/tests/styles.rs index b59f009de..2450fcf64 100644 --- a/crates/render/src/tests/styles.rs +++ b/crates/render/src/tests/styles.rs @@ -44,7 +44,7 @@ fn scene(color: [f32; 3], bg: Option<[f32; 3]>, f: impl FnOnce(&mut Project, &Co if let Some(bg) = bg { layers.push(solid(&mut p, &comp, bg, 200, 100)); } - p.comp_mut(cid).unwrap().layers = layers; + p.comp_mut(cid).unwrap().layers = layers.into(); (p, cid) } diff --git a/crates/render/src/three_d/compose.rs b/crates/render/src/three_d/compose.rs index 8e932819a..ab7919079 100644 --- a/crates/render/src/three_d/compose.rs +++ b/crates/render/src/three_d/compose.rs @@ -469,6 +469,7 @@ fn gather<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Layer], out: (u32, extra = ctx .comp .layers + .as_arcs() .par_iter() .filter(|l| l.switches.three_d && l.has_video() && l.is_active_at(ctx.time) && !in_run.contains(&l.id) && !l.switches.adjustment) .filter(|l| Material::of(ctx, l).casts_shadows != 0) diff --git a/crates/ui-egui/src/audio.rs b/crates/ui-egui/src/audio.rs index 7972e6942..fbfe4b8af 100644 --- a/crates/ui-egui/src/audio.rs +++ b/crates/ui-egui/src/audio.rs @@ -532,7 +532,7 @@ mod tests { let item = p.add_item("long.flac", Label::SeaFoam, None, ItemKind::Footage(footage)); let mut comp = Comp::new(64, 36, FrameRate::new(30, 1), Tick::from_seconds_f64(10.0)); let layer = build::layer(&mut p, &comp, "long.flac", LayerSource::Footage { item }, (0, 0), None); - comp.layers = vec![layer]; + comp.layers = vec![layer].into(); let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.into())); let src = RenderSource { project: Arc::new(p), diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index a45d8e708..bae450455 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -1485,8 +1485,8 @@ fn shift_range(ctx: &egui::Context, cid: ItemId, comp: &Comp, rows: &[Row], sele let anchor = ctx.data(|d| d.get_temp::(anchor_id(cid))).or_else(|| selected.first().copied())?; let a = comp.layers.iter().position(|l| l.id == anchor)?; let b = comp.layers.iter().position(|l| l.id == layer)?; - let span = comp.layers.get(a.min(b)..=a.max(b))?; - Some(span.iter().filter(|l| !l.switches.locked && rows.iter().any(|r| r.layer == l.id)).map(|l| l.id.0).collect()) + let span = comp.layers.iter().skip(a.min(b)).take(a.max(b) - a.min(b) + 1); + Some(span.filter(|l| !l.switches.locked && rows.iter().any(|r| r.layer == l.id)).map(|l| l.id.0).collect()) } /// Set on the frame the reorder drag is released. diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 58383fe43..ac3368b2e 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -602,7 +602,7 @@ pub fn popup_menu(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, options: &[String] pub fn layer_picker( ui: &mut Ui, rect: Rect, - layers: &[effectcraft_engine::project::Layer], + layers: &effectcraft_engine::project::Layers, current: Option, t: &Tokens, id: egui::Id, diff --git a/crates/ui-egui/tests/it/ui_drag_drop.rs b/crates/ui-egui/tests/it/ui_drag_drop.rs index e405bf777..8d17e8a6e 100644 --- a/crates/ui-egui/tests/it/ui_drag_drop.rs +++ b/crates/ui-egui/tests/it/ui_drag_drop.rs @@ -198,7 +198,7 @@ fn dragging_selected_project_items_adds_every_item_to_the_viewer() { let to = comp_to_screen(&h.ctx, [80.0, 45.0]).unwrap(); drag(&mut h, from, to, Modifiers::NONE); assert_eq!(stack(&h), ["Clip", "Other", "Top", "Middle", "Bottom"]); - for layer in &h.state().session.active_comp().unwrap().layers[..2] { + for layer in h.state().session.active_comp().unwrap().layers.iter().take(2) { let Some(KV::Vec3(p)) = layer.props.prop("transform/position").map(|p| p.value.clone()) else { panic!("no position") }; let tol = 1.0 / last_fit(&h.ctx) as f64 + 1e-6; assert!((p[0] - 80.0).abs() <= tol && (p[1] - 45.0).abs() <= tol, "{p:?}"); diff --git a/crates/ui-egui/tests/it/ui_new_primitive.rs b/crates/ui-egui/tests/it/ui_new_primitive.rs index fbb3a34e5..c40aa23ed 100644 --- a/crates/ui-egui/tests/it/ui_new_primitive.rs +++ b/crates/ui-egui/tests/it/ui_new_primitive.rs @@ -70,7 +70,7 @@ fn choose(h: &mut Harness<'_, EffectcraftApp>, kind: PrimitiveKind) { } fn layers(h: &Harness<'_, EffectcraftApp>) -> Vec { - h.state().session.active_comp().unwrap().layers.clone() + h.state().session.active_comp().unwrap().layers.to_vec() } fn renderer(h: &Harness<'_, EffectcraftApp>) -> Renderer {