mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-29 13:26:49 -06:00
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
Current selection التحديد الحالي
|
||||
Current: {color} الحالي: {color}
|
||||
Cut قص
|
||||
Cut with Base Point قص من نقطة أساس
|
||||
Cyan سماوي
|
||||
Cylinder أسطوانة
|
||||
DWG DWG
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -227,6 +227,7 @@
|
||||
Curved Καμπύλο
|
||||
Custom Προσαρμοσμένο
|
||||
Cut Αποκοπή
|
||||
Cut with Base Point Αποκοπή με σημείο βάσης
|
||||
Cyan Κυανό
|
||||
Cylinder Κύλινδρος
|
||||
Dark Σκούρο
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
@@ -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
|
||||
|
||||
|
@@ -187,6 +187,7 @@
|
||||
Current selection 현재 선택 항목
|
||||
Current: {color} 현재: {color}
|
||||
Cut 잘라내기
|
||||
Cut with Base Point 기준점을 지정하여 잘라내기
|
||||
Cyan 청록색
|
||||
Cylinder 원기둥
|
||||
DWG DWG
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -144,6 +144,7 @@
|
||||
Current selection Текущее выделение
|
||||
Current: {color} Текущий: {color}
|
||||
Cut Вырезать
|
||||
Cut with Base Point Вырезать с базовой точкой
|
||||
Cyan Голубой
|
||||
Cylinder Цилиндр
|
||||
DWG DWG
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
@@ -191,6 +191,7 @@
|
||||
Current selection Поточний вибір
|
||||
Current: {color} Поточний: {color}
|
||||
Cut Вирізати
|
||||
Cut with Base Point Вирізати з базовою точкою
|
||||
Cyan Блакитний
|
||||
Cylinder Циліндр
|
||||
DWG DWG
|
||||
|
||||
|
@@ -145,6 +145,7 @@
|
||||
Current selection 当前选择
|
||||
Current: {color} 当前: {color}
|
||||
Cut 剪切
|
||||
Cut with Base Point 带基点剪切
|
||||
Cyan 青色
|
||||
Cylinder 圆柱体
|
||||
DWG DWG
|
||||
|
||||
|
Reference in New Issue
Block a user