Merge pull request #987 from Ni-zav/fix/noissue-stroke-input-atomic-s20261010T1630Z-c8d2e1

fix(stroke): reject invalid dash entries and keep defaults atomic
This commit is contained in:
BFlat
2026-10-11 03:43:47 +07:00
committed by GitHub
2 changed files with 49 additions and 8 deletions
+14 -8
View File
@@ -100,7 +100,12 @@ impl StrokeChange {
let dash = match p.get("dash") {
None => None,
Some(Value::Null) => Some(None),
Some(Value::Array(a)) => Some(Some(a.iter().filter_map(Value::as_f64).collect::<Vec<_>>()).filter(|d| !d.is_empty())),
Some(Value::Array(a)) => {
// Do not silently omit malformed segments and change the user's dash pattern.
let values: Vec<f64> =
a.iter().map(|v| v.as_f64().ok_or_else(|| bad(cmd, format!("dash lengths must be numbers, got {v}")))).collect::<Result<_>>()?;
Some(Some(values).filter(|d| !d.is_empty()))
}
Some(v) => return Err(bad(cmd, format!("dash must be a list of lengths or null, got {v}"))),
};
Ok(Self {
@@ -167,9 +172,6 @@ fn no_targets(ids: &[vectorcraft_doc::NodeId], p: &Value) -> bool {
fn stroke_set(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "stroke.set";
let change = StrokeChange::parse(p, C)?;
if let Some(w) = change.weight {
s.paint.stroke_width = w;
}
let item = item_target(s, p, C)?.of_kind(s, false);
let mut ids = item.targets(s, p)?;
let new_art = no_targets(&ids, p);
@@ -216,10 +218,6 @@ fn stroke_set(s: &mut Session, p: &Value) -> Result<Value> {
st.profile = pr.clone();
}
};
// With nothing selected the Stroke panel sets up the next object drawn.
if new_art && let Some(st) = s.new_art_stroke_mut() {
set(st);
}
edit_items(s, &ids, item, C, "Stroke", false, |n, index| {
if index.is_none()
&& let NodeKind::Text(t) = &mut n.kind
@@ -235,6 +233,14 @@ fn stroke_set(s: &mut Session, p: &Value) -> Result<Value> {
}
Ok(())
})?;
// Commit the new-art template only after the complete request is validated
// and the selected artwork edit has succeeded.
if let Some(w) = change.weight {
s.paint.stroke_width = w;
}
if new_art && let Some(st) = s.new_art_stroke_mut() {
set(st);
}
ok()
}
+35
View File
@@ -134,6 +134,41 @@ fn fill_and_stroke_commands() {
s.execute("paint.setFill", &json!({"gradient": {"kind": "radial"}})).unwrap();
}
#[test]
fn stroke_rejects_invalid_dash_components_and_preserves_defaults_on_error() {
let mut s = session();
let id = rect(&mut s, 0.0, 0.0, 40.0, 30.0);
s.execute("stroke.set", &json!({"weight": 3, "dash": [6, 3]})).unwrap();
let previous_weight = s.paint.stroke_width;
let previous_stroke = s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().cloned();
for params in [
json!({"weight": 10, "dash": [6, "not a number", 3]}),
json!({"weight": 10, "ids": "not an array"}),
json!({"weight": 10, "item": "invalid"}),
json!({"weight": 10, "arrowAlign": "unknown"}),
] {
assert!(s.execute("stroke.set", &params).is_err(), "{params} must fail");
assert_eq!(s.paint.stroke_width, previous_weight, "failed stroke edit changed new-art defaults");
assert_eq!(s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().cloned(), previous_stroke);
}
// A valid dash list still updates the selected stroke and new-art default width.
s.execute("stroke.set", &json!({"weight": 7, "dash": [4, 2]})).unwrap();
assert_eq!(s.paint.stroke_width, 7.0);
assert_eq!(s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().unwrap().width, 7.0);
// With nothing selected the panel sets up the next object drawn: a rejected edit doesn't.
s.execute("select.none", &json!({})).unwrap();
let next = s.new_art();
assert!(s.execute("stroke.set", &json!({"weight": 12, "dash": [2, 2], "arrowAlign": "unknown"})).is_err());
assert_eq!(s.new_art(), next);
s.execute("stroke.set", &json!({"weight": 12, "dash": [2, 2]})).unwrap();
let next = s.new_art();
let stroke = next.stroke().unwrap();
assert_eq!((stroke.width, stroke.dash.as_ref().map(|d| d.pattern.clone())), (12.0, Some(vec![2.0, 2.0])));
}
#[test]
fn rejected_paint_edits_leave_new_art_defaults_and_focus_unchanged() {
let mut s = session();