perf: share unchanged layers between undo states

Every undoable edit keeps the previous project for undo, so the comp it touches
is shared and Arc::make_mut deep-copies it: every layer and every keyframe. An
edit therefore costs time in proportion to the comp's size, and a script that
sets many keyframes in a big comp goes quadratic (a 1,600-layer build with
~100k sampled keys took ~45 minutes).

Comp::layers is now a Layers collection backed by Vec<Arc<Layer>>: copying the
stack copies pointers, and only the layer an edit changes is copied. The API
mirrors Vec<Layer>; serialized form is unchanged (a list of layers).
Comp::layer_mut and the iter_mut().filter/find call sites use find_mut /
iter_mut_where so they copy only the layers they actually change.

- prop.addKey in a 400-layer comp: 1.36 ms -> 0.16 ms per key (flat in comp size)
- the 1,600-layer script build: ~45 min -> 47 s, frames pixel-identical
- regression tests: an edit leaves the other layers shared with undo; layers
  round-trip as a plain list

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkpGcKfWyCudfSsQNiWAaG
This commit is contained in:
Eduardo Espinosa
2026-10-28 22:20:23 -04:00
co-authored by Claude Opus 5.5
parent c99684e28e
commit e1d66a3c5c
54 changed files with 369 additions and 114 deletions
+1 -1
View File
@@ -1189,7 +1189,7 @@ fn adv3d_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option<Item
set(l, "cameraOptions/highlightGain", Value::Scalar(20.0));
set(l, "cameraOptions/highlightThreshold", Value::Scalar(200.0));
});
comp.layers = layers;
comp.layers = layers.into();
Some(p.add_item("Advanced 3D (bench)", effectcraft_engine::color::Label::None, None, ItemKind::Comp(comp.into())))
}
+1 -1
View File
@@ -240,7 +240,7 @@ fn replace_with_precomp(s: &mut Session, p: &Value) -> Result<Value> {
let ids: Vec<ItemId> = 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;
}
+1 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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);
}
+2 -2
View File
@@ -90,7 +90,7 @@ fn deselect_all(s: &mut Session, _: &Value) -> Result<Value> {
fn relink_copies(comp: &mut Comp, map: &[(LayerId, LayerId)]) {
let present: Vec<LayerId> = 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<Value> {
}
}
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;
}
}
+1 -1
View File
@@ -323,7 +323,7 @@ pub(super) fn set_parent(s: &mut Session, p: &Value) -> Result<Value> {
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);
+4 -4
View File
@@ -19,7 +19,7 @@ pub(super) fn set_switch(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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<Value> {
}
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(),
+3 -3
View File
@@ -18,7 +18,7 @@ pub(super) fn timing(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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);
}
}
}
+8 -8
View File
@@ -61,7 +61,7 @@ fn quality(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
// Track mattes stay hidden (their video switch is off by design).
let mattes: Vec<LayerId> = 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<Value> {
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<Value> {
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<Value> {
fn delete_all_markers(s: &mut Session, p: &Value) -> Result<Value> {
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(())
+2 -2
View File
@@ -34,7 +34,7 @@ fn time_stretch(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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;
+2 -2
View File
@@ -268,7 +268,7 @@ fn stereo_rig(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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,
+1 -1
View File
@@ -532,7 +532,7 @@ fn auto_orient(s: &mut Session, p: &Value) -> Result<Value> {
};
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,
+3 -3
View File
@@ -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;
+1 -1
View File
@@ -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 };
}
}
+2 -2
View File
@@ -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");
+1
View File
@@ -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;
+1 -1
View File
@@ -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" {
+1 -1
View File
@@ -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::<Vec<_>>(), ["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));
}
+42
View File
@@ -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<u64> = (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);
}
+1 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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));
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
}
+6 -6
View File
@@ -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 });
}
}
+1 -1
View File
@@ -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));
+1 -1
View File
@@ -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));
+1 -1
View File
@@ -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)),
+3 -3
View File
@@ -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);
+3 -3
View File
@@ -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<u8> {
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<u64> = 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();
+1 -1
View File
@@ -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 { .. }));
}
+1 -1
View File
@@ -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)
}
+4 -4
View File
@@ -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>,
/// 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<Marker>,
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.
+209
View File
@@ -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<Layer>` 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<Arc<Layer>>);
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<Item = &Layer> + 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<Item = &mut Layer> + 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<Item = &'a mut Layer> + '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<Layer> {
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<K: Ord>(&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<Layer>], Output = [Arc<Layer>]>, 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<Layer>], Output = [Arc<Layer>]>, 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<usize>) -> std::vec::IntoIter<Layer> {
self.0.drain(r).map(Arc::unwrap_or_clone).collect::<Vec<_>>().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<Layer>]`).
pub fn as_arcs(&self) -> &[Arc<Layer>] {
&self.0
}
pub fn to_vec(&self) -> Vec<Layer> {
self.iter().cloned().collect()
}
}
impl Index<usize> for Layers {
type Output = Layer;
fn index(&self, i: usize) -> &Layer {
&self.0[i]
}
}
impl IndexMut<usize> for Layers {
fn index_mut(&mut self, i: usize) -> &mut Layer {
Arc::make_mut(&mut self.0[i])
}
}
impl From<Vec<Layer>> for Layers {
fn from(v: Vec<Layer>) -> Self {
Self(v.into_iter().map(Arc::new).collect())
}
}
impl FromIterator<Layer> for Layers {
fn from_iter<I: IntoIterator<Item = Layer>>(it: I) -> Self {
Self(it.into_iter().map(Arc::new).collect())
}
}
impl Extend<Layer> for Layers {
fn extend<I: IntoIterator<Item = Layer>>(&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<std::vec::IntoIter<Arc<Layer>>, fn(Arc<Layer>) -> Layer>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter().map(Arc::unwrap_or_clone as fn(Arc<Layer>) -> Layer)
}
}
impl<'a> IntoIterator for &'a Layers {
type Item = &'a Layer;
type IntoIter = std::iter::Map<std::slice::Iter<'a, Arc<Layer>>, fn(&'a Arc<Layer>) -> &'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<std::slice::IterMut<'a, Arc<Layer>>, fn(&'a mut Arc<Layer>) -> &'a mut Layer>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter_mut().map(Arc::make_mut)
}
}
impl Serialize for Layers {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_seq(self.iter())
}
}
impl<'de> Deserialize<'de> for Layers {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Vec::<Layer>::deserialize(d).map(Layers::from)
}
}
+4 -2
View File
@@ -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,
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -184,7 +184,7 @@ impl Psd {
r.skip(c.len)?;
}
}
self.layers = layers;
self.layers = layers.into();
Ok(())
}
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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()
}
+4 -4
View File
@@ -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);
+1 -1
View File
@@ -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));
+13 -13
View File
@@ -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)
}
+2 -2
View File
@@ -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);
+14 -14
View File
@@ -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)
};
+2 -2
View File
@@ -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)
}
+3 -3
View File
@@ -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);
+1 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
+1
View File
@@ -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)
+1 -1
View File
@@ -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),
+2 -2
View File
@@ -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::<LayerId>(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.
+1 -1
View File
@@ -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<u64>,
t: &Tokens,
id: egui::Id,
+1 -1
View File
@@ -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:?}");
+1 -1
View File
@@ -70,7 +70,7 @@ fn choose(h: &mut Harness<'_, EffectcraftApp>, kind: PrimitiveKind) {
}
fn layers(h: &Harness<'_, EffectcraftApp>) -> Vec<Layer> {
h.state().session.active_comp().unwrap().layers.clone()
h.state().session.active_comp().unwrap().layers.to_vec()
}
fn renderer(h: &Harness<'_, EffectcraftApp>) -> Renderer {