mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 14:08:38 -06:00
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
@@ -56,6 +56,10 @@ fn compatible(a: &Property, b: &effectcraft_keyframe::Value) -> bool {
|
||||
a.value.kind_name() == b.kind_name() && a.value.components().len() == b.components().len()
|
||||
}
|
||||
|
||||
pub(crate) fn locked_selection(comp: &effectcraft_project::Comp, selected: &[KeyRef]) -> Vec<KeyRef> {
|
||||
selected.iter().filter(|k| comp.layer(k.layer).is_some_and(|l| l.switches.locked)).copied().collect()
|
||||
}
|
||||
|
||||
/// Paste copied keyframes at the current time into the target layers' properties.
|
||||
fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let clip = s.state.key_clipboard.clone();
|
||||
@@ -90,6 +94,9 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut plan: Vec<(LayerId, Uid, usize)> = vec![];
|
||||
for (ti, lid) in targets.iter().enumerate() {
|
||||
let Some(l) = comp.layer(*lid) else { continue };
|
||||
if l.switches.locked {
|
||||
continue;
|
||||
}
|
||||
for (ci, c) in clip.iter().enumerate() {
|
||||
if paired && sources.get(ti) != Some(&c.layer) {
|
||||
continue;
|
||||
@@ -111,11 +118,12 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
if plan.is_empty() {
|
||||
return Err(bad("keys.paste", "no compatible property to paste into (select a property of the same type)"));
|
||||
return Err(bad("keys.paste", "no compatible unlocked property to paste into (unlock the target or select a property of the same type)"));
|
||||
}
|
||||
s.edit("Paste Keyframes", merge_p(p), |proj, st| {
|
||||
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
||||
let mut sel = vec![];
|
||||
let mut sel = locked_selection(comp, &st.selected_keys);
|
||||
let retained = sel.len();
|
||||
for (lid, uid, ci) in &plan {
|
||||
let Some(l) = comp.layer_mut(*lid) else { continue };
|
||||
let times: Vec<Tick> = clip[*ci].keys.iter().map(|k| l.layer_time(t + k.time)).collect();
|
||||
@@ -137,8 +145,9 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
retime_roving(&mut pr.keys, pr.spatial);
|
||||
}
|
||||
let pasted = sel.len().saturating_sub(retained);
|
||||
st.selected_keys = sel;
|
||||
Ok(json!(st.selected_keys.len()))
|
||||
Ok(json!(pasted))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -176,6 +185,9 @@ pub(super) fn paste_path(s: &mut Session, p: &Value, (cid, lid, uid): (ItemId, L
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let t = time_p(s, p, Some(comp));
|
||||
let target = comp.layer(lid).ok_or(EngineError::NoComp)?;
|
||||
if target.switches.locked {
|
||||
return Err(bad("keys.paste", "the target layer is locked"));
|
||||
}
|
||||
let mut order: Vec<usize> = (0..path.vertices.len()).collect();
|
||||
if path.closed && !order.is_empty() {
|
||||
order.push(0);
|
||||
@@ -203,6 +215,9 @@ pub(super) fn paste_path(s: &mut Session, p: &Value, (cid, lid, uid): (ItemId, L
|
||||
})
|
||||
.collect();
|
||||
s.edit("Paste Keyframes", merge_p(p), |proj, st| {
|
||||
let comp = proj.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let mut selected = locked_selection(comp, &st.selected_keys);
|
||||
let retained = selected.len();
|
||||
let l = super::layer_mut(proj, cid, lid)?;
|
||||
let pr = l.props.find_mut(uid).ok_or_else(|| bad("keys.paste", "no property"))?;
|
||||
let mut sel = vec![];
|
||||
@@ -217,8 +232,10 @@ pub(super) fn paste_path(s: &mut Session, p: &Value, (cid, lid, uid): (ItemId, L
|
||||
// Re-timing keeps the keys' order, so the pasted keys are found again by index.
|
||||
let pasted: Vec<usize> = pr.keys.iter().enumerate().filter(|(_, k)| sel.contains(&k.time)).map(|(i, _)| i).collect();
|
||||
retime_roving(&mut pr.keys, pr.spatial);
|
||||
st.selected_keys = pasted.iter().filter_map(|&i| pr.keys.get(i)).map(|k| KeyRef { layer: lid, prop: uid, time: k.time }).collect();
|
||||
Ok(json!(st.selected_keys.len()))
|
||||
selected.extend(pasted.iter().filter_map(|&i| pr.keys.get(i)).map(|k| KeyRef { layer: lid, prop: uid, time: k.time }));
|
||||
let count = selected.len().saturating_sub(retained);
|
||||
st.selected_keys = selected;
|
||||
Ok(json!(count))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -297,6 +314,9 @@ fn set_key(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let base = KeyTimeBase::from_params(p, "keys.set")?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
|
||||
if layer.switches.locked {
|
||||
return Err(bad("keys.set", "the layer is locked"));
|
||||
}
|
||||
let t = key_time(layer, comp.frame_rate, p, "time", base, "keys.set", false)?.ok_or_else(|| bad("keys.set", "missing `time` (layer time by default)"))?;
|
||||
let new_t = key_time(layer, comp.frame_rate, p, "newTime", base, "keys.set", true)?;
|
||||
let value = p.get("value").cloned();
|
||||
@@ -350,6 +370,9 @@ fn set_ease(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let influence = f_p(p, "influence").map(|v| (v / 100.0).clamp(0.001, 1.0));
|
||||
s.edit("Keyframe Velocity", merge_p(p), |proj, _| {
|
||||
let l = super::layer_mut(proj, cid, lid)?;
|
||||
if l.switches.locked {
|
||||
return Err(bad("keys.setEase", "the layer is locked"));
|
||||
}
|
||||
let pr = l.props.find_mut(uid).ok_or_else(|| bad("keys.setEase", "no property"))?;
|
||||
let i = nearest_key(&pr.keys, t).ok_or_else(|| bad("keys.setEase", "no keyframes"))?;
|
||||
let spatial = pr.spatial;
|
||||
@@ -496,7 +519,13 @@ fn time_reverse(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
let mut new_sel = vec![];
|
||||
for ((lid, uid), times) in groups {
|
||||
let Some(pr) = comp.layer_mut(lid).and_then(|l| l.props.find_mut(uid)) else { continue };
|
||||
let Some(layer) = comp.layer_mut(lid) else { continue };
|
||||
// A locked layer's keys stay as they are, and stay selected.
|
||||
if layer.switches.locked {
|
||||
new_sel.extend(times.iter().map(|time| KeyRef { layer: lid, prop: uid, time: *time }));
|
||||
continue;
|
||||
}
|
||||
let Some(pr) = layer.props.find_mut(uid) else { continue };
|
||||
let (mut picked, rest): (Vec<Keyframe>, Vec<Keyframe>) = pr.keys.drain(..).partition(|k| times.contains(&k.time));
|
||||
effectcraft_keyframe::time_reverse(&mut picked);
|
||||
new_sel.extend(picked.iter().map(|k| KeyRef { layer: lid, prop: uid, time: k.time }));
|
||||
|
||||
@@ -213,7 +213,8 @@ fn sample_curve(
|
||||
|
||||
fn grouped_selection(s: &Session) -> std::collections::BTreeMap<(LayerId, Uid), Vec<Tick>> {
|
||||
let mut g: std::collections::BTreeMap<(LayerId, Uid), Vec<Tick>> = Default::default();
|
||||
for k in &s.state.selected_keys {
|
||||
let Some(comp) = s.active_comp() else { return g };
|
||||
for k in super::prop::editable_keys(comp, &s.state.selected_keys) {
|
||||
g.entry((k.layer, k.prop)).or_default().push(k.time);
|
||||
}
|
||||
for v in g.values_mut() {
|
||||
@@ -273,7 +274,7 @@ fn run_on_selection(
|
||||
s.edit(label, None, |proj, st| {
|
||||
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
||||
let fr = comp.frame_rate;
|
||||
let mut sel = vec![];
|
||||
let mut sel = super::anim::locked_selection(comp, &st.selected_keys);
|
||||
for ((lid, uid), times) in groups {
|
||||
let Some(l) = comp.layer_mut(lid) else { continue };
|
||||
let lc = l.clone();
|
||||
|
||||
@@ -271,7 +271,8 @@ fn add_keyframe(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
/// Selected keys grouped by (layer, property).
|
||||
fn keys_by_prop(s: &Session) -> BTreeMap<(LayerId, Uid), Vec<Tick>> {
|
||||
let mut m: BTreeMap<(LayerId, Uid), Vec<Tick>> = BTreeMap::new();
|
||||
for k in &s.state.selected_keys {
|
||||
let Some(comp) = s.active_comp() else { return m };
|
||||
for k in super::prop::editable_keys(comp, &s.state.selected_keys) {
|
||||
m.entry((k.layer, k.prop)).or_default().push(k.time);
|
||||
}
|
||||
m
|
||||
|
||||
@@ -6,7 +6,7 @@ use effectcraft_project::{Comp, Expression, ItemId, ItemKind, Layer, LayerId, La
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, has_comp, has_layers, layers_p, match_path_of, selected_leaf_props, str_p};
|
||||
use super::{CommandSpec, has_comp, has_layers, layers_or, layers_p, match_path_of, selected_leaf_props, str_p};
|
||||
use crate::{EngineError, KeyRef, LinkClip, Result, Session, cmd};
|
||||
|
||||
fn can_undo(s: &Session) -> std::result::Result<(), String> {
|
||||
@@ -104,9 +104,16 @@ pub(crate) fn reid(layer: &mut Layer, next: &mut u64) {
|
||||
*next += 1;
|
||||
}
|
||||
|
||||
/// `edit.duplicate`: copies the targets in place. Without `layers` the targets are, in order:
|
||||
/// during a text edit the edited layer (the edit is committed first, as `text.endEdit` does),
|
||||
/// else the selected effects, shape items or text animators, else the selected layers. Layer
|
||||
/// copies are selected and their ids returned; the other paths return their delegated result.
|
||||
fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Effects selected → duplicate them (Edit ▸ Duplicate in Effect Controls).
|
||||
if p.get("layers").is_none() {
|
||||
let editing = s.state.text_edit.is_some();
|
||||
// Effects selected → duplicate them (Edit ▸ Duplicate in Effect Controls). An open text edit
|
||||
// comes first, as it does for Clear: opening the editor keeps the property selection, but
|
||||
// while typing, Duplicate means the edited layer, not an effect or item selected before.
|
||||
if p.get("layers").is_none() && !editing {
|
||||
let fx = super::effect::selected_effects(s);
|
||||
if !fx.is_empty() {
|
||||
let mut out = vec![];
|
||||
@@ -126,7 +133,17 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return super::prop_groups::duplicate_contents(s, &items);
|
||||
}
|
||||
}
|
||||
let (cid, ids) = layers_p(s, p)?;
|
||||
// A deselected layer never keeps an open text edit (#2226): the copies take the selection, so
|
||||
// commit an open edit first, exactly as `text.endEdit` and Deselect All do — one call on the
|
||||
// command path so every route (menu, shortcut, palette, MCP, script) ends the edit and leaves
|
||||
// the same final state. Unnamed, the target is the edited layer even when an engine route
|
||||
// selected another one meanwhile.
|
||||
let edited = s.state.text_edit.as_ref().map(|e| e.layer);
|
||||
super::text_edit::end_edit(s, &Value::Null)?;
|
||||
let (cid, ids) = match edited {
|
||||
Some(layer) => layers_or(s, p, &[layer])?,
|
||||
None => layers_p(s, p)?,
|
||||
};
|
||||
let new = s.edit("Duplicate", None, |proj, st| {
|
||||
let mut next = proj.next_id;
|
||||
let comp = proj.comp_mut(cid).ok_or(crate::EngineError::NoComp)?;
|
||||
@@ -143,14 +160,16 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
relink_copies(comp, &map);
|
||||
proj.next_id = next;
|
||||
st.selected_layers = created.clone();
|
||||
// The property, keyframe and mask-vertex selection the edit kept belongs to the originals;
|
||||
// left in place, the next Duplicate or Clear would act on it, not on the copies. So during
|
||||
// an edit the copies take the selection the way a click on them would.
|
||||
if editing {
|
||||
super::layer::select_only(st, created.clone());
|
||||
} else {
|
||||
st.selected_layers = created.clone();
|
||||
}
|
||||
Ok(created)
|
||||
})?;
|
||||
// Duplicating selects the copies, so an open text edit on a now-deselected layer ends
|
||||
// (#2226): typing must not keep editing an unselected layer.
|
||||
if s.state.text_edit.as_ref().is_some_and(|e| !s.state.selected_layers.contains(&e.layer)) {
|
||||
super::text_edit::end_edit(s, &Value::Null)?;
|
||||
}
|
||||
Ok(json!(new.iter().map(|l| l.0).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
@@ -968,7 +987,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
paste_reversed
|
||||
),
|
||||
cmd!("edit.clear", "Clear", ["Edit"], Some("Delete"), "{layers?}", layers_or_keys, delete),
|
||||
cmd!("edit.duplicate", "Duplicate", ["Edit"], Some("Cmd+D"), "{layers?} (effects or shape items when they are selected)", has_layers, duplicate),
|
||||
cmd!(
|
||||
"edit.duplicate",
|
||||
"Duplicate",
|
||||
["Edit"],
|
||||
Some("Cmd+D"),
|
||||
"{layers?} (without layers: during a text edit the edited layer, else the selected effects, shape items or text animators, else the selected layers)",
|
||||
layers_or_text_edit,
|
||||
duplicate
|
||||
),
|
||||
cmd!("edit.splitLayer", "Split Layer", ["Edit"], Some("Cmd+Shift+D"), "{layers?}", has_layers, split),
|
||||
cmd!("edit.liftWorkArea", "Lift Work Area", ["Edit"], None, "{layers?}", has_comp, lift),
|
||||
cmd!("edit.extractWorkArea", "Extract Work Area", ["Edit"], None, "{layers?}", has_comp, extract),
|
||||
@@ -1038,6 +1065,11 @@ fn layers_or_keys(s: &Session) -> std::result::Result<(), String> {
|
||||
if !s.state.selected_keys.is_empty() || !s.state.selected_vertices.is_empty() { super::has_comp(s) } else { has_layers(s) }
|
||||
}
|
||||
|
||||
/// Duplicate's unnamed target: the edited layer during a text edit, else the selection.
|
||||
fn layers_or_text_edit(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.state.text_edit.is_some() { super::has_comp(s) } else { has_layers(s) }
|
||||
}
|
||||
|
||||
fn always_ok(_: &Session) -> std::result::Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -72,6 +72,10 @@ fn transform(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut new_sel = vec![];
|
||||
for ((lid, uid), times) in groups {
|
||||
let Some(layer) = comp.layer_mut(lid) else { continue };
|
||||
if layer.switches.locked {
|
||||
new_sel.extend(times.into_iter().map(|time| KeyRef { layer: lid, prop: uid, time }));
|
||||
continue;
|
||||
}
|
||||
let (start, stretch) = (layer.start_time, layer.stretch);
|
||||
let to_comp = |lt: Tick| if (stretch - 100.0).abs() < 1e-9 { start + lt } else { start + Tick((lt.0 as f64 * stretch / 100.0) as i64) };
|
||||
let to_layer = |ct: Tick| {
|
||||
@@ -122,6 +126,9 @@ fn spatial_tangents(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let brk = b_p(p, "break").unwrap_or(false);
|
||||
s.edit("Edit Spatial Tangents", merge_p(p), |proj, _| {
|
||||
let l = super::layer_mut(proj, cid, lid)?;
|
||||
if l.switches.locked {
|
||||
return Err(bad("keys.setSpatialTangents", "the layer is locked"));
|
||||
}
|
||||
let pr = l.props.find_mut(uid).ok_or_else(|| bad("keys.setSpatialTangents", "no property"))?;
|
||||
if !pr.spatial {
|
||||
return Err(bad("keys.setSpatialTangents", "not a spatial property"));
|
||||
|
||||
@@ -37,6 +37,8 @@ pub(crate) fn label_keys(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad("keys.label", "select keyframes first"));
|
||||
}
|
||||
let cid = comp_id(s, p)?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let keys = super::prop::editable_keys(comp, &keys);
|
||||
let n = s.edit("Keyframe Label", None, |proj, _| {
|
||||
let mut n = 0;
|
||||
for k in &keys {
|
||||
|
||||
@@ -28,6 +28,7 @@ pub(crate) use create::{color_p, index_p, insert_layer, insert_layer_at, place,
|
||||
pub(crate) use parent::parent_fixes;
|
||||
use parent::set_parent;
|
||||
use replace::{has_replace_source, replace_source};
|
||||
pub(crate) use select::select_only;
|
||||
use select::{select, select_next, select_prev, select_step};
|
||||
use switches::{blend, rename, set_comment, set_switch, track_matte};
|
||||
use timing::{arrange, move_frames, precompose, slip, timing};
|
||||
@@ -147,7 +148,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
cmd!("layer.moveLater", "Move Layers 1 Frame Later", [], Some("Alt+PageDown"), "{layers?}", has_layers, |s, p| move_frames(s, p, 1)),
|
||||
cmd!("layer.moveEarlier10", "Move Layers 10 Frames Earlier", [], Some("Alt+Shift+PageUp"), "{layers?}", has_layers, |s, p| move_frames(s, p, -10)),
|
||||
cmd!("layer.moveLater10", "Move Layers 10 Frames Later", [], Some("Alt+Shift+PageDown"), "{layers?}", has_layers, |s, p| move_frames(s, p, 10)),
|
||||
cmd!("layer.slip", "Slip Edit", [], None, "{layers?, delta? (seconds) | frames?, merge?}", has_layers, slip),
|
||||
cmd!(
|
||||
"layer.slip",
|
||||
"Slip Edit",
|
||||
[],
|
||||
None,
|
||||
"{layers?, delta? (seconds) | frames?, merge?} → {requested, slipped: [{layer, delta, clamped}]}",
|
||||
has_layers,
|
||||
slip
|
||||
),
|
||||
cmd!("layer.slipBack", "Slip Edit 1 Frame Earlier", [], None, "{layers?}", has_layers, |s, p| {
|
||||
let mut p = p.clone();
|
||||
p["frames"] = json!(-1);
|
||||
|
||||
@@ -25,15 +25,21 @@ pub(super) fn select(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.state.selected_layers = ids;
|
||||
s.state.selected_props.clear();
|
||||
s.state.selected_keys.clear();
|
||||
let keep = s.state.selected_layers.clone();
|
||||
s.state.selected_vertices.retain(|v| keep.contains(&v.layer));
|
||||
select_only(&mut s.state, ids);
|
||||
}
|
||||
Ok(json!(s.state.selected_layers.iter().map(|l| l.0).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
/// Make `ids` the whole layer selection, as a plain click on them does: no property or keyframe
|
||||
/// stays selected, and mask vertices stay selected only on layers that are still selected.
|
||||
pub(crate) fn select_only(st: &mut crate::EditorState, ids: Vec<LayerId>) {
|
||||
st.selected_layers = ids;
|
||||
st.selected_props.clear();
|
||||
st.selected_keys.clear();
|
||||
let keep = st.selected_layers.clone();
|
||||
st.selected_vertices.retain(|v| keep.contains(&v.layer));
|
||||
}
|
||||
|
||||
pub(super) fn select_step(s: &mut Session, p: &Value, dir: i64) -> Result<Value> {
|
||||
let comp = s.active_comp().ok_or(EngineError::NoComp)?;
|
||||
if comp.layers.is_empty() {
|
||||
|
||||
@@ -84,8 +84,9 @@ pub(super) fn move_frames(s: &mut Session, p: &Value, frames: i64) -> Result<Val
|
||||
|
||||
/// Slip edit: move a layer's source (footage or precomp) under its fixed in and out points, like
|
||||
/// dragging the source-duration bar in After Effects. `delta` in seconds or `frames`; positive
|
||||
/// moves the source later. Clamped so the source still covers the in–out span. Keyframes and
|
||||
/// layer markers are in layer time, so they travel with the source.
|
||||
/// moves the source later. Clamped so the source still covers the in–out span: the result gives
|
||||
/// the `requested` shift and, per layer, the `delta` applied and whether it was `clamped` (#2371).
|
||||
/// Keyframes and layer markers are in layer time, so they travel with the source.
|
||||
pub(super) fn slip(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, ids) = layers_p(s, p)?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
@@ -130,7 +131,10 @@ pub(super) fn slip(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({"slipped": shifts.iter().map(|(id, d)| json!({"layer": id.0, "delta": d.seconds()})).collect::<Vec<_>>()}))
|
||||
Ok(json!({
|
||||
"requested": delta.seconds(),
|
||||
"slipped": shifts.iter().map(|(id, d)| json!({"layer": id.0, "delta": d.seconds(), "clamped": *d != delta})).collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -438,20 +438,22 @@ fn interpolate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// The layer: given, else the layer of selected Mask Path keys, else the selection.
|
||||
let cid = super::comp_id(s, p)?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let selected = super::prop::editable_keys(comp, &s.state.selected_keys);
|
||||
let lid = match p.get("layer") {
|
||||
Some(v) => super::resolve_layer(comp, v).ok_or_else(|| bad(CMD, format!("no layer {v}")))?,
|
||||
None => match s.state.selected_keys.iter().find(|k| comp.layer(k.layer).is_some_and(|l| l.masks().is_some_and(|m| m.find(k.prop).is_some()))) {
|
||||
None => match selected.iter().find(|k| comp.layer(k.layer).is_some_and(|l| l.masks().is_some_and(|m| m.find(k.prop).is_some()))) {
|
||||
Some(k) => k.layer,
|
||||
None => layer_p(s, p, CMD)?.1,
|
||||
},
|
||||
};
|
||||
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
|
||||
if !super::prop::key_layer_editable(comp, lid) {
|
||||
return Err(bad(CMD, "the layer is locked"));
|
||||
}
|
||||
let masks = layer.masks().ok_or_else(|| bad(CMD, "the layer has no masks"))?;
|
||||
let uid = match p.get("mask") {
|
||||
Some(_) => mask_uid(s, layer, p),
|
||||
None => s
|
||||
.state
|
||||
.selected_keys
|
||||
None => selected
|
||||
.iter()
|
||||
.filter(|k| k.layer == lid)
|
||||
.find_map(|k| masks.groups().find(|g| g.get("path").is_some_and(|pr| pr.uid == k.prop)).map(|g| g.uid))
|
||||
@@ -466,7 +468,7 @@ fn interpolate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ts: Vec<Tick> = a.iter().filter_map(Value::as_f64).map(|t| layer.layer_time(comp.frame_rate.snap_nearest(Tick::from_seconds_f64(t)))).collect();
|
||||
ts.into_iter().filter_map(|t| path_prop.keys.iter().find(|k| (k.time.0 - t.0).abs() <= comp.frame_duration().0 / 2).map(|k| k.time)).collect()
|
||||
}
|
||||
None => s.state.selected_keys.iter().filter(|k| k.layer == lid && k.prop == path_prop.uid).map(|k| k.time).collect(),
|
||||
None => selected.iter().filter(|k| k.layer == lid && k.prop == path_prop.uid).map(|k| k.time).collect(),
|
||||
};
|
||||
times.sort();
|
||||
times.dedup();
|
||||
|
||||
@@ -148,6 +148,11 @@ pub(crate) fn layer_p(s: &Session, p: &Value, cmd: &str) -> Result<(ItemId, Laye
|
||||
|
||||
/// `layers` param (array) or the selection.
|
||||
pub(crate) fn layers_p(s: &Session, p: &Value) -> Result<(ItemId, Vec<LayerId>)> {
|
||||
layers_or(s, p, &s.state.selected_layers)
|
||||
}
|
||||
|
||||
/// `layers` param (array), else `layer`, else those of `fallback` still in the comp.
|
||||
pub(crate) fn layers_or(s: &Session, p: &Value, fallback: &[LayerId]) -> Result<(ItemId, Vec<LayerId>)> {
|
||||
let cid = comp_id(s, p)?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
// Explicit references that match no layer are an error (agents otherwise get a silent no-op).
|
||||
@@ -159,7 +164,7 @@ pub(crate) fn layers_p(s: &Session, p: &Value) -> Result<(ItemId, Vec<LayerId>)>
|
||||
if let Some(v) = p.get("layer").filter(|v| !v.is_null()) {
|
||||
return Ok((cid, vec![resolve(v)?]));
|
||||
}
|
||||
Ok((cid, s.state.selected_layers.iter().copied().filter(|l| comp.layer(*l).is_some()).collect()))
|
||||
Ok((cid, fallback.iter().copied().filter(|l| comp.layer(*l).is_some()).collect()))
|
||||
}
|
||||
|
||||
/// Whether a layer can be picked by the user (Select All, Ctrl+Up/Down, a Timeline click): not
|
||||
|
||||
@@ -247,7 +247,8 @@ fn property_reply(p: &Value, layer: &Layer, pr: &Property, time: Tick, value: Va
|
||||
/// property (by match path, `transform/position`, `effects/<effect>#n/<param>`) of every other
|
||||
/// selected, unlocked layer of the comp whose value is of the same kind, as (layer, uid). Empty
|
||||
/// when `selected` is not set or the edited layer isn't selected. After Effects applies a value
|
||||
/// edit, the stopwatch and the keyframe button to all selected layers (#355, #491).
|
||||
/// edit, the stopwatch and the keyframe button to all selected layers (#355, #491). A parameter
|
||||
/// of a selected Puppet pin also reaches the same parameter of the other selected pins (#1751).
|
||||
fn selected_twins(s: &Session, p: &Value, cmd: &str) -> Result<Vec<(LayerId, Uid)>> {
|
||||
if b_p(p, "selected") != Some(true) {
|
||||
return Ok(vec![]);
|
||||
@@ -260,14 +261,31 @@ fn selected_twins(s: &Session, p: &Value, cmd: &str) -> Result<Vec<(LayerId, Uid
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let kind = (pr.value.kind_name(), pr.value.components().len());
|
||||
Ok(s.state
|
||||
let same_kind = |q: &&Property| (q.value.kind_name(), q.value.components().len()) == kind;
|
||||
let mut twins: Vec<(LayerId, Uid)> = s
|
||||
.state
|
||||
.selected_layers
|
||||
.iter()
|
||||
.filter(|o| **o != lid)
|
||||
.filter_map(|o| comp.layer(*o))
|
||||
.filter(|l| !l.switches.locked)
|
||||
.filter_map(|l| l.props.prop(&path).filter(|q| (q.value.kind_name(), q.value.components().len()) == kind).map(|q| (l.id, q.uid)))
|
||||
.collect())
|
||||
.filter_map(|l| l.props.prop(&path).filter(same_kind).map(|q| (l.id, q.uid)))
|
||||
.collect();
|
||||
// A parameter of a selected Puppet pin: the same parameter of every other selected pin, on
|
||||
// this layer too, so several pins move or key together (#1751).
|
||||
let pin = super::puppet::layer_pins(layer).find(|g| g.props().any(|q| q.uid == uid)).map(|g| g.uid);
|
||||
if let Some(pin) = pin
|
||||
&& s.state.selected_props.contains(&(lid, pin))
|
||||
{
|
||||
for (ol, ou) in &s.state.selected_props {
|
||||
let Some(other) = comp.layer(*ol).filter(|l| !l.switches.locked) else { continue };
|
||||
let Some(q) = super::puppet::layer_pins(other).find(|g| g.uid == *ou).and_then(|g| g.get(&pr.match_id)).filter(same_kind) else { continue };
|
||||
if (*ol, q.uid) != (lid, uid) && !twins.contains(&(*ol, q.uid)) {
|
||||
twins.push((*ol, q.uid));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(twins)
|
||||
}
|
||||
|
||||
/// `p` aimed at property `uid` of layer `lid` instead.
|
||||
@@ -286,8 +304,13 @@ fn twin_params(p: &Value, lid: LayerId, uid: Uid) -> Value {
|
||||
/// The value of property `uid` of `lid` at the CTI as numbers (Position with Separate
|
||||
/// Dimensions: its X / Y / Z Position).
|
||||
fn current_components(s: &Session, cid: ItemId, lid: LayerId, uid: Uid) -> Option<Vec<f64>> {
|
||||
components_at(s, cid, lid, uid, None)
|
||||
}
|
||||
|
||||
/// [`current_components`] at layer time `at` (the CTI when `None`).
|
||||
fn components_at(s: &Session, cid: ItemId, lid: LayerId, uid: Uid, at: Option<Tick>) -> Option<Vec<f64>> {
|
||||
let l = s.project.comp(cid)?.layer(lid)?;
|
||||
let lt = l.layer_time(s.time_of(cid));
|
||||
let lt = at.unwrap_or_else(|| l.layer_time(s.time_of(cid)));
|
||||
if let Some(tr) = l.transform()
|
||||
&& tr.get("position").is_some_and(|q| q.uid == uid)
|
||||
&& tr.get("positionX").is_some()
|
||||
@@ -319,7 +342,9 @@ pub(crate) fn proportional_share(index: usize, count: usize) -> f64 {
|
||||
/// `prop.set`; with `selected`, also on the other selected layers: the same value, or with
|
||||
/// `offset` (a scrub) the same change from each layer's own value. A typed value with `dims`
|
||||
/// (the components edited, e.g. `[2]` for Z) sets only those components on the other layers,
|
||||
/// which keep the rest of their own value (#1434). With `offset` and
|
||||
/// which keep the rest of their own value (#1434); `follow` components move on each layer by the
|
||||
/// change its first typed component takes (linked Audio Levels keep each layer's balance, #2485).
|
||||
/// With `offset` and
|
||||
/// `proportional` (Proportional Scrubbing) each layer takes [`proportional_share`] of the change
|
||||
/// by its place in the selection order, the scrubbed layer too. One undo step (with `merge`,
|
||||
/// one that a continuing scrub keeps folding into).
|
||||
@@ -344,10 +369,19 @@ fn set_selected(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(None, Some(a), Some(_)) if !scalar => Some(a.iter().filter_map(Value::as_u64).filter_map(|d| usize::try_from(d).ok()).collect()),
|
||||
_ => None,
|
||||
};
|
||||
let follow: Vec<usize> =
|
||||
p.get("follow").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_u64).filter_map(|d| usize::try_from(d).ok()).collect();
|
||||
// Each layer's own value where `set` writes it (a `time` other than the CTI keys there).
|
||||
let typed = |s: &Session, q: &mut Value, olid: LayerId, ouid: Uid| {
|
||||
if let (Some(dims), Some(n)) = (&dims, &new)
|
||||
&& let Some(mut c) = current_components(s, cid, olid, ouid)
|
||||
&& let Some(mut c) = components_at(s, cid, olid, ouid, snapped_key_time(s, cid, olid, q, "prop.set").ok().flatten())
|
||||
{
|
||||
let change = dims.first().and_then(|&d| Some(n.get(d)? - c.get(d)?)).unwrap_or(0.0);
|
||||
for &d in &follow {
|
||||
if let Some(slot) = c.get_mut(d) {
|
||||
*slot += change;
|
||||
}
|
||||
}
|
||||
for &d in dims {
|
||||
if let (Some(slot), Some(v)) = (c.get_mut(d), n.get(d)) {
|
||||
*slot = *v;
|
||||
@@ -914,6 +948,15 @@ fn select_keys(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// References that can be edited, including explicit key lists supplied by commands.
|
||||
pub(crate) fn editable_keys(comp: &effectcraft_project::Comp, selected: &[KeyRef]) -> Vec<KeyRef> {
|
||||
selected.iter().filter(|key| key_layer_editable(comp, key.layer)).copied().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn key_layer_editable(comp: &effectcraft_project::Comp, layer: LayerId) -> bool {
|
||||
comp.layer(layer).is_some_and(|layer| !layer.switches.locked)
|
||||
}
|
||||
|
||||
/// Apply `f` to every selected key; keeps the selection pointing at moved keys.
|
||||
pub(crate) fn edit_keys(s: &mut Session, label: &str, merge: Option<&str>, f: impl Fn(&mut Vec<Keyframe>, usize, &mut Tick) -> bool) -> Result<Value> {
|
||||
let cid = s.active_comp_id().ok_or(EngineError::NoComp)?;
|
||||
@@ -926,7 +969,16 @@ pub(crate) fn edit_keys(s: &mut Session, label: &str, merge: Option<&str>, f: im
|
||||
groups.entry((k.layer, k.prop)).or_default().push(k.time);
|
||||
}
|
||||
for ((lid, uid), times) in groups {
|
||||
let Some(pr) = comp.layer_mut(lid).and_then(|l| l.props.find_mut(uid)) else { continue };
|
||||
let Some(layer) = comp.layer_mut(lid) else { continue };
|
||||
// A locked layer's keys stay as they are, and stay selected.
|
||||
if layer.switches.locked {
|
||||
for time in times {
|
||||
let r = KeyRef { layer: lid, prop: uid, time };
|
||||
remapped.insert(r, r);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some(pr) = layer.props.find_mut(uid) else { continue };
|
||||
// Process each stored key once, in an order that avoids collisions when moving.
|
||||
let mut idx: Vec<(usize, Tick)> = times.iter().filter_map(|t| key_at(&pr.keys, *t).map(|i| (i, *t))).collect();
|
||||
idx.sort_by_key(|x| x.0);
|
||||
@@ -1079,6 +1131,10 @@ fn delete_keys(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let mut removed = 0usize;
|
||||
let mut changed = std::collections::BTreeSet::new();
|
||||
for k in &sel {
|
||||
// A layer locked after its keys were selected keeps them (the selection stays too).
|
||||
if comp.layer(k.layer).is_some_and(|l| l.switches.locked) {
|
||||
continue;
|
||||
}
|
||||
if let Some(pr) = comp.layer_mut(k.layer).and_then(|l| l.props.find_mut(k.prop))
|
||||
&& let Some(i) = key_at(&pr.keys, k.time)
|
||||
{
|
||||
@@ -1092,7 +1148,7 @@ fn delete_keys(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
st.selected_keys.clear();
|
||||
st.selected_keys.retain(|k| comp.layer(k.layer).is_some_and(|l| l.switches.locked));
|
||||
for (layer, prop) in changed {
|
||||
sync_polystar_property(proj, st, cid, layer, prop, time)?;
|
||||
}
|
||||
@@ -1366,7 +1422,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Set Property Value",
|
||||
[],
|
||||
None,
|
||||
"{layer?, path|prop, value, time? (layer s by default), timeBase?: layer|comp, merge?, selected? (also the other selected layers), offset? (them by the same change), proportional? (with offset: Proportional Scrubbing, each layer by its share in selection order, the first none, the last all), dims? (with selected, a typed value: the components edited, e.g. [2] for Z; the other layers keep the rest of their own value)}",
|
||||
"{layer?, path|prop, value, time? (layer s by default), timeBase?: layer|comp, merge?, selected? (also the other selected layers), offset? (them by the same change), proportional? (with offset: Proportional Scrubbing, each layer by its share in selection order, the first none, the last all), dims? (with selected, a typed value: the components edited, e.g. [2] for Z; the other layers keep the rest of their own value), follow? (with dims: components that move on each other layer by the change its first typed component takes)}",
|
||||
has_layers,
|
||||
set_selected
|
||||
),
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
//! The native `.prproj` format has no public specification and is not read or written.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_interchange::{ExportOptions, ImportOptions, PrecompMode, PrerenderMode, Prerendered, TimelineFormat};
|
||||
use effectcraft_project::render_queue::{AudioOutput, Channels, OutputFormat, OutputModule, ProResProfile, RenderQueueItem, TimeSpan};
|
||||
use effectcraft_project::{ItemId, LayerId, LayerSource, Project};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, bad, comp_id, f_p, has_comp, str_p};
|
||||
use crate::{EngineError, Result, Session, cmd};
|
||||
use super::{CommandSpec, always, b_p, bad, comp_id, f_p, has_comp, str_p};
|
||||
use crate::jobs::TaskCtl;
|
||||
use crate::{EngineError, Exporter, Result, Session, cmd};
|
||||
|
||||
fn format_p(p: &Value, cmd: &str) -> Result<Option<TimelineFormat>> {
|
||||
match str_p(p, "format") {
|
||||
@@ -95,18 +97,34 @@ fn isolate(project: &Project, cid: ItemId, lid: LayerId) -> Option<Project> {
|
||||
Some(q)
|
||||
}
|
||||
|
||||
/// Render one layer to ProRes 4444 with alpha over its in–out span.
|
||||
fn prerender(s: &Session, cid: ItemId, lid: LayerId, path: &str) -> std::result::Result<Prerendered, String> {
|
||||
let exporter = s.exporter.clone().ok_or("export is not available in this build")?;
|
||||
/// What a pre-render needs from the session, so it can run on a background thread.
|
||||
struct PrerenderCx {
|
||||
exporter: Option<Arc<dyn Exporter>>,
|
||||
footage: Arc<dyn effectcraft_render::FootageSource>,
|
||||
expr: Option<Arc<dyn effectcraft_render::ExprHost>>,
|
||||
accel: Option<Arc<dyn effectcraft_render::Accelerator>>,
|
||||
nested_switches: bool,
|
||||
}
|
||||
|
||||
/// Render one layer to ProRes 4444 with alpha over its in–out span; `progress` as the exporter's.
|
||||
fn prerender(
|
||||
cx: &PrerenderCx,
|
||||
project: &Project,
|
||||
cid: ItemId,
|
||||
lid: LayerId,
|
||||
path: &str,
|
||||
progress: &mut dyn FnMut(u64, u64) -> bool,
|
||||
) -> std::result::Result<Prerendered, String> {
|
||||
let exporter = cx.exporter.as_ref().ok_or("export is not available in this build")?;
|
||||
if !exporter.formats().contains(&OutputFormat::ProRes) {
|
||||
return Err("ProRes export is not available in this build".into());
|
||||
}
|
||||
let comp = s.project.comp(cid).ok_or("no composition")?;
|
||||
let comp = project.comp(cid).ok_or("no composition")?;
|
||||
let l = comp.layer(lid).ok_or("no layer")?;
|
||||
let fd = comp.frame_duration();
|
||||
let start = comp.frame_rate.snap(l.in_point.max(effectcraft_time::Tick::ZERO));
|
||||
let end = l.out_point.min(comp.duration).max(start + fd);
|
||||
let q = isolate(&s.project, cid, lid).ok_or("no composition")?;
|
||||
let q = isolate(project, cid, lid).ok_or("no composition")?;
|
||||
let mut item = RenderQueueItem::new(0, cid);
|
||||
item.settings.time_span = TimeSpan::Custom { start, end };
|
||||
item.output = OutputModule::for_format(OutputFormat::ProRes);
|
||||
@@ -116,16 +134,16 @@ fn prerender(s: &Session, cid: ItemId, lid: LayerId, path: &str) -> std::result:
|
||||
item.output.output = path.to_string();
|
||||
let job = crate::ExportJob {
|
||||
project: &q,
|
||||
footage: s.footage.as_ref(),
|
||||
expr: s.expr.as_deref(),
|
||||
accel: s.accel.as_deref(),
|
||||
footage: cx.footage.as_ref(),
|
||||
expr: cx.expr.as_deref(),
|
||||
accel: cx.accel.as_deref(),
|
||||
item: &item,
|
||||
path,
|
||||
storage: None,
|
||||
label: format!("Pre-render {}", l.name),
|
||||
nested_switches: s.prefs.general.switches_affect_nested_comps,
|
||||
nested_switches: cx.nested_switches,
|
||||
};
|
||||
let r = exporter.export(&job, &mut |_, _| true)?;
|
||||
let r = exporter.export(&job, progress)?;
|
||||
Ok(Prerendered { path: r.path, width: comp.width, height: comp.height, frame_rate: comp.frame_rate, duration: end - start })
|
||||
}
|
||||
|
||||
@@ -157,33 +175,67 @@ fn export_timeline(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let stem = fp.file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "Timeline".into());
|
||||
let relative_to = Some(dir.to_string_lossy().to_string()).filter(|d| !d.is_empty());
|
||||
let opts = ExportOptions { format, prerender: prerender_mode, precomps, relative_to };
|
||||
// Pre-render what the timeline can't represent.
|
||||
let project = s.project.clone();
|
||||
let services = s.services.clone();
|
||||
let cx = PrerenderCx {
|
||||
exporter: s.exporter.clone(),
|
||||
footage: s.footage.clone(),
|
||||
expr: s.expr.clone(),
|
||||
accel: s.accel.clone(),
|
||||
nested_switches: s.prefs.general.switches_affect_nested_comps,
|
||||
};
|
||||
// The pre-renders can take minutes: with `background` (the export dialog) they run as a job
|
||||
// in Window ▸ Progress instead of with the window waiting (#2269).
|
||||
let wait = !b_p(p, "background").unwrap_or(false);
|
||||
s.spawn_task("exportTimeline", format!("Export {}", format.label()), wait, move |ctl| {
|
||||
let (bytes, mut result) = export_timeline_work(&cx, &project, cid, &opts, &dir, &stem, ctl)?;
|
||||
services.write_file(&path, &bytes).map_err(|e| format!("cannot write {path}: {e}"))?;
|
||||
result["path"] = json!(path);
|
||||
result["toast"] = json!(match format {
|
||||
TimelineFormat::Fcp7Xml => format!("Exported {path} (Final Cut Pro XML; import it in Premiere Pro with File ▸ Import)"),
|
||||
_ => format!("Exported {path} ({})", format.label()),
|
||||
});
|
||||
Ok(Box::new(move |_: &mut Session| Ok(result)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Pre-render what the timeline can't represent, then build the timeline: its bytes and the
|
||||
/// command's result (without `path`).
|
||||
fn export_timeline_work(
|
||||
cx: &PrerenderCx,
|
||||
project: &Project,
|
||||
cid: ItemId,
|
||||
opts: &ExportOptions,
|
||||
dir: &std::path::Path,
|
||||
stem: &str,
|
||||
ctl: &TaskCtl,
|
||||
) -> std::result::Result<(Vec<u8>, Value), String> {
|
||||
let mut pre: HashMap<(ItemId, LayerId), Prerendered> = HashMap::new();
|
||||
let mut rendered = vec![];
|
||||
let mut errors = vec![];
|
||||
for (c, l) in effectcraft_interchange::plan_prerender(&s.project, cid, &opts) {
|
||||
let comp_name = s.project.item(c).map(|i| i.name.clone()).unwrap_or_default();
|
||||
let layer_name = s.project.comp(c).and_then(|cc| cc.layer(l)).map(|x| x.name.clone()).unwrap_or_default();
|
||||
let plan = effectcraft_interchange::plan_prerender(project, cid, opts);
|
||||
let n = plan.len() as u64;
|
||||
for (i, (c, l)) in (0u64..).zip(plan) {
|
||||
let comp_name = project.item(c).map(|it| it.name.clone()).unwrap_or_default();
|
||||
let layer_name = project.comp(c).and_then(|cc| cc.layer(l)).map(|x| x.name.clone()).unwrap_or_default();
|
||||
ctl.message(format!("Pre-render {layer_name}"));
|
||||
let file = dir.join(format!("{stem}_{}_{}_{}.mov", safe(&comp_name), safe(&layer_name), l.0)).to_string_lossy().to_string();
|
||||
match prerender(s, c, l, &file) {
|
||||
// Progress over all the layers, each one's frames a share of it.
|
||||
let mut progress = |done: u64, total: u64| ctl.progress(i * 1000 + done.saturating_mul(1000) / total.max(1), n * 1000);
|
||||
match prerender(cx, project, c, l, &file, &mut progress) {
|
||||
Ok(r) => {
|
||||
rendered.push(r.path.clone());
|
||||
pre.insert((c, l), r);
|
||||
}
|
||||
Err(_) if ctl.cancelled() => return Err(crate::render_queue::CANCELLED.into()),
|
||||
Err(e) => errors.push(format!("{comp_name} ▸ {layer_name}: {e}")),
|
||||
}
|
||||
}
|
||||
let out = effectcraft_interchange::export(&s.project, cid, &opts, &pre).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
s.services.write_file(&path, &out.bytes).map_err(|e| EngineError::Other(format!("cannot write {path}: {e}")))?;
|
||||
let out = effectcraft_interchange::export(project, cid, opts, &pre).map_err(|e| e.to_string())?;
|
||||
let mut warnings = out.warnings.clone();
|
||||
warnings.extend(errors.iter().map(|e| format!("pre-render failed: {e}")));
|
||||
s.toast(match format {
|
||||
TimelineFormat::Fcp7Xml => format!("Exported {path} (Final Cut Pro XML; import it in Premiere Pro with File ▸ Import)"),
|
||||
_ => format!("Exported {path} ({})", format.label()),
|
||||
});
|
||||
Ok(json!({
|
||||
"path": path,
|
||||
"format": format.id(),
|
||||
let result = json!({
|
||||
"format": opts.format.id(),
|
||||
"bytes": out.bytes.len(),
|
||||
"sequences": out.sequences,
|
||||
"videoTracks": out.video_tracks,
|
||||
@@ -191,7 +243,8 @@ fn export_timeline(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"clips": out.clips,
|
||||
"prerendered": rendered,
|
||||
"warnings": warnings,
|
||||
}))
|
||||
});
|
||||
Ok((out.bytes, result))
|
||||
}
|
||||
|
||||
/// The formats, for agents and the export dialog.
|
||||
@@ -220,7 +273,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Adobe Premiere Pro Project...",
|
||||
["File", "Export"],
|
||||
None,
|
||||
"{comp?, path (Final Cut Pro XML .xml for Premiere Pro or .fcpxml .otio .edl .aaf .omf), format?: xml|fcpxml|otio|edl|aaf|omf, prerender?: none|unsupported|all, precomps?: nest|prerender}",
|
||||
"{comp?, path (Final Cut Pro XML .xml for Premiere Pro or .fcpxml .otio .edl .aaf .omf), format?: xml|fcpxml|otio|edl|aaf|omf, prerender?: none|unsupported|all, precomps?: nest|prerender, background?: bool (pre-render and write as a job, jobs.list / jobs.wait; returns {job})}",
|
||||
has_comp,
|
||||
export_timeline
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user