diff --git a/crates/engine/src/cmd/distortcmds.rs b/crates/engine/src/cmd/distortcmds.rs index 2dbb7a9a18..8f1b5b97e6 100644 --- a/crates/engine/src/cmd/distortcmds.rs +++ b/crates/engine/src/cmd/distortcmds.rs @@ -26,7 +26,7 @@ pub use perspective::perspective_click; use perspective::{ attach, edit_text, grid_set, grid_show, persp_draw, persp_move, persp_nudge, persp_transform, plane_match, plane_move, plane_set, release, }; -pub(crate) use perspective::{finish_edit_text, grid_of, perspective_rewrite, silent, store_grid, transform_again}; +pub(crate) use perspective::{finish_edit_text, grid_kept, grid_of, perspective_rewrite, silent, store_grid, transform_again}; pub fn specs() -> Vec { vec![ @@ -107,7 +107,7 @@ pub fn specs() -> Vec { "Perspective Grid Preset", [], None, - "{kind: 1|2|3 (its normal view) | name: a preset (see perspective.presets.list)} reset the grid to the preset fitted to the first artboard (and show it; attached objects stay attached)", + "{kind: 1|2|3 (its normal view) | name: a preset (see perspective.presets.list)} reset the grid to the preset fitted to the first artboard (and show it; attached objects stay attached). No undo step when the document already holds that grid; when only whether it shows or its active plane differs, those are set without one", has_doc, super::perspgrid::grid_preset ), diff --git a/crates/engine/src/cmd/distortcmds/perspective.rs b/crates/engine/src/cmd/distortcmds/perspective.rs index 62b72a8e0f..89091248df 100644 --- a/crates/engine/src/cmd/distortcmds/perspective.rs +++ b/crates/engine/src/cmd/distortcmds/perspective.rs @@ -18,7 +18,7 @@ pub(crate) fn store_grid(d: &mut Document, g: &PerspectiveGrid) { /// Whether `d` holds `g` as [`store_grid`] writes it (with `attached` cleared): then storing `g` /// leaves `d` as it is. -fn grid_kept(d: &Document, g: &PerspectiveGrid) -> bool { +pub(crate) fn grid_kept(d: &Document, g: &PerspectiveGrid) -> bool { let mut g = g.clone(); g.attached.clear(); serde_json::to_value(&g).is_ok_and(|v| d.unknown.get(persp::DOC_KEY) == Some(&v)) diff --git a/crates/engine/src/cmd/perspgrid.rs b/crates/engine/src/cmd/perspgrid.rs index d13e7d8c52..aaeee36d80 100644 --- a/crates/engine/src/cmd/perspgrid.rs +++ b/crates/engine/src/cmd/perspgrid.rs @@ -19,7 +19,7 @@ use vectorcraft_tools::distort::perspective::widget::{WidgetCorner, WidgetPlace} use vectorcraft_tools::distort::perspective::{GridDefinition, PerspectiveGrid}; use vectorcraft_tools::{PointerEvent, PointerKind, ToolKey}; -use super::distortcmds::{grid_of, silent, store_grid}; +use super::distortcmds::{grid_kept, grid_of, silent, store_grid}; use super::{CommandSpec, EngineError, Result, Session, always, bad, bool_or, cmd, has_doc, str_param, switch_or, unique_name}; pub fn specs() -> Vec { @@ -261,10 +261,21 @@ pub(crate) fn grid_preset(s: &mut Session, p: &Value) -> Result { let g = s.perspective_preset_grid(&name, ab).ok_or_else(|| bad(C, format!("no perspective grid preset named `{name}`")))?; // The objects stay attached and the view options stay as they were. let g = PerspectiveGrid { attached: old.attached, locked: old.locked, lock_station: old.lock_station, snap: old.snap, rulers: old.rulers, ..g }; - s.edit("Perspective Grid Preset", |d, _| { - store_grid(d, &g); - Ok(()) - })?; + // When the document holds this grid already, apart from whether it shows and its active plane, + // the preset only sets those two, without an undo step (as perspective.grid.show and + // perspective.plane.set do). + let view = PerspectiveGrid { visible: old.visible, plane: old.plane, ..g.clone() }; + if !grid_kept(&s.doc()?.doc, &view) { + s.edit("Perspective Grid Preset", |d, _| { + store_grid(d, &g); + Ok(()) + })?; + } else if (old.visible, old.plane) != (g.visible, g.plane) { + silent(s, |cur| { + cur.visible = g.visible; + cur.plane = g.plane; + })?; + } Ok(g.definition_json()) } diff --git a/crates/engine/src/tests/perspgrid.rs b/crates/engine/src/tests/perspgrid.rs index 07fce547da..dce149e98c 100644 --- a/crates/engine/src/tests/perspgrid.rs +++ b/crates/engine/src/tests/perspgrid.rs @@ -150,6 +150,42 @@ fn built_in_views_apply_by_name_or_type() { assert!(s.execute("perspective.grid.preset", &json!({})).is_err()); } +#[test] +fn reapplying_the_current_grid_preset_records_no_undo_step() { + let mut s = session(); + // On a document with no grid defined, a preset defines and shows one: an undo step. + run(&mut s, "perspective.grid.preset", json!({"kind": 2})); + assert_eq!(undo_len(&s), 1); + let g = grid(&s); + // The same preset again, by type or by name, leaves the grid as it is: no undo step. + run(&mut s, "perspective.grid.preset", json!({"kind": 2})); + run(&mut s, "perspective.grid.preset", json!({"name": "[2P-Normal View]"})); + assert_eq!((grid(&s), undo_len(&s)), (g.clone(), 1)); + // Another active plane, or a hidden grid, is set back without an undo step. + run(&mut s, "perspective.plane.set", json!({"plane": "ground"})); + run(&mut s, "perspective.grid.show", json!({"visible": false})); + assert_ne!(grid(&s), g); + run(&mut s, "perspective.grid.preset", json!({"kind": 2})); + assert_eq!((grid(&s), undo_len(&s)), (g, 1)); + // Another preset changes the grid. + run(&mut s, "perspective.grid.preset", json!({"kind": 3})); + assert_eq!((grid(&s).kind, undo_len(&s)), (3, 2)); +} + +#[test] +fn define_grid_with_unchanged_fields_records_no_undo_step() { + let mut s = session(); + // On a document with no grid defined, `perspective.grid.set` stores the grid: an undo step, + // and the grid no longer follows the first artboard. + run(&mut s, "perspective.grid.set", json!({})); + assert_eq!((run(&mut s, "perspective.grid.get", json!({}))["defined"].clone(), undo_len(&s)), (json!(true), 1)); + // The stored grid's own values leave it as it is: no undo step. + let g = grid(&s); + run(&mut s, "perspective.grid.set", json!({})); + run(&mut s, "perspective.grid.set", json!({"cell": g.cell, "horizon": g.horizon})); + assert_eq!((grid(&s), undo_len(&s)), (g, 1)); +} + #[test] fn presets_save_rename_delete_and_protect_the_built_in_ones() { let mut s = session(); diff --git a/docs/mcp.md b/docs/mcp.md index 1ad05ba3df..3154b9110b 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -10028,8 +10028,10 @@ Perspective grid presets: `perspective.presets.list` lists the built-in views (` `[1P-High View]`, `[2P-Normal View]`, `[2P-Low View]`, `[2P-High View]`, `[3P-Normal View]`, `[3P-Low View]`; `builtIn: true`, fitted to the first artboard, protected), then the saved ones, each with its Define Grid fields. `perspective.grid.preset {name}` (or `{kind}`, that type's normal view) resets the grid to one, fitted to the first -artboard. `perspective.presets.save {name?, newName?, preset?, …fields}` saves a preset: just `{name}` saves the -document's grid (View → Perspective Grid → Save Grid as Preset). `perspective.presets.delete {name}`, +artboard. It adds no undo step when the document already holds that grid. When only whether the grid shows or its +active plane differs, it sets them without an undo step, as `perspective.grid.show` and `perspective.plane.set` do. +`perspective.presets.save {name?, newName?, preset?, …fields}` saves a preset: just `{name}` saves the document's grid +(View → Perspective Grid → Save Grid as Preset). `perspective.presets.delete {name}`, `perspective.presets.export {names?, path?}` (a `.vcperspective` JSON file; without `path` it returns `data`) and `perspective.presets.import {path? | data? | dataBase64?, replace?}` manage them. Saved presets are kept with the preferences, and a grid whose fields are a preset's is named after it (`name`).