From 7694627a39c18ea3df8606a5cf22a8592674ef58 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 02:07:47 -0800 Subject: [PATCH] Perspective: reapplying the current grid preset adds no undo step perspective.grid.preset stored the preset's grid every time, so applying the preset the document already holds recorded an undo step that changed nothing. It now compares the new grid with the stored one, as perspective.grid.set does through grid_kept, and records a step only when the grid changes. When only whether the grid shows or its active plane differs, the preset sets them without an undo step, as perspective.grid.show and perspective.plane.set do. --- crates/engine/src/cmd/distortcmds.rs | 4 +-- .../engine/src/cmd/distortcmds/perspective.rs | 2 +- crates/engine/src/cmd/perspgrid.rs | 21 ++++++++--- crates/engine/src/tests/perspgrid.rs | 36 +++++++++++++++++++ docs/mcp.md | 6 ++-- 5 files changed, 59 insertions(+), 10 deletions(-) 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`).