mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 15:48:33 +00:00
Merge pull request #2262 from SusieeTheLinuxUser/fix/2198-fit-to-comp-shapes
Transform: Fit to Comp sizes shape and text layers by their painted bounds (#2198)
This commit is contained in:
2 files changed
+94
-5
No files matched your search
@@ -74,14 +74,39 @@ pub(super) fn transform_op(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let op = str_p(p, "op").unwrap_or("reset").to_string();
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone();
|
||||
let t = s.time();
|
||||
let sizes: Vec<(LayerId, (u32, u32))> =
|
||||
comp.layers.iter().filter(|l| ids.contains(&l.id)).map(|l| (l.id, effectcraft_render::source_size(&s.project, l))).collect();
|
||||
// A shape or text layer has no source item, so source_size is (0, 0): the fit ops size it by
|
||||
// its painted bounds at the current time instead, and anchor at the bounds' centre — content
|
||||
// bounds live in layer space, where a shape is centred on the origin and text sits above its
|
||||
// baseline, so (w/2, h/2) would push it off-centre (#2198). Reset and the other ops keep the
|
||||
// source size.
|
||||
let ectx = effectcraft_render::EvalCtx {
|
||||
expr: s.expr.as_deref(),
|
||||
footage: Some(s.footage.as_ref()),
|
||||
..effectcraft_render::EvalCtx::new(&s.project, cid, &comp, t)
|
||||
};
|
||||
let fitting = matches!(op.as_str(), "fit" | "fitWidth" | "fitHeight");
|
||||
let sizes: Vec<(LayerId, [f64; 2], [f64; 2])> = comp
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|l| ids.contains(&l.id))
|
||||
.map(|l| {
|
||||
let (w, h) = effectcraft_render::source_size(&s.project, l);
|
||||
if fitting && (w == 0 || h == 0) {
|
||||
match effectcraft_render::content_bounds(&ectx, l).filter(|b| b.iter().all(|v| v.is_finite()) && b[2] > b[0] && b[3] > b[1]) {
|
||||
Some(b) => (l.id, [b[2] - b[0], b[3] - b[1]], [(b[0] + b[2]) / 2.0, (b[1] + b[3]) / 2.0]),
|
||||
None => (l.id, [0.0; 2], [0.0; 2]),
|
||||
}
|
||||
} else {
|
||||
(l.id, [w as f64, h as f64], [w as f64 / 2.0, h as f64 / 2.0])
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
s.edit("Transform", None, |proj, _| {
|
||||
for (lid, (w, h)) in &sizes {
|
||||
for (lid, size, anchor) in &sizes {
|
||||
let l = layer_mut(proj, cid, *lid)?;
|
||||
let lt = l.layer_time(t);
|
||||
let (cw, ch) = (comp.width as f64, comp.height as f64);
|
||||
let (w, h) = (*w as f64, *h as f64);
|
||||
let (w, h) = (size[0], size[1]);
|
||||
let Some(tr) = l.transform_mut() else { continue };
|
||||
let set = |tr: &mut PropGroup, m: &str, v: KV| {
|
||||
if let Some(pr) = tr.get_mut(m) {
|
||||
@@ -105,11 +130,15 @@ pub(super) fn transform_op(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"fitHeight" => (ch / h, ch / h),
|
||||
_ => (cw / w, ch / h),
|
||||
};
|
||||
// A tiny shape's bounds can overflow the scale; refuse rather than write infinity.
|
||||
if !(sx * 100.0).is_finite() || !(sy * 100.0).is_finite() {
|
||||
return Err(EngineError::Other(format!("layer {} is too small to fit to the composition", lid.0)));
|
||||
}
|
||||
let z = |m: &str, d: f64| tr.get(m).map(|p| p.value_at(lt).as_vec3()[2]).unwrap_or(d);
|
||||
let (sz, az) = (z("scale", 100.0), z("anchor", 0.0));
|
||||
set(tr, "scale", KV::Vec3([sx * 100.0, sy * 100.0, sz]));
|
||||
update_position(tr, lt, |p| [cw / 2.0, ch / 2.0, p[2]]);
|
||||
set(tr, "anchor", KV::Vec3([w / 2.0, h / 2.0, az]));
|
||||
set(tr, "anchor", KV::Vec3([anchor[0], anchor[1], az]));
|
||||
}
|
||||
"flipH" | "flipV" => {
|
||||
let cur = tr.get("scale").map(|p| p.value_at(lt).as_vec3()).unwrap_or([100.0; 3]);
|
||||
|
||||
@@ -1103,6 +1103,66 @@ fn transform_commands_keep_depth_and_follow_separated_dimensions() {
|
||||
assert_eq!(pos(&s, b)[..2], [200.0, 100.0], "the 200×100 solid's centre");
|
||||
}
|
||||
|
||||
/// The layer's painted bounds mapped to comp space — what the viewer shows.
|
||||
fn seen_bounds(s: &Session, lid: u64) -> [f64; 4] {
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let comp = s.project.comp(cid).unwrap().clone();
|
||||
let layer = comp.layer(effectcraft_project::LayerId(lid)).unwrap();
|
||||
let ectx = effectcraft_render::EvalCtx::new(&s.project, cid, &comp, s.time());
|
||||
let b = effectcraft_render::content_bounds(&ectx, layer).expect("painted bounds");
|
||||
let m = ectx.layer_to_comp(layer).0;
|
||||
let corners = [[b[0], b[1]], [b[2], b[1]], [b[2], b[3]], [b[0], b[3]]];
|
||||
let pts: Vec<[f64; 2]> = corners
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let q = m.apply(effectcraft_geom::vec2(p[0], p[1]));
|
||||
[q.x, q.y]
|
||||
})
|
||||
.collect();
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
|
||||
for p in &pts {
|
||||
x0 = x0.min(p[0]);
|
||||
y0 = y0.min(p[1]);
|
||||
x1 = x1.max(p[0]);
|
||||
y1 = y1.max(p[1]);
|
||||
}
|
||||
[x0, y0, x1, y1]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_to_comp_fills_the_comp_with_shape_and_text_bounds() {
|
||||
let mut s = comp();
|
||||
let close = |x: f64, y: f64| (x - y).abs() < 1e-6;
|
||||
// A shape or text layer has no source item: fit sizes it by its painted bounds, anchored at
|
||||
// the bounds' centre, so what the viewer shows fills the frame (#2198).
|
||||
let a = s.execute("layer.newShape", json!({"kind": "rect", "size": [180, 80]})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.transform", json!({"layers": [a], "op": "fit"})).unwrap();
|
||||
let seen = seen_bounds(&s, a);
|
||||
assert!(close(seen[0], 0.0) && close(seen[1], 0.0) && close(seen[2], 640.0) && close(seen[3], 360.0), "the rect's painted bounds fill the comp: {seen:?}");
|
||||
|
||||
let t = s.execute("layer.newText", json!({"text": "Title", "size": 80})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.transform", json!({"layers": [t], "op": "fit"})).unwrap();
|
||||
let seen = seen_bounds(&s, t);
|
||||
assert!(close(seen[0], 0.0) && close(seen[1], 0.0) && close(seen[2], 640.0) && close(seen[3], 360.0), "the text's painted bounds fill the comp: {seen:?}");
|
||||
|
||||
// fitWidth spans the comp's width and centres vertically.
|
||||
for lid in [a, t] {
|
||||
s.execute("layer.transform", json!({"layers": [lid], "op": "fitWidth"})).unwrap();
|
||||
let seen = seen_bounds(&s, lid);
|
||||
assert!(close(seen[0], 0.0) && close(seen[2], 640.0), "fitWidth spans the comp width: {seen:?}");
|
||||
assert!(close((seen[1] + seen[3]) / 2.0, 180.0), "fitWidth centres vertically: {seen:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_to_comp_refuses_a_scale_that_overflows() {
|
||||
let mut s = comp();
|
||||
let a = s.execute("layer.newShape", json!({"kind": "rect", "size": [1e-305, 80]})).unwrap()["layer"].as_u64().unwrap();
|
||||
let before = s.project.clone();
|
||||
assert!(s.execute("layer.transform", json!({"layers": [a], "op": "fit"})).is_err(), "an infinite scale is refused");
|
||||
assert_eq!(s.project, before, "nothing was written");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicated_and_pasted_layers_keep_their_links() {
|
||||
let mut s = comp();
|
||||
|
||||
Reference in new issue
Block a user