fix(paint): preserve new-art defaults when paint edits are rejected

This commit is contained in:
Nigel Ihza Firdausy 2026-10-10 23:36:51 +07:00 committed by bflatastic
parent 6f7f32c64d
commit 4c011cf42e
2 changed files with 47 additions and 11 deletions

View File

@ -375,15 +375,7 @@ fn apply_to_proxy(s: &mut Session, p: &Value, paint: Paint) -> Result<Value> {
/// remember the paint.
fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<Value> {
let cmd = if fill { "paint.setFill" } else { "paint.setStroke" };
// New art gets the paint fitted to itself, not placed where this one is.
if fill {
s.paint.fill = unplaced(&paint);
} else {
s.paint.stroke = unplaced(&paint);
}
if bool_or(p, "focus", true) {
s.fill_active = fill;
}
// Resolve the requested targets before mutating defaults or the active proxy.
let item = item_target(s, p, cmd)?.of_kind(s, fill);
let selected = item.targets(s, p)?;
let ids = super::graph::paint_targets(&s.doc()?.doc, &selected);
@ -423,6 +415,16 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<V
Ok(())
},
)?;
// Rejected edits leave the next shape's appearance and active proxy untouched.
// New art takes an unplaced gradient so that it fits the new object's own bounds.
if fill {
s.paint.fill = unplaced(&paint);
} else {
s.paint.stroke = unplaced(&paint);
}
if bool_or(p, "focus", true) {
s.fill_active = fill;
}
s.remember_paint(&paint);
ok()
}
@ -436,7 +438,6 @@ fn set_run_stroke(style: &mut CharStyle, paint: Paint) {
}
fn swap(s: &mut Session, p: &Value) -> Result<Value> {
std::mem::swap(&mut s.paint.fill, &mut s.paint.stroke);
let ids = paint_targets(s, p)?;
if !ids.is_empty() {
s.edit("Swap Fill and Stroke", |d, _| {
@ -458,11 +459,11 @@ fn swap(s: &mut Session, p: &Value) -> Result<Value> {
Ok(())
})?;
}
std::mem::swap(&mut s.paint.fill, &mut s.paint.stroke);
ok()
}
fn default_paint(s: &mut Session, p: &Value) -> Result<Value> {
super::newart::reset(s);
let ids = paint_targets(s, p)?;
if !ids.is_empty() {
s.edit("Default Fill and Stroke", |d, _| {
@ -481,6 +482,7 @@ fn default_paint(s: &mut Session, p: &Value) -> Result<Value> {
Ok(())
})?;
}
super::newart::reset(s);
ok()
}

View File

@ -134,6 +134,40 @@ fn fill_and_stroke_commands() {
s.execute("paint.setFill", &json!({"gradient": {"kind": "radial"}})).unwrap();
}
#[test]
fn rejected_paint_edits_leave_new_art_defaults_and_focus_unchanged() {
let mut s = session();
let id = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
s.execute("paint.setFill", &json!({"color": "#00ff00"})).unwrap();
s.execute("paint.setStroke", &json!({"color": "#ff0000"})).unwrap();
let defaults = s.paint.clone();
let focused = s.fill_active;
let original_fill = s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint();
let original_stroke = s.doc().unwrap().doc.node(id).unwrap().appearance.stroke_paint();
// Both shortcut commands used to change defaults before checking invalid ids.
for (command, params) in [
("paint.swap", json!({"ids": "not an array"})),
("paint.default", json!({"ids": ["not an object id"]})),
("paint.setFill", json!({"color": "#123456", "ids": [false]})),
("paint.setStroke", json!({"color": "#123456", "ids": "not an array"})),
("paint.setFill", json!({"color": "#123456", "item": "invalid"})),
] {
assert!(s.execute(command, &params).is_err(), "{command} should reject {params}");
assert_eq!(s.paint, defaults, "{command} changed new-art defaults on failure");
assert_eq!(s.fill_active, focused, "{command} changed the active proxy on failure");
let appearance = &s.doc().unwrap().doc.node(id).unwrap().appearance;
assert_eq!(appearance.fill_paint(), original_fill);
assert_eq!(appearance.stroke_paint(), original_stroke);
}
// Successful operations still update both the selected object and new-art defaults.
s.execute("paint.swap", &json!({})).unwrap();
assert_eq!(s.paint.fill, defaults.stroke);
assert_eq!(s.paint.stroke, defaults.fill);
}
#[test]
fn select_same_fill() {
let mut s = session();