mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 05:58:19 -06:00
Shapes: per-corner roundness for Rectangle Path (#1896)
Add Per-Corner Roundness to Rectangle Path: a checkbox (off by default) and four Pixels properties (top-left, top-right, bottom-right, bottom-left). New rectangles initialise the four values from their Roundness, so ticking the box keeps the shape; the radii are animatable and expression-driven.\n\nrect_radii() builds the path clockwise from the top-left with kappa quarter-circle arcs. Overlapping radii scale down like CSS border-radius; negative and non-finite radii are square; rect() delegates to it, so uniform roundness clamps to half the shorter side as before.\n\nLottie rc has one r: four equal static radii export as that value; mixed or animated radii export roundness 0 with a warning. Import puts r into the four per-corner values so ticking the box on an imported rectangle keeps the shape.\n\nTests: path geometry and clamping, rendered per-corner pixels, Lottie equal/mixed/checkbox-off export.
This commit is contained in:
@@ -585,6 +585,70 @@ fn convert_to_bezier_reads_the_shape_as_it_is_set() {
|
||||
assert!((mid(&xs) - 10.0).abs() < 1e-6 && (mid(&ys) - 5.0).abs() < 1e-6, "centred on the position: {:?} {:?}", mid(&xs), mid(&ys));
|
||||
}
|
||||
|
||||
/// #1896: Convert To Bezier Path reads Per-Corner Roundness, not just the uniform Roundness, so
|
||||
/// the editable path is the shape that was on screen.
|
||||
#[test]
|
||||
fn convert_to_bezier_honours_per_corner_roundness() {
|
||||
let mut s = comp();
|
||||
let l = s.execute("layer.newShape", json!({"kind": "rect", "size": [120, 80]})).unwrap()["layer"].as_u64().unwrap();
|
||||
let (uid, _) = first_shape(&s, l);
|
||||
let uid_of = |s: &Session, id: &str| layer(s, l).props.sub("contents").unwrap().find_group(uid).unwrap().get(id).unwrap().uid;
|
||||
let per_corner = uid_of(&s, "perCorner");
|
||||
let top_left = uid_of(&s, "topLeftRoundness");
|
||||
let bottom_right = uid_of(&s, "bottomRightRoundness");
|
||||
for (u, v) in [(per_corner, json!(true)), (top_left, json!(20.0)), (bottom_right, json!(10.0))] {
|
||||
s.execute("prop.set", json!({"layer": l, "path": format!("@{u}"), "value": v})).unwrap();
|
||||
}
|
||||
|
||||
s.state.selected_props = vec![(LayerId(l), uid)];
|
||||
s.execute("path.convertToBezier", json!({})).unwrap();
|
||||
let (new_uid, now) = first_shape(&s, l);
|
||||
assert_eq!(now, "path");
|
||||
let path = shape_path_of(&s, l, new_uid);
|
||||
let expected = effectcraft_path::from_kurbo(&effectcraft_path::rect_radii([120.0, 80.0], [0.0, 0.0], [20.0, 0.0, 10.0, 0.0])).into_iter().next().unwrap();
|
||||
assert_eq!(path, expected, "the converted path has the per-corner geometry");
|
||||
}
|
||||
|
||||
/// #1896: Convert To Bezier Path evaluates the shape's expressions, so an expression-driven
|
||||
/// Per-Corner checkbox and radius convert to the geometry on screen.
|
||||
#[test]
|
||||
fn convert_to_bezier_evaluates_corner_expressions() {
|
||||
struct Corners;
|
||||
impl effectcraft_render::ExprHost for Corners {
|
||||
fn eval(
|
||||
&self,
|
||||
_: &effectcraft_render::EvalCtx,
|
||||
_: &effectcraft_project::Layer,
|
||||
prop: &effectcraft_project::Property,
|
||||
value: &KV,
|
||||
) -> std::result::Result<KV, String> {
|
||||
Ok(match prop.match_id.as_str() {
|
||||
"perCorner" => KV::Bool(true),
|
||||
"topLeftRoundness" => KV::Scalar(20.0),
|
||||
_ => value.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let mut s = comp();
|
||||
s.expr = Some(Arc::new(Corners));
|
||||
let l = s.execute("layer.newShape", json!({"kind": "rect", "size": [120, 80]})).unwrap()["layer"].as_u64().unwrap();
|
||||
let (uid, _) = first_shape(&s, l);
|
||||
let uid_of = |s: &Session, id: &str| layer(s, l).props.sub("contents").unwrap().find_group(uid).unwrap().get(id).unwrap().uid;
|
||||
for (id, expression) in [("perCorner", "true"), ("topLeftRoundness", "20")] {
|
||||
let u = uid_of(&s, id);
|
||||
s.execute("prop.setExpression", json!({"layer": l, "path": format!("@{u}"), "expression": expression})).unwrap();
|
||||
}
|
||||
|
||||
s.state.selected_props = vec![(LayerId(l), uid)];
|
||||
s.execute("path.convertToBezier", json!({})).unwrap();
|
||||
let (new_uid, now) = first_shape(&s, l);
|
||||
assert_eq!(now, "path");
|
||||
let path = shape_path_of(&s, l, new_uid);
|
||||
let expected = effectcraft_path::from_kurbo(&effectcraft_path::rect_radii([120.0, 80.0], [0.0, 0.0], [20.0, 0.0, 0.0, 0.0])).into_iter().next().unwrap();
|
||||
assert_eq!(path, expected, "the expression-evaluated corner is converted");
|
||||
}
|
||||
|
||||
/// #2201: a viewer drag on the free-transform box sends one `path.freeTransform` per frame with a
|
||||
/// shared merge key; one undo must revert the whole drag, not the last frame.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user