Merge pull request #7429 from dotCooCoo/fix/live-shape-replay-negative-radius

Live Shapes: saved object.setLiveShape steps with a negative radius or out-of-range sides replay again
This commit is contained in:
BFlat
2026-11-14 20:36:38 +07:00
committed by GitHub
4 changed files with 43 additions and 4 deletions
+15 -2
View File
@@ -149,9 +149,22 @@ pub(super) fn set_live_shape(s: &mut Session, p: &Value) -> Result<Value> {
};
let angle = |k: &str| Ok::<_, EngineError>(number(k, |_| true, "an angle in degrees")?.map(|a| a.rem_euclid(360.0)));
let length = |k: &str| number(k, |x| x > 0.0 && x <= crate::MAX_COORD, "a length in points above 0");
let radius = number("radius", |r| r >= 0.0, "a radius of 0 or more")?;
// Saved steps ([`Session::replay`]) can hold values a direct call refuses, recorded before these
// checks: a negative radius is 0, a whole side count of 0 or more is brought into 3 to 1000, and
// a fractional or negative one is ignored. A whole number written as 5.0 counts as 5, as in a
// direct call.
let replaying = s.replaying();
let radius = if replaying {
number("radius", |_| true, "a number")?.map(|r| r.max(0.0))
} else {
number("radius", |r| r >= 0.0, "a radius of 0 or more")?
};
// A polygon's side count: a whole number from 3 to 1000.
let sides = number("sides", |n| n.fract() == 0.0 && (3.0..=1000.0).contains(&n), "a whole number from 3 to 1000")?.map(|n| n as u64);
let sides = if replaying {
number("sides", |_| true, "a number")?.filter(|n| n.fract() == 0.0 && *n >= 0.0).map(|n| n.min(1000.0) as u64)
} else {
number("sides", |n| n.fract() == 0.0 && (3.0..=1000.0).contains(&n), "a whole number from 3 to 1000")?.map(|n| n as u64)
};
// An ellipse's pie (Ellipse Properties: Pie Start and End Angle, degrees; Invert Pie swaps them).
let (pie_start, pie_end) = (angle("pieStart")?, angle("pieEnd")?);
let invert = p.get("invertPie").and_then(Value::as_bool) == Some(true);
+24
View File
@@ -126,6 +126,30 @@ fn bad_sides_radii_and_keys_are_errors() {
assert_eq!(corners(&s, id).0, [0.0; 4]);
}
/// Saved steps play as they did before `sides` and `radius` were checked: a negative radius
/// removes the rounding, a whole side count outside 3 to 1000 is brought into that range, and a
/// fractional or negative side count is ignored.
#[test]
fn saved_steps_with_out_of_range_sides_or_radius_still_play() {
let (mut s, id) = session();
let poly = NodeId(s.execute("shape.polygon", &json!({"cx": 200, "cy": 200, "radius": 80, "sides": 6})).unwrap()["id"].as_u64().unwrap());
let replay = |s: &mut Session, p: Value| s.replay("object.setLiveShape", &p).unwrap_or_else(|e| panic!("{p}: {e}"));
run(&mut s, json!({"id": id.0, "radius": 6}));
replay(&mut s, json!({"id": id.0, "radius": -4}));
assert_eq!(corners(&s, id).0, [0.0; 4]);
let sides = |s: &Session| match node_path(s, poly).1 {
Some(LiveShape::Polygon { sides, .. }) => sides,
other => panic!("{other:?}"),
};
for (n, want) in [(json!(2), 3), (json!(1001), 1000), (json!(2.5), 1000), (json!(-1), 1000), (json!(5.0), 5)] {
replay(&mut s, json!({"ids": [poly.0], "sides": n}));
assert_eq!(sides(&s), want, "{n}");
}
// A direct call still refuses them.
assert!(s.execute("object.setLiveShape", &json!({"id": id.0, "radius": -4})).is_err());
assert!(s.execute("object.setLiveShape", &json!({"ids": [poly.0], "sides": 2})).is_err());
}
#[test]
fn direct_selected_corners_round_alone_and_stay_selected() {
let (mut s, id) = session();