diff --git a/crates/engine/src/cmd/live/commands.rs b/crates/engine/src/cmd/live/commands.rs index 0db3987e29..2a427be32d 100644 --- a/crates/engine/src/cmd/live/commands.rs +++ b/crates/engine/src/cmd/live/commands.rs @@ -290,7 +290,7 @@ pub fn specs() -> Vec { "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 ), diff --git a/crates/engine/src/cmd/live/mesh.rs b/crates/engine/src/cmd/live/mesh.rs index 187d2149f2..3a4d1851a0 100644 --- a/crates/engine/src/cmd/live/mesh.rs +++ b/crates/engine/src/cmd/live/mesh.rs @@ -122,9 +122,14 @@ fn with_mesh(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(d: &mut Document, id: NodeId, cmd: &str, envelopes: bool, f: impl FnOnce(&mut GradientMesh) -> Result) -> Result { - 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(k: &mut NodeKind, cmd: &str, envelopes: bool, f: impl FnOnce(&mut GradientMesh) -> Result) -> Result { + 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(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 { 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 })) } diff --git a/crates/engine/src/tests/envelope.rs b/crates/engine/src/tests/envelope.rs index 4ffd7a361a..4a9e6395c3 100644 --- a/crates/engine/src/tests/envelope.rs +++ b/crates/engine/src/tests/envelope.rs @@ -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. diff --git a/crates/engine/src/tests/live.rs b/crates/engine/src/tests/live.rs index 4556e6f75f..7beb512ed0 100644 --- a/crates/engine/src/tests/live.rs +++ b/crates/engine/src/tests/live.rs @@ -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] diff --git a/docs/mcp.md b/docs/mcp.md index 30fe6efc65..a75646d618 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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.