From c42763ba0f50c552bb1e31cab8d4ce0a47ca480b Mon Sep 17 00:00:00 2001 From: aiongg Date: Sun, 25 Oct 2026 04:12:57 +0000 Subject: [PATCH] CUTCLIP and CUTBASE leave the clipboard alone when every object is on a locked layer, and say how many were locked --- crates/engine/src/cmd/edit.rs | 44 +++++++++++++++++++++++++------- crates/engine/tests/clipboard.rs | 31 +++++++++++++++++++++- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index f6cb4412d..dab5d4eb7 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -463,29 +463,53 @@ fn run_copybase(s: &mut Session, p: &Value) -> Result { Ok(json!({ "copied": n })) } -/// Copy the objects that are not on locked layers to the clipboard and erase them. -fn cut_to_clip(s: &mut Session, hs: &[Handle], base: Option) -> Result { - let hs: Vec = hs.iter().copied().filter(|h| !super::curves::is_locked(s, *h)).collect(); +/// Copy the objects that are not on locked layers to the clipboard and erase them. Returns how +/// many were cut and the message to show: how many were on a locked layer, and that nothing was +/// cut when that is all of them (the clipboard then keeps what it held). +fn cut_to_clip(s: &mut Session, hs: &[Handle], base: Option) -> Result<(usize, Option)> { + let mut seen = std::collections::HashSet::new(); + let live: Vec = { + let d = s.doc()?; + hs.iter().copied().filter(|h| seen.insert(*h) && d.entity(*h).is_some()).collect() + }; + let (hs, locked): (Vec, Vec) = live.into_iter().partition(|h| !super::curves::is_locked(s, *h)); + let note = match locked.len() { + 0 => None, + 1 => Some("1 object was on a locked layer.".to_string()), + n => Some(format!("{n} objects were on a locked layer.")), + }; + if hs.is_empty() { + let msg = note.map_or_else(|| "Nothing was cut.".to_string(), |m| format!("{m} Nothing was cut.")); + return Ok((0, Some(msg))); + } let n = copy_to_clip(s, &hs, base)?; let d = s.doc_mut()?; for h in &hs { d.remove_entity(*h); } s.set_selection(Vec::new()); - Ok(n) + Ok((n, note)) +} + +fn cut_result(n: usize, msg: Option) -> Value { + let mut r = json!({ "cut": n }); + if let Some(m) = msg { + r["message"] = json!(m); + } + r } fn run_cut(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; - let n = cut_to_clip(s, &hs, None)?; - Ok(json!({ "cut": n })) + let (n, msg) = cut_to_clip(s, &hs, None)?; + Ok(cut_result(n, msg)) } fn run_cutbase(s: &mut Session, p: &Value) -> Result { let base = point_req("cutbase", p, "base")?; let hs = targets(s, p)?; - let n = cut_to_clip(s, &hs, Some(base))?; - Ok(json!({ "cut": n })) + let (n, msg) = cut_to_clip(s, &hs, Some(base))?; + Ok(cut_result(n, msg)) } /// Paste the clipboard moved by `offset` (the clipboard brings the layers, styles and blocks its @@ -680,7 +704,9 @@ struct CopyBaseM { impl CopyBaseM { fn copy(&self, s: &mut Session, hs: &[Handle]) -> Result { if self.cut { - cut_to_clip(s, hs, self.base)?; + if let (_, Some(m)) = cut_to_clip(s, hs, self.base)? { + s.echo(m); + } } else { copy_to_clip(s, hs, self.base)?; } diff --git a/crates/engine/tests/clipboard.rs b/crates/engine/tests/clipboard.rs index c410db91c..650d63092 100644 --- a/crates/engine/tests/clipboard.rs +++ b/crates/engine/tests/clipboard.rs @@ -182,7 +182,7 @@ fn cutbase_cuts_with_the_given_base_point() { s.active = a; let r = s.execute("cutbase", &json!({ "handles": handles(&s), "base": [5, 5] })).unwrap(); // The wall is on a locked layer, so only the door is cut. - assert_eq!(r, json!({ "cut": 1 })); + assert_eq!(r, json!({ "cut": 1, "message": "1 object was on a locked layer." })); assert_eq!(s.clipboard.len(), 1); assert_eq!(s.clipboard_base, Vec2::new(5.0, 5.0)); let left = model(&s); @@ -201,6 +201,35 @@ fn cutbase_cuts_with_the_given_base_point() { assert_eq!(s.doc().unwrap().model.len(), 2); } +#[test] +fn cutting_only_locked_objects_keeps_the_clipboard() { + let mut s = Session::empty(); + let mut da = drawing_a(); + for l in &mut da.layers { + l.locked = true; + } + s.open_drawing(da, "A", None); + let hs = handles(&s); + s.execute("copyclip", &json!({ "handles": [hs[0]] })).unwrap(); + let held = s.clipboard.clone(); + let r = s.execute("cutclip", &json!({ "handles": hs })).unwrap(); + assert_eq!(r, json!({ "cut": 0, "message": "2 objects were on a locked layer. Nothing was cut." })); + assert_eq!(model(&s).len(), 2, "nothing was erased"); + assert_eq!(s.clipboard.len(), 1, "the clipboard keeps what it held"); + assert_eq!(s.clipboard[0].handle, held[0].handle); + + // CUTBASE at the command line says so too. + s.start("cutbase").unwrap(); + s.input(cadcraft_engine::Input::Point(Vec2::new(3.0, 4.0))).unwrap(); + let wall = s.doc().unwrap().model.iter().next().unwrap().handle; + s.input(cadcraft_engine::Input::Pick(vec![wall])).unwrap(); + s.input(cadcraft_engine::Input::Enter).unwrap(); + assert!(s.running.is_none()); + assert!(s.log.iter().any(|l| l.contains("1 object was on a locked layer. Nothing was cut.")), "{:?}", s.log); + assert_eq!(s.clipboard.len(), 1); + assert_eq!(model(&s).len(), 2); +} + #[test] fn cutbase_and_copybase_need_a_base_point_and_take_each_object_once() { let mut s = Session::empty();