Files
cadcraft/crates/engine/tests/dim_commands.rs

260 lines
14 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! DIMEDIT (Home/New/Rotate/Oblique), DIMJOGGED, DIMJOGLINE, DIMBREAK and DIMINSPECT: the JSON
//! forms, the prompt sequences and what they draw.
use cadcraft_engine::Session;
use cadcraft_engine::doc::{Dimension, EntityKind, Handle};
use cadcraft_engine::geom::Vec2;
use cadcraft_engine::render::{DimGeometry, LineRole};
use serde_json::{Value, json};
fn dim_of(s: &Session, h: &str) -> (Dimension, DimGeometry) {
let d = s.doc().unwrap();
match &d.entity(Handle::parse_hex(h).unwrap()).unwrap().kind {
EntityKind::Dimension(dm) => (dm.clone(), cadcraft_engine::render::dimension_in(d, dm)),
k => panic!("not a dimension: {k:?}"),
}
}
fn handle(r: &Value) -> String {
r["handle"].as_str().unwrap().to_string()
}
fn linear(s: &mut Session) -> String {
handle(&s.execute("dimlinear", &json!({ "p1": [0, 0], "p2": [10, 0], "at": [5, 3] })).unwrap())
}
/// Whether a line of the role on y = `y` covers `x`.
fn covers(g: &DimGeometry, role: LineRole, y: f64, x: f64) -> bool {
g.lines_of(role)
.any(|l| l.windows(2).any(|w| (w[0].y - y).abs() < 1e-9 && (w[1].y - y).abs() < 1e-9 && w[0].x.min(w[1].x) < x && x < w[0].x.max(w[1].x)))
}
#[test]
fn dimedit_modes() {
let mut s = Session::new();
let h = linear(&mut s);
s.execute("dimedit", &json!({ "handles": [h], "mode": "new", "text": "<> TYP" })).unwrap();
assert_eq!(dim_of(&s, &h).1.value, "10.0000 TYP");
s.execute("dimedit", &json!({ "handles": [h], "mode": "rotate", "angle": 30 })).unwrap();
assert!((dim_of(&s, &h).1.text_angle - 30f64.to_radians()).abs() < 1e-9);
s.execute("dimedit", &json!({ "handles": [h], "mode": "home" })).unwrap();
let (dm, g) = dim_of(&s, &h);
assert!(dm.text_rotation == 0.0 && !dm.user_text_pos && g.text_angle == 0.0);
// Oblique: extension lines at 60°, the measurement unchanged.
s.execute("dimedit", &json!({ "handles": [h], "mode": "oblique", "angle": 60 })).unwrap();
let (dm, g) = dim_of(&s, &h);
assert!((dm.marks.oblique.unwrap() - 60f64.to_radians()).abs() < 1e-9);
assert_eq!(g.value, "10.0000 TYP");
for l in g.lines_of(LineRole::Ext) {
let d = l[1] - l[0];
assert!((d.y.atan2(d.x).to_degrees() - 60.0).abs() < 1e-6, "{l:?}");
}
// The dimension line still runs along y = 3, between the oblique feet.
let foot = 3.0 / 60f64.to_radians().tan();
assert!(covers(&g, LineRole::Dim, 3.0, foot + 0.5) && covers(&g, LineRole::Dim, 3.0, 10.0 + foot - 0.5));
// No angle: back to perpendicular. Other kinds are left alone.
s.execute("dimedit", &json!({ "handles": [h], "mode": "oblique" })).unwrap();
assert_eq!(dim_of(&s, &h).0.marks.oblique, None);
s.execute("circle", &json!({ "center": [30, 0], "radius": 5 })).unwrap();
let c = s.doc().unwrap().model.last().unwrap().handle.hex();
let r = handle(&s.execute("dimradius", &json!({ "handle": c })).unwrap());
let n = s.execute("dimedit", &json!({ "handles": [r], "mode": "oblique", "angle": 60 })).unwrap();
assert_eq!(n["changed"], json!(0));
assert!(s.execute("dimedit", &json!({ "handles": [h], "mode": "bogus" })).is_err());
assert!(s.execute("dimedit", &json!({ "handles": [h], "mode": "rotate" })).is_err());
assert!(s.execute("dimedit", &json!({ "handles": [h], "mode": "rotate", "angle": "x" })).is_err());
}
#[test]
fn dimedit_prompts() {
let mut s = Session::new();
let h = linear(&mut s);
s.cmdline("dimedit").unwrap();
assert!(s.prompt_text().contains("[Home/New/Rotate/Oblique] <Home>"), "{}", s.prompt_text());
// Oblique: the dimensions first, then the angle.
s.script("o\n4,3\n\n45\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert!((dim_of(&s, &h).0.marks.oblique.unwrap() - 45f64.to_radians()).abs() < 1e-9);
// Rotate: the angle first.
s.script("dimedit r 15\n4,3\n\n").unwrap();
assert!(s.running.is_none());
assert!((dim_of(&s, &h).0.text_rotation - 15f64.to_radians()).abs() < 1e-9);
// Home by default.
s.script("dimedit\n\n4,3\n\n").unwrap();
assert_eq!(dim_of(&s, &h).0.text_rotation, 0.0);
// The Dimension ▸ Oblique menu item asks for the dimensions straight away.
s.cmdline("dimedit.oblique").unwrap();
assert!(s.prompt_text().contains("Select objects"), "{}", s.prompt_text());
s.script("4,3\n\n\n").unwrap();
assert_eq!(dim_of(&s, &h).0.marks.oblique, None);
}
#[test]
fn jogged_radius() {
let mut s = Session::new();
s.execute("circle", &json!({ "center": [0, 0], "radius": 50 })).unwrap();
let c = s.doc().unwrap().model.last().unwrap().handle.hex();
let r = s.execute("dimjogged", &json!({ "handle": c, "centerOverride": [20, -10], "at": [40, 40], "jog": [30, 15] })).unwrap();
let h = handle(&r);
let (dm, g) = dim_of(&s, &h);
let j = dm.marks.jogged.clone().unwrap();
assert_eq!(g.value, "R50.0000", "the true radius");
assert!(j.center.xy().dist(Vec2::new(20.0, -10.0)) < 1e-9);
// The line runs from the arrowhead on the circle to the override, with a 45° jog (DIMJOGANG).
let tip = Vec2::new(1.0, 1.0).normalized() * 50.0;
let path = g.lines_of(LineRole::Dim).find(|l| l.len() == 4).unwrap().clone();
assert!(path[0].dist(tip) < 1e-9 && path[3].dist(Vec2::new(20.0, -10.0)) < 1e-9, "{path:?}");
let u = tip.normalized();
let jog = path[2] - path[1];
let angle = jog.normalized().dot(u).abs().acos().to_degrees();
assert!((angle - 45.0).abs() < 1e-6, "{angle}");
assert!((path[3] - path[2]).normalized().cross(u).abs() < 1e-9, "parallel to the first part");
// DIMJOGANG changes the jog.
s.execute("setvar", &json!({ "name": "DIMJOGANG", "value": 60 })).unwrap();
let h2 = handle(&s.execute("dimjogged", &json!({ "handle": c, "centerOverride": [20, -10], "at": [40, 40] })).unwrap());
let g = dim_of(&s, &h2).1;
let path = g.lines_of(LineRole::Dim).find(|l| l.len() == 4).unwrap().clone();
assert!(((path[2] - path[1]).normalized().dot(u).abs().acos().to_degrees() - 60.0).abs() < 1e-6);
// At the command line.
s.script("dimjogged 0,50\n10,-20\n0,70\n5,20\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
let last = s.doc().unwrap().model.last().unwrap().handle.hex();
let (dm, _) = dim_of(&s, &last);
let j = dm.marks.jogged.unwrap();
assert!(j.center.xy().dist(Vec2::new(10.0, -20.0)) < 1e-9 && j.jog.xy().dist(Vec2::new(5.0, 20.0)) < 1e-9);
assert!(dm.p15.xy().dist(Vec2::new(0.0, 50.0)) < 1e-9);
// Not a circle: refused.
s.execute("line", &json!({ "points": [[100, 0], [110, 0]] })).unwrap();
let l = s.doc().unwrap().model.last().unwrap().handle.hex();
assert!(s.execute("dimjogged", &json!({ "handle": l, "centerOverride": [0, 0], "at": [1, 1] })).is_err());
}
#[test]
fn jogged_linear() {
let mut s = Session::new();
let h = linear(&mut s);
s.execute("dimjogline", &json!({ "handle": h, "at": [2, 3] })).unwrap();
let (dm, g) = dim_of(&s, &h);
assert!(dm.marks.jog_line.is_some());
// The line is cut around the jog and a zigzag drawn there (1.5 × the text height).
assert!(!covers(&g, LineRole::Dim, 3.0, 2.0));
let zig = g.lines_of(LineRole::Dim).find(|l| l.len() == 4).unwrap();
let h_jog = zig.iter().map(|p| p.y).fold(f64::MIN, f64::max) - zig.iter().map(|p| p.y).fold(f64::MAX, f64::min);
assert!((h_jog - 1.5 * 0.18).abs() < 1e-9, "{h_jog}");
// Default place: halfway between the first extension line and the text.
s.execute("dimjogline", &json!({ "handle": h })).unwrap();
assert!(dim_of(&s, &h).0.marks.jog_line.unwrap().xy().dist(Vec2::new(2.5, 3.0)) < 1e-9);
s.execute("dimjogline", &json!({ "handle": h, "remove": true })).unwrap();
assert_eq!(dim_of(&s, &h).0.marks.jog_line, None);
// Prompts: pick the dimension, then the jog location; Remove picks the jog to remove.
s.script("dimjogline 1,3\n8,3\n").unwrap();
assert!(s.running.is_none());
assert!(dim_of(&s, &h).0.marks.jog_line.unwrap().xy().dist(Vec2::new(8.0, 3.0)) < 1e-9);
s.script("dimjogline r\n1,3\n").unwrap();
assert!(s.running.is_none());
assert_eq!(dim_of(&s, &h).0.marks.jog_line, None);
// Radial dimensions take no jog.
s.execute("circle", &json!({ "center": [30, 0], "radius": 5 })).unwrap();
let c = s.doc().unwrap().model.last().unwrap().handle.hex();
let r = handle(&s.execute("dimradius", &json!({ "handle": c })).unwrap());
assert!(s.execute("dimjogline", &json!({ "handle": r })).is_err());
}
#[test]
fn breaks() {
let mut s = Session::new();
let h = linear(&mut s);
// A line crossing the dimension line and the second extension line.
s.execute("line", &json!({ "points": [[4, -1], [4, 6]] })).unwrap();
s.execute("line", &json!({ "points": [[8, 1], [12, 1]] })).unwrap();
s.execute("dimbreak", &json!({ "handles": [h] })).unwrap();
let (dm, g) = dim_of(&s, &h);
assert_eq!(dm.marks.breaks.len(), 2, "{:?}", dm.marks.breaks);
assert!(dm.marks.breaks.iter().all(|b| b.auto && (b.size - 0.125).abs() < 1e-12));
assert!(!covers(&g, LineRole::Dim, 3.0, 4.0) && covers(&g, LineRole::Dim, 3.0, 3.8));
let ext_at = |g: &DimGeometry, y: f64| {
g.lines_of(LineRole::Ext).any(|l| l.windows(2).any(|w| (w[0].x - 10.0).abs() < 1e-9 && w[0].y.min(w[1].y) < y && y < w[0].y.max(w[1].y)))
};
assert!(!ext_at(&g, 1.0) && ext_at(&g, 2.0), "extension line 2 broken at y = 1");
// Auto again doesn't pile up; only the chosen objects; manual; remove.
let first = s.doc().unwrap().model.iter().nth(1).unwrap().handle.hex();
s.execute("dimbreak", &json!({ "handles": [h], "objects": [first], "size": 0.5 })).unwrap();
assert_eq!(dim_of(&s, &h).0.marks.breaks.len(), 1);
s.execute("dimbreak", &json!({ "handles": [h], "mode": "manual", "first": [6, 3], "second": [7, 3] })).unwrap();
let (dm, g) = dim_of(&s, &h);
assert_eq!(dm.marks.breaks.len(), 2);
assert!(!covers(&g, LineRole::Dim, 3.0, 6.5) && covers(&g, LineRole::Dim, 3.0, 7.2));
s.execute("dimbreak", &json!({ "handles": [h], "mode": "remove" })).unwrap();
assert!(dim_of(&s, &h).0.marks.breaks.is_empty());
// Prompts: the dimension, then objects (Enter ends) or Auto/Manual/Remove.
s.script("dimbreak 1,3\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(dim_of(&s, &h).0.marks.breaks.len(), 2);
s.script("dimbreak 1,3 r\n").unwrap();
assert!(dim_of(&s, &h).0.marks.breaks.is_empty());
s.script("dimbreak 1,3 m 1,3 2,3\n").unwrap();
assert_eq!(dim_of(&s, &h).0.marks.breaks.len(), 1);
// Bad input.
assert!(s.execute("dimbreak", &json!({ "handles": [h], "size": -1 })).is_err());
assert!(s.execute("dimbreak", &json!({ "handles": [h], "mode": "manual", "first": [1, 1], "second": [1, 1] })).is_err());
assert!(s.execute("dimbreak", &json!({ "handles": [h], "mode": "nope" })).is_err());
}
#[test]
fn inspection_dimensions() {
let mut s = Session::new();
let h = linear(&mut s);
s.execute("diminspect", &json!({ "handles": [h], "label": "A", "rate": "50%" })).unwrap();
let (dm, g) = dim_of(&s, &h);
assert_eq!(dm.marks.inspect.as_ref().unwrap().shape, 0);
assert_eq!(g.value, "A 10.0000 50%");
// The frame: two long sides, two separators, two round ends.
let plain = {
let mut p = dm.clone();
p.marks.inspect = None;
cadcraft_engine::render::dimension_in(s.doc().unwrap(), &p)
};
assert_eq!(g.lines_of(LineRole::Dim).count(), plain.lines_of(LineRole::Dim).count() + 6);
s.execute("diminspect", &json!({ "handles": [h], "shape": "none", "label": "", "rate": "" })).unwrap();
let (_, g) = dim_of(&s, &h);
assert_eq!(g.value, "10.0000");
assert_eq!(g.lines_of(LineRole::Dim).count(), plain.lines_of(LineRole::Dim).count());
s.execute("diminspect", &json!({ "handles": [h], "remove": true })).unwrap();
assert_eq!(dim_of(&s, &h).0.marks.inspect, None);
assert!(s.execute("diminspect", &json!({ "handles": [h], "shape": "oval" })).is_err());
// Prompts: dimensions, shape, label, rate.
s.script("diminspect 1,3\n\na\nQC\n25%\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
let ins = dim_of(&s, &h).0.marks.inspect.unwrap();
assert_eq!((ins.shape, ins.label.as_str(), ins.rate.as_str()), (1, "QC", "25%"));
s.script("diminspect 1,3\n\nremove\n").unwrap();
assert_eq!(dim_of(&s, &h).0.marks.inspect, None);
}
#[test]
fn marks_follow_move_and_rotate_and_hostile_values_do_not_panic() {
let mut s = Session::new();
let h = linear(&mut s);
s.execute("dimjogline", &json!({ "handle": h, "at": [2, 3] })).unwrap();
s.execute("dimedit", &json!({ "handles": [h], "mode": "oblique", "angle": 60 })).unwrap();
s.execute("move", &json!({ "handles": [h], "from": [0, 0], "to": [10, 5] })).unwrap();
let (dm, _) = dim_of(&s, &h);
assert!(dm.marks.jog_line.unwrap().xy().dist(Vec2::new(12.0, 8.0)) < 1e-9);
s.execute("rotate", &json!({ "handles": [h], "base": [10, 5], "angle": 90 })).unwrap();
let (dm, _) = dim_of(&s, &h);
assert!((dm.marks.oblique.unwrap() - 150f64.to_radians()).abs() < 1e-9);
// Damaged marks still draw.
let d = s.doc_mut().unwrap();
let hh = Handle::parse_hex(&h).unwrap();
d.modify_entity(hh, |e| {
if let EntityKind::Dimension(dm) = &mut e.kind {
dm.marks.oblique = Some(f64::NAN);
dm.marks.jog_line = Some(cadcraft_engine::geom::Vec3::new(f64::INFINITY, 0.0, 0.0));
dm.marks.breaks.push(cadcraft_engine::doc::DimBreak { at: Default::default(), size: f64::NAN, auto: false });
dm.marks.inspect = Some(cadcraft_engine::doc::Inspection { shape: 9, label: "x".into(), rate: String::new() });
}
})
.unwrap();
let _ = dim_of(&s, &h);
}