mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-26 03:46:53 -06:00
Squash of PR #572 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
8.8 KiB
Rust
197 lines
8.8 KiB
Rust
//! COMPARE: differences between the drawing and another one, matched by geometry and by handle;
|
|
//! next/previous areas; COMPARECLOSE (#563).
|
|
|
|
use std::sync::Arc;
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::cmd::file::{IoHooks, set_io};
|
|
use cadcraft_engine::doc::geom::Vec3;
|
|
use cadcraft_engine::doc::{Block, Common, Drawing, Entity, EntityKind, Insert, Line, Space};
|
|
use serde_json::{Value, json};
|
|
|
|
fn line(ax: f64, ay: f64, bx: f64, by: f64) -> EntityKind {
|
|
EntityKind::Line(Line { a: Vec3::new(ax, ay, 0.0), b: Vec3::new(bx, by, 0.0) })
|
|
}
|
|
|
|
/// A drawing with a line, a circle and a text; returns the session (the drawing is "Drawing1").
|
|
fn base() -> Session {
|
|
let mut s = Session::new();
|
|
s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap();
|
|
s.execute("circle", &json!({"center": [5, 5], "radius": 2})).unwrap();
|
|
s.execute("text", &json!({"at": [0, 10], "height": 1, "text": "Note"})).unwrap();
|
|
s
|
|
}
|
|
|
|
fn handle_of(s: &Session, ty: &str) -> String {
|
|
s.doc().unwrap().model.iter().find(|e| e.kind.type_name() == ty).unwrap().handle.hex()
|
|
}
|
|
|
|
fn counts(r: &Value) -> (u64, u64, u64, u64) {
|
|
let c = &r["counts"];
|
|
(c["added"].as_u64().unwrap(), c["removed"].as_u64().unwrap(), c["changed"].as_u64().unwrap(), c["common"].as_u64().unwrap())
|
|
}
|
|
|
|
/// Open a copy of the drawing as "B" and edit it: the circle moves, the text goes, an arc comes.
|
|
fn edited_copy(s: &mut Session) {
|
|
let d: Drawing = s.doc().unwrap().clone();
|
|
s.open_drawing(d, "B", None);
|
|
let circle = handle_of(s, "Circle");
|
|
let text = handle_of(s, "Text");
|
|
s.execute("move", &json!({"handles": [circle], "from": [0, 0], "to": [3, 0]})).unwrap();
|
|
s.execute("erase", &json!({"handles": [text]})).unwrap();
|
|
s.execute("arc", &json!({"center": [20, 0], "radius": 1, "start": 0, "end": 90})).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn finds_added_removed_and_changed_objects() {
|
|
let mut s = base();
|
|
edited_copy(&mut s);
|
|
let r = s.execute("compare", &json!({"document": "drawing1"})).unwrap();
|
|
assert_eq!(counts(&r), (1, 1, 1, 1), "{r}");
|
|
assert_eq!(r["added"][0]["type"], "Arc", "{r}");
|
|
assert_eq!(r["removed"][0]["type"], "Text", "{r}");
|
|
assert_eq!((r["changed"][0]["type"].as_str(), r["changed"][0]["what"].as_str()), (Some("Circle"), Some("geometry")), "{r}");
|
|
assert_eq!(r["message"], "Compared with Drawing1: 1 only in this drawing, 1 only in Drawing1, 1 changed, 1 the same.");
|
|
let c = s.compare.as_ref().unwrap();
|
|
assert_eq!((c.added.len(), c.removed.len(), c.changed.len(), c.space.clone()), (1, 1, 1, Space::Model));
|
|
assert!(!c.regions.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn matches_objects_by_geometry_when_handles_differ() {
|
|
let mut s = base();
|
|
// The same objects written by another program: every handle is new.
|
|
let mut d = Drawing::new_imperial();
|
|
d.handseed = 0x5000;
|
|
for e in s.doc().unwrap().model.iter() {
|
|
d.add(&Space::Model, e.common.clone(), e.kind.clone()).unwrap();
|
|
}
|
|
d.add(&Space::Model, Common::default(), line(0.0, 0.0, 0.0, 10.0)).unwrap();
|
|
let theirs: Vec<_> = d.model.iter().map(|e| e.handle).collect();
|
|
assert!(s.doc().unwrap().model.iter().all(|e| !theirs.contains(&e.handle)), "no handle in common");
|
|
s.open_drawing(d, "Other", None);
|
|
let r = s.execute("compare", &json!({"document": 0})).unwrap();
|
|
assert_eq!(counts(&r), (1, 0, 0, 3), "only the extra line differs: {r}");
|
|
// A colour change on the same handle is a property change.
|
|
let mut s = base();
|
|
let d: Drawing = s.doc().unwrap().clone();
|
|
s.open_drawing(d, "B", None);
|
|
let l = handle_of(&s, "Line");
|
|
s.execute("properties.set", &json!({"handles": [l], "color": "red"})).unwrap();
|
|
let r = s.execute("compare", &json!({"document": 0})).unwrap();
|
|
assert_eq!(counts(&r), (0, 0, 1, 2), "{r}");
|
|
assert_eq!(r["changed"][0]["what"], "properties", "{r}");
|
|
}
|
|
|
|
#[test]
|
|
fn navigates_areas_and_closes() {
|
|
let mut s = base();
|
|
edited_copy(&mut s);
|
|
assert!(s.execute("compare.next", &json!({})).is_err(), "no comparison yet");
|
|
s.execute("compare", &json!({"document": "Drawing1"})).unwrap();
|
|
let n = s.compare.as_ref().unwrap().regions.len() as u64;
|
|
let before = s.state().unwrap().view();
|
|
let r = s.execute("compare.next", &json!({})).unwrap();
|
|
assert_eq!((r["region"].as_u64(), r["of"].as_u64()), (Some(0), Some(n)), "{r}");
|
|
assert_ne!(s.state().unwrap().view(), before, "the view zoomed to the area");
|
|
let r = s.execute("compare.previous", &json!({})).unwrap();
|
|
assert_eq!(r["region"].as_u64(), Some(n - 1), "previous wraps around: {r}");
|
|
// The comparison belongs to the drawing it was made in.
|
|
s.execute("document.switch", &json!({"index": 0})).unwrap();
|
|
assert!(s.execute("compare.next", &json!({})).is_err());
|
|
s.execute("document.switch", &json!({"index": 1})).unwrap();
|
|
assert_eq!(s.execute("compareclose", &json!({})).unwrap()["closed"], true);
|
|
assert!(s.compare.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn compares_a_file_read_through_the_io_hooks() {
|
|
// Test file format: the bytes are "x" — a drawing holding a line from 0,0 to x,0.
|
|
fn read(bytes: &[u8], _name: &str) -> Result<Drawing, String> {
|
|
let x: f64 = String::from_utf8_lossy(bytes).trim().parse().map_err(|_| "bad test file")?;
|
|
let mut d = Drawing::new_imperial();
|
|
d.add(&Space::Model, Common::default(), line(0.0, 0.0, x, 0.0)).map_err(|e| e.to_string())?;
|
|
Ok(d)
|
|
}
|
|
set_io(IoHooks { read, write: |_, _| Err("no writer in this test".into()), plot: None });
|
|
let dir = std::env::temp_dir().join(format!("cadcraft-compare-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join("other.dxf");
|
|
std::fs::write(&path, "10").unwrap();
|
|
let mut s = base();
|
|
let r = s.execute("compare", &json!({"path": path.to_string_lossy()})).unwrap();
|
|
assert_eq!(counts(&r), (2, 0, 0, 1), "the circle and the text are only here: {r}");
|
|
// The same through the command line.
|
|
s.cmdline("compare").unwrap();
|
|
s.cmdline(&path.to_string_lossy()).unwrap();
|
|
assert!(s.log.last().unwrap().starts_with("Compared with other.dxf: 2 only in this drawing"), "{:?}", s.log.last());
|
|
let r = s.execute("compare", &json!({"data": "MTA=", "name": "ten.dxf"})).unwrap();
|
|
assert_eq!(r["other"], "ten.dxf");
|
|
assert!(s.execute("compare", &json!({"path": dir.join("missing.dxf").to_string_lossy()})).is_err());
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_inputs_are_errors() {
|
|
let mut s = base();
|
|
for p in
|
|
[json!({}), json!({"data": "%%%"}), json!({"document": 99}), json!({"document": "nope"}), json!({"document": 0}), json!({"document": true})]
|
|
{
|
|
assert!(s.execute("compare", &p).is_err(), "{p}");
|
|
}
|
|
// Self-referencing blocks in both drawings: signatures stay bounded.
|
|
let mut b = Block::new("LOOP");
|
|
let ins = Insert {
|
|
block: "LOOP".into(),
|
|
insert: Vec3::ZERO,
|
|
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,
|
|
};
|
|
b.entities.push(Entity::new(cadcraft_engine::doc::Handle(0x900), EntityKind::Insert(ins.clone())));
|
|
let d = s.doc_mut().unwrap();
|
|
d.blocks.insert("LOOP".into(), Arc::new(b));
|
|
d.add(&Space::Model, Common::default(), EntityKind::Insert(ins)).unwrap();
|
|
let copy: Drawing = s.doc().unwrap().clone();
|
|
s.open_drawing(copy, "B", None);
|
|
let r = s.execute("compare", &json!({"document": 0})).unwrap();
|
|
assert_eq!(counts(&r), (0, 0, 0, 4), "{r}");
|
|
}
|
|
|
|
/// A drawing written and read back recomputes what dimensions derive from their geometry (the
|
|
/// measurement, the text position): that is not a change.
|
|
#[test]
|
|
fn derived_dimension_fields_are_not_differences() {
|
|
use cadcraft_engine::doc::{DimKind, Dimension};
|
|
let dim = |measurement: f64, text_x: f64| Dimension {
|
|
kind: DimKind::Linear { rotation: 0.0 },
|
|
defpt: Vec3::new(15.0, 5.0, 0.0),
|
|
text_mid: Vec3::new(text_x, 5.0, 0.0),
|
|
p13: Vec3::ZERO,
|
|
p14: Vec3::new(30.0, 0.0, 0.0),
|
|
p15: Vec3::ZERO,
|
|
p16: Vec3::ZERO,
|
|
text: String::new(),
|
|
style: "Standard".into(),
|
|
measurement,
|
|
text_rotation: 0.0,
|
|
user_text_pos: false,
|
|
block: None,
|
|
overrides: Default::default(),
|
|
assoc: Vec::new(),
|
|
marks: Default::default(),
|
|
};
|
|
let mut s = Session::new();
|
|
s.doc_mut().unwrap().add(&Space::Model, Common::default(), EntityKind::Dimension(dim(30.0, 7.0))).unwrap();
|
|
let mut other = s.doc().unwrap().clone();
|
|
let h = other.model.iter().next().unwrap().handle;
|
|
other.modify_entity(h, |e| e.kind = EntityKind::Dimension(Dimension { block: Some("*D1".into()), ..dim(0.0, 15.0) })).unwrap();
|
|
s.open_drawing(other, "B", None);
|
|
let r = s.execute("compare", &json!({"document": 0})).unwrap();
|
|
assert_eq!(counts(&r), (0, 0, 0, 1), "{r}");
|
|
}
|