mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 15:48:33 +00:00
feat(layer): layer.addItem takes in, out and anchor (#2026)
This commit is contained in:
1 parent
7a9572d8b9
commit
38ed20f569
4 files changed
+146
-1
No files matched your search
@@ -79,7 +79,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add Footage to Comp",
|
||||
["File"],
|
||||
Some("Cmd+/"),
|
||||
"{item: id|name, time? (s, the In point), index? (1-based stack position; default above the selected layer), position? ([x, y] comp px; default the centre), duration? (s, for a still)}",
|
||||
"{item: id|name, time? (s, where the layer starts), index? (1-based stack position; default above the selected layer), position? ([x, y] comp px; default the centre), duration? (s, for a still), in? (s, the In point in comp time; default the start), out? (s, the Out point; default the end of the footage or the comp; in must be before out), anchor? ([x, y] layer px; default the centre)}",
|
||||
has_comp,
|
||||
add_item
|
||||
),
|
||||
|
||||
@@ -258,6 +258,47 @@ pub(super) fn new_null(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
new_simple(s, p, LayerSource::Null, "Null 1", "New Null Object")
|
||||
}
|
||||
|
||||
/// Largest `in` / `out` (s) and `anchor` coordinate (px) a command accepts.
|
||||
const MAX_PLACEMENT: f64 = 1.0e6;
|
||||
|
||||
/// An `in` / `out` parameter: comp time in seconds, finite and within `0..=MAX_PLACEMENT`.
|
||||
fn time_param(p: &Value, key: &str, cmd: &str) -> Result<Option<Tick>> {
|
||||
match p.get(key) {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(v) => v
|
||||
.as_f64()
|
||||
.filter(|t| t.is_finite() && (0.0..=MAX_PLACEMENT).contains(t))
|
||||
.map(|t| Some(Tick::from_seconds_f64(t)))
|
||||
.ok_or_else(|| bad(cmd, format!("{key}: seconds in the composition, 0 to {MAX_PLACEMENT}"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// An `anchor` parameter: `[x, y]` in layer pixels (finite, within `MAX_PLACEMENT`).
|
||||
fn anchor_param(p: &Value, cmd: &str) -> Result<Option<[f64; 2]>> {
|
||||
match p.get("anchor") {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(v) => v
|
||||
.as_array()
|
||||
.and_then(|a| Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?]))
|
||||
.filter(|c| c.iter().all(|x| x.is_finite() && x.abs() <= MAX_PLACEMENT))
|
||||
.map(Some)
|
||||
.ok_or_else(|| bad(cmd, "anchor: [x, y] in layer pixels")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Put a new layer's Anchor Point at `pos` (layer pixels), keeping its z.
|
||||
fn set_anchor(l: &mut Layer, pos: Option<[f64; 2]>) {
|
||||
if let Some([x, y]) = pos
|
||||
&& let Some(pr) = l.props.prop_mut("transform/anchor")
|
||||
{
|
||||
let z = match pr.value {
|
||||
KV::Vec3(v) => v[2],
|
||||
_ => 0.0,
|
||||
};
|
||||
pr.value = KV::Vec3([x, y, z]);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn add_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cid = comp_id(s, p)?;
|
||||
let item = match p.get("item") {
|
||||
@@ -270,6 +311,13 @@ pub(super) fn add_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone();
|
||||
let it = s.project.item(item).ok_or_else(|| bad("layer.addItem", "no such item"))?.clone();
|
||||
let (in_p, out_p) = (time_param(p, "in", "layer.addItem")?, time_param(p, "out", "layer.addItem")?);
|
||||
if let (Some(i), Some(o)) = (in_p, out_p)
|
||||
&& i >= o
|
||||
{
|
||||
return Err(bad("layer.addItem", "in must be before out"));
|
||||
}
|
||||
let anchor = anchor_param(p, "layer.addItem")?;
|
||||
let (src, size, dur) = match &it.kind {
|
||||
ItemKind::Comp(c) => (LayerSource::Comp { item }, (c.width, c.height), Some(c.duration)),
|
||||
ItemKind::Footage(f) if f.kind == effectcraft_project::FootageKind::Still => {
|
||||
@@ -285,6 +333,9 @@ pub(super) fn add_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad("layer.addItem", "data files can't be layers; read them in expressions with footage(\"name\").sourceData"));
|
||||
}
|
||||
ItemKind::Footage(f) if f.kind == effectcraft_project::FootageKind::Model => {
|
||||
if in_p.is_some() || out_p.is_some() || anchor.is_some() {
|
||||
return Err(bad("layer.addItem", "in, out and anchor aren't supported for 3D models"));
|
||||
}
|
||||
return crate::cmd::model3d::new_model(
|
||||
s,
|
||||
&serde_json::json!({"comp": cid.0, "item": item.0, "time": f_p(p, "time"), "index": p.get("index"), "position": p.get("position")}),
|
||||
@@ -308,7 +359,16 @@ pub(super) fn add_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// without a duration (solids, comp-length stills) end with the comp.
|
||||
let end = dur.map_or((start + comp.duration).min(comp.duration), |d| start + d);
|
||||
l.out_point = fr.snap_nearest(end).max(start + fr.frame_duration());
|
||||
// `in` / `out` are comp times like `layer.timing`'s; each keeps at least one frame.
|
||||
let fd = fr.frame_duration();
|
||||
if let Some(o) = out_p {
|
||||
l.out_point = fr.snap_nearest(o).max(l.in_point + fd);
|
||||
}
|
||||
if let Some(i) = in_p {
|
||||
l.in_point = fr.snap_nearest(i).min(l.out_point - fd);
|
||||
}
|
||||
place(&mut l, position);
|
||||
set_anchor(&mut l, anchor);
|
||||
insert_layer_at(proj, st, cid, l, index)
|
||||
})?;
|
||||
Ok(json!({"layer": id.0}))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod add_item_options;
|
||||
mod anim_presets;
|
||||
mod anim_tools;
|
||||
mod blender;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! `layer.addItem {in, out, anchor}` (#2026): place a layer's In / Out points and Anchor Point in
|
||||
//! the same call, so one command does what addItem + layer.timing + prop.set did.
|
||||
|
||||
use effectcraft_project::{Layer, LayerId, LayerSource};
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// A 10 s, 30 fps comp with a 100 x 50 solid item; returns the session, the item id and the solid's first layer.
|
||||
fn setup() -> (Session, u64, u64) {
|
||||
let mut s = Session::default();
|
||||
s.execute_checked("comp.new", json!({"width": 640, "height": 360, "frameRate": 30, "duration": 10})).unwrap();
|
||||
let l = s.execute_checked("layer.newSolid", json!({"name": "S", "color": "#fff", "width": 100, "height": 50})).unwrap()["layer"].as_u64().unwrap();
|
||||
let item = match &s.active_comp().unwrap().layer(LayerId(l)).unwrap().source {
|
||||
LayerSource::Solid { item } => item.0,
|
||||
other => panic!("not a solid: {other:?}"),
|
||||
};
|
||||
(s, item, l)
|
||||
}
|
||||
|
||||
fn layer(s: &Session, id: u64) -> &Layer {
|
||||
s.active_comp().unwrap().layer(LayerId(id)).unwrap()
|
||||
}
|
||||
|
||||
fn anchor(s: &Session, id: u64) -> [f64; 3] {
|
||||
layer(s, id).props.prop("transform/anchor").unwrap().value.as_vec3()
|
||||
}
|
||||
|
||||
fn secs(t: Tick) -> f64 {
|
||||
t.seconds()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_out_and_anchor_in_one_call() {
|
||||
let (mut s, item, _) = setup();
|
||||
let id = s.execute_checked("layer.addItem", json!({"item": item, "time": 1.0, "in": 2.0, "out": 4.0, "anchor": [10.0, 20.0]})).unwrap()["layer"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let l = layer(&s, id);
|
||||
assert_eq!((secs(l.start_time), secs(l.in_point), secs(l.out_point)), (1.0, 2.0, 4.0));
|
||||
assert_eq!(anchor(&s, id)[..2], [10.0, 20.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_in_or_only_out_keeps_the_other_default() {
|
||||
let (mut s, item, _) = setup();
|
||||
let a = s.execute_checked("layer.addItem", json!({"item": item, "in": 3.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
assert_eq!((secs(layer(&s, a).in_point), secs(layer(&s, a).out_point)), (3.0, 10.0));
|
||||
let b = s.execute_checked("layer.addItem", json!({"item": item, "out": 5.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
assert_eq!((secs(layer(&s, b).in_point), secs(layer(&s, b).out_point)), (0.0, 5.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_params_keep_the_default_placement() {
|
||||
let (mut s, item, first) = setup();
|
||||
let before = anchor(&s, first);
|
||||
let id = s.execute_checked("layer.addItem", json!({"item": item})).unwrap()["layer"].as_u64().unwrap();
|
||||
let l = layer(&s, id);
|
||||
assert_eq!((secs(l.in_point), secs(l.out_point)), (0.0, 10.0));
|
||||
assert_eq!(anchor(&s, id), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_values_are_errors_and_change_nothing() {
|
||||
let (mut s, item, _) = setup();
|
||||
let n = s.active_comp().unwrap().layers.len();
|
||||
let bad: [Value; 8] = [
|
||||
json!({"in": 5.0, "out": 5.0}),
|
||||
json!({"in": 6.0, "out": 2.0}),
|
||||
json!({"in": -1.0}),
|
||||
json!({"in": "soon"}),
|
||||
json!({"out": 1e300}),
|
||||
json!({"anchor": [1.0]}),
|
||||
json!({"anchor": [1.0, "x"]}),
|
||||
json!({"anchor": [1e300, 0.0]}),
|
||||
];
|
||||
for extra in bad {
|
||||
let mut p = json!({"item": item});
|
||||
p.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
|
||||
assert!(s.execute("layer.addItem", p.clone()).is_err(), "{p}");
|
||||
}
|
||||
assert_eq!(s.active_comp().unwrap().layers.len(), n);
|
||||
}
|
||||
Reference in new issue
Block a user