fix(stroke): reject malformed dashes and keep defaults atomic

This commit is contained in:
Nigel Ihza Firdausy
2026-10-11 01:45:19 +07:00
committed by bflatastic
parent 6eeabcd742
commit 059bd82676
2 changed files with 42 additions and 8 deletions
+16 -8
View File
@@ -100,7 +100,14 @@ 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 +174,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 +220,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 +235,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()
}
+26
View File
@@ -134,6 +134,32 @@ 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);
}
#[test]
fn rejected_paint_edits_leave_new_art_defaults_and_focus_unchanged() {
let mut s = session();