Mesh: adding a line at a mesh point records no undo step

object.mesh.addLine at a point that is already a mesh point (a corner,
a point on an edge or inside) adds neither a row nor a column, yet it
recorded an undo step. The lines are now added to a copy of the object
first: when the grid stays the same, including a mesh envelope stored
without handles, the command returns the point's index and records
nothing. A new color at the point is still one undo step.
This commit is contained in:
dotCooCoo
2026-11-12 18:06:23 -08:00
parent 13a4d271d4
commit fe1d56f339
5 changed files with 77 additions and 8 deletions
+1 -1
View File
@@ -290,7 +290,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Add Mesh Line",
[],
None,
"{id, x, y, color? (#rrggbbaa and #rgba are refused), keepModel?: bool (as paint.setFill), tolerance?: pt (0)} add a row and a column through (x, y), inside or on the edge, to a gradient mesh (new point in `color`) or a mesh envelope; a point outside within `tolerance` of the edge adds them through the nearest point of the edge → {index}",
"{id, x, y, color? (#rrggbbaa and #rgba are refused), keepModel?: bool (as paint.setFill), tolerance?: pt (0)} add a row and a column through (x, y), inside or on the edge, to a gradient mesh (new point in `color`) or a mesh envelope; a point outside within `tolerance` of the edge adds them through the nearest point of the edge; at a point that is already a mesh point it adds no line, and records no undo step unless `color` changes it → {index}",
has_doc,
mesh_add_line
),
+36 -6
View File
@@ -122,9 +122,14 @@ fn with_mesh<T>(s: &mut Session, p: &Value, cmd: &str, label: &str, envelopes: b
/// Run `f` on the gradient mesh `id` of `d`; with `envelopes`, a mesh envelope's grid too (its
/// points and handles are written back: editing fills in the handles).
fn edit_grid<T>(d: &mut Document, id: NodeId, cmd: &str, envelopes: bool, f: impl FnOnce(&mut GradientMesh) -> Result<T>) -> Result<T> {
match d.node_mut(id).map(|n| &mut n.kind) {
Some(NodeKind::Mesh(m)) => f(m),
Some(NodeKind::Envelope { kind, .. }) if envelopes && matches!(**kind, EnvelopeKind::Mesh { .. }) => {
edit_kind(&mut d.node_mut(id).ok_or(EngineError::NoNode(id))?.kind, cmd, envelopes, f)
}
/// [`edit_grid`] on a node's kind.
fn edit_kind<T>(k: &mut NodeKind, cmd: &str, envelopes: bool, f: impl FnOnce(&mut GradientMesh) -> Result<T>) -> Result<T> {
match k {
NodeKind::Mesh(m) => f(m),
NodeKind::Envelope { kind, .. } if envelopes && matches!(**kind, EnvelopeKind::Mesh { .. }) => {
let EnvelopeKind::Mesh { rows, cols, points, handles } = &mut **kind else {
return Err(bad(cmd, "not a gradient mesh or mesh envelope"));
};
@@ -135,8 +140,7 @@ fn edit_grid<T>(d: &mut Document, id: NodeId, cmd: &str, envelopes: bool, f: imp
*handles = m.points.iter().map(|q| q.handles).collect();
Ok(r)
}
Some(_) => Err(bad(cmd, if envelopes { "not a gradient mesh or mesh envelope" } else { "not a gradient mesh" })),
None => Err(EngineError::NoNode(id)),
_ => Err(bad(cmd, if envelopes { "not a gradient mesh or mesh envelope" } else { "not a gradient mesh" })),
}
}
@@ -281,12 +285,38 @@ pub(super) fn mesh_add_line(s: &mut Session, p: &Value) -> Result<Value> {
check_numbers(p, &["tolerance"], C)?;
let tol = f64_or(p, "tolerance", 0.0).max(0.0);
let color = color_param(p, "color", C)?.map(|c| crate::cmd::paint::new_color(s, p, c));
let index = with_mesh(s, p, C, "Add Mesh Line", true, |m| {
let add = move |m: &mut GradientMesh| {
let i = m.add_lines_at(q, tol).ok_or_else(|| bad(C, "point is outside the mesh"))?;
if let Some(c) = color {
m.points[i].color = c;
}
Ok(i)
};
let id = object_param(p, "id", C)?.ok_or_else(|| bad(C, "missing `id`"))?;
if s.script_running() {
return Err(EngineError::Busy);
}
// The lines are added to a copy of the object. A point that is already a mesh point adds none:
// when its color stays too, nothing changes and no undo step is recorded. The grid is compared,
// not the node: a mesh envelope stored without handles takes them in any edit.
let orig = s.doc()?.doc.node(id).ok_or(EngineError::NoNode(id))?.kind.clone();
let mut kind = orig.clone();
let (i, same) = edit_kind(&mut kind, C, true, |m| {
let before = m.clone();
let i = add(m)?;
Ok((i, *m == before))
})?;
if same {
return Ok(json!({ "index": i }));
}
let index = s.edit("Add Mesh Line", move |d, _| {
let n = d.node_mut(id).ok_or(EngineError::NoNode(id))?;
// Settling typed values before the edit may have changed the object: add the lines again.
if n.kind != orig {
return edit_kind(&mut n.kind, C, true, add);
}
n.kind = kind;
Ok(i)
})?;
Ok(json!({ "index": index }))
}
+19
View File
@@ -123,6 +123,25 @@ fn reset_with_warp_and_mesh_switch_the_kind_and_keep_the_content() {
assert!(s.execute("object.envelope.resetWithMesh", &json!({})).is_err());
}
/// `object.mesh.addLine` at a point a mesh envelope already has (one stored without handles) adds
/// no line and records no undo step; a point between its lines adds them as one step.
#[test]
fn mesh_envelope_add_line_at_a_mesh_point_records_no_undo_step() {
let mut s = session();
let (_, e) = enveloped(&mut s, "object.envelope.makeWithMesh", json!({"rows": 2, "cols": 2}));
let before = kind(&s, e);
assert!(matches!(&before, EnvelopeKind::Mesh { handles, .. } if handles.is_empty()), "{before:?}");
let undo_len = |s: &Session| s.doc().unwrap().history.undo.len();
let n = undo_len(&s);
for (x, y) in [(100.0, 100.0), (300.0, 200.0), (200.0, 150.0), (300.0, 150.0)] {
s.execute("object.mesh.addLine", &json!({"id": e.0, "x": x, "y": y})).unwrap();
assert_eq!((kind(&s, e), undo_len(&s)), (before.clone(), n), "({x}, {y}): nothing changed");
}
s.execute("object.mesh.addLine", &json!({"id": e.0, "x": 150, "y": 125})).unwrap();
assert!(matches!(kind(&s, e), EnvelopeKind::Mesh { rows: 3, cols: 3, .. }), "{:?}", kind(&s, e));
assert_eq!(undo_len(&s), n + 1);
}
/// A new warp envelope bends with its style's Bézier net (`version` [`live::WARP_NET`]) and keeps
/// it through Reset with Warp. One saved before the version existed keeps the earlier map when its
/// warp or its options change.
+19
View File
@@ -1206,6 +1206,25 @@ fn mesh_add_line_exactly_on_the_edge_needs_no_tolerance() {
assert_eq!(undo_len(&s), n, "nothing changed");
}
/// `object.mesh.addLine` at a point that is already a mesh point (a corner, a point on an edge or
/// inside) adds no line and records no undo step; with a new `color` there it is one step.
#[test]
fn mesh_add_line_at_a_mesh_point_records_no_undo_step() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
sel(&mut s, &[a]);
s.execute("object.mesh.create", &json!({"rows": 2, "cols": 2})).unwrap();
let (before, n) = (mesh_of(&s, a), undo_len(&s));
for (x, y) in [(0.0, 0.0), (100.0, 100.0), (50.0, 100.0), (100.0, 50.0), (50.0, 50.0)] {
let r = s.execute("object.mesh.addLine", &json!({"id": a.0, "x": x, "y": y})).unwrap();
let p = mesh_of(&s, a).points[r["index"].as_u64().unwrap() as usize].p;
assert!(p.distance(Point::new(x, y)) < 0.5, "({x}, {y}): {p:?}");
assert_eq!((mesh_of(&s, a), undo_len(&s)), (before.clone(), n), "({x}, {y}): nothing changed");
}
s.execute("object.mesh.addLine", &json!({"id": a.0, "x": 100, "y": 100, "color": "#00ff00"})).unwrap();
assert_eq!(undo_len(&s), n + 1, "a new color is a step");
}
/// A mesh point `index`, and `rows`, `cols` or `columns` for Create Gradient Mesh, that is not a
/// whole number is an error that changes nothing; 4.0 names point 4.
#[test]
+2 -1
View File
@@ -9627,7 +9627,8 @@ right/left/down/up; none stored means the smooth mesh through the points, as old
`object.mesh.deletePoint` edit them like a gradient mesh's (one undo step each). On the canvas a selected envelope
shows its mesh; the Mesh tool and Direct Selection drag its points and the clicked point's handles, and the Mesh tool
adds a row and a column where it clicks inside. `object.mesh.addLine`, and `object.mesh.create` with `at`, take a point exactly on
the mesh's outline (within a millionth of a point) on any side without `tolerance`; a point outside needs `tolerance`. `press_key` Escape during a drag of a point or handle of a mesh
the mesh's outline (within a millionth of a point) on any side without `tolerance`; a point outside needs `tolerance`. `object.mesh.addLine` at a point that is already a mesh
point adds no line and records no undo step unless its `color` changes the point. `press_key` Escape during a drag of a point or handle of a mesh
envelope or a gradient mesh cancels it: the point goes back and no undo step is added. The Control bar shows Edit Envelope / Edit Contents, the warp's
style, orientation, bend and distortions (or the mesh's rows and columns), Reset (an unbent warp, a flat mesh) and
Envelope Options.