From 0c322fd89d634efdd63339819cd6b48347c40129 Mon Sep 17 00:00:00 2001 From: Jerome CAMUS Date: Sun, 25 Oct 2026 17:17:44 +0100 Subject: [PATCH] #2201: one undo reverts a whole mask free-transform drag path.freeTransform ignored the drag's merge key, so each frame was its own undo step. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/paths.rs | 4 ++-- crates/engine/src/tests/stubs.rs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/paths.rs b/crates/engine/src/cmd/paths.rs index afd7aba12..04cf2b439 100644 --- a/crates/engine/src/cmd/paths.rs +++ b/crates/engine/src/cmd/paths.rs @@ -8,7 +8,7 @@ use effectcraft_project::{FeatherFalloff, GroupKind, ItemId, LayerId, LayerSourc use serde_json::{Value, json}; use super::layer_menu::target_masks; -use super::{CommandSpec, b_p, bad, f_p, has_comp, has_layers, layer_mut, layer_p, str_p}; +use super::{CommandSpec, b_p, bad, f_p, has_comp, has_layers, layer_mut, layer_p, merge_p, str_p}; use crate::{EngineError, Result, Session, VertexRef, cmd}; // ---------------------------------------------------------------- RotoBezier @@ -308,7 +308,7 @@ fn free_transform(s: &mut Session, p: &Value) -> Result { let y = v[1] * scale[1]; [x * c - y * sn, x * sn + y * c] }; - let n = s.edit("Free Transform Points", None, |proj, _| { + let n = s.edit("Free Transform Points", merge_p(p), |proj, _| { let mut n = 0; for (lid, uid) in &targets { let l = layer_mut(proj, cid, *lid)?; diff --git a/crates/engine/src/tests/stubs.rs b/crates/engine/src/tests/stubs.rs index 84321ee7e..0d7dd47ac 100644 --- a/crates/engine/src/tests/stubs.rs +++ b/crates/engine/src/tests/stubs.rs @@ -583,3 +583,19 @@ fn convert_to_bezier_reads_the_shape_as_it_is_set() { let mid = |v: &[f64]| (v.iter().copied().fold(f64::MAX, f64::min) + v.iter().copied().fold(f64::MIN, f64::max)) / 2.0; assert!((mid(&xs) - 10.0).abs() < 1e-6 && (mid(&ys) - 5.0).abs() < 1e-6, "centred on the position: {:?} {:?}", mid(&xs), mid(&ys)); } + +/// #2201: a viewer drag on the free-transform box sends one `path.freeTransform` per frame with a +/// shared merge key; one undo must revert the whole drag, not the last frame. +#[test] +fn free_transform_drag_undoes_in_one_step() { + let mut s = comp(); + let (l, m) = masked(&mut s); + s.state.selected_vertices.clear(); + let before = mask_path(&s, l, m); + for k in [90, 90, 90, 90] { + s.execute("path.freeTransform", json!({"layer": l, "scale": k, "merge": "viewer-drag-1"})).unwrap(); + } + assert_ne!(mask_path(&s, l, m), before); + s.execute("edit.undo", json!({})).unwrap(); + assert_eq!(mask_path(&s, l, m), before, "one undo reverts the whole drag"); +}