fix: 線の色追加時にゼロ幅を既定値へ戻す #1253

This commit is contained in:
Ikki Nishihara
2026-10-12 07:25:29 +07:00
committed by bflatastic
parent 540505fcad
commit 2f64b5a86b
6 changed files with 141 additions and 6 deletions
+21
View File
@@ -446,6 +446,9 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<V
if n.appearance.paint_at(index, fill).is_none() {
n.appearance.set_paint_at(index, fill, Paint::None);
}
if !fill {
restore_unpainted_stroke(&mut n.appearance, index, &paint);
}
let placed = place_paint(&paint, p, item_paint_bounds(n, index, fill));
n.appearance.set_paint_at(index, fill, placed);
Ok(())
@@ -470,6 +473,9 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<V
if fill {
s.paint.fill = unplaced(&paint);
} else {
if !paint.is_none() && s.paint.stroke.is_none() && s.paint.stroke_width == 0.0 {
s.paint.stroke_width = 1.0;
}
s.paint.stroke = unplaced(&paint);
}
if bool_or(p, "focus", true) {
@@ -495,6 +501,17 @@ fn set_run_stroke(style: &mut CharStyle, paint: Paint) {
style.stroke = paint;
}
/// Use the existing 1 pt default when adding paint to an unpainted zero-width stroke.
fn restore_unpainted_stroke(ap: &mut Appearance, index: Option<usize>, paint: &Paint) {
if !paint.is_none()
&& let Some(stroke) = ap.stroke_at_mut(index)
&& stroke.paint.is_none()
&& stroke.width == 0.0
{
stroke.width = 1.0;
}
}
fn swap(s: &mut Session, p: &Value) -> Result<Value> {
let ids = paint_targets(s, p)?;
if !ids.is_empty() {
@@ -512,11 +529,15 @@ fn swap(s: &mut Session, p: &Value) -> Result<Value> {
let f = n.appearance.fill_paint();
let st = n.appearance.stroke_paint();
n.appearance.set_fill(st);
restore_unpainted_stroke(&mut n.appearance, None, &f);
n.appearance.set_stroke(f);
}
Ok(())
})?;
}
if s.paint.stroke.is_none() && !s.paint.fill.is_none() && s.paint.stroke_width == 0.0 {
s.paint.stroke_width = 1.0;
}
std::mem::swap(&mut s.paint.fill, &mut s.paint.stroke);
ok()
}
+107
View File
@@ -0,0 +1,107 @@
//! Painting a previously unpainted zero-width stroke restores the existing 1 pt default.
#![allow(clippy::unwrap_used)]
use serde_json::json;
use vectorcraft_doc::NodeId;
use vectorcraft_engine::{Session, ViewInfo};
use vectorcraft_tools::{PointerEvent, PointerKind};
fn fixture() -> (Session, NodeId) {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
let id = rectangle(&mut s, 10.0);
(s, id)
}
fn rectangle(s: &mut Session, x: f64) -> NodeId {
NodeId(s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 50, "height": 40})).unwrap()["id"].as_u64().unwrap())
}
fn width(s: &Session, id: NodeId) -> f64 {
s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().unwrap().width
}
#[test]
fn adding_a_stroke_after_sampling_strokeless_art_makes_it_visible_in_one_undo_step() {
let (mut s, _) = fixture();
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
s.execute("select.set", &json!({"ids": []})).unwrap();
s.select_tool("eyedropper", ViewInfo::default()).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Down, 25.0, 25.0), ViewInfo::default()).unwrap();
let id = rectangle(&mut s, 100.0);
assert_eq!(width(&s, id), 0.0, "sampled None paint is a normal source of a zero-width stroke");
let st = s.doc().unwrap();
let before = st.doc.clone();
let sel = st.selection.clone();
let history = st.history.undo.len();
let geometry = st.doc.node(id).unwrap().geometric_bounds();
s.execute("paint.setStroke", &json!({"color": "#ee2222"})).unwrap();
assert_eq!(width(&s, id), 1.0);
let st = s.doc().unwrap();
assert_eq!(st.doc.node(id).unwrap().geometric_bounds(), geometry);
assert_eq!(json!(st.selection), json!(sel));
assert_eq!(st.history.undo.len(), history + 1);
let after = st.doc.clone();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(json!(s.doc().unwrap().doc), json!(before));
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(json!(s.doc().unwrap().doc), json!(after));
}
#[test]
fn adding_stroke_paint_to_the_defaults_gives_the_next_shape_a_visible_stroke() {
let (mut s, _) = fixture();
s.execute("select.set", &json!({"ids": []})).unwrap();
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
s.execute("stroke.set", &json!({"weight": 0})).unwrap();
s.execute("paint.setStroke", &json!({"color": "#ee2222"})).unwrap();
assert_eq!(s.new_art_stroke().width, 1.0);
let id = rectangle(&mut s, 100.0);
assert_eq!(width(&s, id), 1.0);
}
#[test]
fn swapping_fill_into_an_unpainted_zero_width_stroke_makes_it_visible() {
let (mut s, id) = fixture();
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
s.execute("stroke.set", &json!({"weight": 0})).unwrap();
let fill = s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint();
s.execute("paint.swap", &json!({})).unwrap();
let n = s.doc().unwrap().doc.node(id).unwrap();
assert!(n.appearance.fill_paint().is_none());
assert_eq!(n.appearance.stroke_paint(), fill);
assert_eq!(width(&s, id), 1.0);
assert_eq!(s.new_art_stroke().width, 1.0);
}
#[test]
fn a_targeted_stroke_is_restored_without_changing_the_other_stroke() {
let (mut s, id) = fixture();
s.execute("stroke.set", &json!({"weight": 5})).unwrap();
s.execute("appearance.addStroke", &json!({})).unwrap();
s.execute("paint.setStroke", &json!({"item": 1, "none": true})).unwrap();
s.execute("stroke.set", &json!({"item": 1, "weight": 0})).unwrap();
s.execute("paint.setStroke", &json!({"item": 1, "color": "#ee2222"})).unwrap();
let ap = &s.doc().unwrap().doc.node(id).unwrap().appearance;
assert_eq!(ap.stroke_at(Some(1)).unwrap().width, 1.0);
assert_eq!(ap.stroke_at(Some(2)).unwrap().width, 5.0);
}
#[test]
fn existing_weights_and_explicitly_zero_painted_strokes_keep_their_widths() {
for (was_none, weight) in [(true, 5.0), (false, 5.0), (false, 0.0)] {
let (mut s, id) = fixture();
if was_none {
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
}
s.execute("stroke.set", &json!({"weight": weight})).unwrap();
s.execute("paint.setStroke", &json!({"color": "#ee2222"})).unwrap();
assert_eq!(width(&s, id), weight);
}
let (mut s, id) = fixture();
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
s.execute("stroke.set", &json!({"weight": 0})).unwrap();
s.execute("paint.setFill", &json!({"color": "#ffaa00"})).unwrap();
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
assert_eq!(width(&s, id), 0.0, "fill edits and removing paint do not enable a stroke");
}