mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-29 13:26:49 -06:00
327 lines
14 KiB
Rust
327 lines
14 KiB
Rust
//! The clipboard between open drawings (#325): pasted objects bring the layers and blocks they
|
|
//! use, PASTEORIG pastes into another drawing at the same coordinates, and the clipboard can
|
|
//! travel as text through the system clipboard.
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::cmd::clipboard;
|
|
use cadcraft_engine::cmd::file::{IoHooks, set_io};
|
|
use cadcraft_engine::doc::color::Color;
|
|
use cadcraft_engine::doc::{Block, Common, Drawing, Entity, EntityKind, Handle, Insert, Layer, Line};
|
|
use cadcraft_engine::geom::{Vec2, Vec3};
|
|
use serde_json::json;
|
|
|
|
fn line(d: &mut Drawing, layer: &str, a: (f64, f64), b: (f64, f64)) -> Entity {
|
|
let kind = EntityKind::Line(Line { a: Vec3::new(a.0, a.1, 0.0), b: Vec3::new(b.0, b.1, 0.0) });
|
|
Entity { handle: d.new_handle(), common: Common { layer: layer.into(), ..Common::default() }, kind }
|
|
}
|
|
|
|
fn insert(d: &mut Drawing, block: &str, at: (f64, f64)) -> Entity {
|
|
let kind = EntityKind::Insert(Insert {
|
|
block: block.into(),
|
|
insert: Vec3::new(at.0, at.1, 0.0),
|
|
scale: Vec3::new(1.0, 1.0, 1.0),
|
|
rotation: 0.0,
|
|
attribs: Vec::new(),
|
|
cols: 1,
|
|
rows: 1,
|
|
col_spacing: 0.0,
|
|
row_spacing: 0.0,
|
|
});
|
|
Entity { handle: d.new_handle(), common: Common::default(), kind }
|
|
}
|
|
|
|
fn block(d: &mut Drawing, name: &str, ents: Vec<Entity>) {
|
|
let mut b = Block::new(name);
|
|
b.anonymous = name.starts_with('*');
|
|
for e in ents {
|
|
b.entities.push(e);
|
|
}
|
|
d.blocks.insert(name.into(), Arc::new(b));
|
|
}
|
|
|
|
/// Drawing A: a line on WALLS and a DOOR block reference; DOOR holds a line on HINGE and a
|
|
/// reference to the generated block *U1.
|
|
fn drawing_a() -> Drawing {
|
|
let mut d = Drawing::new_metric();
|
|
d.layers.push(Layer { color: Color::Index(1), ..Layer::new("WALLS") });
|
|
d.layers.push(Layer { color: Color::Index(5), ..Layer::new("HINGE") });
|
|
let u = line(&mut d, "0", (0.0, 0.0), (1.0, 1.0));
|
|
block(&mut d, "*U1", vec![u]);
|
|
let leaf = line(&mut d, "HINGE", (0.0, 0.0), (0.0, 2.0));
|
|
let nested = insert(&mut d, "*U1", (0.0, 0.0));
|
|
block(&mut d, "DOOR", vec![leaf, nested]);
|
|
let wall = line(&mut d, "WALLS", (10.0, 20.0), (30.0, 20.0));
|
|
let door = insert(&mut d, "DOOR", (15.0, 20.0));
|
|
d.model.push(wall);
|
|
d.model.push(door);
|
|
d
|
|
}
|
|
|
|
/// Drawing B already has a WALLS layer (green) and a different *U1.
|
|
fn drawing_b() -> Drawing {
|
|
let mut d = Drawing::new_metric();
|
|
d.layers.push(Layer { color: Color::Index(3), ..Layer::new("WALLS") });
|
|
let other = line(&mut d, "0", (5.0, 5.0), (6.0, 6.0));
|
|
block(&mut d, "*U1", vec![other]);
|
|
d
|
|
}
|
|
|
|
fn handles(s: &Session) -> Vec<String> {
|
|
s.doc().unwrap().model.iter().map(|e| e.handle.hex()).collect()
|
|
}
|
|
|
|
fn model(s: &Session) -> Vec<Entity> {
|
|
s.doc().unwrap().model.iter().map(|e| (**e).clone()).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn paste_into_another_drawing_brings_layers_and_blocks() {
|
|
let mut s = Session::empty();
|
|
let a = s.open_drawing(drawing_a(), "A", None);
|
|
let b = s.open_drawing(drawing_b(), "B", None);
|
|
s.active = a;
|
|
s.execute("copyclip", &json!({ "handles": handles(&s) })).unwrap();
|
|
assert_eq!(s.clipboard.len(), 2);
|
|
// The base point is the lower-left corner of the copied objects (the wall starts at 10,20).
|
|
assert_eq!(s.clipboard_base, Vec2::new(10.0, 20.0));
|
|
|
|
s.active = b;
|
|
let before: Vec<Handle> = s.doc().unwrap().model.iter().map(|e| e.handle).collect();
|
|
let r = s.execute("pasteclip", &json!({ "at": [110, 20] })).unwrap();
|
|
assert_eq!(r["handles"].as_array().map(Vec::len), Some(2));
|
|
let d = s.doc().unwrap();
|
|
// HINGE comes from A; WALLS keeps B's definition.
|
|
assert_eq!(d.layer("HINGE").map(|l| l.color), Some(Color::Index(5)));
|
|
assert_eq!(d.layer("WALLS").map(|l| l.color), Some(Color::Index(3)));
|
|
// DOOR comes along, its nested *U1 under a fresh name because B has a *U1 of its own.
|
|
let door = d.block("DOOR").expect("DOOR copied");
|
|
let nested: Vec<String> =
|
|
door.entities.iter().filter_map(|e| if let EntityKind::Insert(i) = &e.kind { Some(i.block.clone()) } else { None }).collect();
|
|
assert_eq!(nested, ["*U2"]);
|
|
assert_eq!(d.block("*U2").map(|b| b.entities.len()), Some(1));
|
|
let theirs = d.block("*U1").and_then(|b| b.entities.iter().next().cloned()).unwrap();
|
|
assert!(matches!(&theirs.kind, EntityKind::Line(l) if l.a.x == 5.0), "B's *U1 is unchanged");
|
|
// The objects moved by 100 in X, with new handles.
|
|
let pasted = model(&s);
|
|
assert_eq!(pasted.len(), 2);
|
|
assert!(pasted.iter().all(|e| !before.contains(&e.handle)));
|
|
assert!(matches!(&pasted[0].kind, EntityKind::Line(l) if l.a.x == 110.0 && l.a.y == 20.0));
|
|
assert_eq!(pasted[0].common.layer, "WALLS");
|
|
assert!(matches!(&pasted[1].kind, EntityKind::Insert(i) if i.block == "DOOR" && i.insert.x == 115.0));
|
|
assert_eq!(s.selection().len(), 2, "the pasted objects are selected");
|
|
|
|
// Undo removes the paste.
|
|
s.execute("u", &json!({})).unwrap();
|
|
assert!(s.doc().unwrap().model.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn pasteorig_pastes_only_into_another_drawing() {
|
|
let mut s = Session::empty();
|
|
let a = s.open_drawing(drawing_a(), "A", None);
|
|
let b = s.open_drawing(drawing_b(), "B", None);
|
|
s.active = a;
|
|
s.execute("copyclip", &json!({ "handles": handles(&s) })).unwrap();
|
|
|
|
// In the drawing it came from, PASTEORIG is unavailable (greyed in menus).
|
|
assert!(clipboard::from_active(&s));
|
|
assert!(s.execute("pasteorig", &json!({})).is_err());
|
|
assert!(s.start("pasteorig").is_err());
|
|
assert_eq!(s.doc().unwrap().model.len(), 2);
|
|
let info = cadcraft_engine::find_command("pasteorig").unwrap().info(&s);
|
|
assert!(!info.enabled);
|
|
|
|
// In another drawing it pastes at the same coordinates without asking.
|
|
s.active = b;
|
|
s.start("pasteorig").unwrap();
|
|
assert!(s.running.is_none());
|
|
let pasted = model(&s);
|
|
assert!(matches!(&pasted[0].kind, EntityKind::Line(l) if l.a.x == 10.0 && l.b.x == 30.0 && l.a.y == 20.0));
|
|
assert!(s.doc().unwrap().block("DOOR").is_some());
|
|
|
|
// PASTECLIP asks for the insertion point and places the base point there.
|
|
s.start("pasteclip").unwrap();
|
|
assert!(s.prompt_text().contains("insertion point"), "{}", s.prompt_text());
|
|
s.input(cadcraft_engine::Input::Point(Vec2::new(0.0, 0.0))).unwrap();
|
|
assert!(s.running.is_none());
|
|
let pasted = model(&s);
|
|
assert_eq!(pasted.len(), 4);
|
|
assert!(matches!(&pasted[2].kind, EntityKind::Line(l) if l.a.x == 0.0 && l.a.y == 0.0));
|
|
}
|
|
|
|
#[test]
|
|
fn pasteblock_makes_a_block_of_the_clipboard() {
|
|
let mut s = Session::empty();
|
|
let a = s.open_drawing(drawing_a(), "A", None);
|
|
let b = s.open_drawing(drawing_b(), "B", None);
|
|
s.active = a;
|
|
s.execute("copyclip", &json!({ "handles": handles(&s) })).unwrap();
|
|
s.active = b;
|
|
let r = s.execute("pasteblock", &json!({ "at": [0, 0] })).unwrap();
|
|
let name = r["block"].as_str().unwrap().to_string();
|
|
assert!(name.starts_with("A$C"), "{name}");
|
|
let d = s.doc().unwrap();
|
|
assert_eq!(d.model.len(), 1);
|
|
let blk = d.block(&name).unwrap();
|
|
assert_eq!(blk.entities.len(), 2);
|
|
assert_eq!(blk.base, Vec3::new(10.0, 20.0, 0.0), "the clipboard's base point");
|
|
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, "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);
|
|
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 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();
|
|
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);
|
|
|
|
#[test]
|
|
fn clipboard_travels_as_text() {
|
|
set_io(IoHooks {
|
|
read: |bytes, _| {
|
|
if bytes.starts_with(b"0\nSECTION") {
|
|
WRITTEN.lock().unwrap().clone().ok_or_else(|| "nothing written".into())
|
|
} else {
|
|
Err("not DXF".into())
|
|
}
|
|
},
|
|
write: |d, name| {
|
|
assert!(name.ends_with(".dxf"));
|
|
*WRITTEN.lock().unwrap() = Some(d.clone());
|
|
Ok(b"0\nSECTION\n2\nHEADER\n0\nENDSEC\n0\nEOF\n".to_vec())
|
|
},
|
|
plot: None,
|
|
});
|
|
let mut s = Session::empty();
|
|
let a = s.open_drawing(drawing_a(), "A", None);
|
|
s.execute("copybase", &json!({ "handles": handles(&s), "base": [10, 0] })).unwrap();
|
|
let text = clipboard::system_text(&s).expect("DXF text");
|
|
let written = WRITTEN.lock().unwrap().clone().unwrap();
|
|
assert_eq!(written.model.len(), 2);
|
|
assert!(written.block("DOOR").is_some() && written.layer("HINGE").is_some());
|
|
assert_eq!(written.header.point("INSBASE"), Some(Vec3::new(10.0, 0.0, 0.0)));
|
|
|
|
// Another CADCraft (here: a fresh session) pastes it, with its layers and blocks.
|
|
let mut t = Session::empty();
|
|
t.open_drawing(drawing_b(), "B", None);
|
|
assert!(!clipboard::load_system_text(&mut t, "just some words"));
|
|
assert!(t.clipboard.is_empty());
|
|
assert!(clipboard::load_system_text(&mut t, &text));
|
|
assert_eq!(t.clipboard.len(), 2);
|
|
assert_eq!(t.clipboard_base, Vec2::new(10.0, 0.0));
|
|
t.execute("pasteorig", &json!({})).unwrap();
|
|
assert_eq!(t.doc().unwrap().model.len(), 2);
|
|
assert!(t.doc().unwrap().block("DOOR").is_some() && t.doc().unwrap().layer("HINGE").is_some());
|
|
let _ = a;
|
|
}
|