# Conflicts:
#	docs/parity-checklist.md
This commit is contained in:
aiongg
2026-10-25 02:22:16 +00:00
17 changed files with 133 additions and 10 deletions
+35 -10
View File
@@ -26,6 +26,10 @@ pub fn specs() -> Vec<CommandSpec> {
.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<Vec2>) -> Result<usize> {
let d = s.doc()?;
let ents: Vec<Entity> = hs.iter().filter_map(|h| d.entity(*h).map(|e| (**e).clone())).collect();
let mut seen = std::collections::HashSet::new();
let ents: Vec<Entity> = 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<Value> {
}
fn run_copybase(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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<Vec2>) -> Result<usize> {
let hs: Vec<Handle> = 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<Value> {
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<Value> {
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<Vec2>,
sel: SelectPhase,
cut: bool,
}
impl CopyBaseM {
fn copy(&self, s: &mut Session, hs: &[Handle]) -> Result<Step> {
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<Step> {
self.sel = SelectPhase::begin(s);
+83
View File
@@ -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<Handle> = 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::<Vec<_>>(), [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<Option<Drawing>> = Mutex::new(None);
+1
View File
@@ -203,6 +203,7 @@
Current selection التحديد الحالي
Current: {color} الحالي: {color}
Cut قص
Cut with Base Point قص من نقطة أساس
Cyan سماوي
Cylinder أسطوانة
DWG DWG
1 # CADCraft Arabic interface catalog — original translations.
203
204
205
206
207
208
209
+1
View File
@@ -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
1 # CADCraft Czech interface catalog — original translations by @Losminek (AI-assisted).
186
187
188
189
190
191
192
+1
View File
@@ -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
1 # CADCraft German interface catalog — original translations by the CADCraft contributors.
145
146
147
148
149
150
151
+1
View File
@@ -227,6 +227,7 @@
Curved Καμπύλο
Custom Προσαρμοσμένο
Cut Αποκοπή
Cut with Base Point Αποκοπή με σημείο βάσης
Cyan Κυανό
Cylinder Κύλινδρος
Dark Σκούρο
1 # Greek interface catalog for CADCraft. Original, AI-assisted translation by @Camthalion77 with ChatGPT.
227
228
229
230
231
232
233
+1
View File
@@ -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
1 # CADCraft Spanish interface catalog — original translations.
192
193
194
195
196
197
198
+1
View File
@@ -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
1 # CADCraft French interface catalog — original translations by @pechnikov2026-star.
144
145
146
147
148
149
150
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 16 and column 72.
+1
View File
@@ -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
1 # CADCraft Italian interface catalog — original translations by Emanuele Bottelli (AI-assisted, Claude).
194
195
196
197
198
199
200
+1
View File
@@ -187,6 +187,7 @@
Current selection 현재 선택 항목
Current: {color} 현재: {color}
Cut 잘라내기
Cut with Base Point 기준점을 지정하여 잘라내기
Cyan 청록색
Cylinder 원기둥
DWG DWG
1 # CADCraft Korean interface catalog — original Korean translations.
187
188
189
190
191
192
193
+1
View File
@@ -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
1 # CADCraft Brazilian Portuguese (pt-BR) interface catalog.
191
192
193
194
195
196
197
+1
View File
@@ -144,6 +144,7 @@
Current selection Текущее выделение
Current: {color} Текущий: {color}
Cut Вырезать
Cut with Base Point Вырезать с базовой точкой
Cyan Голубой
Cylinder Цилиндр
DWG DWG
1 # CADCraft Russian interface catalog — original translations by @pechnikov2026-star.
144
145
146
147
148
149
150
+1
View File
@@ -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
1 # CADCraft Swedish interface catalog — original translations by the CADCraft contributors.
195
196
197
198
199
200
201
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 11 and column 13.
+1
View File
@@ -191,6 +191,7 @@
Current selection Поточний вибір
Current: {color} Поточний: {color}
Cut Вирізати
Cut with Base Point Вирізати з базовою точкою
Cyan Блакитний
Cylinder Циліндр
DWG DWG
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
191
192
193
194
195
196
197
+1
View File
@@ -145,6 +145,7 @@
Current selection 当前选择
Current: {color} 当前: {color}
Cut 剪切
Cut with Base Point 带基点剪切
Cyan 青色
Cylinder 圆柱体
DWG DWG
1 # CADCraft Simplified Chinese interface catalog — original translations.
145
146
147
148
149
150
151