mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Merge pull request #1951 from ki2pixel/fix/1747-collapse-essential-overrides
Fix #1747: keep collapsed precomp instance overrides unclipped
This commit is contained in:
@@ -207,7 +207,7 @@ pub(crate) fn source_frame(
|
||||
t: Tick,
|
||||
expr: Option<&dyn ExprHost>,
|
||||
) -> Option<(Arc<Image>, [f64; 2])> {
|
||||
let ctx = EvalCtx { project, comp_id: cid, comp, time: t, expr, footage: None };
|
||||
let ctx = EvalCtx { project, comp_id: cid, comp, time: t, expr, footage: None, property_overrides: &[], media_overrides: &[] };
|
||||
if let effectcraft_project::LayerSource::Footage { item } = &layer.source
|
||||
&& let Some(effectcraft_project::ItemKind::Footage(f)) = project.item(*item).map(|i| &i.kind)
|
||||
&& f.has_video
|
||||
|
||||
@@ -136,7 +136,16 @@ fn warning(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
|
||||
let g = layer.effects().and_then(|fx| fx.groups().find(|g| g.uid == uid)).ok_or_else(|| bad(cmd, "no such effect"))?;
|
||||
let t = super::time_p(s, p, Some(comp));
|
||||
let ctx = effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None };
|
||||
let ctx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: t,
|
||||
expr: s.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
Ok(json!({"effect": uid, "warning": effectcraft_effects::warning(g, &mut |pr| ctx.value(layer, pr))}))
|
||||
}
|
||||
|
||||
@@ -160,7 +169,16 @@ fn pick_color(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.filter(|pr| pr.ui == effectcraft_project::ParamUi::Color)
|
||||
.ok_or_else(|| bad(cmd, "not a colour parameter of this effect"))?;
|
||||
let t = super::time_p(s, p, Some(comp));
|
||||
let ctx = effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None };
|
||||
let ctx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: t,
|
||||
expr: s.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let mut r = effectcraft_render::Renderer::new(&s.project, &*s.footage, effectcraft_render::RenderOpts::default());
|
||||
r.expr = s.expr.as_deref();
|
||||
r.cache = Some(&s.layer_cache);
|
||||
@@ -288,7 +306,16 @@ impl CryptoProbe {
|
||||
let comp = r.project.comp(cid).ok_or("no composition")?;
|
||||
let layer = comp.layer(lid).ok_or("no such layer")?;
|
||||
let g = layer.effects().and_then(|fx| fx.groups().nth(pos)).ok_or("no such effect")?;
|
||||
let ctx = effectcraft_render::EvalCtx { project: r.project, comp_id: cid, comp, time, expr: r.expr, footage: None };
|
||||
let ctx = effectcraft_render::EvalCtx {
|
||||
project: r.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time,
|
||||
expr: r.expr,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let params = effectcraft_effects::flatten_params(g, &mut |pr| ctx.value(layer, pr));
|
||||
let want = effectcraft_effects::CRYPTO_LAYERS.get(params.e("layer") as usize).copied().unwrap_or("CryptoAsset");
|
||||
let aux = r.layer_aux(&ctx, layer).ok_or(NO_CRYPTO)?;
|
||||
@@ -351,7 +378,16 @@ fn probe_key<'a>(s: &'a Session, p: &Value, cmd: &str) -> Result<(ProbeKey, Tick
|
||||
let g = fx.groups().nth(pos).ok_or_else(|| bad(cmd, "no such effect"))?;
|
||||
let prop = g.prop("selection").filter(|_| g.match_id == "ec.3d.cryptomatte").ok_or_else(|| bad(cmd, "not a Cryptomatte effect"))?;
|
||||
let time = super::time_p(s, p, Some(comp));
|
||||
let ctx = effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time, expr: s.expr.as_deref(), footage: None };
|
||||
let ctx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time,
|
||||
expr: s.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let frame = probe_frame(&ctx, layer, pos);
|
||||
let o = s.render_opts(Default::default());
|
||||
let prefs = (o.nested_switches, o.draft_shadows);
|
||||
@@ -503,7 +539,16 @@ fn crypto_hit(s: &Session, p: &Value, cmd: &str) -> Result<Hit> {
|
||||
let Some(hash) = b.aux.index_at(ax, ay).and_then(|i| b.aux.crypto_id_at(&b.layer, i)) else { return Ok(Hit::Nothing) };
|
||||
let (cid, lid, ..) = key.at;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let ctx = effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time, expr: s.expr.as_deref(), footage: None };
|
||||
let ctx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time,
|
||||
expr: s.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let selection = effectcraft_effects::flatten_params(g, &mut |pr| ctx.value(layer, pr)).s("selection").to_string();
|
||||
let driven = prop.expr.as_ref().is_some_and(|e| e.enabled);
|
||||
let (aux, layer_name) = (b.aux.clone(), b.layer.clone());
|
||||
|
||||
@@ -362,8 +362,16 @@ fn aim_correction(rotation: effectcraft_geom::Mat4, aim: [f64; 3]) -> Option<eff
|
||||
/// time.
|
||||
pub(crate) fn parent_fixes(s: &Session, cid: ItemId, ids: &[LayerId], parent: Option<LayerId>) -> Vec<ParentFix> {
|
||||
let Some(comp) = s.project.comp(cid) else { return vec![] };
|
||||
let ectx =
|
||||
effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time: s.time(), expr: s.expr.as_deref(), footage: Some(s.footage.as_ref()) };
|
||||
let ectx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: s.time(),
|
||||
expr: s.expr.as_deref(),
|
||||
footage: Some(s.footage.as_ref()),
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let space = |id: Option<LayerId>| -> effectcraft_geom::Mat3 {
|
||||
id.and_then(|i| comp.layer(i)).filter(|l| !l.is_3d()).map(|l| ectx.layer_to_comp(l).0).unwrap_or(effectcraft_geom::Mat3::IDENTITY)
|
||||
};
|
||||
|
||||
@@ -185,7 +185,16 @@ fn freeze(s: &mut Session, p: &Value, last: bool) -> Result<Value> {
|
||||
return Err(bad("layer.freezeFrame", "freezing needs a footage or composition layer"));
|
||||
}
|
||||
// What each layer shows now (honouring an existing remap) — evaluated before the edit.
|
||||
let ectx = effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time: cti, expr: s.expr.as_deref(), footage: Some(s.footage.as_ref()) };
|
||||
let ectx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: cti,
|
||||
expr: s.expr.as_deref(),
|
||||
footage: Some(s.footage.as_ref()),
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let now: Vec<(effectcraft_project::LayerId, f64)> = comp
|
||||
.layers
|
||||
.iter()
|
||||
|
||||
@@ -78,7 +78,16 @@ fn info(s: &Session, cid: ItemId, lid: LayerId, uid: Uid, cmd: &str) -> Result<P
|
||||
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
|
||||
let pr = layer.props.find(uid).ok_or_else(|| bad(cmd, "no such property"))?;
|
||||
let t = s.time();
|
||||
let ctx = effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: Some(s.footage.as_ref()) };
|
||||
let ctx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: t,
|
||||
expr: s.expr.as_deref(),
|
||||
footage: Some(s.footage.as_ref()),
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let KV::Path(path) = ctx.value(layer, pr) else { return Err(bad(cmd, "that property is not a path")) };
|
||||
if path.vertices.is_empty() {
|
||||
return Err(bad(cmd, "the path has no vertices"));
|
||||
@@ -147,8 +156,16 @@ fn pin_targets(s: &Session, p: &Value, cmd: &str) -> Result<Option<(ItemId, Vec<
|
||||
};
|
||||
let cid = pins.first().map(|x| x.0).ok_or_else(|| bad(cmd, "no puppet pins"))?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let ctx =
|
||||
effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time: s.time(), expr: s.expr.as_deref(), footage: Some(s.footage.as_ref()) };
|
||||
let ctx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: s.time(),
|
||||
expr: s.expr.as_deref(),
|
||||
footage: Some(s.footage.as_ref()),
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let mut out = vec![];
|
||||
for (_, lid, uid) in pins {
|
||||
let Some(layer) = comp.layer(lid) else { continue };
|
||||
|
||||
@@ -1285,8 +1285,16 @@ fn convert_one_expr(s: &mut Session, cid: ItemId, lid: LayerId, uid: Uid) -> Res
|
||||
let fd = comp.frame_duration();
|
||||
let mut t = layer.in_point;
|
||||
while t < layer.out_point {
|
||||
let ctx =
|
||||
effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp: &comp, time: t, expr: s.expr.as_deref(), footage: Some(s.footage.as_ref()) };
|
||||
let ctx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp: &comp,
|
||||
time: t,
|
||||
expr: s.expr.as_deref(),
|
||||
footage: Some(s.footage.as_ref()),
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
keys.push(Keyframe::new(layer.layer_time(t), ctx.value(&layer, &pr)));
|
||||
t += fd;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ pub fn puppet_eval(s: &Session, cid: ItemId, lid: LayerId, fx_uid: Uid, t: Tick)
|
||||
let fx = layer.effects()?;
|
||||
let index = fx.groups().position(|g| g.uid == fx_uid)?;
|
||||
let g = fx.groups().nth(index)?;
|
||||
let ctx = EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None };
|
||||
let ctx =
|
||||
EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None, property_overrides: &[], media_overrides: &[] };
|
||||
let mut r = Renderer::new(&s.project, &*s.footage, RenderOpts::default());
|
||||
r.expr = s.expr.as_deref();
|
||||
r.cache = Some(&s.layer_cache);
|
||||
|
||||
@@ -48,7 +48,16 @@ impl Env {
|
||||
Env { time: s.time_of(cid), expr: s.expr.clone(), footage: s.footage.clone() }
|
||||
}
|
||||
fn ctx<'a>(&'a self, proj: &'a Project, cid: ItemId, comp: &'a Comp) -> EvalCtx<'a> {
|
||||
EvalCtx { project: proj, comp_id: cid, comp, time: self.time, expr: self.expr.as_deref(), footage: Some(self.footage.as_ref()) }
|
||||
EvalCtx {
|
||||
project: proj,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: self.time,
|
||||
expr: self.expr.as_deref(),
|
||||
footage: Some(self.footage.as_ref()),
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -413,7 +413,8 @@ fn get_3d(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (w, h) = (comp.width as f64, comp.height as f64);
|
||||
let vs = s.state.views3d.get(&cid).cloned().unwrap_or_default();
|
||||
let t = s.state.times.get(&cid).copied().unwrap_or(Tick::ZERO);
|
||||
let ctx = EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None };
|
||||
let ctx =
|
||||
EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None, property_overrides: &[], media_overrides: &[] };
|
||||
let cam = vs.override_camera(w, h).unwrap_or_else(|| camera::active_camera(&ctx));
|
||||
let f = cam.forward();
|
||||
let lights: Vec<Value> = comp
|
||||
@@ -555,7 +556,8 @@ fn auto_orient(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// through each layer's world matrix; cameras and lights contribute their position).
|
||||
fn layer_points(s: &Session, cid: ItemId, ids: &[LayerId], t: Tick) -> Vec<Vec3> {
|
||||
let Some(comp) = s.project.comp(cid) else { return vec![] };
|
||||
let ctx = EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None };
|
||||
let ctx =
|
||||
EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None, property_overrides: &[], media_overrides: &[] };
|
||||
let mut out = vec![];
|
||||
for l in comp.layers.iter().filter(|l| ids.contains(&l.id)) {
|
||||
let m = ctx.world_matrix(l);
|
||||
|
||||
@@ -190,7 +190,16 @@ fn rendered_matte_of_pick(s: &mut Session, x: f64, y: f64, scale: f64) -> (Strin
|
||||
s.execute_checked("prop.set", json!({"path": format!("effects/#{fx}/matteOnly"), "value": true})).unwrap();
|
||||
let (cid, comp) = (s.active_comp_id().unwrap(), s.active_comp().unwrap());
|
||||
let layer = &comp.layers[0];
|
||||
let ctx = effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time: Tick::ZERO, expr: None, footage: None };
|
||||
let ctx = effectcraft_render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: Tick::ZERO,
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let buf = s.renderer(effectcraft_render::RenderOpts { scale, ..Default::default() }).content_buf(&ctx, layer).unwrap();
|
||||
let (px, py) = buf.to_px([x, y]);
|
||||
let m = buf.img.get(px.floor() as i64, py.floor() as i64)[0];
|
||||
|
||||
@@ -367,6 +367,128 @@ fn media_replacement_swaps_footage_per_instance() {
|
||||
assert!(center(&s, main)[0] > 0.99);
|
||||
}
|
||||
|
||||
/// #1747: a collapsed precomp with an Essential Property override must keep drawing its nested
|
||||
/// layers past the precomp's frame instead of falling back to a flattened, clipped frame.
|
||||
#[test]
|
||||
fn collapsed_precomp_applies_property_overrides_without_clipping() {
|
||||
struct AddY;
|
||||
impl effectcraft_render::ExprHost for AddY {
|
||||
fn eval(
|
||||
&self,
|
||||
_: &effectcraft_render::EvalCtx,
|
||||
_: &effectcraft_project::Layer,
|
||||
_: &effectcraft_project::Property,
|
||||
value: &effectcraft_keyframe::Value,
|
||||
) -> Result<effectcraft_keyframe::Value, String> {
|
||||
Ok(match value {
|
||||
effectcraft_keyframe::Value::Vec3([x, y, z]) => effectcraft_keyframe::Value::Vec3([*x, y + 100.0, *z]),
|
||||
_ => value.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Inner", "width": 400, "height": 300, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let inner = s.active_comp_id().unwrap();
|
||||
let box_ = s.execute("layer.newSolid", json!({"name": "Box", "color": "#ffffff", "width": 100, "height": 100})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("prop.set", json!({"layer": box_, "path": "transform/position", "value": [200, 50, 0]})).unwrap();
|
||||
// The source property's expression still runs after the instance override.
|
||||
s.execute("prop.setExpression", json!({"layer": box_, "path": "transform/position", "expression": "value + [0, 100, 0]"})).unwrap();
|
||||
let position = s.execute("essential.addProperty", json!({"layer": box_, "path": "transform/position"})).unwrap()["controls"][0].as_u64().unwrap();
|
||||
s.execute("comp.new", json!({"name": "Outer", "width": 400, "height": 600, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let outer = s.active_comp_id().unwrap();
|
||||
let instance = s.execute("layer.addItem", json!({"item": inner.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
let other = s.execute("layer.addItem", json!({"item": inner.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [instance, other], "switch": "collapse", "value": true})).unwrap();
|
||||
s.expr = Some(std::sync::Arc::new(AddY));
|
||||
s.execute("layer.select", json!({"layers": [instance]})).unwrap();
|
||||
s.execute("essential.set", json!({"layer": instance, "control": position, "value": [200, -120, 0]})).unwrap();
|
||||
s.execute("layer.select", json!({"layers": [other]})).unwrap();
|
||||
s.execute("essential.set", json!({"layer": other, "control": position, "value": [200, 220, 0]})).unwrap();
|
||||
let img = s.render(outer, Tick::ZERO, RenderOpts::default());
|
||||
// The expression adds 100 px: the overrides give inner y = -20 and 320. The first maps to
|
||||
// outer y = 130 (the precomp is centred at 300) and its 100 px box spans 80..179, 70 px above
|
||||
// the instance's frame (150..449), where a flattened precomp would stop. The second maps to
|
||||
// 470 and spans 420..519, past the bottom.
|
||||
assert!(img.get(200, 100)[3] > 0.99, "the override draws above the nested frame: {:?}", img.get(200, 100));
|
||||
assert!(img.get(200, 90)[3] > 0.99, "the box reaches the top of its overridden bounds");
|
||||
assert!(img.get(200, 500)[3] > 0.99, "the other instance keeps its own override: {:?}", img.get(200, 500));
|
||||
assert!(img.get(200, 200)[3] < 0.01, "nothing is left between the instances");
|
||||
}
|
||||
|
||||
/// #1747: two collapsed instances of the same comp keep their own source-affecting property
|
||||
/// overrides in the layer cache (the nested layers' ids and static values are identical).
|
||||
#[test]
|
||||
fn collapsed_instances_keep_distinct_property_overrides_in_the_cache() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Card", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let card = s.active_comp_id().unwrap();
|
||||
let solid = s.execute("layer.newSolid", json!({"name": "BG", "color": "#ffffff", "width": 64, "height": 64})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("effect.apply", json!({"layer": solid, "effect": "Fill"})).unwrap();
|
||||
let color = s.execute("essential.addProperty", json!({"layer": solid, "path": "effects/#1/color"})).unwrap()["controls"][0].as_u64().unwrap();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 200, "height": 100, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let main = s.active_comp_id().unwrap();
|
||||
let a = s.execute("layer.addItem", json!({"item": card.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
let b = s.execute("layer.addItem", json!({"item": card.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [a, b], "switch": "collapse", "value": true})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": a, "path": "transform/position", "value": [60, 50, 0]})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": b, "path": "transform/position", "value": [140, 50, 0]})).unwrap();
|
||||
s.execute("layer.select", json!({"layers": [a]})).unwrap();
|
||||
s.execute("essential.set", json!({"layer": a, "control": color, "value": [1.0, 0.0, 0.0, 1.0]})).unwrap();
|
||||
s.execute("layer.select", json!({"layers": [b]})).unwrap();
|
||||
s.execute("essential.set", json!({"layer": b, "control": color, "value": [0.0, 0.0, 1.0, 1.0]})).unwrap();
|
||||
let img = s.render(main, Tick::ZERO, RenderOpts::default());
|
||||
let left = img.get(60, 50);
|
||||
let right = img.get(140, 50);
|
||||
assert!(left[0] > 0.99 && left[2] < 0.01, "left instance is red: {left:?}");
|
||||
assert!(right[2] > 0.99 && right[0] < 0.01, "right instance is blue: {right:?}");
|
||||
}
|
||||
|
||||
/// #1747 with Media Replacement: the replacement footage and the position override both apply
|
||||
/// while the precomp stays collapsed, so the box is blue and still draws above the frame.
|
||||
#[test]
|
||||
fn collapsed_precomp_applies_media_replacement_without_clipping() {
|
||||
struct Colors;
|
||||
impl crate::render::FootageSource for Colors {
|
||||
fn frame(&self, _: ItemId, f: &effectcraft_project::Footage, _: Tick) -> Option<std::sync::Arc<effectcraft_raster::Image>> {
|
||||
let c = if f.path.contains("blue") { [0.0, 0.0, 1.0, 1.0] } else { [1.0, 0.0, 0.0, 1.0] };
|
||||
Some(std::sync::Arc::new(effectcraft_raster::Image::filled(100, 100, c)))
|
||||
}
|
||||
}
|
||||
let mut s = Session { footage: std::sync::Arc::new(Colors), ..Default::default() };
|
||||
let footage = |path: &str| effectcraft_project::Footage {
|
||||
path: path.into(),
|
||||
kind: effectcraft_project::FootageKind::Still,
|
||||
width: 100,
|
||||
height: 100,
|
||||
has_video: true,
|
||||
..Default::default()
|
||||
};
|
||||
let (red, blue) = s
|
||||
.edit("test", None, |proj, _| {
|
||||
let red = proj.add_item("red.png", Default::default(), None, effectcraft_project::ItemKind::Footage(footage("red.png")));
|
||||
let blue = proj.add_item("blue.png", Default::default(), None, effectcraft_project::ItemKind::Footage(footage("blue.png")));
|
||||
Ok((red, blue))
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("comp.new", json!({"name": "Inner", "width": 400, "height": 300, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let inner = s.active_comp_id().unwrap();
|
||||
let clip = s.execute("layer.addItem", json!({"item": red.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("prop.set", json!({"layer": clip, "path": "transform/position", "value": [200, 50, 0]})).unwrap();
|
||||
let media = s.execute("essential.addMedia", json!({"layer": clip})).unwrap()["control"].as_u64().unwrap();
|
||||
let position = s.execute("essential.addProperty", json!({"layer": clip, "path": "transform/position"})).unwrap()["controls"][0].as_u64().unwrap();
|
||||
s.execute("comp.new", json!({"name": "Outer", "width": 400, "height": 600, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let outer = s.active_comp_id().unwrap();
|
||||
let instance = s.execute("layer.addItem", json!({"item": inner.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [instance], "switch": "collapse", "value": true})).unwrap();
|
||||
s.execute("layer.select", json!({"layers": [instance]})).unwrap();
|
||||
s.execute("essential.set", json!({"layer": instance, "control": position, "value": [200, -20, 0]})).unwrap();
|
||||
s.execute("essential.set", json!({"layer": instance, "control": media, "item": blue.0})).unwrap();
|
||||
let img = s.render(outer, Tick::ZERO, RenderOpts::default());
|
||||
let above = img.get(200, 100);
|
||||
assert!(above[2] > 0.99 && above[0] < 0.01, "the replacement media is blue above the frame: {above:?}");
|
||||
assert!(img.get(200, 200)[3] < 0.01, "the collapsed box is not clipped at the nested frame");
|
||||
}
|
||||
|
||||
/// Responsive Design — Time: stretching a precomp keeps its protected regions at their
|
||||
/// original speed; the unprotected parts absorb the stretch.
|
||||
#[test]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use effectcraft_keyframe::Value as KV;
|
||||
use effectcraft_project::essential::{self, ControlType};
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Event, Session};
|
||||
@@ -102,3 +103,33 @@ fn open_in_essential_graphics_sets_the_primary_comp() {
|
||||
);
|
||||
assert_eq!(s.execute("essential.list", json!({})).unwrap()["name"], "A");
|
||||
}
|
||||
|
||||
/// #1747: through a collapsed precomp, a Text override and a Font override of the same Source
|
||||
/// Text both apply: the Font merges into the overridden text, as `essential::with_overrides`
|
||||
/// does on the flattened path, instead of one entry replacing the other.
|
||||
#[test]
|
||||
fn collapsed_instance_merges_text_and_font_overrides() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Title", "width": 64, "height": 64, "frameRate": 30, "duration": 1})).unwrap();
|
||||
let title = s.active_comp_id().unwrap();
|
||||
let text = s.execute("layer.newText", json!({"text": "Hello"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let body = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText"})).unwrap()["controls"][0].as_u64().unwrap();
|
||||
let font = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText", "as": "font"})).unwrap()["controls"][0].as_u64().unwrap();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 64, "frameRate": 30, "duration": 1})).unwrap();
|
||||
let main = s.active_comp_id().unwrap();
|
||||
let inst = s.execute("layer.addItem", json!({"item": title.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [inst], "switch": "collapse", "value": true})).unwrap();
|
||||
s.execute("essential.set", json!({"layer": inst, "control": body, "value": "Bye"})).unwrap();
|
||||
s.execute("essential.set", json!({"layer": inst, "control": font, "value": {"font": "Serif Test", "size": 40}})).unwrap();
|
||||
// Evaluate the nested text in the collapsed instance's context, as the renderer does.
|
||||
let r = effectcraft_render::Renderer::new(&s.project, &*s.footage, crate::render::RenderOpts::default());
|
||||
let ctx = effectcraft_render::EvalCtx::new(&s.project, main, s.project.comp(main).unwrap(), Tick::ZERO);
|
||||
let layer = ctx.comp.layer(effectcraft_project::LayerId(inst)).unwrap();
|
||||
let collapsed = r.collapsed(&ctx, layer).expect("the instance collapses");
|
||||
let (_, nctx) = r.collapse_into(&ctx, layer, &collapsed, 1.0).expect("a nested context");
|
||||
let src = nctx.comp.layer(effectcraft_project::LayerId(text)).unwrap();
|
||||
match nctx.value(src, src.props.prop("text/sourceText").unwrap()) {
|
||||
KV::Text(d) => assert_eq!((d.text.as_str(), d.font.as_str(), d.size), ("Bye", "Serif Test", 40.0)),
|
||||
o => panic!("{o:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ fn close3(a: [f64; 3], b: [f64; 3], tol: f64) -> bool {
|
||||
fn frame(s: &Session, id: u64) -> (Vec3, Vec3) {
|
||||
let c = cid(s);
|
||||
let comp = s.project.comp(c).unwrap();
|
||||
let ctx = EvalCtx { project: &s.project, comp_id: c, comp, time: Tick::ZERO, expr: None, footage: None };
|
||||
let ctx = EvalCtx { project: &s.project, comp_id: c, comp, time: Tick::ZERO, expr: None, footage: None, property_overrides: &[], media_overrides: &[] };
|
||||
let (e, f, _) = layer_frame(&ctx, comp.layer(LayerId(id)).unwrap());
|
||||
(e, f)
|
||||
}
|
||||
@@ -244,7 +244,7 @@ fn parented_camera_keeps_its_frame_under_an_offset_null() {
|
||||
fn frame_at(s: &Session, id: u64, t: Tick) -> (Vec3, Vec3, Vec3) {
|
||||
let c = cid(s);
|
||||
let comp = s.project.comp(c).unwrap();
|
||||
let ctx = EvalCtx { project: &s.project, comp_id: c, comp, time: t, expr: None, footage: None };
|
||||
let ctx = EvalCtx { project: &s.project, comp_id: c, comp, time: t, expr: None, footage: None, property_overrides: &[], media_overrides: &[] };
|
||||
layer_frame(&ctx, comp.layer(LayerId(id)).unwrap())
|
||||
}
|
||||
|
||||
|
||||
+20
-5
@@ -10,7 +10,7 @@
|
||||
use effectcraft_geom::{Mat3, Mat4};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::{Comp, ItemId, ItemKind, Layer, LayerId, LayerSource, Node, PropGroup, Property};
|
||||
use effectcraft_render::{EvalCtx, source_size};
|
||||
use effectcraft_render::EvalCtx;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
/// A name-or-index argument (`layer("A")`, `layer(2)`, `effect(1)`).
|
||||
@@ -299,7 +299,17 @@ impl<'a> Resolver<'a> {
|
||||
}
|
||||
fn ctx_at(&self, c: u64, t: f64) -> Option<EvalCtx<'a>> {
|
||||
let comp = self.comp(c)?;
|
||||
Some(EvalCtx { project: self.ctx.project, comp_id: ItemId(c), comp, time: Tick::from_seconds_f64(t), expr: self.ctx.expr, footage: self.ctx.footage })
|
||||
let same_comp = c == self.ctx.comp_id.0;
|
||||
Some(EvalCtx {
|
||||
project: self.ctx.project,
|
||||
comp_id: ItemId(c),
|
||||
comp,
|
||||
time: Tick::from_seconds_f64(t),
|
||||
expr: self.ctx.expr,
|
||||
footage: self.ctx.footage,
|
||||
property_overrides: if same_comp { self.ctx.property_overrides } else { &[] },
|
||||
media_overrides: if same_comp { self.ctx.media_overrides } else { &[] },
|
||||
})
|
||||
}
|
||||
fn prop(&self, c: u64, l: u64, path: &str) -> Option<(&'a Layer, &'a Property)> {
|
||||
let layer = self.layer(c, l)?;
|
||||
@@ -372,7 +382,7 @@ impl<'a> Resolver<'a> {
|
||||
Req::LayerInfo { comp, layer } => {
|
||||
let c = self.comp(*comp)?;
|
||||
let l = self.layer(*comp, *layer)?;
|
||||
let (mut w, mut h) = source_size(project, l);
|
||||
let (mut w, mut h) = self.ctx_at(*comp, self.ctx.time.seconds())?.source_size(l);
|
||||
if w == 0 || h == 0 {
|
||||
(w, h) = (c.width, c.height);
|
||||
}
|
||||
@@ -467,7 +477,7 @@ impl<'a> Resolver<'a> {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let (w, h) = source_size(project, l);
|
||||
let (w, h) = ctx.source_size(l);
|
||||
[0.0, 0.0, w as f64, h as f64]
|
||||
}
|
||||
};
|
||||
@@ -501,7 +511,12 @@ impl<'a> Resolver<'a> {
|
||||
Req::SourceInfo { comp, layer } => {
|
||||
let c = self.comp(*comp)?;
|
||||
let l = self.layer(*comp, *layer)?;
|
||||
let it = project.item(l.source.item()?)?;
|
||||
let ctx = self.ctx_at(*comp, self.ctx.time.seconds())?;
|
||||
let item = match &l.source {
|
||||
LayerSource::Footage { item } => ctx.media_item(l, *item),
|
||||
_ => l.source.item()?,
|
||||
};
|
||||
let it = project.item(item)?;
|
||||
let (kind, aspect, frame, duration) = match &it.kind {
|
||||
ItemKind::Comp(nc) => ("comp", nc.pixel_aspect, nc.frame_duration().seconds(), nc.duration.seconds()),
|
||||
ItemKind::Footage(f) => ("footage", f.pixel_aspect, f.frame_rate.frame_duration().seconds(), f.duration.seconds()),
|
||||
|
||||
+10
-1
@@ -163,7 +163,16 @@ fn evaluate_out(
|
||||
return Err("Error: expressions reference each other too deeply (circular reference?)".into());
|
||||
}
|
||||
budget_left()?;
|
||||
let ctx = EvalCtx { project: ctx.project, comp_id: ctx.comp_id, comp: ctx.comp, time: ctx.time, expr: Some(host), footage: ctx.footage };
|
||||
let ctx = EvalCtx {
|
||||
project: ctx.project,
|
||||
comp_id: ctx.comp_id,
|
||||
comp: ctx.comp,
|
||||
time: ctx.time,
|
||||
expr: Some(host),
|
||||
footage: ctx.footage,
|
||||
property_overrides: ctx.property_overrides,
|
||||
media_overrides: ctx.media_overrides,
|
||||
};
|
||||
let comp = ctx.comp_id.0;
|
||||
let path = layer.props.path_of(prop.uid).unwrap_or_else(|| format!("@{}", prop.uid));
|
||||
let resolver = Resolver { ctx, own: Own { comp, layer, prop, path: path.clone() } };
|
||||
|
||||
@@ -978,6 +978,49 @@ fn source_rect_at_time() {
|
||||
assert!((s[0] + 31.0).abs() < 2.0 && (s[1] + 16.0).abs() < 2.0 && (s[2] - 62.0).abs() < 3.0, "{s:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_dimensions_follow_media_replacement() {
|
||||
let mut p = Project::default();
|
||||
let comp = Comp::new(200, 100, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let cid = p.add_item("Main", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
|
||||
let footage = |p: &mut Project, name: &str, width, height| {
|
||||
p.add_item(
|
||||
name,
|
||||
Label::SeaFoam,
|
||||
None,
|
||||
ItemKind::Footage(effectcraft_project::Footage {
|
||||
path: name.into(),
|
||||
kind: effectcraft_project::FootageKind::Still,
|
||||
width,
|
||||
height,
|
||||
has_video: true,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
};
|
||||
let original = footage(&mut p, "original.png", 100, 80);
|
||||
let replacement = footage(&mut p, "replacement.png", 30, 40);
|
||||
let mut l = build::layer(&mut p, &comp, "Clip", LayerSource::Footage { item: original }, (100, 80), None);
|
||||
l.props.prop_mut(ROT).unwrap().expr =
|
||||
Some(Expression { text: "thisLayer.width + thisLayer.height + thisLayer.source.width + sourceRectAtTime().width".into(), enabled: true });
|
||||
let lid = l.id;
|
||||
p.comp_mut(cid).unwrap().layers.push(l);
|
||||
let c = p.comp(cid).unwrap();
|
||||
let l = c.layer(lid).unwrap();
|
||||
let media = effectcraft_render::MediaOverride::new(lid, replacement);
|
||||
let ctx = EvalCtx {
|
||||
project: &p,
|
||||
comp_id: cid,
|
||||
comp: c,
|
||||
time: Tick::ZERO,
|
||||
expr: Some(&Expressions),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[media],
|
||||
};
|
||||
assert_eq!(ctx.value(l, l.props.prop(ROT).unwrap()), Value::Scalar(130.0));
|
||||
}
|
||||
|
||||
// ---- colour -----------------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
@@ -1185,7 +1228,16 @@ fn text_expression_selector_values() {
|
||||
let sel_of = |p: &Project| {
|
||||
let c = p.comp(cid).unwrap();
|
||||
let l = c.layer(lid).unwrap();
|
||||
let ctx = EvalCtx { project: p, comp_id: cid, comp: c, time: Tick::ZERO, expr: Some(&Expressions), footage: None };
|
||||
let ctx = EvalCtx {
|
||||
project: p,
|
||||
comp_id: cid,
|
||||
comp: c,
|
||||
time: Tick::ZERO,
|
||||
expr: Some(&Expressions),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let doc = effectcraft_render::text::source_text(&ctx, l).unwrap();
|
||||
let lay = effectcraft_text::layout_doc(&doc);
|
||||
let anim = l.props.group("text/animators/#1").unwrap();
|
||||
|
||||
@@ -96,7 +96,16 @@ pub(crate) fn scene_project() -> (Project, ItemId) {
|
||||
|
||||
pub(crate) fn scene(p: &Project, cid: ItemId) -> Scene {
|
||||
let r = Renderer::new(p, &NoFootage, RenderOpts::default());
|
||||
let ctx = EvalCtx { project: p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None, footage: None };
|
||||
let ctx = EvalCtx {
|
||||
project: p,
|
||||
comp_id: cid,
|
||||
comp: p.comp(cid).unwrap(),
|
||||
time: Tick::ZERO,
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let run: Vec<&effectcraft_project::Layer> = ctx.comp.layers.iter().rev().filter(|l| l.is_3d() && l.has_video()).collect();
|
||||
adv::scene_of(&r, &ctx, &run, (ctx.comp.width, ctx.comp.height))
|
||||
}
|
||||
@@ -277,7 +286,16 @@ pub(crate) fn off_share(a: &[[f32; 4]], b: &[[f32; 4]]) -> (f64, f32) {
|
||||
}
|
||||
|
||||
pub(crate) fn run_of(p: &Project, cid: ItemId) -> (EvalCtx<'_>, Vec<&effectcraft_project::Layer>) {
|
||||
let ctx = EvalCtx { project: p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None, footage: None };
|
||||
let ctx = EvalCtx {
|
||||
project: p,
|
||||
comp_id: cid,
|
||||
comp: p.comp(cid).unwrap(),
|
||||
time: Tick::ZERO,
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let run: Vec<&effectcraft_project::Layer> = ctx.comp.layers.iter().rev().filter(|l| l.is_3d() && l.has_video()).collect();
|
||||
(ctx, run)
|
||||
}
|
||||
|
||||
+11
-4
@@ -89,7 +89,7 @@ fn draw_comp<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, canvas: &mut
|
||||
i = j;
|
||||
} else {
|
||||
match r.collapsed(ctx, visible[i]) {
|
||||
Some(item) => draw_collapsed(e, r, ctx, visible[i], item, canvas)?,
|
||||
Some(collapsed) => draw_collapsed(e, r, ctx, visible[i], &collapsed, canvas)?,
|
||||
None => draw_layer(e, r, ctx, visible[i], canvas)?,
|
||||
}
|
||||
i += 1;
|
||||
@@ -187,19 +187,26 @@ fn draw_split(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, split: &effectcraft_rend
|
||||
Some(cv)
|
||||
}
|
||||
|
||||
fn draw_collapsed<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &Layer, item: ItemId, canvas: &mut GpuImage) -> Option<()> {
|
||||
fn draw_collapsed<'a>(
|
||||
e: &mut Enc,
|
||||
r: &Renderer<'a>,
|
||||
ctx: &EvalCtx<'a>,
|
||||
layer: &'a Layer,
|
||||
collapsed: &effectcraft_render::Collapsed,
|
||||
canvas: &mut GpuImage,
|
||||
) -> Option<()> {
|
||||
let opacity = r.layer_opacity(ctx, layer);
|
||||
if opacity <= 0.0 {
|
||||
return Some(());
|
||||
}
|
||||
let matte = r.track_matte(ctx, layer).is_some();
|
||||
if layer.blend_mode == BlendMode::Normal && !matte && !layer.preserve_transparency {
|
||||
if let Some((sub, nctx)) = r.collapse_into(ctx, layer, item, opacity) {
|
||||
if let Some((sub, nctx)) = r.collapse_into(ctx, layer, collapsed, opacity) {
|
||||
draw_comp(e, &sub, &nctx, canvas)?;
|
||||
}
|
||||
return Some(());
|
||||
}
|
||||
let Some((sub, nctx)) = r.collapse_into(ctx, layer, item, 1.0) else { return Some(()) };
|
||||
let Some((sub, nctx)) = r.collapse_into(ctx, layer, collapsed, 1.0) else { return Some(()) };
|
||||
let mut iso = e.zeros(canvas.width, canvas.height);
|
||||
draw_comp(e, &sub, &nctx, &mut iso)?;
|
||||
composite_iso(e, r, ctx, layer, iso, canvas, opacity)
|
||||
|
||||
@@ -269,7 +269,16 @@ fn value(s: &effectcraft_engine::Session, l: u64, path: &str) -> effectcraft_eng
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let comp = s.project.comp(cid).unwrap();
|
||||
let layer = comp.layer(effectcraft_engine::project::LayerId(l)).unwrap();
|
||||
let ctx = effectcraft_engine::render::EvalCtx { project: &s.project, comp_id: cid, comp, time: Default::default(), expr: s.expr.as_deref(), footage: None };
|
||||
let ctx = effectcraft_engine::render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: Default::default(),
|
||||
expr: s.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
ctx.value(layer, layer.props.prop(path).unwrap())
|
||||
}
|
||||
|
||||
@@ -523,8 +532,16 @@ fn stereo_rig_expressions_follow_the_master_camera() {
|
||||
let v3 = |s: &effectcraft_engine::Session, path: &str| {
|
||||
let comp = s.project.comp(left).unwrap();
|
||||
let layer = &comp.layers[0];
|
||||
let ctx =
|
||||
effectcraft_engine::render::EvalCtx { project: &s.project, comp_id: left, comp, time: Default::default(), expr: s.expr.as_deref(), footage: None };
|
||||
let ctx = effectcraft_engine::render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: left,
|
||||
comp,
|
||||
time: Default::default(),
|
||||
expr: s.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
ctx.value(layer, layer.props.prop(path).unwrap()).as_vec3()
|
||||
};
|
||||
let close = |a: [f64; 3], b: [f64; 3]| (0..3).all(|i| (a[i] - b[i]).abs() < 1e-6);
|
||||
|
||||
@@ -184,8 +184,9 @@ impl<'a> Renderer<'a> {
|
||||
pub fn layer_aux(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Arc<effectcraft_raster::AuxChannels>> {
|
||||
match &layer.source {
|
||||
LayerSource::Footage { item } => {
|
||||
let ItemKind::Footage(f) = &self.project.item(*item)?.kind else { return None };
|
||||
let a = self.footage.aux(*item, f, ctx.source_time(layer))?;
|
||||
let item = ctx.media_item(layer, *item);
|
||||
let ItemKind::Footage(f) = &self.project.item(item)?.kind else { return None };
|
||||
let a = self.footage.aux(item, f, ctx.source_time(layer))?;
|
||||
// Aux pixels per layer pixel (the layer is the footage at its nominal size). The
|
||||
// planes are only copied when that changes the scale.
|
||||
let scale = a.width as f64 / f.width as f64;
|
||||
@@ -332,8 +333,8 @@ impl<'a> Renderer<'a> {
|
||||
}
|
||||
|
||||
/// Draw a collapsed precomp layer into `canvas` on the CPU.
|
||||
pub fn draw_collapsed_cpu(&self, ctx: &EvalCtx<'a>, layer: &Layer, item: ItemId, canvas: &mut Image) {
|
||||
self.draw_collapsed(ctx, layer, item, canvas, false);
|
||||
pub fn draw_collapsed_cpu(&self, ctx: &EvalCtx<'a>, layer: &'a Layer, collapsed: &crate::Collapsed, canvas: &mut Image) {
|
||||
self.draw_collapsed(ctx, layer, collapsed, canvas, false);
|
||||
}
|
||||
|
||||
/// The layer's finished buffers (styles included) in the blending space.
|
||||
|
||||
@@ -479,6 +479,19 @@ fn group_has(g: &PropGroup, match_id: &str) -> bool {
|
||||
|
||||
/// Feed every property of `g` that can change over time (keyframes or expression) as its value
|
||||
/// at the context time. The static structure is hashed separately.
|
||||
/// Mix the collapsed-instance overrides that target `id` into a cache key. They replace values
|
||||
/// without editing the layer, so a key without them would reuse another instance's buffer.
|
||||
fn hash_overrides(h: &mut KeyHasher, ctx: &EvalCtx, id: effectcraft_project::LayerId) {
|
||||
for o in ctx.property_overrides.iter().filter(|o| o.layer == id) {
|
||||
o.prop.hash(h);
|
||||
o.font.hash(h);
|
||||
hash_debug(h, &o.value);
|
||||
}
|
||||
for m in ctx.media_overrides.iter().filter(|m| m.layer == id) {
|
||||
m.item.hash(h);
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_values(h: &mut KeyHasher, ctx: &EvalCtx, layer: &Layer, g: &PropGroup) {
|
||||
for c in &g.children {
|
||||
match c {
|
||||
@@ -605,8 +618,9 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, fo
|
||||
hash_debug(&mut h, s);
|
||||
}
|
||||
LayerSource::Text | LayerSource::Shape => {}
|
||||
LayerSource::Footage { item } if footage || still_footage(ctx, *item) => {
|
||||
let it = ctx.project.item(*item)?;
|
||||
LayerSource::Footage { item } if footage || still_footage(ctx, ctx.media_item(layer, *item)) => {
|
||||
let item = ctx.media_item(layer, *item);
|
||||
let it = ctx.project.item(item)?;
|
||||
let ItemKind::Footage(f) = &it.kind else { return None };
|
||||
hash_debug(&mut h, f);
|
||||
// A proxy (and its Use Proxy switch) changes the pixels.
|
||||
@@ -629,6 +643,10 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, fo
|
||||
scale.to_bits().hash(&mut h);
|
||||
draft.hash(&mut h);
|
||||
layer.id.hash(&mut h);
|
||||
// Collapsed-instance overrides replace values without editing the nested layer, so mix them
|
||||
// into the key: a Font override, effect parameter or any other source-affecting override
|
||||
// must not reuse another instance's processed buffer.
|
||||
hash_overrides(&mut h, ctx, layer.id);
|
||||
hash_debug(&mut h, &layer.source);
|
||||
// (Audio, Lock and Shy don't change pixels.)
|
||||
hash_debug(&mut h, &layer.switches.pixels());
|
||||
|
||||
+7
@@ -76,6 +76,9 @@ impl Dependencies<'_> {
|
||||
// Include transforms/parents as well as pixels: selected-layer anchors and
|
||||
// continuously rasterized sources can depend on them.
|
||||
hash_debug(self.h, layer);
|
||||
// A referenced layer's static values can be overridden by the collapsed instance without
|
||||
// editing it (`hash_values` only sees animated ones), so the owner's key needs them too.
|
||||
super::hash_overrides(self.h, ctx, layer.id);
|
||||
hash_values(self.h, ctx, layer, &layer.props);
|
||||
hash_debug(self.h, &ctx.world_matrix(layer));
|
||||
// A cached owner never runs this layer's effects, so the comp camera and lights its
|
||||
@@ -88,6 +91,10 @@ impl Dependencies<'_> {
|
||||
}
|
||||
match layer.source {
|
||||
LayerSource::Solid { item } | LayerSource::Footage { item } | LayerSource::Comp { item } => {
|
||||
let item = match layer.source {
|
||||
LayerSource::Footage { .. } => ctx.media_item(layer, item),
|
||||
_ => item,
|
||||
};
|
||||
let source = ctx.project.item(item);
|
||||
hash_debug(self.h, &source);
|
||||
if let Some(source) = source {
|
||||
|
||||
+64
-10
@@ -39,17 +39,33 @@ pub struct EvalCtx<'a> {
|
||||
/// Footage frames for expressions that read rendered pixels (`sampleImage`): the renderer's
|
||||
/// own source while rendering; `None` renders footage as transparent.
|
||||
pub footage: Option<&'a dyn crate::FootageSource>,
|
||||
/// Property values overridden by the Essential Properties of a collapsed precomp instance.
|
||||
pub property_overrides: &'a [crate::PropertyOverride],
|
||||
/// Media Replacement values overridden by the Essential Properties of a collapsed precomp
|
||||
/// instance.
|
||||
pub media_overrides: &'a [crate::MediaOverride],
|
||||
}
|
||||
|
||||
impl<'a> EvalCtx<'a> {
|
||||
pub fn new(project: &'a Project, comp_id: ItemId, comp: &'a Comp, time: Tick) -> EvalCtx<'a> {
|
||||
EvalCtx { project, comp_id, comp, time, expr: None, footage: None }
|
||||
EvalCtx { project, comp_id, comp, time, expr: None, footage: None, property_overrides: &[], media_overrides: &[] }
|
||||
}
|
||||
pub fn at(&self, time: Tick) -> EvalCtx<'a> {
|
||||
EvalCtx { time, ..*self }
|
||||
}
|
||||
/// Value of a property at the context time (keyframes, then expression).
|
||||
pub fn value(&self, layer: &Layer, prop: &Property) -> Value {
|
||||
// A collapsed precomp instance's Essential Property override replaces the base value;
|
||||
// its expression (if any) still runs, and sees the overrides of this context.
|
||||
if let Some(o) = self.property_overrides.iter().rev().find(|o| o.layer == layer.id && o.prop == prop.uid) {
|
||||
let v = o.value(layer, prop, self.time);
|
||||
if prop.has_expression()
|
||||
&& let Some(h) = self.expr
|
||||
{
|
||||
return h.eval(self, layer, prop, &v).unwrap_or(v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
// Separated Position reads as the combination of X/Y/Z Position.
|
||||
if prop.match_id == "position"
|
||||
&& let Some(tr) = layer.transform()
|
||||
@@ -145,13 +161,15 @@ impl<'a> EvalCtx<'a> {
|
||||
/// Source pixel aspect relative to the comp's: non-square footage (or solids, or precomps
|
||||
/// with another pixel aspect) is stretched horizontally by this factor in the comp.
|
||||
pub fn par_ratio(&self, layer: &Layer) -> f64 {
|
||||
let par = match &layer.source {
|
||||
LayerSource::Footage { item } | LayerSource::Solid { item } | LayerSource::Comp { item } => match self.project.item(*item).map(|i| &i.kind) {
|
||||
Some(effectcraft_project::ItemKind::Footage(f)) => f.pixel_aspect,
|
||||
Some(effectcraft_project::ItemKind::Solid(s)) => s.pixel_aspect,
|
||||
Some(effectcraft_project::ItemKind::Comp(c)) => c.pixel_aspect,
|
||||
_ => 1.0,
|
||||
},
|
||||
let item = match &layer.source {
|
||||
LayerSource::Footage { item } => Some(self.media_item(layer, *item)),
|
||||
LayerSource::Solid { item } | LayerSource::Comp { item } => Some(*item),
|
||||
_ => None,
|
||||
};
|
||||
let par = match item.and_then(|item| self.project.item(item)).map(|i| &i.kind) {
|
||||
Some(effectcraft_project::ItemKind::Footage(f)) => f.pixel_aspect,
|
||||
Some(effectcraft_project::ItemKind::Solid(s)) => s.pixel_aspect,
|
||||
Some(effectcraft_project::ItemKind::Comp(c)) => c.pixel_aspect,
|
||||
_ => 1.0,
|
||||
};
|
||||
let r = par / if self.comp.pixel_aspect > 0.0 { self.comp.pixel_aspect } else { 1.0 };
|
||||
@@ -237,9 +255,27 @@ impl<'a> EvalCtx<'a> {
|
||||
(Mat3([[m[0][0], m[0][1], m[0][3]], [m[1][0], m[1][1], m[1][3]], [0.0, 0.0, 1.0]]), 0.0)
|
||||
}
|
||||
}
|
||||
/// The footage item a layer uses, honouring this instance's Media Replacement override.
|
||||
pub fn media_item(&self, layer: &Layer, item: ItemId) -> ItemId {
|
||||
self.media_overrides.iter().rev().find(|m| m.layer == layer.id).map_or(item, |m| m.item)
|
||||
}
|
||||
|
||||
/// Size of a layer's source in layer pixels, honouring Media Replacement.
|
||||
pub fn source_size(&self, layer: &Layer) -> (u32, u32) {
|
||||
let item = match &layer.source {
|
||||
LayerSource::Footage { item } => Some(self.media_item(layer, *item)),
|
||||
LayerSource::Comp { item } | LayerSource::Solid { item } => Some(*item),
|
||||
_ => None,
|
||||
};
|
||||
match layer.source {
|
||||
LayerSource::Null => (100, 100),
|
||||
_ => item.and_then(|item| self.project.item(item)).and_then(|i| i.dimensions()).unwrap_or((0, 0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`effect_bounds`] of a layer of this comp.
|
||||
pub fn effect_bounds(&self, layer: &Layer) -> ([f64; 2], [f64; 2]) {
|
||||
effect_bounds(self.project, self.comp, layer)
|
||||
effect_bounds_of(self.comp, self.source_size(layer))
|
||||
}
|
||||
/// Effect space (see [`effect_bounds`]) → comp matrix.
|
||||
pub fn effect_to_comp(&self, layer: &Layer) -> Mat3 {
|
||||
@@ -248,6 +284,20 @@ impl<'a> EvalCtx<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::PropertyOverride {
|
||||
fn value(&self, layer: &Layer, prop: &Property, time: Tick) -> Value {
|
||||
if self.font {
|
||||
// A Font control merges only the font into the target text, as
|
||||
// `essential::with_overrides` does.
|
||||
let mut value = prop.value_at(layer.layer_time(time));
|
||||
effectcraft_project::essential::merge_font(&mut value, &self.value);
|
||||
value
|
||||
} else {
|
||||
self.value.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The protected regions of a comp (Responsive Design — Time markers), merged, sorted and
|
||||
/// clamped to the comp, in seconds.
|
||||
pub fn protected_regions(comp: &Comp) -> Vec<(f64, f64)> {
|
||||
@@ -321,7 +371,11 @@ pub fn responsive_source_time(comp: &Comp, stretch: f64, elapsed: f64) -> Option
|
||||
/// surrounds. Effects work in **effect space**, measured from that corner (After Effects' layer
|
||||
/// space for effect points: a shape layer's default effect point is its bounds' centre).
|
||||
pub fn effect_bounds(project: &Project, comp: &Comp, layer: &Layer) -> ([f64; 2], [f64; 2]) {
|
||||
match source_size(project, layer) {
|
||||
effect_bounds_of(comp, source_size(project, layer))
|
||||
}
|
||||
|
||||
fn effect_bounds_of(comp: &Comp, size: (u32, u32)) -> ([f64; 2], [f64; 2]) {
|
||||
match size {
|
||||
(0, _) => {
|
||||
let (w, h) = (comp.width as f64, comp.height as f64);
|
||||
([w, h], [-w / 2.0, -h / 2.0])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::footage::NoFootage;
|
||||
use super::opts::RenderOpts;
|
||||
use super::{EvalCtx, Footage, FrameBlend, FrameRate, Image, ItemId, Layer, LayerSource, Project, Renderer, TICKS_PER_SECOND, Tick, shapes, source_size, text};
|
||||
use super::{EvalCtx, Footage, FrameBlend, FrameRate, Image, ItemId, Layer, LayerSource, Project, Renderer, TICKS_PER_SECOND, Tick, shapes, text};
|
||||
|
||||
/// The layer's frame blending mode, if the comp's Enable Frame Blending switch is on.
|
||||
/// One field of an interlaced frame as a full frame: the field's lines (`parity` 0 = even /
|
||||
@@ -132,7 +132,7 @@ pub fn content_bounds(ctx: &EvalCtx, layer: &effectcraft_project::Layer) -> Opti
|
||||
LayerSource::Shape => layer.props.sub("contents").and_then(|c| shapes::content_bounds(ctx, layer, c)).map(|r| [r.x0, r.y0, r.x1, r.y1]),
|
||||
LayerSource::Camera | LayerSource::Light { .. } => None,
|
||||
_ => {
|
||||
let (w, h) = source_size(ctx.project, layer);
|
||||
let (w, h) = ctx.source_size(layer);
|
||||
(w > 0 && h > 0).then_some([0.0, 0.0, w as f64, h as f64])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +90,13 @@ impl EffectHost for FxHost<'_, '_, '_> {
|
||||
fn audio(&self, id: u64, start: f64, frames: usize, rate: u32) -> Option<Vec<f32>> {
|
||||
let other = self.ctx.layer(effectcraft_project::LayerId(id))?;
|
||||
let LayerSource::Footage { item } = &other.source else { return None };
|
||||
let ItemKind::Footage(f) = &self.r.project.item(*item)?.kind else { return None };
|
||||
let item = self.ctx.media_item(other, *item);
|
||||
let ItemKind::Footage(f) = &self.r.project.item(item)?.kind else { return None };
|
||||
if !f.has_audio {
|
||||
return None;
|
||||
}
|
||||
let st = other.layer_time(Tick::from_seconds_f64(start));
|
||||
self.r.footage.audio(*item, f, st, frames, rate)
|
||||
self.r.footage.audio(item, f, st, frames, rate)
|
||||
}
|
||||
|
||||
fn self_at(&self, layer_time: f64, effects: usize) -> Option<Buf> {
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::frames::adaptive_samples;
|
||||
use super::opts::{LayerTiming, Through};
|
||||
use super::{
|
||||
Arc, BlendMode, Buf, Comp, EvalCtx, Image, IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, Layer, LayerSource, Mat3,
|
||||
MatteKind, ParallelIterator, Quality, Region, Renderer, Sampling, Tick, WarpOpts, cache, color, composite_warp_changed, masks, source_size, styles, vec2,
|
||||
MatteKind, ParallelIterator, Quality, Region, Renderer, Sampling, Tick, WarpOpts, cache, color, composite_warp_changed, masks, styles, vec2,
|
||||
};
|
||||
|
||||
/// Blend policy for placing a layer, including optional output-depth quantization.
|
||||
@@ -52,7 +52,7 @@ impl<'a> Renderer<'a> {
|
||||
}
|
||||
}
|
||||
let t0 = web_time::Instant::now();
|
||||
let size = source_size(self.project, layer);
|
||||
let size = ctx.source_size(layer);
|
||||
let layer_size = if size.0 == 0 { [ctx.comp.width as f64, ctx.comp.height as f64] } else { [size.0 as f64, size.1 as f64] };
|
||||
let st = styles::render(ctx, layer, &content, layer_size);
|
||||
if let Some(t) = timing {
|
||||
|
||||
@@ -60,6 +60,7 @@ pub use layers::{Placement, StyledLayer};
|
||||
pub(crate) use opts::Collapse3d;
|
||||
pub use opts::{LayerTiming, RenderOpts};
|
||||
use opts::{MAX_THROUGH, Through};
|
||||
pub use pipeline::{Collapsed, MediaOverride, PropertyOverride};
|
||||
|
||||
pub struct Renderer<'a> {
|
||||
pub project: &'a Project,
|
||||
|
||||
+117
-16
@@ -7,6 +7,8 @@ use super::{
|
||||
Arc, AutoKey, AutoPick, BlendMode, Buf, Comp, EffectCtx, EffectEnv, EvalCtx, Footage, FootageKind, FrameBlend, FrameRate, GroupKind, Image, ItemId,
|
||||
ItemKind, Layer, LayerCache, LayerSource, Params, Project, Quality, Region, Renderer, Tick, cache, color, masks, shapes, styles, text, three_d, vec2,
|
||||
};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::LayerId;
|
||||
|
||||
/// Coordinate space and stack range shared by CPU and accelerated effect evaluation.
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -16,6 +18,88 @@ pub(crate) struct EffectStack {
|
||||
pub origin: [f64; 2],
|
||||
}
|
||||
|
||||
/// A property value an Essential Properties override gives a layer of a collapsed precomp.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PropertyOverride {
|
||||
pub(crate) layer: LayerId,
|
||||
pub(crate) prop: u64,
|
||||
pub(crate) value: Value,
|
||||
/// A Font control changes only the font of the target text, not its text.
|
||||
pub(crate) font: bool,
|
||||
}
|
||||
|
||||
/// A Media Replacement an Essential Properties override gives a footage layer of a collapsed
|
||||
/// precomp.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct MediaOverride {
|
||||
pub(crate) layer: LayerId,
|
||||
pub(crate) item: ItemId,
|
||||
}
|
||||
|
||||
impl MediaOverride {
|
||||
/// A Media Replacement of `layer`'s footage by `item`.
|
||||
pub fn new(layer: LayerId, item: ItemId) -> Self {
|
||||
MediaOverride { layer, item }
|
||||
}
|
||||
}
|
||||
|
||||
/// A collapsed precomp layer: the nested comp and the instance overrides that apply to it.
|
||||
pub struct Collapsed {
|
||||
pub item: ItemId,
|
||||
pub(crate) property_overrides: Vec<PropertyOverride>,
|
||||
pub(crate) media_overrides: Vec<MediaOverride>,
|
||||
}
|
||||
|
||||
/// The property and media overrides of a collapsed precomp layer. `None` when its Essential
|
||||
/// Properties group or the source comp's definitions are missing (the flattened path then
|
||||
/// preserves the old behaviour).
|
||||
fn collapsed_instance(ctx: &EvalCtx, layer: &Layer, item: ItemId) -> Option<(Vec<PropertyOverride>, Vec<MediaOverride>)> {
|
||||
use effectcraft_project::essential::{self, ControlType, EgKind};
|
||||
let over = essential::overridden(layer);
|
||||
if over.is_empty() {
|
||||
return Some((vec![], vec![]));
|
||||
}
|
||||
let g = essential::group(layer)?;
|
||||
let eg = ctx.project.comp(item)?.essential.as_ref()?;
|
||||
let mut property = vec![];
|
||||
let mut fonts: Vec<PropertyOverride> = vec![];
|
||||
let mut media = vec![];
|
||||
g.walk("", &mut |_, p| {
|
||||
if !over.contains(&p.uid) {
|
||||
return;
|
||||
}
|
||||
let Some(control) = essential::control_of(&p.match_id) else { return };
|
||||
let Some(ctl) = eg.resolve(control) else { return };
|
||||
let value = ctx.value(layer, p);
|
||||
match &ctl.kind {
|
||||
EgKind::Property { .. } => {
|
||||
let font = ctl.as_type == Some(ControlType::Font);
|
||||
for (layer, prop) in ctl.kind.targets() {
|
||||
let o = PropertyOverride { layer, prop, value: value.clone(), font };
|
||||
if font { fonts.push(o) } else { property.push(o) }
|
||||
}
|
||||
}
|
||||
EgKind::Media { layer: target } => {
|
||||
let Value::Scalar(id) = value else { return };
|
||||
let item = ItemId(id.max(0.0) as u64);
|
||||
if matches!(ctx.project.item(item).map(|i| &i.kind), Some(ItemKind::Footage(_))) {
|
||||
media.push(MediaOverride { layer: *target, item });
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
// A Font control merges into the text its instance shows, after a whole-value (Text) override
|
||||
// of the same property, as `essential::with_overrides` does: keep one entry per property.
|
||||
for f in fonts {
|
||||
match property.iter_mut().rev().find(|o| o.layer == f.layer && o.prop == f.prop) {
|
||||
Some(o) => effectcraft_project::essential::merge_font(&mut o.value, &f.value),
|
||||
None => property.push(f),
|
||||
}
|
||||
}
|
||||
Some((property, media))
|
||||
}
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub fn new(project: &'a Project, footage: &'a dyn FootageSource, opts: RenderOpts) -> Renderer<'a> {
|
||||
Renderer {
|
||||
@@ -122,8 +206,10 @@ impl<'a> Renderer<'a> {
|
||||
|
||||
/// The nested comp of a precomp layer whose transformations collapse into this comp: the
|
||||
/// Collapse Transformations switch is on and the layer has no masks, effects or layer
|
||||
/// styles (those force a flattened render of the precomp, as in After Effects).
|
||||
pub fn collapsed(&self, ctx: &EvalCtx, layer: &Layer) -> Option<ItemId> {
|
||||
/// styles (those force a flattened render of the precomp, as in After Effects). Essential
|
||||
/// Properties overrides travel with it and are applied as the nested layers are evaluated,
|
||||
/// so the nested comp is not rasterised at its frame.
|
||||
pub fn collapsed(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Collapsed> {
|
||||
if !layer.switches.collapse || layer.switches.adjustment || self.depth > 12 {
|
||||
return None;
|
||||
}
|
||||
@@ -140,24 +226,37 @@ impl<'a> Renderer<'a> {
|
||||
if styles::active(ctx, layer) {
|
||||
return None;
|
||||
}
|
||||
// Instance overrides (Essential Properties) need the nested comp rendered on its own.
|
||||
if !effectcraft_project::essential::overridden(layer).is_empty() {
|
||||
// A composition proxy or frame blending is drawn by the flattened path (`precomp_source`);
|
||||
// collapsing renders the nested comp directly and would ignore both.
|
||||
if self.proxy_for(*item).is_some() || frame_blend_mode(ctx, layer) != FrameBlend::Off {
|
||||
return None;
|
||||
}
|
||||
Some(*item)
|
||||
let (property_overrides, media_overrides) = collapsed_instance(ctx, layer, *item)?;
|
||||
Some(Collapsed { item: *item, property_overrides, media_overrides })
|
||||
}
|
||||
|
||||
/// A renderer and context that draw a collapsed precomp's layers straight into this comp:
|
||||
/// their transforms are concatenated with the precomp layer's (one resample, no
|
||||
/// rasterisation at the precomp's bounds), their opacity multiplied by `opacity`, and their
|
||||
/// 3D layers use this comp's camera and lights.
|
||||
pub fn collapse_into(&self, ctx: &EvalCtx<'a>, layer: &'a Layer, item: ItemId, opacity: f32) -> Option<(Renderer<'a>, EvalCtx<'a>)> {
|
||||
pub fn collapse_into<'p>(&self, ctx: &EvalCtx<'a>, layer: &'a Layer, collapsed: &'p Collapsed, opacity: f32) -> Option<(Renderer<'p>, EvalCtx<'p>)>
|
||||
where
|
||||
'a: 'p,
|
||||
{
|
||||
let item = collapsed.item;
|
||||
let nc = self.project.comp(item)?;
|
||||
let t = ctx.nested_time(layer);
|
||||
if !nc.covers(t) {
|
||||
return None;
|
||||
}
|
||||
let nctx = EvalCtx { comp_id: item, comp: nc, time: t, ..*ctx };
|
||||
let nctx = EvalCtx {
|
||||
comp_id: item,
|
||||
comp: nc,
|
||||
time: t,
|
||||
property_overrides: &collapsed.property_overrides,
|
||||
media_overrides: &collapsed.media_overrides,
|
||||
..*ctx
|
||||
};
|
||||
let (l2c, _) = ctx.layer_to_comp(layer);
|
||||
let outer = Some(self.outer.map_or(l2c, |o| o * l2c));
|
||||
let world = ctx.world_matrix(layer);
|
||||
@@ -189,19 +288,19 @@ impl<'a> Renderer<'a> {
|
||||
/// that then takes the precomp layer's track matte, Preserve Transparency and blend mode.
|
||||
/// Returns the region left to re-quantise (nested layers drawn straight into `canvas`
|
||||
/// quantise their own footprints).
|
||||
pub(crate) fn draw_collapsed(&self, ctx: &EvalCtx<'a>, layer: &Layer, item: ItemId, canvas: &mut Image, blank: bool) -> Region {
|
||||
pub(crate) fn draw_collapsed(&self, ctx: &EvalCtx<'a>, layer: &'a Layer, collapsed: &Collapsed, canvas: &mut Image, blank: bool) -> Region {
|
||||
let opacity = ctx.opacity(layer) as f32 * self.opacity_mul;
|
||||
if opacity <= 0.0 {
|
||||
return Region::Empty;
|
||||
}
|
||||
let matte = layer.track_matte.is_some_and(|tm| tm.layer != layer.id && ctx.comp.layer(tm.layer).is_some());
|
||||
if layer.blend_mode == BlendMode::Normal && !matte && !layer.preserve_transparency {
|
||||
if let Some((sub, nctx)) = self.collapse_into(ctx, layer, item, opacity) {
|
||||
if let Some((sub, nctx)) = self.collapse_into(ctx, layer, collapsed, opacity) {
|
||||
sub.draw_comp(&nctx, canvas, blank);
|
||||
}
|
||||
return Region::Empty;
|
||||
}
|
||||
let Some((sub, nctx)) = self.collapse_into(ctx, layer, item, 1.0) else { return Region::Empty };
|
||||
let Some((sub, nctx)) = self.collapse_into(ctx, layer, collapsed, 1.0) else { return Region::Empty };
|
||||
let mut iso = Image::new(canvas.width, canvas.height);
|
||||
sub.draw_comp(&nctx, &mut iso, true);
|
||||
self.composite_iso(ctx, layer, iso, canvas, opacity);
|
||||
@@ -230,6 +329,7 @@ impl<'a> Renderer<'a> {
|
||||
let continuous = match layer.source {
|
||||
LayerSource::Text | LayerSource::Shape => true,
|
||||
LayerSource::Footage { item } => {
|
||||
let item = ctx.media_item(layer, item);
|
||||
layer.switches.collapse && matches!(self.project.item(item).map(|i| &i.kind), Some(ItemKind::Footage(f)) if is_vector_footage(f))
|
||||
}
|
||||
_ => false,
|
||||
@@ -267,7 +367,7 @@ impl<'a> Renderer<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn ctx(&self, comp_id: ItemId, comp: &'a Comp, t: Tick) -> EvalCtx<'a> {
|
||||
EvalCtx { project: self.project, comp_id, comp, time: t, expr: self.expr, footage: Some(self.footage) }
|
||||
EvalCtx { project: self.project, comp_id, comp, time: t, expr: self.expr, footage: Some(self.footage), property_overrides: &[], media_overrides: &[] }
|
||||
}
|
||||
|
||||
/// This renderer with another accelerator (or none): the passes of
|
||||
@@ -406,7 +506,7 @@ impl<'a> Renderer<'a> {
|
||||
blank = false;
|
||||
} else {
|
||||
let changed = match self.collapsed(ctx, visible[i]) {
|
||||
Some(item) => self.draw_collapsed(ctx, visible[i], item, canvas, blank),
|
||||
Some(collapsed) => self.draw_collapsed(ctx, visible[i], &collapsed, canvas, blank),
|
||||
None => self.draw_layer(ctx, visible[i], canvas, blank),
|
||||
};
|
||||
i += 1;
|
||||
@@ -637,7 +737,8 @@ impl<'a> Renderer<'a> {
|
||||
Some(Buf { img: sub.comp_frame(*item, lt), offset: [0.0; 2], scale: bs })
|
||||
}
|
||||
LayerSource::Footage { item } => {
|
||||
let it = self.project.item(*item)?;
|
||||
let item = ctx.media_item(layer, *item);
|
||||
let it = self.project.item(item)?;
|
||||
let ItemKind::Footage(f) = &it.kind else { return None };
|
||||
if !f.has_video {
|
||||
return None;
|
||||
@@ -645,7 +746,7 @@ impl<'a> Renderer<'a> {
|
||||
// Continuously rasterised vector footage: drawn at the on-screen scale.
|
||||
if layer.switches.collapse && is_vector_footage(f) {
|
||||
let k = self.raster_scale(ctx, layer);
|
||||
if let Some(img) = self.footage.vector_frame(*item, f, k) {
|
||||
if let Some(img) = self.footage.vector_frame(item, f, k) {
|
||||
let mut buf = Buf { img: (*img).clone(), offset: [0.0; 2], scale: k };
|
||||
if let Some(c) = self.pipe.media_in(f) {
|
||||
color::convert(&mut buf.img, &c);
|
||||
@@ -655,10 +756,10 @@ impl<'a> Renderer<'a> {
|
||||
}
|
||||
let lt = ctx.source_time(layer);
|
||||
// The proxy is decoded instead, at its own size, and fills the footage's frame.
|
||||
let proxy = self.proxy_for(*item);
|
||||
let proxy = self.proxy_for(item);
|
||||
let pf = proxy.unwrap_or(f);
|
||||
let size = self.reduced_size(f.width, f.height).filter(|_| proxy.is_none());
|
||||
let img = self.footage_frame(ctx, layer, *item, pf, lt, size)?;
|
||||
let img = self.footage_frame(ctx, layer, item, pf, lt, size)?;
|
||||
Some(self.footage_buf(&img, pf, f.width, f.height))
|
||||
}
|
||||
LayerSource::Text => Some(self.authored(text::render(ctx, layer, self.raster_scale(ctx, layer)))),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use effectcraft_color::{BlendMode, Label};
|
||||
use effectcraft_keyframe::{TextDoc, Value};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{BitDepth, Comp, ItemId, ItemKind, Layer, LayerSource, Project, Quality, Solid};
|
||||
use effectcraft_project::{BitDepth, Comp, FrameBlend, ItemId, ItemKind, Layer, LayerSource, Project, Quality, Solid};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use crate::{EvalCtx, Image, NoFootage, RenderOpts, Renderer, render_frame};
|
||||
@@ -110,6 +110,66 @@ fn masks_on_a_collapsed_layer_force_a_flattened_render() {
|
||||
assert!(max_diff(&a, &b) < 1e-4, "{}", max_diff(&a, &b));
|
||||
}
|
||||
|
||||
/// #1747: the flattened path honours a composition proxy and frame blending; a collapsed layer
|
||||
/// renders the nested comp directly and ignores both, so such a layer must not collapse.
|
||||
#[test]
|
||||
fn proxy_and_frame_blending_keep_a_precomp_on_the_flattened_path() {
|
||||
let (mut p, cid) = scene(true, false);
|
||||
let id = p.comp(cid).unwrap().layers[0].id;
|
||||
let collapses = |p: &Project| {
|
||||
let r = Renderer::new(p, &NoFootage, RenderOpts::default());
|
||||
let ctx = EvalCtx::new(p, cid, p.comp(cid).unwrap(), Tick::ZERO);
|
||||
r.collapsed(&ctx, ctx.comp.layer(id).unwrap()).is_some()
|
||||
};
|
||||
assert!(collapses(&p), "a plain precomp collapses");
|
||||
// Frame blending needs both the comp's switch and the layer's.
|
||||
p.comp_mut(cid).unwrap().enable_frame_blending = true;
|
||||
p.comp_mut(cid).unwrap().layers[0].switches.frame_blend = FrameBlend::FrameMix;
|
||||
assert!(!collapses(&p), "frame blending keeps the flattened path");
|
||||
p.comp_mut(cid).unwrap().enable_frame_blending = false;
|
||||
p.comp_mut(cid).unwrap().layers[0].switches.frame_blend = FrameBlend::Off;
|
||||
// A composition proxy stands in for the precomp's pixels.
|
||||
let LayerSource::Comp { item } = p.comp(cid).unwrap().layers[0].source else { panic!("a precomp layer") };
|
||||
let proxy =
|
||||
effectcraft_project::Proxy { footage: effectcraft_project::Footage { has_video: true, width: 32, height: 32, ..Default::default() }, enabled: true };
|
||||
p.item_mut(item).unwrap().proxy = Some(Box::new(proxy));
|
||||
assert!(!collapses(&p), "a proxy keeps the flattened path");
|
||||
}
|
||||
|
||||
/// #1747: an effect on B that reads A must not reuse its cached buffer when two collapsed
|
||||
/// instances give A different static overrides (`hash_values` only sees animated values).
|
||||
#[test]
|
||||
fn referenced_layer_overrides_change_the_owner_cache_key() {
|
||||
let mut p = project();
|
||||
let comp = Comp::new(100, 100, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let a = solid(&mut p, &comp, [1.0, 1.0, 1.0], (40, 40), [30.0, 50.0, 0.0]);
|
||||
let aid = a.id;
|
||||
let mut b = solid(&mut p, &comp, [1.0, 1.0, 1.0], (100, 100), [50.0, 50.0, 0.0]);
|
||||
let bid = b.id;
|
||||
let mut next = p.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
let spec = effectcraft_effects::find("ec.channel.setmatte").unwrap();
|
||||
let mut fx = effectcraft_effects::instantiate(spec, &mut ids, spec.name, [100.0, 100.0]);
|
||||
fx.prop_mut("takeMatteFromLayer").unwrap().value = Value::Layer(Some(a.id.0));
|
||||
p.next_id = next;
|
||||
b.props.sub_mut("effects").unwrap().children.push(fx.into());
|
||||
let mut c = comp;
|
||||
c.layers = vec![b, a];
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(c.into()));
|
||||
let comp = p.comp(cid).unwrap();
|
||||
let b = comp.layer(bid).unwrap();
|
||||
let opacity = comp.layer(aid).unwrap().props.prop("transform/opacity").unwrap().uid;
|
||||
let key = |value: f64| {
|
||||
let overrides = [crate::PropertyOverride { layer: aid, prop: opacity, value: Value::Scalar(value), font: false }];
|
||||
let ctx =
|
||||
EvalCtx { project: &p, comp_id: cid, comp, time: Tick::ZERO, expr: None, footage: None, property_overrides: &overrides, media_overrides: &[] };
|
||||
crate::cache::layer_key(&ctx, b, 1.0, false, false)
|
||||
};
|
||||
let (opaque, half) = (key(100.0), key(50.0));
|
||||
assert!(opaque.is_some(), "the owner is cacheable");
|
||||
assert_ne!(opaque, half, "A's static override changes B's key");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collapsed_3d_layers_join_the_parent_scene() {
|
||||
// Parent: a red 3D solid at z = 0 above (in the stack) a collapsed 3D precomp holding a
|
||||
|
||||
@@ -134,7 +134,16 @@ fn input_keys_ignore_later_effects() {
|
||||
}
|
||||
let key = |p: &Project| {
|
||||
let comp = p.comp(cid).unwrap();
|
||||
let ctx = crate::EvalCtx { project: p, comp_id: cid, comp, time: Tick::from_seconds_f64(0.5), expr: None, footage: None };
|
||||
let ctx = crate::EvalCtx {
|
||||
project: p,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: Tick::from_seconds_f64(0.5),
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let l = &comp.layers[0];
|
||||
(crate::cache::input_key(&ctx, l, 1.0, false, false, 1), crate::cache::input_key(&ctx, l, 1.0, false, false, 2))
|
||||
};
|
||||
|
||||
@@ -857,8 +857,8 @@ fn emit_layer<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &Layer, b: &mut Bu
|
||||
if layer.environment || layer.switches.adjustment || ctx.opacity(layer) <= 0.0 || !layer.has_video() {
|
||||
return;
|
||||
}
|
||||
if let Some(item) = r.collapsed(ctx, layer) {
|
||||
collapsed_layer(r, ctx, layer, item, b);
|
||||
if let Some(collapsed) = r.collapsed(ctx, layer) {
|
||||
collapsed_layer(r, ctx, layer, &collapsed, b);
|
||||
} else if layer.source.is_model() {
|
||||
model_layer(r, ctx, layer, b);
|
||||
} else if matches!(layer.source, LayerSource::Text | LayerSource::Shape)
|
||||
@@ -874,9 +874,9 @@ fn emit_layer<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &Layer, b: &mut Bu
|
||||
/// (meshes, extrusions, cards) placed by the precomp layer's world transform, so they
|
||||
/// intersect, occlude and shadow the parent's layers. Nested 2D layers lie on the precomp
|
||||
/// layer's plane; nested lights and cameras are ignored (the parent's light the scene).
|
||||
fn collapsed_layer<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &Layer, item: effectcraft_project::ItemId, b: &mut Builder) {
|
||||
fn collapsed_layer<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &Layer, collapsed: &crate::Collapsed, b: &mut Builder) {
|
||||
let op = ctx.opacity(layer) as f32 * r.opacity_mul();
|
||||
let Some((sub, nctx)) = r.collapse_into(ctx, layer, item, op) else { return };
|
||||
let Some((sub, nctx)) = r.collapse_into(ctx, layer, collapsed, op) else { return };
|
||||
let saved = b.outer;
|
||||
b.outer = saved * ctx.world_matrix(layer);
|
||||
let t = nctx.time;
|
||||
|
||||
@@ -370,7 +370,16 @@ fn extruded_text_renders_as_a_mesh() {
|
||||
set(l, "geometryOptions/bevelStyle", Value::Enum(1));
|
||||
set(l, "transform/rotationY", Value::Scalar(40.0));
|
||||
});
|
||||
let ctx = crate::EvalCtx { project: &p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None, footage: None };
|
||||
let ctx = crate::EvalCtx {
|
||||
project: &p,
|
||||
comp_id: cid,
|
||||
comp: p.comp(cid).unwrap(),
|
||||
time: Tick::ZERO,
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let l = &ctx.comp.layers[0];
|
||||
let params = scene::extrusion_params(&ctx, l).unwrap();
|
||||
let meshes = scene::extruded_meshes(&ctx, l, ¶ms);
|
||||
@@ -409,7 +418,16 @@ fn shape_surface_items_colour_the_extruded_surfaces() {
|
||||
set(l, "geometryOptions/extrusionDepth", Value::Scalar(40.0));
|
||||
set(l, "transform/position", Value::Vec3([100.0, 100.0, 0.0]));
|
||||
});
|
||||
let ctx = crate::EvalCtx { project: &p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None, footage: None };
|
||||
let ctx = crate::EvalCtx {
|
||||
project: &p,
|
||||
comp_id: cid,
|
||||
comp: p.comp(cid).unwrap(),
|
||||
time: Tick::ZERO,
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let l = &ctx.comp.layers[0];
|
||||
let params = scene::extrusion_params(&ctx, l).unwrap();
|
||||
let meshes = scene::extruded_meshes(&ctx, l, ¶ms);
|
||||
@@ -445,7 +463,16 @@ fn text_animators_colour_the_extruded_surfaces() {
|
||||
set(l, "geometryOptions/extrusionDepth", Value::Scalar(30.0));
|
||||
set(l, "geometryOptions/bevelStyle", Value::Enum(1));
|
||||
});
|
||||
let ctx = crate::EvalCtx { project: &p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None, footage: None };
|
||||
let ctx = crate::EvalCtx {
|
||||
project: &p,
|
||||
comp_id: cid,
|
||||
comp: p.comp(cid).unwrap(),
|
||||
time: Tick::ZERO,
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let l = &ctx.comp.layers[0];
|
||||
let params = scene::extrusion_params(&ctx, l).unwrap();
|
||||
let meshes = scene::extruded_meshes(&ctx, l, ¶ms);
|
||||
@@ -643,7 +670,16 @@ fn extruded_strokes_render_as_bevelled_meshes() {
|
||||
set(l, "geometryOptions/extrusionDepth", Value::Scalar(20.0));
|
||||
set(l, "geometryOptions/bevelStyle", Value::Enum(1));
|
||||
});
|
||||
let ctx = crate::EvalCtx { project: &p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None, footage: None };
|
||||
let ctx = crate::EvalCtx {
|
||||
project: &p,
|
||||
comp_id: cid,
|
||||
comp: p.comp(cid).unwrap(),
|
||||
time: Tick::ZERO,
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let l = &ctx.comp.layers[0];
|
||||
let params = scene::extrusion_params(&ctx, l).unwrap();
|
||||
let meshes = scene::extruded_meshes(&ctx, l, ¶ms);
|
||||
|
||||
@@ -195,13 +195,13 @@ fn mb_samples(r: &Renderer, ctx: &EvalCtx, layer: &Layer) -> usize {
|
||||
}
|
||||
|
||||
/// Prepare a layer (buffer, geometry, material). `None` when it shows nothing.
|
||||
pub(crate) fn prepare<'a>(r: &Renderer, ctx: &EvalCtx<'a>, layer: &'a Layer, order: usize, out: (u32, u32), in_run: bool) -> Option<Item<'a>> {
|
||||
pub(crate) fn prepare<'a>(r: &Renderer, ctx: &EvalCtx<'_>, layer: &'a Layer, order: usize, out: (u32, u32), in_run: bool) -> Option<Item<'a>> {
|
||||
let buf = r.blend_layer_buf(ctx, layer)?;
|
||||
prepare_with(r, ctx, layer, order, out, in_run, PlaneInput { buf, local: Mat4::IDENTITY })
|
||||
}
|
||||
|
||||
/// Prepare a layer as planes: one per character for per-character 3D text, else one.
|
||||
pub(crate) fn prepare_all<'a>(r: &Renderer, ctx: &EvalCtx<'a>, layer: &'a Layer, order: usize, out: (u32, u32), in_run: bool) -> Vec<Item<'a>> {
|
||||
pub(crate) fn prepare_all<'a>(r: &Renderer, ctx: &EvalCtx<'_>, layer: &'a Layer, order: usize, out: (u32, u32), in_run: bool) -> Vec<Item<'a>> {
|
||||
if crate::text::per_char_3d(ctx, layer) {
|
||||
return crate::text::per_char_planes(ctx, layer, r.opts.scale)
|
||||
.into_iter()
|
||||
@@ -218,7 +218,7 @@ struct PlaneInput {
|
||||
}
|
||||
|
||||
/// Prepare the plane's geometry, motion samples and material.
|
||||
fn prepare_with<'a>(r: &Renderer, ctx: &EvalCtx<'a>, layer: &'a Layer, order: usize, out: (u32, u32), in_run: bool, plane: PlaneInput) -> Option<Item<'a>> {
|
||||
fn prepare_with<'a>(r: &Renderer, ctx: &EvalCtx<'_>, layer: &'a Layer, order: usize, out: (u32, u32), in_run: bool, plane: PlaneInput) -> Option<Item<'a>> {
|
||||
let PlaneInput { mut buf, local } = plane;
|
||||
if buf.img.is_empty() {
|
||||
return None;
|
||||
@@ -449,8 +449,8 @@ fn gather<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Layer], out: (u32,
|
||||
.flat_map_iter(|(i, l)| {
|
||||
// Stack order with room for a collapsed precomp's layers in between.
|
||||
let i = i * 1024;
|
||||
if let Some(item) = r.collapsed(ctx, l) {
|
||||
return collapsed_items(r, ctx, l, item, i, out);
|
||||
if let Some(collapsed) = r.collapsed(ctx, l) {
|
||||
return collapsed_items(r, ctx, l, &collapsed, i, out);
|
||||
}
|
||||
let mut its = prepare_all(r, ctx, l, i, out, true);
|
||||
if !its.is_empty() {
|
||||
@@ -483,20 +483,13 @@ fn gather<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Layer], out: (u32,
|
||||
/// 3D space (world = precomp layer's world × nested world), depth-sorted with the run, so they
|
||||
/// intersect the parent's 3D layers; nested 2D layers lie on the precomp layer's plane in stack
|
||||
/// order. (Nested adjustment layers are skipped here.)
|
||||
fn collapsed_items<'a>(
|
||||
r: &Renderer<'a>,
|
||||
ctx: &EvalCtx<'a>,
|
||||
layer: &'a Layer,
|
||||
item: effectcraft_project::ItemId,
|
||||
order: usize,
|
||||
out: (u32, u32),
|
||||
) -> Vec<Item<'a>> {
|
||||
fn collapsed_items<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &'a Layer, collapsed: &crate::Collapsed, order: usize, out: (u32, u32)) -> Vec<Item<'a>> {
|
||||
let op = ctx.opacity(layer) as f32 * r.opacity_mul();
|
||||
let Some((sub, nctx)) = r.collapse_into(ctx, layer, item, op) else { return vec![] };
|
||||
let Some((sub, nctx)) = r.collapse_into(ctx, layer, collapsed, op) else { return vec![] };
|
||||
let t = nctx.time;
|
||||
let any_solo = nctx.comp.any_solo();
|
||||
let nested: Vec<&'a Layer> = nctx
|
||||
.comp
|
||||
let Some(nc) = r.project.comp(collapsed.item) else { return vec![] };
|
||||
let any_solo = nc.any_solo();
|
||||
let nested: Vec<&'a Layer> = nc
|
||||
.layers
|
||||
.iter()
|
||||
.rev()
|
||||
@@ -863,7 +856,7 @@ pub(crate) fn aux_pass(r: &Renderer, ctx: &EvalCtx) -> effectcraft_raster::AuxCh
|
||||
if nv[2] > 0.0 {
|
||||
nv = nv.map(|v| -v);
|
||||
}
|
||||
let (sw, sh) = crate::source_size(r.project, it.layer);
|
||||
let (sw, sh) = ctx.source_size(it.layer);
|
||||
let size = if sw == 0 { [ctx.comp.width as f64, ctx.comp.height as f64] } else { [sw as f64, sh as f64] };
|
||||
(number(it.layer), source_id(it.layer), crypto_hash(&it.layer.name), crypto_hash(&source_name(it.layer)), nv, size)
|
||||
})
|
||||
|
||||
@@ -222,7 +222,16 @@ fn position_now(s: &Session, layer: u64) -> [f64; 2] {
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let comp = s.project.comp(cid).unwrap();
|
||||
let l = comp.layer(effectcraft_engine::project::LayerId(layer)).unwrap();
|
||||
let ctx = effectcraft_engine::render::EvalCtx { project: &s.project, comp_id: cid, comp, time: s.time(), expr: s.expr.as_deref(), footage: None };
|
||||
let ctx = effectcraft_engine::render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: s.time(),
|
||||
expr: s.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let v = ctx.value(l, l.props.prop("transform/position").unwrap()).components();
|
||||
[v[0], v[1]]
|
||||
}
|
||||
|
||||
@@ -151,7 +151,16 @@ fn vertex_delta(app: &EffectcraftApp, layer: effectcraft_engine::project::LayerI
|
||||
let s = &app.session;
|
||||
let (Some(cid), Some(comp)) = (s.active_comp_id(), s.active_comp()) else { return d };
|
||||
let Some(l) = comp.layer(layer) else { return d };
|
||||
let ectx = effectcraft_engine::render::EvalCtx { project: &s.project, comp_id: cid, comp, time: s.time(), expr: s.expr.as_deref(), footage: None };
|
||||
let ectx = effectcraft_engine::render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: s.time(),
|
||||
expr: s.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let (l2c, _) = crate::panels::viewer::l2c(&ectx, l);
|
||||
l2c.inverse().map_or(d, |inv| {
|
||||
let v = inv.apply_vec(vec2(d[0], d[1]));
|
||||
|
||||
@@ -166,7 +166,16 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let comp_name = app.session.project.item(cid).map(|i| i.name.clone()).unwrap_or_default();
|
||||
let snap_project = app.session.project.clone();
|
||||
let snap_expr = app.session.expr.clone();
|
||||
let ectx = EvalCtx { project: &snap_project, comp_id: cid, comp: &comp, time: app.session.time(), expr: snap_expr.as_deref(), footage: None };
|
||||
let ectx = EvalCtx {
|
||||
project: &snap_project,
|
||||
comp_id: cid,
|
||||
comp: &comp,
|
||||
time: app.session.time(),
|
||||
expr: snap_expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let hdr = Rect::from_min_size(rect.min, vec2(rect.width(), 24.0));
|
||||
p.text(pos2(hdr.min.x + 10.0, hdr.center().y), Align2::LEFT_CENTER, format!("{} • {}", comp_name, layer.name), Tokens::ui(11.5), t.text_dim);
|
||||
p.line_segment([hdr.left_bottom(), hdr.right_bottom()], Stroke::new(1.0, t.separator));
|
||||
|
||||
@@ -104,7 +104,16 @@ fn render_tex(
|
||||
match cached {
|
||||
Some(c) if c.key == key => Some(c),
|
||||
_ => {
|
||||
let ectx = EvalCtx { project: &app.session.project, comp_id: cid, comp, time, expr: app.session.expr.as_deref(), footage: None };
|
||||
let ectx = EvalCtx {
|
||||
project: &app.session.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time,
|
||||
expr: app.session.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let mut r = Renderer::new(&app.session.project, &*app.session.footage, RenderOpts { scale, ..Default::default() });
|
||||
r.expr = app.session.expr.as_deref();
|
||||
r.cache = Some(&app.session.layer_cache);
|
||||
|
||||
@@ -70,7 +70,16 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let project = app.session.project.clone();
|
||||
let expr = app.session.expr.clone();
|
||||
let now = app.session.time();
|
||||
let ectx = EvalCtx { project: &project, comp_id: cid, comp: &comp, time: now, expr: expr.as_deref(), footage: None };
|
||||
let ectx = EvalCtx {
|
||||
project: &project,
|
||||
comp_id: cid,
|
||||
comp: &comp,
|
||||
time: now,
|
||||
expr: expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let x0 = rect.min.x + PAD;
|
||||
let w = rect.width() - 2.0 * PAD;
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::unique("props-scroll"), rect);
|
||||
|
||||
@@ -69,7 +69,16 @@ pub fn draw_overlay(
|
||||
let ov = match cached {
|
||||
Some(c) if c.key == key => Some(c),
|
||||
_ => {
|
||||
let ectx = EvalCtx { project: &app.session.project, comp_id: cid, comp, time, expr: app.session.expr.as_deref(), footage: None };
|
||||
let ectx = EvalCtx {
|
||||
project: &app.session.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time,
|
||||
expr: app.session.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let mut r = Renderer::new(&app.session.project, &*app.session.footage, RenderOpts { scale, ..Default::default() });
|
||||
r.expr = app.session.expr.as_deref();
|
||||
r.cache = Some(&app.session.layer_cache);
|
||||
|
||||
@@ -57,7 +57,16 @@ pub fn text_target(app: &EffectcraftApp) -> Option<TextTarget> {
|
||||
.iter()
|
||||
.filter_map(|id| comp.layer(*id))
|
||||
.find(|l| matches!(l.source, effectcraft_engine::project::LayerSource::Text))?;
|
||||
let ectx = EvalCtx { project: &app.session.project, comp_id: cid, comp, time: app.session.time(), expr: app.session.expr.as_deref(), footage: None };
|
||||
let ectx = EvalCtx {
|
||||
project: &app.session.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: app.session.time(),
|
||||
expr: app.session.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
effectcraft_engine::render::text::source_text(&ectx, layer).map(|d| TextTarget { layer: layer.id.0, doc: d, range: None })
|
||||
}
|
||||
|
||||
|
||||
@@ -1865,7 +1865,16 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
pick_whip_edge_scroll(app, g);
|
||||
let snap_project = app.session.project.clone();
|
||||
let snap_expr = app.session.expr.clone();
|
||||
let ectx = EvalCtx { project: &snap_project, comp_id: cid, comp: &comp, time: g.time, expr: snap_expr.as_deref(), footage: None };
|
||||
let ectx = EvalCtx {
|
||||
project: &snap_project,
|
||||
comp_id: cid,
|
||||
comp: &comp,
|
||||
time: g.time,
|
||||
expr: snap_expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let mut rw = begin_rows(app, ui, tl);
|
||||
let g = &mut rw;
|
||||
draw_rows(app, ui, g, &ectx);
|
||||
|
||||
@@ -178,7 +178,16 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
phase_frame(app, &mut v);
|
||||
let snap_project = app.session.project.clone();
|
||||
let snap_expr = app.session.expr.clone();
|
||||
let ectx = EvalCtx { project: &snap_project, comp_id: cid, comp: &comp, time: v.time, expr: snap_expr.as_deref(), footage: None };
|
||||
let ectx = EvalCtx {
|
||||
project: &snap_project,
|
||||
comp_id: cid,
|
||||
comp: &comp,
|
||||
time: v.time,
|
||||
expr: snap_expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
phase_guides(app, &ectx, &mut v);
|
||||
phase_layers(app, ui, &ectx, &mut v);
|
||||
phase_scene(app, &ectx, &mut v);
|
||||
|
||||
@@ -202,6 +202,8 @@ pub(super) fn create_shape(app: &mut EffectcraftApp, tool: Tool, a: [f64; 2], b:
|
||||
time: app.session.time(),
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let inv = l2c(&ectx, &l).0.inverse().unwrap_or(Mat3::IDENTITY);
|
||||
let p0 = inv.apply(gv2(cx - w / 2.0, cy - h / 2.0));
|
||||
|
||||
@@ -44,7 +44,16 @@ pub(super) fn aux_views(
|
||||
// layout shows them), evaluated on a snapshot of the project.
|
||||
let snap_project = app.session.project.clone();
|
||||
let snap_expr = app.session.expr.clone();
|
||||
let ectx = EvalCtx { project: &snap_project, comp_id: cid, comp, time: t, expr: snap_expr.as_deref(), footage: None };
|
||||
let ectx = EvalCtx {
|
||||
project: &snap_project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: t,
|
||||
expr: snap_expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let selected = app.session.state.selected_layers.clone();
|
||||
let mut picked: Option<(usize, View3D)> = None;
|
||||
for (i, (r, v)) in aux.into_iter().enumerate() {
|
||||
|
||||
@@ -401,7 +401,10 @@ hierarchical block-matching optical flow and a bidirectional warp (Pixel Motion,
|
||||
its nested layers straight into the parent with concatenated transforms (one resample), so
|
||||
nested blend modes and adjustment layers act on the parent's layers; nested 3D layers use the
|
||||
parent's camera and lights and, when the precomp layer is 3D, are depth-sorted with the parent's
|
||||
3D run. Masks, effects or styles force a flattened render. Text layers are always rasterised at
|
||||
3D run. Essential Properties overrides (properties, Font and Media Replacement) travel with the
|
||||
collapsed context and are applied while the nested layers are evaluated, so they do not force a
|
||||
flattened render or clip at the precomp's frame. Masks, effects or styles force a flattened
|
||||
render. Text layers are always rasterised at
|
||||
their on-screen scale, as text is vector in After Effects; on shape layers and vector footage the
|
||||
switch is Continuously Rasterize and does the same. Quality: Draft samples
|
||||
nearest-neighbour, Wireframe draws the layer bounds. Slip edit (`layer.slip`, Alt+PageUp/Down,
|
||||
@@ -468,8 +471,10 @@ effects crate, hence the `effects → track` edge.
|
||||
`essential` field lists its exposed controls (properties, Media Replacement, groups, comments).
|
||||
Every precomp layer of such a comp gets an Essential Properties group (`GroupKind::Essential`,
|
||||
children `eg<control id>`), kept in step by `Session::edit`; editing a child overrides it for
|
||||
that instance. The renderer draws an instance with overrides from a copy of the project with the
|
||||
overridden source properties set (`essential::with_overrides`). Templates are `.ectemplate`
|
||||
that instance. A collapsed instance carries its property, Font and Media Replacement overrides
|
||||
into the nested render context (`Collapsed`, `EvalCtx::{property_overrides, media_overrides}`);
|
||||
other instances are drawn from a copy of the project with the overridden source properties set
|
||||
(`essential::with_overrides`). Templates are `.ectemplate`
|
||||
ZIPs: `manifest.json` (controls), `project.ecproj` (the comp and its dependencies), `media/…` and
|
||||
`poster.png`; importing offsets every id past the project's (`essential::offset_ids`).
|
||||
**Responsive Design — Time**: a time-stretched precomp maps time piecewise so its protected
|
||||
|
||||
Reference in New Issue
Block a user