//! Layer menu: quality / sampling / frame blending, switches, transform dialogs, masks on existing //! layers, markers, track matte shortcuts, open/reveal, and Keyframe Assistant ▸ Sequence Layers. use effectcraft_keyframe::{Keyframe, ShapePath, Value as KV}; use effectcraft_project::{FrameBlend, GroupKind, ItemId, LayerId, LayerSource, Marker, MaskMode, MatteKind, Node, Quality, Sampling, TrackMatte, Uid}; use effectcraft_time::Tick; use serde_json::{Value, json}; use super::{CommandSpec, b_p, bad, comp_id, f_p, frontend, has_comp, has_layers, layer_mut, layer_p, layers_p, merge_p, str_p}; use crate::{EngineError, Result, Session, cmd}; // ---------------------------------------------------------------- enablement fn has_masks(s: &Session) -> std::result::Result<(), String> { has_layers(s)?; let c = s.active_comp().ok_or("no composition")?; let any = s.state.selected_layers.iter().filter_map(|l| c.layer(*l)).any(|l| l.masks().is_some_and(|m| !m.children.is_empty())); if any { Ok(()) } else { Err("the selected layers have no masks".into()) } } fn has_markers(s: &Session) -> std::result::Result<(), String> { has_layers(s)?; let c = s.active_comp().ok_or("no composition")?; if s.state.selected_layers.iter().filter_map(|l| c.layer(*l)).any(|l| !l.markers.is_empty()) { Ok(()) } else { Err("the selected layers have no markers".into()) } } fn has_source_item(s: &Session) -> std::result::Result<(), String> { has_layers(s)?; let c = s.active_comp().ok_or("no composition")?; if s.state.selected_layers.iter().filter_map(|l| c.layer(*l)).any(|l| l.source.item().is_some()) { Ok(()) } else { Err("the selected layer has no source item".into()) } } fn has_footage_layer(s: &Session) -> std::result::Result<(), String> { has_layers(s)?; let c = s.active_comp().ok_or("no composition")?; let any = s.state.selected_layers.iter().filter_map(|l| c.layer(*l)).any(|l| matches!(l.source, LayerSource::Footage { .. })); if any { Ok(()) } else { Err("select a footage layer".into()) } } fn has_multiple_layers(s: &Session) -> std::result::Result<(), String> { has_comp(s)?; if s.state.selected_layers.len() >= 2 { Ok(()) } else { Err("select two or more layers".into()) } } // ---------------------------------------------------------------- quality / sampling / blending fn quality(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; let q = match str_p(p, "quality").unwrap_or("best").to_ascii_lowercase().as_str() { "best" => Quality::Best, "draft" => Quality::Draft, "wireframe" => Quality::Wireframe, 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)) { l.switches.quality = q; } Ok(()) })?; Ok(Value::Null) } fn sampling(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; let q = match str_p(p, "sampling").unwrap_or("bilinear").to_ascii_lowercase().as_str() { "bilinear" => Sampling::Bilinear, "bicubic" => Sampling::Bicubic, q => return Err(bad("layer.sampling", format!("sampling must be bilinear|bicubic, not `{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)) { l.switches.sampling = q; } Ok(()) })?; Ok(Value::Null) } fn frame_blending(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; let m = match str_p(p, "mode").unwrap_or("off") { "off" => FrameBlend::Off, "frameMix" => FrameBlend::FrameMix, "pixelMotion" => FrameBlend::PixelMotion, 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)) { l.switches.frame_blend = m; } Ok(()) })?; Ok(Value::Null) } // ---------------------------------------------------------------- switches fn hide_other_video(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; s.edit("Hide Other Video", None, |proj, _| { for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut() { if !ids.contains(&l.id) && l.source.is_av() { l.switches.video = false; } } Ok(()) })?; Ok(Value::Null) } fn show_all_video(s: &mut Session, p: &Value) -> Result { let cid = comp_id(s, p)?; s.edit("Show All Video", None, |proj, _| { 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)) { l.switches.video = true; } Ok(()) })?; Ok(Value::Null) } fn unlock_all(s: &mut Session, p: &Value) -> Result { let cid = comp_id(s, p)?; s.edit("Unlock All Layers", None, |proj, _| { for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut() { l.switches.locked = false; } Ok(()) })?; Ok(Value::Null) } fn expressions(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; 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)) { l.props.walk_mut(&mut |pr| { if let Some(e) = &mut pr.expr { e.enabled = on; n += 1; } }); } Ok(n) })?; Ok(json!({"expressions": n})) } // ---------------------------------------------------------------- transform fn transform_member(prop: &str) -> Option<&'static str> { Some(match prop { "anchor" | "anchorPoint" => "anchor", "position" => "position", "scale" => "scale", "orientation" => "orientation", "rotation" => "rotation", "opacity" => "opacity", _ => return None, }) } fn set_transform(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; let prop = str_p(p, "prop").ok_or_else(|| bad("layer.setTransform", "missing `prop`"))?; let m = transform_member(prop).ok_or_else(|| bad("layer.setTransform", "prop: anchor|position|scale|orientation|rotation|opacity"))?; let v = p.get("value").ok_or_else(|| bad("layer.setTransform", "missing `value`"))?.clone(); let t = s.time(); let label = match m { "anchor" => "Anchor Point", "position" => "Position", "scale" => "Scale", "orientation" => "Orientation", "rotation" => "Rotation", _ => "Opacity", }; s.edit(label, merge_p(p), |proj, _| { for lid in &ids { let l = layer_mut(proj, cid, *lid)?; let lt = l.layer_time(t); let Some(pr) = l.transform_mut().and_then(|tr| tr.get_mut(m)) else { continue }; let cur = pr.value_at(lt); // Two numbers for a 3D value keep the current z. let j = match (&cur, &v) { (KV::Vec3(c), Value::Array(a)) if a.len() == 2 => json!([a[0], a[1], c[2]]), _ => v.clone(), }; let nv = cur.coerce_json(&j).ok_or_else(|| bad("layer.setTransform", format!("can't use {v} for {label}")))?; pr.set_value_at(lt, nv); } Ok(()) })?; Ok(Value::Null) } fn center_anchor(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; let t = s.time(); let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?; let ctx = effectcraft_render::EvalCtx::new(&s.project, cid, comp, t); let mut centers = vec![]; for lid in &ids { let Some(l) = comp.layer(*lid) else { continue }; if let Some([x0, y0, x1, y1]) = effectcraft_render::content_bounds(&ctx, l) { centers.push((*lid, [(x0 + x1) / 2.0, (y0 + y1) / 2.0])); } } s.edit("Center Anchor Point", None, |proj, _| { for (lid, c) in ¢ers { let l = layer_mut(proj, cid, *lid)?; let lt = l.layer_time(t); let Some(tr) = l.transform_mut() else { continue }; let get = |tr: &effectcraft_project::PropGroup, m: &str, d: [f64; 3]| tr.get(m).map(|p| p.value_at(lt).as_vec3()).unwrap_or(d); let anchor = get(tr, "anchor", [0.0; 3]); let scale = get(tr, "scale", [100.0; 3]); let rot = tr.get("rotation").map(|p| p.value_at(lt).as_f64()).unwrap_or(0.0).to_radians(); let (dx, dy) = ((c[0] - anchor[0]) * scale[0] / 100.0, (c[1] - anchor[1]) * scale[1] / 100.0); let (sn, cs) = rot.sin_cos(); let shift = [dx * cs - dy * sn, dx * sn + dy * cs]; if let Some(a) = tr.get_mut("anchor") { a.set_value_at(lt, KV::Vec3([c[0], c[1], anchor[2]])); } if let Some(pos) = tr.get_mut("position") { // Shift every key so the layer stays put over its whole animation. if pos.keys.is_empty() { let v = pos.value.as_vec3(); pos.value = KV::Vec3([v[0] + shift[0], v[1] + shift[1], v[2]]); } else { for k in &mut pos.keys { let v = k.value.as_vec3(); k.value = KV::Vec3([v[0] + shift[0], v[1] + shift[1], v[2]]); } } } } Ok(()) })?; Ok(json!({"layers": centers.len()})) } // ---------------------------------------------------------------- masks /// Masks the command applies to: `mask` (index / uid / name) on `layer`, else selected masks, /// else every mask of the selected layers. pub(crate) fn target_masks(s: &Session, p: &Value) -> Result<(ItemId, Vec<(LayerId, Uid)>)> { let (cid, ids) = layers_p(s, p)?; let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?; let mut out = vec![]; if let Some(m) = p.get("mask") { for lid in &ids { let Some(masks) = comp.layer(*lid).and_then(|l| l.masks()) else { continue }; let g = match m { Value::Number(n) => { let n = n.as_u64().unwrap_or(1); masks.children.iter().find(|c| c.uid() == n).or_else(|| masks.children.get(n.saturating_sub(1) as usize)) } Value::String(name) => masks.children.iter().find(|c| c.name() == name), _ => None, }; if let Some(g) = g { out.push((*lid, g.uid())); } } return Ok((cid, out)); } for (lid, uid) in &s.state.selected_props { let Some(masks) = comp.layer(*lid).and_then(|l| l.masks()) else { continue }; for g in masks.groups() { if g.uid == *uid || g.find(*uid).is_some() { out.push((*lid, g.uid)); } } } if out.is_empty() { for lid in &ids { if let Some(masks) = comp.layer(*lid).and_then(|l| l.masks()) { out.extend(masks.groups().map(|g| (*lid, g.uid))); } } } out.dedup(); Ok((cid, out)) } fn edit_masks( s: &mut Session, p: &Value, label: &str, mut f: impl FnMut(&mut effectcraft_project::PropGroup, (f64, f64), Tick) -> Result<()>, ) -> Result { let (cid, targets) = target_masks(s, p)?; if targets.is_empty() { return Err(EngineError::Other("no masks".into())); } let t = s.time(); let sizes: Vec<(LayerId, (f64, f64))> = targets .iter() .filter_map(|(lid, _)| s.project.comp(cid)?.layer(*lid).map(|l| (*lid, effectcraft_render::source_size(&s.project, l)))) .map(|(l, (w, h))| (l, (w as f64, h as f64))) .collect(); let n = targets.len(); s.edit(label, merge_p(p), |proj, _| { for (lid, uid) in &targets { let size = sizes.iter().find(|(l, _)| l == lid).map(|x| x.1).unwrap_or((0.0, 0.0)); let l = layer_mut(proj, cid, *lid)?; let lt = l.layer_time(t); let Some(g) = l.props.sub_mut("masks").and_then(|m| m.find_group_mut(*uid)) else { continue }; f(g, size, lt)?; } Ok(()) })?; Ok(json!({"masks": n})) } fn mask_kind(g: &mut effectcraft_project::PropGroup) -> Option<(&mut MaskMode, &mut bool, &mut bool)> { match &mut g.kind { GroupKind::Mask { mode, inverted, locked, .. } => Some((mode, inverted, locked)), _ => None, } } fn mask_set(s: &mut Session, p: &Value) -> Result { let field = str_p(p, "field").ok_or_else(|| bad("layer.mask.set", "missing `field` (feather|opacity|expansion)"))?.to_string(); if !matches!(field.as_str(), "feather" | "opacity" | "expansion") { return Err(bad("layer.mask.set", "field: feather|opacity|expansion")); } let v = p.get("value").ok_or_else(|| bad("layer.mask.set", "missing `value`"))?.clone(); let label = match field.as_str() { "feather" => "Mask Feather", "opacity" => "Mask Opacity", _ => "Mask Expansion", }; edit_masks(s, p, label, |g, _, lt| { let pr = g.get_mut(&field).ok_or_else(|| bad("layer.mask.set", "mask has no such property"))?; let cur = pr.value_at(lt); let j = match (&cur, &v) { (KV::Vec2(_), Value::Number(n)) => json!([n, n]), _ => v.clone(), }; let nv = cur.coerce_json(&j).ok_or_else(|| bad("layer.mask.set", format!("can't use {v} for {label}")))?; pr.set_value_at(lt, nv); Ok(()) }) } fn mask_shape(s: &mut Session, p: &Value) -> Result { let r = p.get("rect").and_then(Value::as_array).filter(|a| a.len() == 4).map(|a| [0, 1, 2, 3].map(|i| a[i].as_f64().unwrap_or(0.0))); let r = r.ok_or_else(|| bad("layer.mask.shape", "missing `rect` [left, top, width, height] (layer px)"))?; let ellipse = str_p(p, "shape") == Some("ellipse"); edit_masks(s, p, "Mask Shape", |g, _, lt| { let (cx, cy) = (r[0] + r[2] / 2.0, r[1] + r[3] / 2.0); let path = if ellipse { ShapePath::ellipse([cx, cy], r[2], r[3]) } else { ShapePath::rect([cx, cy], r[2], r[3]) }; if let Some(pr) = g.get_mut("path") { pr.set_value_at(lt, KV::Path(path)); } Ok(()) }) } fn mask_reset(s: &mut Session, p: &Value) -> Result { edit_masks(s, p, "Reset Mask", |g, (w, h), _| { let (w, h) = if w > 0.0 { (w, h) } else { (400.0, 300.0) }; let reset = |g: &mut effectcraft_project::PropGroup, m: &str, v: KV| { if let Some(pr) = g.get_mut(m) { pr.keys.clear(); pr.expr = None; pr.value = v; } }; reset(g, "path", KV::Path(ShapePath::rect([w / 2.0, h / 2.0], w, h))); reset(g, "feather", KV::Vec2([0.0, 0.0])); reset(g, "opacity", KV::Scalar(100.0)); reset(g, "expansion", KV::Scalar(0.0)); if let Some((mode, inv, _)) = mask_kind(g) { *mode = MaskMode::Add; *inv = false; } Ok(()) }) } fn mask_remove(s: &mut Session, p: &Value) -> Result { let (cid, targets) = target_masks(s, p)?; // Without an explicit `mask`, Remove Mask only removes selected masks (AE), else the first. let selected: Vec<(LayerId, Uid)> = targets .iter() .copied() .filter(|(l, u)| { p.get("mask").is_some() || s.state.selected_props.iter().any(|(sl, su)| { sl == l && (su == u || s.active_comp().and_then(|c| c.layer(*l)).and_then(|ly| ly.props.find_group(*u)).is_some_and(|g| g.find(*su).is_some())) }) }) .collect(); let remove = if selected.is_empty() { targets.into_iter().take(1).collect() } else { selected }; remove_masks(s, cid, remove, "Remove Mask") } fn remove_masks(s: &mut Session, cid: ItemId, remove: Vec<(LayerId, Uid)>, label: &str) -> Result { let n = remove.len(); s.edit(label, None, |proj, st| { for (lid, uid) in &remove { if let Some(m) = layer_mut(proj, cid, *lid)?.props.sub_mut("masks") { m.children.retain(|c| c.uid() != *uid); } } st.selected_props.retain(|(l, u)| !remove.iter().any(|(rl, ru)| rl == l && ru == u)); Ok(()) })?; Ok(json!({"removed": n})) } fn mask_remove_all(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?; let all: Vec<(LayerId, Uid)> = ids.iter().filter_map(|l| comp.layer(*l)).flat_map(|l| l.masks().into_iter().flat_map(|m| m.groups().map(|g| (l.id, g.uid)))).collect(); remove_masks(s, cid, all, "Remove All Masks") } fn mask_mode(s: &mut Session, p: &Value) -> Result { let m = str_p(p, "mode").and_then(MaskMode::from_name).ok_or_else(|| bad("layer.mask.mode", "mode: None|Add|Subtract|Intersect|Lighten|Darken|Difference"))?; edit_masks(s, p, "Mask Mode", |g, _, _| { if let Some((mode, ..)) = mask_kind(g) { *mode = m; } Ok(()) }) } fn mask_invert(s: &mut Session, p: &Value) -> Result { let v = b_p(p, "value"); edit_masks(s, p, "Mask Inverted", |g, _, _| { if let Some((_, inv, _)) = mask_kind(g) { *inv = v.unwrap_or(!*inv); } Ok(()) }) } fn mask_lock(s: &mut Session, p: &Value) -> Result { let v = b_p(p, "value"); edit_masks(s, p, "Mask Locked", |g, _, _| { if let Some((_, _, locked)) = mask_kind(g) { *locked = v.unwrap_or(!*locked); } Ok(()) }) } fn mask_unlock_all(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; s.edit("Unlock All Masks", None, |proj, _| { for lid in &ids { if let Some(m) = layer_mut(proj, cid, *lid)?.props.sub_mut("masks") { for c in &mut m.children { if let Node::Group(g) = c && let Some((_, _, locked)) = mask_kind(g) { *locked = false; } } } } Ok(()) })?; Ok(Value::Null) } fn mask_lock_others(s: &mut Session, p: &Value) -> Result { let (cid, keep) = target_masks(s, p)?; let layers: Vec = keep.iter().map(|(l, _)| *l).collect(); s.edit("Lock Other Masks", None, |proj, _| { for lid in &layers { if let Some(m) = layer_mut(proj, cid, *lid)?.props.sub_mut("masks") { for c in &mut m.children { let uid = c.uid(); if let Node::Group(g) = c && let Some((_, _, locked)) = mask_kind(g) { *locked = !keep.contains(&(*lid, uid)); } } } } Ok(()) })?; Ok(Value::Null) } // ---------------------------------------------------------------- markers fn add_marker(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; if ids.is_empty() { return Err(bad("layer.addMarker", "no layer")); } let t = f_p(p, "time").map(Tick::from_seconds_f64).unwrap_or(s.time()); let comment = str_p(p, "comment").unwrap_or_default().to_string(); s.edit("Add Marker", None, |proj, _| { for lid in &ids { let l = layer_mut(proj, cid, *lid)?; if l.markers_locked { continue; } // Layer markers are stored in layer time. let lt = l.layer_time(t); l.markers.retain(|m| m.time != lt); l.markers.push(Marker { time: lt, comment: comment.clone(), ..Default::default() }); l.markers.sort_by_key(|m| m.time); } Ok(()) })?; Ok(Value::Null) } fn markers_lock(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; let v = b_p(p, "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)) { l.markers_locked = target; } Ok(target) })?; Ok(json!(r)) } 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) { l.markers.clear(); } Ok(()) })?; Ok(Value::Null) } // ---------------------------------------------------------------- track matte fn track_matte_op(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; let op = str_p(p, "op").ok_or_else(|| bad("layer.trackMatte", "missing `op`"))?.to_string(); let kind = match op.as_str() { "alpha" => Some(MatteKind::Alpha), "alphaInverted" => Some(MatteKind::AlphaInverted), "luma" => Some(MatteKind::Luma), "lumaInverted" => Some(MatteKind::LumaInverted), "none" | "above" | "below" => None, o => return Err(bad("layer.trackMatte", format!("op: none|alpha|alphaInverted|luma|lumaInverted|above|below, not `{o}`"))), }; s.edit("Track Matte", None, |proj, _| { let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; let order: Vec = c.layers.iter().map(|l| l.id).collect(); let mut hide = vec![]; for lid in &ids { let i = order.iter().position(|x| x == lid).unwrap_or(0); let l = c.layer_mut(*lid).ok_or(EngineError::NoComp)?; let cur_kind = l.track_matte.map(|m| m.kind).unwrap_or_default(); l.track_matte = match op.as_str() { "none" => None, "above" | "below" => { let j = if op == "above" { i.checked_sub(1) } else { Some(i + 1) }; match j.and_then(|j| order.get(j)) { Some(m) => Some(TrackMatte { layer: *m, kind: cur_kind }), None => return Err(bad("layer.trackMatte", format!("there is no layer {op}"))), } } _ => match (l.track_matte, i.checked_sub(1).and_then(|j| order.get(j))) { (Some(m), _) => Some(TrackMatte { layer: m.layer, kind: kind.unwrap_or_default() }), (None, Some(above)) => Some(TrackMatte { layer: *above, kind: kind.unwrap_or_default() }), (None, None) => return Err(bad("layer.trackMatte", "the top layer has no layer above to use as a matte")), }, }; if let Some(m) = l.track_matte { hide.push(m.layer); } } for m in hide { if let Some(ml) = c.layer_mut(m) { ml.switches.video = false; } } Ok(()) })?; Ok(Value::Null) } // ---------------------------------------------------------------- open / reveal fn source_item(s: &Session, p: &Value, cmd: &str) -> Result { let (cid, lid) = layer_p(s, p, cmd)?; s.project.comp(cid).and_then(|c| c.layer(lid)).and_then(|l| l.source.item()).ok_or_else(|| bad(cmd, "the layer has no source item")) } fn open_source(s: &mut Session, p: &Value) -> Result { let item = source_item(s, p, "layer.openSource")?; if s.project.comp(item).is_some() { s.open_comp(item); return Ok(json!({"comp": item.0})); } frontend(s, "window.panel", &json!({"panel": "layer"})) } /// Folder containing a file as a `file://` URL. pub(crate) fn folder_url(path: &str) -> String { let dir = std::path::Path::new(path).parent().map(|d| d.to_string_lossy().to_string()).unwrap_or_default(); format!("file://{dir}") } fn reveal_in_finder(s: &mut Session, p: &Value) -> Result { let item = source_item(s, p, "layer.revealInFinder")?; let path = match s.project.item(item).map(|i| &i.kind) { Some(effectcraft_project::ItemKind::Footage(f)) if !f.path.is_empty() => f.path.clone(), _ => return Err(bad("layer.revealInFinder", "the layer's source is not a file")), }; let url = folder_url(&path); s.events.push(crate::Event::OpenUrl(url.clone())); Ok(json!({"url": url})) } fn reveal_source(s: &mut Session, p: &Value) -> Result { let item = source_item(s, p, "layer.revealSource")?; s.state.project_selection = vec![item]; frontend(s, "window.panel", &json!({"panel": "project"}))?; Ok(json!({"item": item.0})) } fn reveal_comp(s: &mut Session, p: &Value) -> Result { let cid = comp_id(s, p)?; s.state.project_selection = vec![cid]; frontend(s, "window.panel", &json!({"panel": "project"}))?; Ok(json!({"item": cid.0})) } fn reveal_expression_errors(s: &mut Session, p: &Value) -> Result { let cid = comp_id(s, p)?; let t = s.time(); let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?; let mut errors = vec![]; for l in &comp.layers { let mut has = vec![]; l.props.walk("", &mut |_, pr| { if pr.has_expression() { has.push(pr); } }); for pr in has { let text = pr.expr.as_ref().map(|e| e.text.clone()).unwrap_or_default(); let err = match &s.expr { Some(host) => { let mut ctx = effectcraft_render::EvalCtx::new(&s.project, cid, comp, t); ctx.expr = Some(host.as_ref()); host.eval(&ctx, l, pr, &pr.value_at(l.layer_time(t))).err() } None => None, }; if let Some(e) = err { errors.push(json!({"layer": l.id.0, "prop": pr.uid, "name": pr.name, "expression": text, "error": e})); } } } let props: Vec = errors.iter().map(|e| json!({"layer": e["layer"], "prop": e["prop"]})).collect(); frontend(s, "timeline.revealProps", &json!({"props": props}))?; if errors.is_empty() { s.toast("No expression errors"); } Ok(json!({"errors": errors})) } // ---------------------------------------------------------------- sequence layers fn sequence(s: &mut Session, p: &Value) -> Result { let cid = comp_id(s, p)?; // Selection order (AE sequences in the order layers were selected). let ids: Vec = match p.get("layers") { Some(_) => layers_p(s, p)?.1, None => s.state.selected_layers.clone(), }; if ids.len() < 2 { return Err(bad("layer.sequence", "select two or more layers")); } let overlap = b_p(p, "overlap").unwrap_or(false); let dur = if overlap { Tick::from_seconds_f64(f_p(p, "duration").unwrap_or(1.0)) } else { Tick::ZERO }; let transition = str_p(p, "transition").unwrap_or("off").to_string(); s.edit("Sequence Layers", None, |proj, _| { let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; let mut next: Option = None; for (k, lid) in ids.iter().enumerate() { let l = c.layer_mut(*lid).ok_or(EngineError::NoComp)?; if let Some(at) = next { let d = at - l.in_point; l.start_time += d; l.in_point += d; l.out_point += d; } if overlap && transition != "off" { let (a, b) = (l.layer_time(l.in_point), l.layer_time(l.in_point + dur)); let (oa, ob) = (l.layer_time(l.out_point - dur), l.layer_time(l.out_point)); let last = k + 1 == ids.len(); if let Some(op) = l.transform_mut().and_then(|tr| tr.get_mut("opacity")) { op.keys.clear(); if k > 0 { op.keys.push(Keyframe::new(a, KV::Scalar(0.0))); op.keys.push(Keyframe::new(b, KV::Scalar(100.0))); } if transition == "crossDissolve" && !last { op.keys.push(Keyframe::new(oa, KV::Scalar(100.0))); op.keys.push(Keyframe::new(ob, KV::Scalar(0.0))); } op.keys.sort_by_key(|k| k.time); op.keys.dedup_by_key(|k| k.time); } } next = Some(l.out_point - dur); } Ok(()) })?; Ok(Value::Null) } pub fn specs() -> Vec { vec![ cmd!("layer.quality", "Quality", [], None, "{layers?, quality: best|draft|wireframe}", has_layers, quality), cmd!("layer.sampling", "Sampling", [], None, "{layers?, sampling: bilinear|bicubic}", has_layers, sampling), cmd!("layer.frameBlending", "Frame Blending", [], None, "{layers?, mode: off|frameMix|pixelMotion}", has_layers, frame_blending), cmd!("layer.hideOtherVideo", "Hide Other Video", ["Layer", "Switches"], Some("Cmd+Alt+Shift+V"), "{layers?}", has_layers, hide_other_video), cmd!("layer.showAllVideo", "Show All Video", ["Layer", "Switches"], None, "{comp?}", has_comp, show_all_video), cmd!("layer.unlockAll", "Unlock All Layers", ["Layer", "Switches"], Some("Cmd+Shift+L"), "{comp?}", has_comp, unlock_all), cmd!("layer.expressions", "Enable/Disable Expressions", [], None, "{layers?, enabled: bool}", has_layers, expressions), cmd!( "layer.setTransform", "Transform Value", [], None, "{layers?, prop: anchor|position|scale|orientation|rotation|opacity, value}", has_layers, set_transform ), cmd!( "layer.centerAnchor", "Center Anchor Point in Layer Content", ["Layer", "Transform"], Some("Cmd+Alt+Home"), "{layers?}", has_layers, center_anchor ), cmd!( "layer.mask.shape", "Mask Shape...", ["Layer", "Mask"], Some("Cmd+Shift+M"), "{layer?, mask?, rect: [x, y, w, h], shape?: rect|ellipse}", has_masks, mask_shape ), cmd!("layer.mask.set", "Mask Settings", [], None, "{layer?, mask?, field: feather|opacity|expansion, value}", has_masks, mask_set), cmd!("layer.mask.reset", "Reset Mask", ["Layer", "Mask"], None, "{layer?, mask?}", has_masks, mask_reset), cmd!("layer.mask.remove", "Remove Mask", ["Layer", "Mask"], None, "{layer?, mask?}", has_masks, mask_remove), cmd!("layer.mask.removeAll", "Remove All Masks", ["Layer", "Mask"], None, "{layers?}", has_masks, mask_remove_all), cmd!("layer.mask.mode", "Mask Mode", [], None, "{layer?, mask?, mode: None|Add|Subtract|Intersect|Lighten|Darken|Difference}", has_masks, mask_mode), cmd!("layer.mask.invert", "Inverted", ["Layer", "Mask"], Some("Cmd+Shift+I"), "{layer?, mask?, value?}", has_masks, mask_invert), cmd!("layer.mask.lock", "Locked", ["Layer", "Mask"], None, "{layer?, mask?, value?}", has_masks, mask_lock), cmd!("layer.mask.unlockAll", "Unlock All Masks", ["Layer", "Mask"], None, "{layers?}", has_masks, mask_unlock_all), cmd!("layer.mask.lockOthers", "Lock Other Masks", ["Layer", "Mask"], None, "{layer?, mask?}", has_masks, mask_lock_others), cmd!("layer.addMarker", "Add Marker", ["Layer", "Markers"], None, "{layers?, time?, comment?}", has_layers, add_marker), cmd!("layer.markersLock", "Lock Markers", ["Layer", "Markers"], None, "{layers?, value?}", has_layers, markers_lock), cmd!("layer.deleteAllMarkers", "Delete All Markers", ["Layer", "Markers"], None, "{layers?}", has_markers, delete_all_markers), cmd!("layer.trackMatte", "Track Matte", [], None, "{layers?, op: none|alpha|alphaInverted|luma|lumaInverted|above|below}", has_layers, track_matte_op), cmd!("layer.openSource", "Open Layer Source", ["Layer"], None, "{layer?}", has_source_item, open_source), cmd!("layer.revealInFinder", "Reveal in Finder", ["Layer"], None, "{layer?}", has_footage_layer, reveal_in_finder), cmd!("layer.revealSource", "Reveal Layer Source in Project", ["Layer", "Reveal"], None, "{layer?}", has_source_item, reveal_source), cmd!("comp.revealInProject", "Reveal Composition in Project", ["Layer", "Reveal"], None, "{comp?}", has_comp, reveal_comp), cmd!("layer.revealExpressionErrors", "Reveal Expression Errors", ["Layer", "Reveal"], None, "{comp?}", has_comp, reveal_expression_errors), cmd!( "layer.sequence", "Sequence Layers...", ["Animation", "Keyframe Assistant"], None, "{layers? (in order), overlap?: bool, duration? (s), transition?: off|dissolveFront|crossDissolve}", has_multiple_layers, sequence ), ] }