Merge pull request #1698 from saimskywalker/issue-1681-dash-pattern-edit

M4: editing a stroke's Dash or Gap keeps the rest of its dash pattern (#1681)
This commit is contained in:
BFlat
2026-10-31 02:13:10 +07:00
committed by GitHub
3 changed files with 86 additions and 2 deletions
+66
View File
@@ -0,0 +1,66 @@
//! The stroke settings' Dash and Gap fields edit the first pair of a dash pattern and keep the
//! rest of it (#1681): an imported `[4, 2, 1, 2]` used to collapse to `[dash, gap]` on the first edit.
use egui::{Event, Id, Key, Modifiers};
use figcraft_doc::NodeId;
use figcraft_engine::Session;
use serde_json::json;
use crate::layers_drag_tests::Ui;
/// A selected rectangle with a stroke dashed `dashes`, its advanced stroke settings shown.
fn scene(dashes: serde_json::Value) -> (Ui, NodeId) {
let mut s = Session::new();
let r = NodeId(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 200, "height": 120})).unwrap()["id"].as_u64().unwrap());
s.execute("node.set", &json!({"ids": [r], "props": {"strokes": ["#000000"], "strokeWeight": 2, "dashes": dashes}})).unwrap();
s.execute("select.set", &json!({"ids": [r]})).unwrap();
let mut u = Ui::new(s);
u.ctx.data_mut(|d| d.insert_temp(Id::unique("stroke_adv"), true));
u.frame(vec![]);
(u, r)
}
fn dashes(u: &Ui, r: NodeId) -> Vec<f64> {
u.app.session.doc().unwrap().node(r).unwrap().stroke.dashes.clone()
}
/// Type `text` into the number field `id` and press Enter.
fn type_into(u: &mut Ui, id: &str, text: &str) {
let at = u.ctx.read_response(Id::unique(id).with("field")).unwrap_or_else(|| panic!("{id} shows")).rect.center();
u.button(at, true);
u.button(at, false);
u.frame(vec![]);
u.frame(vec![Event::Text(text.into())]);
u.frame(vec![
Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE },
Event::Key { key: Key::Enter, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE },
]);
u.frame(vec![]);
}
#[test]
fn editing_the_dash_or_gap_keeps_the_rest_of_the_pattern() {
let (mut u, r) = scene(json!([4, 2, 1, 2]));
type_into(&mut u, "stroke_dash", "6");
assert_eq!(dashes(&u, r), [6.0, 2.0, 1.0, 2.0]);
type_into(&mut u, "stroke_gap", "3");
assert_eq!(dashes(&u, r), [6.0, 3.0, 1.0, 2.0]);
}
#[test]
fn a_single_value_pattern_is_the_dash_and_the_gap() {
let (mut u, r) = scene(json!([5]));
type_into(&mut u, "stroke_gap", "2");
assert_eq!(dashes(&u, r), [5.0, 2.0]);
}
/// An odd-length pattern draws twice over (`[4, 2, 1]` is `[4, 2, 1, 4, 2, 1]`): editing its Dash
/// writes the repeat out, so only the first dash changes and every gap stays as drawn.
#[test]
fn an_odd_length_pattern_keeps_its_repeat() {
let (mut u, r) = scene(json!([4, 2, 1]));
type_into(&mut u, "stroke_dash", "6");
assert_eq!(dashes(&u, r), [6.0, 2.0, 1.0, 4.0, 2.0, 1.0]);
type_into(&mut u, "stroke_gap", "3");
assert_eq!(dashes(&u, r), [6.0, 3.0, 1.0, 4.0, 2.0, 1.0]);
}
+2
View File
@@ -90,6 +90,8 @@ mod crop_image_tests;
#[cfg(test)]
mod cursors_tests;
#[cfg(test)]
mod dash_pattern_tests;
#[cfg(test)]
mod design_paste_tests;
#[cfg(test)]
mod dev_mode_tests;
+18 -2
View File
@@ -187,6 +187,22 @@ fn brush_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], brush: Option<Option
});
}
/// Dash pattern `dashes` with its first dash (`i` 0) or first gap (`i` 1) set to `v`, keeping the
/// rest of the pattern: the panel shows only the first pair, and editing it must not collapse an
/// imported `[4, 2, 1, 2]` to two values (#1681). An odd-length pattern is drawn repeated
/// (`figcraft_pathops::width::dash_pattern`: `[4, 2, 1]` is `[4, 2, 1, 4, 2, 1]`), so it is
/// written out that way first and only the edited entry changes; `[5]` becomes `[5, 5]`.
fn set_dash(dashes: &[f64], i: usize, v: f64) -> Vec<f64> {
let mut d = if dashes.is_empty() { vec![4.0] } else { dashes.to_vec() };
if d.len() % 2 == 1 {
d = d.repeat(2);
}
if let Some(x) = d.get_mut(i) {
*x = v.max(0.0);
}
d
}
/// Advanced stroke settings: dash pattern, width profile, join and the end points of open paths.
pub(super) fn stroke_settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
let ids = sel.to_vec();
@@ -214,9 +230,9 @@ pub(super) fn stroke_settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel
widgets::sublabels(ui, tr("Dash"), Some(tr("Gap")));
row(ui, 32.0, |ui| {
let e = widgets::number_field(ui, Id::unique("stroke_dash"), Lead::None, Some(dash), "", half, 1.0);
app.field(&ids, "dashes", e, |v| json!([v.max(0.0), gap]));
app.field(&ids, "dashes", e, |v| json!(set_dash(&d, 0, v)));
let e = widgets::number_field(ui, Id::unique("stroke_gap"), Lead::None, Some(gap), "", half, 1.0);
app.field(&ids, "dashes", e, |v| json!([dash, v.max(0.0)]));
app.field(&ids, "dashes", e, |v| json!(set_dash(&d, 1, v)));
});
}
width_profile_row(app, ui, &ids, st.as_ref().map(|s| s.profile().cloned()));