Files
cadcraft/crates/engine/tests/it/constraint_infer.rs
T
aiongg 9cf115e808 CI: one test binary per crate, nextest, line tables only (#1989)
* Build: line tables only in dev, no debug info for dependencies

* Tests: one integration-test binary per crate

Each crate's tests/*.rs becomes a module of tests/it/main.rs. Files that
install process-wide state (the file io hooks, user aliases, SAVETIME and
SAVEFILEPATH, user linetype and pattern files, registered fonts) stay their
own test binaries.

* xtask ci: nextest, tests without examples, one wasm check call
2026-10-23 13:34:26 -04:00

130 lines
6.0 KiB
Rust

//! CONSTRAINTINFER: constraints inferred for what a drawing or editing command places with
//! object snaps, ortho or polar (#546).
use cadcraft_doc::{ConstraintKind as K, EntityKind, Handle, Sub};
use cadcraft_engine::Session;
use serde_json::json;
/// A session with a circle at 20,20 radius 3 (not constrained) and CONSTRAINTINFER on.
fn session() -> Session {
let mut s = Session::new();
s.script("CIRCLE 20,20 3\n").unwrap();
s.cmdline("constraintinfer 1").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(s.execute("getvar", &json!({ "name": "CONSTRAINTINFER" })).unwrap()["value"], json!(1));
s
}
fn kinds(s: &Session) -> Vec<K> {
s.doc().unwrap().constraints.iter().map(|c| c.kind).collect()
}
fn count(s: &Session, k: K) -> usize {
kinds(s).iter().filter(|x| **x == k).count()
}
fn lines(s: &Session) -> Vec<Handle> {
s.doc().unwrap().model.iter().filter(|e| matches!(e.kind, EntityKind::Line(_))).map(|e| e.handle).collect()
}
fn circle(s: &Session) -> Handle {
s.doc().unwrap().model.iter().find(|e| matches!(e.kind, EntityKind::Circle(_))).map(|e| e.handle).unwrap()
}
#[test]
fn off_by_default_and_settable_by_setvar() {
let mut s = Session::new();
s.script("LINE 0,0 10,0 10,5\n\n").unwrap();
assert!(kinds(&s).is_empty());
assert_eq!(s.execute("getvar", &json!({ "name": "constraintinfer" })).unwrap()["value"], json!(0));
s.execute("setvar", &json!({ "name": "CONSTRAINTINFER", "value": 1 })).unwrap();
assert!(s.doc().unwrap().parametric.settings.infer);
assert!(s.execute("setvar", &json!({ "name": "CONSTRAINTINFER", "value": "yes" })).is_err());
for bad in [json!({ "value": 2 }), json!({ "value": "x" }), json!({ "value": [] }), json!({ "value": f64::MAX })] {
assert!(s.execute("constraintinfer", &bad).is_err(), "{bad}");
}
assert_eq!(s.execute("constraintinfer", &json!({})).unwrap()["value"], json!(1));
s.execute("constraintinfer", &json!({ "value": false })).unwrap();
assert!(!s.doc().unwrap().parametric.settings.infer);
// At the command line: Enter keeps the value, a bad one re-prompts.
s.cmdline("constraintinfer").unwrap();
assert!(s.prompt_text().contains("CONSTRAINTINFER"), "{}", s.prompt_text());
s.cmdline("7").unwrap();
assert!(s.running.is_some());
s.cmdline("1").unwrap();
assert!(s.running.is_none() && s.doc().unwrap().parametric.settings.infer);
}
#[test]
fn line_segments_join_and_ortho_directions_are_kept() {
let mut s = session();
s.script("LINE 0,0 10,0 10,5\n\n").unwrap();
// The joint, and the horizontal and vertical segments.
assert_eq!(count(&s, K::Coincident), 1, "{:?}", kinds(&s));
assert_eq!(count(&s, K::Horizontal), 1, "{:?}", kinds(&s));
assert_eq!(count(&s, K::Vertical), 1, "{:?}", kinds(&s));
// A sloped line drawn exactly parallel to another (a parallel snap) is kept parallel.
s.script("LINE 0,10 6,13\n\nLINE 0,15 @6,3\n\n").unwrap();
assert_eq!(count(&s, K::Parallel), 1, "{:?}", kinds(&s));
// Hand-placed points that touch nothing infer nothing.
let before = kinds(&s).len();
s.script("LINE 30.3,0.2 37.1,3.3\n\n").unwrap();
assert_eq!(kinds(&s).len(), before);
// Undo takes the inferred constraints with the line.
s.execute("u", &json!({})).unwrap();
assert_eq!(lines(&s).len(), 4);
assert_eq!(kinds(&s).len(), before);
}
#[test]
fn snapped_points_become_coincident_and_tangent() {
let mut s = session();
let c = circle(&s);
// Centre snap, then an endpoint snap back to the first line.
s.script("LINE 0,0 9,1\n\nLINE CEN 20.05,19.98 END 8.98,1.01\n\n").unwrap();
let d = s.doc().unwrap();
let centre = d.constraints.iter().find(|k| k.refs.iter().any(|r| r.handle == c && r.sub == Sub::Center));
assert!(centre.is_some_and(|k| k.kind == K::Coincident), "{:?}", d.constraints);
let ls = lines(&s);
let joined = d.constraints.iter().any(|k| k.kind == K::Coincident && k.refs.iter().all(|r| ls.contains(&r.handle)));
assert!(joined, "{:?}", d.constraints);
// A tangent snap to the circle: on it and tangent to it.
s.script("LINE 40,20 TAN 20.45,22.97\n\n").unwrap();
let last = *lines(&s).last().unwrap();
let d = s.doc().unwrap();
let with = |k: K| d.constraints.iter().any(|x| x.kind == k && x.refs.iter().any(|r| r.handle == last) && x.refs.iter().any(|r| r.handle == c));
assert!(with(K::Tangent), "{:?}", d.constraints);
// A perpendicular snap onto the first line, ending on it.
s.script("LINE 3,8 PER 3.84,0.43\n\n").unwrap();
let last = *lines(&s).last().unwrap();
let first = *lines(&s).first().unwrap();
let d = s.doc().unwrap();
let pair =
|k: K| d.constraints.iter().any(|x| x.kind == k && x.refs.iter().any(|r| r.handle == last) && x.refs.iter().any(|r| r.handle == first));
assert!(pair(K::Perpendicular) && pair(K::Coincident), "{:?}", d.constraints);
assert_eq!(s.execute("constraints.inspect", &json!({})).unwrap()["satisfied"], json!(true));
}
#[test]
fn editing_with_snaps_infers_and_constraint_commands_do_not() {
let mut s = session();
s.script("LINE 0,0 10,0\n\nLINE 30,7 34,9\n\n").unwrap();
let ls = lines(&s);
let n0 = kinds(&s).len();
// MOVE the second line's start exactly onto the first line's end.
s.execute("move", &json!({ "handles": [ls[1].hex()], "from": [30, 7], "to": [10, 0] })).unwrap();
let d = s.doc().unwrap();
assert!(d.constraints.iter().skip(n0).any(|k| k.kind == K::Coincident), "{:?}", d.constraints);
// A constraint command adds only its own constraint.
let n = kinds(&s).len();
s.execute("gcequal", &json!({ "h1": ls[0].hex(), "h2": ls[1].hex() })).unwrap();
assert_eq!(kinds(&s).len(), n + 1);
// Many objects at once (an array) are not inferred.
let n = kinds(&s).len();
let before = s.doc().unwrap().model.len();
s.execute("arrayrect", &json!({ "handles": [ls[0].hex()], "rows": 1, "cols": 60, "rowSpacing": 1, "colSpacing": 0.5, "associative": false }))
.unwrap();
assert!(s.doc().unwrap().model.len() > before + 50);
assert_eq!(kinds(&s).len(), n);
}