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();
+3 -1
View File
@@ -5616,7 +5616,9 @@ one undo step.
`object.setLiveShape` returns a parameter error, and leaves the document and its undo history as they were, for a key
it does not take, at the top level (`` unknown parameter `path` ``) or in one of `items`
(`` unknown parameter `items[0].corner`; did you mean `items[0].corners`? ``), for a `sides` that is not a whole number
from 3 to 1000, such as 2.5, 2 or 1001, and for a negative `radius`. A `radius` of 0 removes the rounding.
from 3 to 1000, such as 2.5, 2 or 1001, and for a negative `radius`. A `radius` of 0 removes the rounding. Saved
steps (a journal or an action) replay these values without an error: a negative `radius` counts as 0, a whole `sides`
below 3 or above 1000 counts as 3 or 1000, and a fractional or negative `sides` is ignored.
```json
{"name":"run_command","arguments":{"command":"object.setLiveShape","params":{"sides":8,"polygonAngle":22.5,"sideLength":40}}}
+1 -1
View File
@@ -1617,7 +1617,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Create Object Mosaic (its columns and rows), Transform Each (its reference point and Random seed) and Rearrange All Artboards (its columns) refuse a fraction, a negative number or a string, such as 2.5, -1 or "4"; 4.0 counts as 4.
Move, Rotate, Scale, Reflect, Shear, Transform Again, Nudge and the Transform panel's values (their copy, Scale Strokes & Effects, Scale Corners, Transform Patterns, Transform Objects and area type options), Align, Distribute and Distribute Spacing (Transform Patterns), Object Properties (visible, locked, Isolate Blending and Opacity & Mask Define Knockout Shape) and Live Shape Properties (Invert Pie, Make Sides Equal and relative rounding) refuse a value other than true, false or null, such as "false", and leave the document as it was.
Object Properties (its opacity), Live Shape Properties (corner radius and polygon sides), the Transform panel's reference point and Align, Distribute and Distribute Spacing to an artboard (the artboard's number) refuse a value other than a number or null, such as "50", and leave the document as it was.
Live Shape Properties refuses a key it does not take, at the top level or in one of its items, a polygon side count that is not a whole number from 3 to 1000 and a negative corner radius, and leaves the document as it was; the Transform panel's Sides field takes 3 to 1000.
Live Shape Properties refuses a key it does not take, at the top level or in one of its items, a polygon side count that is not a whole number from 3 to 1000 and a negative corner radius, and leaves the document as it was; the Transform panel's Sides field takes 3 to 1000. Saved steps with such a side count or radius still replay, as they did before the check.
Transform Each (its scales, moves and rotation), Rasterize (its resolution), Create Object Mosaic (its new width and height and its spacing), Create Trim Marks (its offset, length, bleed and weight) and Rearrange All Artboards (its spacing) refuse a value other than a number or null, such as "50", and change nothing. Crop Image refuses a rectangle that is not four numbers, such as "0, 0, 20, 20".
Transform Each (Reflect X, Reflect Y, Random, Copy, Scale Strokes & Effects, Scale Corners, Transform Objects and Transform Patterns), Create Object Mosaic (gray tiles and deleting the image), Crop Image (its trim), Create Trim Marks (all artboards) and Rearrange All Artboards (by column, Move Artwork with Artboard and Transform Patterns) refuse a value other than true, false or null, such as "false", and change nothing. Create Trim Marks refuses a style that is not text, such as 5.
New Artboard refuses a position or size other than a number or null, such as "10", and New Artboard and Artboard Options refuse a name other than text or null, such as 5; each leaves the artboards and the undo history as they were.