Files
effectcraft/crates/engine/src/commands/comp.rs
T

230 lines
8.7 KiB
Rust

//! Composition menu.
use effectcraft_color::Label;
use effectcraft_project::{Comp, ItemId, ItemKind, Marker};
use effectcraft_time::{FrameRate, Tick};
use serde_json::{Value, json};
use super::{CommandSpec, always, b_p, bad, comp_id, f_p, has_comp, str_p, time_p};
use crate::{EngineError, Result, Session, cmd};
pub(crate) fn rate_p(p: &Value) -> Option<FrameRate> {
f_p(p, "frameRate").or(f_p(p, "fps")).map(FrameRate::from_f64)
}
fn apply_settings(c: &mut Comp, p: &Value) {
if let Some(w) = p.get("width").and_then(Value::as_u64) {
c.width = (w as u32).clamp(4, 30000);
}
if let Some(h) = p.get("height").and_then(Value::as_u64) {
c.height = (h as u32).clamp(4, 30000);
}
if let Some(r) = rate_p(p) {
c.frame_rate = r;
}
if let Some(d) = f_p(p, "duration") {
let old = c.duration;
c.duration = Tick::from_seconds_f64(d.max(c.frame_rate.frame_duration().seconds()));
if c.work_area.1 == old {
c.work_area.1 = c.duration;
}
c.work_area.1 = c.work_area.1.min(c.duration);
}
if let Some(pa) = f_p(p, "pixelAspect") {
c.pixel_aspect = pa.max(0.1);
}
if let Some(Value::Array(bg)) = p.get("background") {
let g = |i: usize| bg.get(i).and_then(Value::as_f64).unwrap_or(0.0) as f32;
c.background = [g(0), g(1), g(2)];
} else if let Some(hex) = str_p(p, "background").and_then(effectcraft_color::Rgba::from_hex) {
c.background = [hex.r, hex.g, hex.b];
}
if let Some(a) = f_p(p, "shutterAngle") {
c.shutter_angle = a.clamp(0.0, 720.0);
}
if let Some(a) = f_p(p, "shutterPhase") {
c.shutter_phase = a.clamp(-360.0, 360.0);
}
if let Some(n) = p.get("motionBlurSamples").and_then(Value::as_u64) {
c.motion_blur_samples = (n as u32).clamp(2, 64);
}
}
fn new_comp(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_p(p, "name").unwrap_or("Comp 1").to_string();
let rate = rate_p(p).unwrap_or(FrameRate::FPS_29_97);
let mut c = Comp::new(1920, 1080, rate, Tick::from_seconds_f64(10.0));
apply_settings(&mut c, p);
let id = s.edit("New Composition", None, |proj, st| {
let mut name = name.clone();
let mut k = 2;
while proj.items.values().any(|i| i.name == name) {
name = format!("{} {k}", name.trim_end_matches(|ch: char| ch.is_ascii_digit()).trim_end());
k += 1;
}
let id = proj.add_item(&name, Label::Sandstone, None, ItemKind::Comp(c.into()));
st.project_selection = vec![id];
Ok(id)
})?;
if b_p(p, "open") != Some(false) {
s.open_comp(id);
}
Ok(json!({"comp": id.0}))
}
fn settings(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
s.edit("Composition Settings", None, |proj, _| {
if let Some(n) = str_p(p, "name")
&& let Some(it) = proj.item_mut(cid)
{
it.name = n.to_string();
}
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
apply_settings(c, p);
Ok(())
})?;
let c = s.project.comp(cid).ok_or(EngineError::NoComp)?;
Ok(json!({"width": c.width, "height": c.height, "frameRate": c.frame_rate.as_f64(), "duration": c.duration.seconds()}))
}
fn open(s: &mut Session, p: &Value) -> Result<Value> {
let id = match p.get("comp").or(p.get("item")) {
Some(Value::Number(n)) => ItemId(n.as_u64().unwrap_or(0)),
Some(Value::String(name)) => s.project.items.values().find(|i| &i.name == name && i.as_comp().is_some()).map(|i| i.id).ok_or(EngineError::NoComp)?,
_ => *s.state.project_selection.iter().find(|i| s.project.comp(**i).is_some()).ok_or_else(|| bad("comp.open", "missing `comp`"))?,
};
if s.project.comp(id).is_none() {
return Err(EngineError::NoComp);
}
s.open_comp(id);
Ok(json!({"comp": id.0}))
}
fn close(s: &mut Session, p: &Value) -> Result<Value> {
let id = comp_id(s, p)?;
s.state.open_comps.retain(|c| *c != id);
if s.state.active_comp == Some(id) {
s.state.active_comp = s.state.open_comps.last().copied();
s.state.selected_layers.clear();
s.state.selected_keys.clear();
s.state.selected_props.clear();
}
Ok(Value::Null)
}
fn work_area(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
let t = s.time();
let begin = f_p(p, "start").map(Tick::from_seconds_f64);
let end = f_p(p, "end").map(Tick::from_seconds_f64);
let which = str_p(p, "set");
s.edit("Work Area", super::merge_p(p), |proj, _| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
let fd = c.frame_duration();
match which {
Some("begin") => c.work_area.0 = t.min(c.work_area.1 - fd),
Some("end") => c.work_area.1 = (t + fd).max(c.work_area.0 + fd).min(c.duration),
_ => {}
}
if let Some(b) = begin {
c.work_area.0 = b.clamp(Tick::ZERO, c.duration - fd);
}
if let Some(e) = end {
c.work_area.1 = e.clamp(c.work_area.0 + fd, c.duration);
}
Ok(())
})?;
let c = s.project.comp(cid).ok_or(EngineError::NoComp)?;
Ok(json!({"start": c.work_area.0.seconds(), "end": c.work_area.1.seconds()}))
}
fn trim_to_wa(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
s.edit("Trim Comp to Work Area", None, |proj, _| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
let (a, b) = c.work_area;
for l in &mut c.layers {
l.start_time -= a;
l.in_point = (l.in_point - a).max(Tick::ZERO);
l.out_point = (l.out_point - a).min(b - a);
}
c.duration = b - a;
c.work_area = (Tick::ZERO, b - a);
Ok(())
})?;
Ok(Value::Null)
}
fn comp_switch(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
let name = str_p(p, "switch").ok_or_else(|| bad("comp.setSwitch", "missing `switch`"))?.to_string();
let v = b_p(p, "value");
let r = s.edit("Composition Switch", None, |proj, _| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
let slot = match name.as_str() {
"hideShy" | "shy" => &mut c.hide_shy,
"motionBlur" => &mut c.enable_motion_blur,
"frameBlending" => &mut c.enable_frame_blending,
"draft3d" | "draft3D" => &mut c.draft_3d,
_ => return Err(bad("comp.setSwitch", "switch: hideShy|motionBlur|frameBlending|draft3d")),
};
*slot = v.unwrap_or(!*slot);
Ok(*slot)
})?;
Ok(json!(r))
}
fn add_marker(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
let t = time_p(s, p, s.project.comp(cid));
let comment = str_p(p, "comment").unwrap_or("").to_string();
s.edit("Add Marker", None, |proj, _| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
c.markers.push(Marker { time: t, comment, ..Default::default() });
c.markers.sort_by_key(|m| m.time);
Ok(())
})?;
Ok(json!({"time": t.seconds()}))
}
fn poster(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
let t = s.time();
s.edit("Set Poster Time", None, |proj, _| {
proj.comp_mut(cid).ok_or(EngineError::NoComp)?.poster_time = t;
Ok(())
})?;
Ok(Value::Null)
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"comp.new",
"New Composition…",
["Composition"],
Some("Cmd+N"),
"{name?, width?, height?, frameRate?, duration? (s), background? [r,g,b]|#hex, open?}",
always,
new_comp
),
cmd!(
"comp.settings",
"Composition Settings…",
["Composition"],
Some("Cmd+K"),
"{comp?, name?, width?, height?, frameRate?, duration?, background?, shutterAngle?, shutterPhase?, motionBlurSamples?}",
has_comp,
settings
),
cmd!("comp.setPosterTime", "Set Poster Time", ["Composition"], None, "{}", has_comp, poster),
cmd!("comp.trimToWorkArea", "Trim Comp to Work Area", ["Composition"], Some("Cmd+Shift+X"), "{comp?}", has_comp, trim_to_wa),
cmd!("comp.open", "Open Composition", [], None, "{comp: id|name}", always, open),
cmd!("comp.close", "Close Composition", [], Some("Cmd+W"), "{comp?}", has_comp, close),
cmd!("comp.workArea", "Set Work Area", [], None, "{start?, end?, set?: begin|end (at CTI)}", has_comp, work_area),
cmd!("comp.setSwitch", "Composition Switch", [], None, "{switch: hideShy|motionBlur|frameBlending|draft3d, value?}", has_comp, comp_switch),
cmd!("comp.addMarker", "Add Marker", [], Some("Num*"), "{time?, comment?}", has_comp, add_marker),
]
}