diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 822ef8760..f6cb4412d 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -26,6 +26,10 @@ pub fn specs() -> Vec { .key("Cmd+X") .params("{handles?}") .interactive(|_| Ok(Box::new(SelectRun::new("cutclip", "CUTCLIP")))), + CommandSpec::new("cutbase", "Cut with Base Point", run_cutbase) + .menu(&["Edit", "Cut with Base Point"]) + .params("{handles?, base: [x,y]}") + .interactive(|_| Ok(Box::new(CopyBaseM { cut: true, ..CopyBaseM::default() }))), CommandSpec::new("copyclip", "Copy", run_copyclip) .menu(&["Edit", "Copy"]) .key("Cmd+C") @@ -434,9 +438,11 @@ impl Interactive for MredoM { } } +/// Copy the objects of `hs` to the clipboard, each once however often `hs` lists it. fn copy_to_clip(s: &mut Session, hs: &[Handle], base: Option) -> Result { let d = s.doc()?; - let ents: Vec = hs.iter().filter_map(|h| d.entity(*h).map(|e| (**e).clone())).collect(); + let mut seen = std::collections::HashSet::new(); + let ents: Vec = hs.iter().filter(|h| seen.insert(**h)).filter_map(|h| d.entity(*h).map(|e| (**e).clone())).collect(); let b = ents.iter().fold(Bounds2::EMPTY, |acc, e| acc.union(&entity_bounds(d, e, 0))); let base = base.unwrap_or(if b.min.is_finite() { b.min } else { Vec2::ZERO }); let n = ents.len(); @@ -451,21 +457,34 @@ fn run_copyclip(s: &mut Session, p: &Value) -> Result { } fn run_copybase(s: &mut Session, p: &Value) -> Result { + let base = point_req("copybase", p, "base")?; let hs = targets(s, p)?; - let base = point_param(p, "base"); - let n = copy_to_clip(s, &hs, base)?; + let n = copy_to_clip(s, &hs, Some(base))?; Ok(json!({ "copied": n })) } -fn run_cut(s: &mut Session, p: &Value) -> Result { - let mut hs = targets(s, p)?; - hs.retain(|h| !super::curves::is_locked(s, *h)); - let n = copy_to_clip(s, &hs, None)?; +/// 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(); + 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) +} + +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 })) +} + +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 })) } @@ -649,23 +668,29 @@ impl Interactive for SelectM { } } -/// COPYBASE: base point first, then "Select objects:" (skipped with a pickfirst selection). +/// COPYBASE and CUTBASE: base point first, then "Select objects:" (skipped with a pickfirst +/// selection). CUTBASE also erases the objects it copies. #[derive(Default)] struct CopyBaseM { base: Option, sel: SelectPhase, + cut: bool, } impl CopyBaseM { fn copy(&self, s: &mut Session, hs: &[Handle]) -> Result { - copy_to_clip(s, hs, self.base)?; + if self.cut { + cut_to_clip(s, hs, self.base)?; + } else { + copy_to_clip(s, hs, self.base)?; + } Ok(Step::Done) } } impl Interactive for CopyBaseM { fn name(&self) -> &'static str { - "COPYBASE" + if self.cut { "CUTBASE" } else { "COPYBASE" } } fn begin(&mut self, s: &mut Session) -> Result { self.sel = SelectPhase::begin(s); diff --git a/crates/engine/tests/clipboard.rs b/crates/engine/tests/clipboard.rs index 03ec40842..c410db91c 100644 --- a/crates/engine/tests/clipboard.rs +++ b/crates/engine/tests/clipboard.rs @@ -170,6 +170,89 @@ fn pasteblock_makes_a_block_of_the_clipboard() { assert!(d.block("DOOR").is_some() && d.layer("HINGE").is_some()); } +#[test] +fn cutbase_cuts_with_the_given_base_point() { + let mut s = Session::empty(); + let mut da = drawing_a(); + if let Some(l) = da.layer_mut("WALLS") { + l.locked = true; + } + let a = s.open_drawing(da, "A", None); + let b = s.open_drawing(drawing_b(), "B", None); + 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!(s.clipboard.len(), 1); + assert_eq!(s.clipboard_base, Vec2::new(5.0, 5.0)); + let left = model(&s); + assert_eq!(left.len(), 1); + assert!(matches!(&left[0].kind, EntityKind::Line(_)), "the wall stays"); + + // PASTECLIP places the base point at the insertion point. + s.active = b; + s.execute("pasteclip", &json!({ "at": [105, 5] })).unwrap(); + let pasted = model(&s); + assert!(matches!(&pasted[0].kind, EntityKind::Insert(i) if i.block == "DOOR" && i.insert.x == 115.0 && i.insert.y == 20.0)); + + // Undo in A brings the door back. + s.active = a; + s.execute("u", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().model.len(), 2); +} + +#[test] +fn cutbase_and_copybase_need_a_base_point_and_take_each_object_once() { + let mut s = Session::empty(); + s.open_drawing(drawing_a(), "A", None); + let hs = handles(&s); + for cmd in ["cutbase", "copybase"] { + for p in [json!({ "handles": hs }), json!({ "handles": hs, "base": [5] })] { + let e = s.execute(cmd, &p).unwrap_err().to_string(); + assert!(e.contains("`base` ([x, y]) is required"), "{cmd}: {e}"); + } + } + assert_eq!(model(&s).len(), 2, "nothing was cut"); + assert!(s.clipboard.is_empty(), "nothing was copied"); + + // A handle listed twice is copied and cut once. + let twice = json!({ "handles": [hs[1], hs[1]], "base": [0, 0] }); + assert_eq!(s.execute("copybase", &twice).unwrap(), json!({ "copied": 1 })); + assert_eq!(s.clipboard.len(), 1); + assert_eq!(s.execute("cutbase", &twice).unwrap(), json!({ "cut": 1 })); + assert_eq!(s.clipboard.len(), 1); + assert_eq!(model(&s).len(), 1); +} + +#[test] +fn cutbase_asks_for_the_base_point_then_the_objects() { + let mut s = Session::empty(); + s.open_drawing(drawing_a(), "A", None); + let hs: Vec = s.doc().unwrap().model.iter().map(|e| e.handle).collect(); + let undo_before = s.state().unwrap().undo.len(); + s.start("cutbase").unwrap(); + assert!(s.prompt_text().contains("Specify base point"), "{}", s.prompt_text()); + s.input(cadcraft_engine::Input::Point(Vec2::new(3.0, 4.0))).unwrap(); + assert!(s.prompt_text().contains("Select objects"), "{}", s.prompt_text()); + s.input(cadcraft_engine::Input::Pick(vec![hs[1]])).unwrap(); + s.input(cadcraft_engine::Input::Enter).unwrap(); + assert!(s.running.is_none()); + assert_eq!(s.clipboard.len(), 1); + assert_eq!(s.clipboard_base, Vec2::new(3.0, 4.0)); + assert_eq!(model(&s).iter().map(|e| e.handle).collect::>(), [hs[0]]); + assert_eq!(s.state().unwrap().undo.len(), undo_before + 1, "one undo step"); + + // With a pickfirst selection, the base point is the only prompt. + s.set_selection(vec![hs[0]]); + s.start("cutbase").unwrap(); + assert!(s.prompt_text().contains("Specify base point"), "{}", s.prompt_text()); + s.input(cadcraft_engine::Input::Point(Vec2::new(1.0, 2.0))).unwrap(); + assert!(s.running.is_none()); + assert!(s.doc().unwrap().model.is_empty()); + assert_eq!(s.clipboard_base, Vec2::new(1.0, 2.0)); + assert!(s.selection().is_empty()); +} + /// A stand-in for the DXF writer and reader: "writes" the drawing to memory. static WRITTEN: Mutex> = Mutex::new(None); diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 6400a1f38..58ce93144 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -203,6 +203,7 @@ Current selection التحديد الحالي Current: {color} الحالي: {color} Cut قص + Cut with Base Point قص من نقطة أساس Cyan سماوي Cylinder أسطوانة DWG DWG diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 139dd9d9e..073a3e344 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -186,6 +186,7 @@ Current selection Aktuální výběr Current: {color} Aktuální: {color} Cut Vyjmout + Cut with Base Point Vyjmout se základním bodem Cyan Azurová Cylinder Válec DWG DWG diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index e2e92d4c8..d7feda3bb 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -145,6 +145,7 @@ Current selection Aktuelle Auswahl Current: {color} Aktuell: {color} Cut Ausschneiden + Cut with Base Point Mit Basispunkt ausschneiden Cyan Cyan Cylinder Zylinder DWG DWG diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 1ec3f82fb..2471b4314 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -227,6 +227,7 @@ Curved Καμπύλο Custom Προσαρμοσμένο Cut Αποκοπή + Cut with Base Point Αποκοπή με σημείο βάσης Cyan Κυανό Cylinder Κύλινδρος Dark Σκούρο diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 9685dee94..7124bd2e0 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -192,6 +192,7 @@ Current selection Selección actual Current: {color} Actual: {color} Cut Cortar + Cut with Base Point Cortar con punto base Cyan Cian Cylinder Cilindro DWG DWG diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index d8b100711..ceed3305e 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -144,6 +144,7 @@ Current selection Sélection actuelle Current: {color} Actuel : {color} Cut Couper + Cut with Base Point Couper avec point de base Cyan Cyan Cylinder Cylindre DWG DWG diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 3095ea9af..92bace42a 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -239,6 +239,7 @@ Curved Ívelt Custom Egyéni Cut Kivágás + Cut with Base Point Kivágás bázisponttal Cyan Cián Cylinder Henger DWG DWG diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index cb3a5388e..b513eca74 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -194,6 +194,7 @@ menu Modify Edita Current selection Selezione corrente Current: {color} Corrente: {color} Cut Taglia + Cut with Base Point Taglia con punto base Cyan Ciano Cylinder Cilindro DWG DWG diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 8b7b016fe..44a16e0ff 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -187,6 +187,7 @@ Current selection 현재 선택 항목 Current: {color} 현재: {color} Cut 잘라내기 + Cut with Base Point 기준점을 지정하여 잘라내기 Cyan 청록색 Cylinder 원기둥 DWG DWG diff --git a/crates/ui-egui/src/i18n/pt_br.tsv b/crates/ui-egui/src/i18n/pt_br.tsv index 473b38fb8..3d8c0bd7f 100644 --- a/crates/ui-egui/src/i18n/pt_br.tsv +++ b/crates/ui-egui/src/i18n/pt_br.tsv @@ -191,6 +191,7 @@ Current selection Seleção atual Current: {color} Atual: {color} Cut Recortar + Cut with Base Point Recortar com ponto base Cyan Ciano Cylinder Cilindro DWG DWG diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index c7a6baaf2..a76b06f45 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -144,6 +144,7 @@ Current selection Текущее выделение Current: {color} Текущий: {color} Cut Вырезать + Cut with Base Point Вырезать с базовой точкой Cyan Голубой Cylinder Цилиндр DWG DWG diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 176f1bb38..20d2ab601 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -195,6 +195,7 @@ Current selection Aktuell markering Current: {color} Aktuell: {color} Cut Klipp ut + Cut with Base Point Klipp ut med baspunkt Cyan Cyan Cylinder Cylinder DWG DWG diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f1240778c..a98ac82c5 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -240,6 +240,7 @@ Curved Eğri Custom Özel Cut Kes + Cut with Base Point Taban Noktasıyla Kes Cyan Camgöbeği Cylinder Silindir DWG DWG diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index cb902c57c..eaefc206e 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -191,6 +191,7 @@ Current selection Поточний вибір Current: {color} Поточний: {color} Cut Вирізати + Cut with Base Point Вирізати з базовою точкою Cyan Блакитний Cylinder Циліндр DWG DWG diff --git a/crates/ui-egui/src/i18n/zh.tsv b/crates/ui-egui/src/i18n/zh.tsv index c3aa9f95d..b85c5ffa2 100644 --- a/crates/ui-egui/src/i18n/zh.tsv +++ b/crates/ui-egui/src/i18n/zh.tsv @@ -145,6 +145,7 @@ Current selection 当前选择 Current: {color} 当前: {color} Cut 剪切 + Cut with Base Point 带基点剪切 Cyan 青色 Cylinder 圆柱体 DWG DWG