mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-27 04:16:48 -06:00
* RECTANG: add a Center Rectangle construction mode (#1127) The first point of RECTANG was always a corner. A new Center option makes it the rectangle's centre, so the second point (or Dimensions/Area) builds it outward evenly: centre 10,10 with a corner at 14,16 gives 6,4 to 14,16. - interactive: a Center keyword at the first prompt (Chamfer keeps the c shortcut; Center matches ce or the full word), which also changes the prompt to Specify center point for rectangle; - JSON: center: true on rectang, keeping p1/p2 and the default corner mode unchanged; - rotation turns about the centre; chamfer, fillet, width, elevation and thickness apply as before; the rubber band previews the centred rectangle; - params documentation updated. Tests: centre-point geometry for p2, dimensions and area, rotation, fillet, chamfer, width, thickness, the interactive keyword and preview, and the unchanged default corner mode. Fixes #1127 * docs: document the functions touched by #1127 * RECTANG: the Center option is CEnter, so C stays Chamfer's shortcut (as PLINE) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
936 lines
44 KiB
Rust
936 lines
44 KiB
Rust
use cadcraft_doc::EntityKind;
|
|
use cadcraft_geom::{Arc, Vec2};
|
|
use serde_json::json;
|
|
|
|
use crate::{Input, Session};
|
|
|
|
fn last(s: &Session) -> EntityKind {
|
|
s.doc().unwrap().model.last().unwrap().kind.clone()
|
|
}
|
|
|
|
fn kinds(s: &Session) -> Vec<&'static str> {
|
|
s.doc().unwrap().model.iter().map(|e| e.kind.type_name()).collect()
|
|
}
|
|
|
|
fn as_arc(k: &EntityKind) -> Arc {
|
|
match k {
|
|
EntityKind::Arc(a) => Arc::new(a.center.xy(), a.radius, a.start, a.end),
|
|
other => panic!("not an arc: {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn as_circle(k: &EntityKind) -> (Vec2, f64) {
|
|
match k {
|
|
EntityKind::Circle(c) => (c.center.xy(), c.radius),
|
|
other => panic!("not a circle: {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn h(r: &serde_json::Value) -> String {
|
|
r["handle"].as_str().unwrap().to_string()
|
|
}
|
|
|
|
fn near(a: Vec2, x: f64, y: f64) -> bool {
|
|
a.near(Vec2::new(x, y), 1e-6)
|
|
}
|
|
|
|
// ---------------- ARC ----------------
|
|
|
|
#[test]
|
|
fn arc_variants_json() {
|
|
let mut s = Session::new();
|
|
let quarter =
|
|
|a: &Arc| near(a.center, 0.0, 0.0) && (a.radius - 1.0).abs() < 1e-9 && near(a.start_point(), 1.0, 0.0) && near(a.end_point(), 0.0, 1.0);
|
|
for (id, p) in [
|
|
("arc.sce", json!({"start": [1, 0], "center": [0, 0], "end": [0, 5]})),
|
|
("arc.sca", json!({"start": [1, 0], "center": [0, 0], "angle": 90})),
|
|
("arc.scl", json!({"start": [1, 0], "center": [0, 0], "length": 2f64.sqrt()})),
|
|
("arc.sea", json!({"start": [1, 0], "end": [0, 1], "angle": 90})),
|
|
("arc.sed", json!({"start": [1, 0], "end": [0, 1], "direction": 90})),
|
|
("arc.ser", json!({"start": [1, 0], "end": [0, 1], "radius": 1})),
|
|
("arc.cse", json!({"center": [0, 0], "start": [1, 0], "end": [0, 3]})),
|
|
("arc.csa", json!({"center": [0, 0], "start": [1, 0], "angle": 90})),
|
|
("arc.csl", json!({"center": [0, 0], "start": [1, 0], "length": 2f64.sqrt()})),
|
|
] {
|
|
s.execute(id, &p).unwrap_or_else(|e| panic!("{id}: {e}"));
|
|
assert!(quarter(&as_arc(&last(&s))), "{id}: {:?}", last(&s));
|
|
}
|
|
// Negative angle: clockwise from the start.
|
|
s.execute("arc.sca", &json!({"start": [1, 0], "center": [0, 0], "angle": -90})).unwrap();
|
|
let a = as_arc(&last(&s));
|
|
assert!(near(a.start_point(), 0.0, -1.0) && near(a.end_point(), 1.0, 0.0));
|
|
// Negative radius / chord: the major arc.
|
|
s.execute("arc.ser", &json!({"start": [1, 0], "end": [0, 1], "radius": -1})).unwrap();
|
|
assert!((as_arc(&last(&s)).sweep() - 1.5 * std::f64::consts::PI).abs() < 1e-9);
|
|
s.execute("arc.scl", &json!({"start": [1, 0], "center": [0, 0], "length": -(2f64.sqrt())})).unwrap();
|
|
assert!((as_arc(&last(&s)).sweep() - 1.5 * std::f64::consts::PI).abs() < 1e-9);
|
|
// Direction as a vector, and bad input.
|
|
s.execute("arc.sed", &json!({"start": [1, 0], "end": [0, 1], "direction": [0, 1]})).unwrap();
|
|
assert!(quarter(&as_arc(&last(&s))));
|
|
assert!(s.execute("arc.ser", &json!({"start": [0, 0], "end": [10, 0], "radius": 1})).is_err());
|
|
assert!(s.execute("arc.scl", &json!({"start": [1, 0], "center": [0, 0], "length": 5})).is_err());
|
|
assert!(s.execute("arc.sea", &json!({"start": [1, 0], "end": [1, 0], "angle": 90})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn arc_variants_command_line() {
|
|
for (cmd, expect_quarter) in [
|
|
("arc.sce 1,0 0,0 0,2", true),
|
|
("arc.sca 1,0 0,0 90", true),
|
|
("arc.scl 1,0 0,0 1.41421356237", true),
|
|
("arc.sea 1,0 0,1 90", true),
|
|
("arc.sed 1,0 0,1 90", true),
|
|
("arc.ser 1,0 0,1 1", true),
|
|
("arc.cse 0,0 1,0 0,4", true),
|
|
("arc.csa 0,0 1,0 90", true),
|
|
("arc.csl 0,0 1,0 1.41421356237", true),
|
|
// Angle/direction given by a point.
|
|
("arc.sed 1,0 0,1 1,5", true),
|
|
("arc.csa 0,0 1,0 0,7", true),
|
|
] {
|
|
let mut s = Session::new();
|
|
s.cmdline(cmd).unwrap();
|
|
assert!(s.running.is_none(), "{cmd} still running: {}", s.prompt_text());
|
|
let a = as_arc(&last(&s));
|
|
if expect_quarter {
|
|
assert!(near(a.center, 0.0, 0.0) && near(a.start_point(), 1.0, 0.0) && near(a.end_point(), 0.0, 1.0), "{cmd}: {a:?}");
|
|
}
|
|
assert_eq!(s.state().unwrap().undo.len(), 1, "{cmd}: one undo step");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arc_continue_from_last_line() {
|
|
let mut s = Session::new();
|
|
// Nothing to continue from: the command ends politely.
|
|
s.cmdline("arc.continue").unwrap();
|
|
assert!(s.running.is_none());
|
|
assert!(s.execute("arc.continue", &json!({"end": [2, 1]})).is_err());
|
|
s.cmdline("line 0,0 1,0").unwrap();
|
|
s.cmdline("").unwrap();
|
|
s.cmdline("arc.continue 2,1").unwrap();
|
|
let a = as_arc(&last(&s));
|
|
assert!(near(a.center, 1.0, 1.0), "{a:?}");
|
|
// JSON continues from that arc's end (2,1) heading +Y.
|
|
s.execute("arc.continue", &json!({"end": [1, 2]})).unwrap();
|
|
let b = as_arc(&last(&s));
|
|
assert!(near(b.center, 1.0, 1.0), "{b:?}");
|
|
assert!(s.execute("arc.continue", &json!({"end": [-5, 2]})).is_err(), "end on the tangent line");
|
|
// Explicit start and direction.
|
|
s.execute("arc.continue", &json!({"start": [0, 0], "direction": 0, "end": [1, 1]})).unwrap();
|
|
assert!(near(as_arc(&last(&s)).center, 0.0, 1.0));
|
|
}
|
|
|
|
#[test]
|
|
fn arc_continue_from_last_picked_end_of_three_point_arc() {
|
|
// (picked points, end of the continuation, its center): both arcs leave their last
|
|
// picked point heading -Y, so the continuation is a quarter turn on that side.
|
|
let cases = [
|
|
// Clockwise: stored CCW, the last picked point (2,0) is the arc's start.
|
|
("arc 0,0 1,1 2,0", "3,-1", (3.0, 0.0)),
|
|
// Counter-clockwise: the last picked point (0,0) is the arc's end.
|
|
("arc 2,0 1,1 0,0", "-1,-1", (-1.0, 0.0)),
|
|
];
|
|
for (draw, end, center) in cases {
|
|
let mut s = Session::new();
|
|
s.cmdline(draw).unwrap();
|
|
s.cmdline("arc").unwrap();
|
|
s.cmdline("").unwrap();
|
|
s.cmdline(end).unwrap();
|
|
let a = as_arc(&last(&s));
|
|
assert!(near(a.center, center.0, center.1), "{draw}: {a:?}");
|
|
assert!((a.sweep() - std::f64::consts::FRAC_PI_2).abs() < 1e-6, "{draw}: reversed tangent {a:?}");
|
|
}
|
|
}
|
|
|
|
// ---------------- CIRCLE ----------------
|
|
|
|
#[test]
|
|
fn circle_variants_json_and_command_line() {
|
|
let mut s = Session::new();
|
|
s.execute("circle.cd", &json!({"center": [1, 1], "diameter": 4})).unwrap();
|
|
assert_eq!(as_circle(&last(&s)).1, 2.0);
|
|
s.execute("circle.2p", &json!({"p1": [0, 0], "p2": [6, 0]})).unwrap();
|
|
let (c, r) = as_circle(&last(&s));
|
|
assert!(near(c, 3.0, 0.0) && (r - 3.0).abs() < 1e-9);
|
|
s.execute("circle.3p", &json!({"p1": [1, 0], "p2": [0, 1], "p3": [-1, 0]})).unwrap();
|
|
let (c, r) = as_circle(&last(&s));
|
|
assert!(near(c, 0.0, 0.0) && (r - 1.0).abs() < 1e-9);
|
|
assert!(s.execute("circle.3p", &json!({"p1": [0, 0], "p2": [1, 0], "p3": [2, 0]})).is_err());
|
|
assert!(s.execute("circle.cd", &json!({"center": [0, 0], "diameter": 0})).is_err());
|
|
|
|
s.cmdline("circle.cd 5,5 3").unwrap();
|
|
assert_eq!(as_circle(&last(&s)).1, 1.5);
|
|
s.cmdline("circle.cd 5,5 5,9").unwrap();
|
|
assert_eq!(as_circle(&last(&s)).1, 2.0);
|
|
s.cmdline("circle.2p 0,0 0,4").unwrap();
|
|
assert!(near(as_circle(&last(&s)).0, 0.0, 2.0));
|
|
s.cmdline("circle.3p 0,0 2,2 4,0").unwrap();
|
|
assert!(near(as_circle(&last(&s)).0, 2.0, 0.0));
|
|
assert!(s.running.is_none());
|
|
}
|
|
|
|
fn line_h(s: &mut Session, pts: serde_json::Value) -> String {
|
|
s.execute("line", &json!({ "points": pts })).unwrap()["handles"][0].as_str().unwrap().to_string()
|
|
}
|
|
|
|
fn corner(s: &mut Session) -> (String, String) {
|
|
(line_h(s, json!([[0, 0], [10, 0]])), line_h(s, json!([[0, 0], [0, 10]])))
|
|
}
|
|
|
|
#[test]
|
|
fn circle_ttr_json_and_command_line() {
|
|
let mut s = Session::new();
|
|
let (a, b) = corner(&mut s);
|
|
s.execute("circle.ttr", &json!({"h1": a, "p1": [3, 0], "h2": b, "p2": [0, 3], "radius": 1})).unwrap();
|
|
let (c, r) = as_circle(&last(&s));
|
|
assert!(near(c, 1.0, 1.0) && (r - 1.0).abs() < 1e-9);
|
|
// Circle-circle and line-circle.
|
|
let k = h(&s.execute("circle", &json!({"center": [20, 0], "radius": 2})).unwrap());
|
|
let k2 = h(&s.execute("circle", &json!({"center": [26, 0], "radius": 2})).unwrap());
|
|
s.execute("circle.ttr", &json!({"h1": k, "p1": [21, 1], "h2": k2, "p2": [25, 1], "radius": 2})).unwrap();
|
|
let (c, r) = as_circle(&last(&s));
|
|
assert!((c.x - 23.0).abs() < 1e-9 && c.y > 0.0 && (r - 2.0).abs() < 1e-9, "{c:?}");
|
|
assert!(s.execute("circle.ttr", &json!({"h1": k, "p1": [21, 1], "h2": k2, "p2": [25, 1], "radius": 0.1})).is_err());
|
|
// Command line: picks, then the radius.
|
|
s.cmdline("circle.ttr 5,0 0,5 2").unwrap();
|
|
let (c, r) = as_circle(&last(&s));
|
|
assert!(near(c, 2.0, 2.0) && (r - 2.0).abs() < 1e-9, "{c:?} {r}");
|
|
// CIRCLE's Ttr option.
|
|
s.cmdline("circle ttr 6,0 0,6 0.5").unwrap();
|
|
assert!(s.running.is_none());
|
|
let (c, r) = as_circle(&last(&s));
|
|
assert!(near(c, 0.5, 0.5) && (r - 0.5).abs() < 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn circle_ttt_json_and_command_line() {
|
|
let mut s = Session::new();
|
|
let (a, b) = corner(&mut s);
|
|
let hyp = s.execute("line", &json!({"points": [[4, 0], [0, 3]]})).unwrap()["handles"][0].as_str().unwrap().to_string();
|
|
s.execute("circle.ttt", &json!({"h1": a, "p1": [2, 0], "h2": b, "p2": [0, 1], "h3": hyp, "p3": [2, 1.5]})).unwrap();
|
|
let (c, r) = as_circle(&last(&s));
|
|
assert!(near(c, 1.0, 1.0) && (r - 1.0).abs() < 1e-6, "{c:?} {r}");
|
|
s.cmdline("circle.ttt 2,0 0,1 2,1.5").unwrap();
|
|
assert!(s.running.is_none());
|
|
let (c, _) = as_circle(&last(&s));
|
|
assert!(near(c, 1.0, 1.0));
|
|
// Three circles: the small circle in the middle of three touching circles.
|
|
let mut s = Session::new();
|
|
let r = 1.0;
|
|
let cs: Vec<String> =
|
|
[[0.0, 0.0], [2.0, 0.0], [1.0, 3f64.sqrt()]].iter().map(|c| h(&s.execute("circle", &json!({"center": c, "radius": r})).unwrap())).collect();
|
|
let centroid = Vec2::new(1.0, 3f64.sqrt() / 3.0);
|
|
let picks: Vec<Vec2> = [[0.0, 0.0], [2.0, 0.0], [1.0, 3f64.sqrt()]]
|
|
.iter()
|
|
.map(|c| Vec2::new(c[0], c[1]) + (centroid - Vec2::new(c[0], c[1])).normalized())
|
|
.collect();
|
|
s.execute(
|
|
"circle.ttt",
|
|
&json!({"h1": cs[0], "p1": [picks[0].x, picks[0].y], "h2": cs[1], "p2": [picks[1].x, picks[1].y], "h3": cs[2], "p3": [picks[2].x, picks[2].y]}),
|
|
)
|
|
.unwrap();
|
|
let (c, rr) = as_circle(&last(&s));
|
|
assert!(c.near(centroid, 1e-6) && (rr - (2.0 / 3f64.sqrt() - 1.0)).abs() < 1e-6, "{c:?} {rr}");
|
|
}
|
|
|
|
// ---------------- ELLIPSE ----------------
|
|
|
|
#[test]
|
|
fn ellipse_axis_and_arc() {
|
|
let mut s = Session::new();
|
|
s.execute("ellipse.axis", &json!({"p1": [0, 0], "p2": [4, 0], "distance": 1})).unwrap();
|
|
match last(&s) {
|
|
EntityKind::Ellipse(e) => {
|
|
assert!(near(e.center.xy(), 2.0, 0.0) && (e.ratio - 0.5).abs() < 1e-9);
|
|
}
|
|
k => panic!("{k:?}"),
|
|
}
|
|
s.execute("ellipse.arc", &json!({"center": [0, 0], "major": [2, 0], "ratio": 0.5, "start": 0, "end": 90})).unwrap();
|
|
match last(&s) {
|
|
EntityKind::Ellipse(e) => assert!((e.end - std::f64::consts::FRAC_PI_2).abs() < 1e-9 && e.start.abs() < 1e-9),
|
|
k => panic!("{k:?}"),
|
|
}
|
|
assert!(s.execute("ellipse.arc", &json!({"p1": [0, 0], "p2": [4, 0], "distance": 1})).is_err());
|
|
s.cmdline("ellipse.axis 0,0 6,0 1").unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Ellipse(e) if (e.ratio - 1.0 / 3.0).abs() < 1e-9));
|
|
s.cmdline("ellipse.arc 0,0 4,0 1 0 90").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert!(matches!(last(&s), EntityKind::Ellipse(e) if (e.end - e.start).abs() > 0.1 && (e.end - e.start).abs() < 6.0));
|
|
}
|
|
|
|
#[test]
|
|
fn ellipse_arc_typed_angles_are_true_angles() {
|
|
let want = 2f64.atan2(1.0);
|
|
// Horizontal and rotated major axis, ratio 0.5: typed 0 and 45 degrees match the JSON form.
|
|
for line in ["ellipse.arc 0,0 10,0 2.5 0 45", "ellipse.arc 5,-5 5,5 2.5 0 45"] {
|
|
let mut s = Session::new();
|
|
s.cmdline(line).unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Ellipse(e) if e.start.abs() < 1e-9 && (e.end - want).abs() < 1e-9), "{line}");
|
|
}
|
|
}
|
|
|
|
// ---------------- DIVIDE / MEASURE ----------------
|
|
|
|
#[test]
|
|
fn divide_and_measure_json() {
|
|
let mut s = Session::new();
|
|
let l = s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap()["handles"][0].as_str().unwrap().to_string();
|
|
let r = s.execute("divide", &json!({"handle": l, "segments": 5})).unwrap();
|
|
assert_eq!(r["handles"].as_array().unwrap().len(), 4);
|
|
let pts: Vec<Vec2> =
|
|
s.doc().unwrap().model.iter().filter_map(|e| if let EntityKind::Point(p) = &e.kind { Some(p.p.xy()) } else { None }).collect();
|
|
assert!(near(pts[0], 2.0, 0.0) && near(pts[3], 8.0, 0.0));
|
|
let r = s.execute("measure", &json!({"handle": l, "length": 3})).unwrap();
|
|
assert_eq!(r["handles"].as_array().unwrap().len(), 3);
|
|
// From the other end.
|
|
s.execute("measure", &json!({"handle": l, "length": 4, "from": [10, 0]})).unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Point(p) if near(p.p.xy(), 2.0, 0.0)));
|
|
// A circle divides into n points including the start.
|
|
let c = h(&s.execute("circle", &json!({"center": [20, 0], "radius": 1})).unwrap());
|
|
assert_eq!(s.execute("divide", &json!({"handle": c, "segments": 6})).unwrap()["handles"].as_array().unwrap().len(), 6);
|
|
// Blocks, aligned with the object.
|
|
let tick = s.execute("line", &json!({"points": [[0, -0.1], [0, 0.1]]})).unwrap()["handles"][0].as_str().unwrap().to_string();
|
|
s.execute("block", &json!({"name": "TICK", "base": [0, 0], "handles": [tick], "keep": "delete"})).unwrap();
|
|
let arc = h(&s.execute("arc", &json!({"center": [0, 20], "radius": 2, "start": 0, "end": 180})).unwrap());
|
|
let r = s.execute("divide", &json!({"handle": arc, "segments": 2, "block": "TICK"})).unwrap();
|
|
let ins = s.doc().unwrap().entity(cadcraft_doc::Handle::parse_hex(r["handles"][0].as_str().unwrap()).unwrap()).unwrap().kind.clone();
|
|
assert!(matches!(ins, EntityKind::Insert(i) if near(i.insert.xy(), 0.0, 22.0) && (i.rotation - std::f64::consts::PI).abs() < 1e-6));
|
|
assert!(s.execute("divide", &json!({"handle": l, "segments": 3, "block": "NOPE"})).is_err());
|
|
assert!(s.execute("divide", &json!({"handle": l, "segments": 1})).is_err());
|
|
assert!(s.execute("measure", &json!({"handle": l, "length": 0})).is_err());
|
|
assert!(s.execute("measure", &json!({"handle": l, "length": 1e-300})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn divide_and_measure_command_line() {
|
|
let mut s = Session::new();
|
|
s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap();
|
|
s.cmdline("divide 5,0 4").unwrap();
|
|
assert!(s.running.is_none());
|
|
assert_eq!(kinds(&s).iter().filter(|k| **k == "Point").count(), 3);
|
|
s.cmdline("measure 9,0 2.5").unwrap();
|
|
assert!(s.running.is_none());
|
|
// Measured from the end nearest the pick.
|
|
assert!(matches!(last(&s), EntityKind::Point(p) if near(p.p.xy(), 2.5, 0.0)));
|
|
// Block option.
|
|
let tick = s.execute("circle", &json!({"center": [50, 50], "radius": 0.1})).unwrap();
|
|
s.execute("block", &json!({"name": "DOT", "base": [50, 50], "handles": [h(&tick)], "keep": "delete"})).unwrap();
|
|
s.cmdline("divide 6,0 b DOT n 2").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert!(matches!(last(&s), EntityKind::Insert(i) if near(i.insert.xy(), 5.0, 0.0) && i.rotation == 0.0));
|
|
// Invalid count re-prompts.
|
|
s.cmdline("divide 6,0 1").unwrap();
|
|
assert!(s.running.is_some());
|
|
s.cancel();
|
|
}
|
|
|
|
// ---------------- REVCLOUD ----------------
|
|
|
|
#[test]
|
|
fn revcloud_json() {
|
|
let mut s = Session::new();
|
|
s.execute("revcloud", &json!({"p1": [0, 0], "p2": [4, 2], "arcLength": 1})).unwrap();
|
|
match last(&s) {
|
|
EntityKind::LwPolyline(p) => {
|
|
assert!(p.closed);
|
|
assert_eq!(p.vertices.len(), 12);
|
|
assert!(p.vertices.iter().all(|v| v.bulge > 0.0));
|
|
}
|
|
k => panic!("{k:?}"),
|
|
}
|
|
s.execute("revcloud.polygonal", &json!({"points": [[0, 0], [0, 3], [3, 0]], "arcLength": 0.5})).unwrap();
|
|
assert!(matches!(last(&s), EntityKind::LwPolyline(p) if p.vertices.len() > 10));
|
|
s.execute("revcloud.freehand", &json!({"freehand": [[0, 0], [5, 0], [5, 5], [0, 5]], "arcLength": 2, "style": "calligraphy"})).unwrap();
|
|
assert!(matches!(last(&s), EntityKind::LwPolyline(p) if p.vertices.len() == 10 && p.vertices[0].end_width > 0.0));
|
|
let c = h(&s.execute("circle", &json!({"center": [10, 10], "radius": 1})).unwrap());
|
|
let n = s.doc().unwrap().model.len();
|
|
s.execute("revcloud", &json!({"handle": c, "arcLength": 0.5})).unwrap();
|
|
assert_eq!(s.doc().unwrap().model.len(), n, "the circle is replaced");
|
|
assert!(s.execute("revcloud", &json!({"points": [[0, 0], [1, 1], [2, 2]]})).is_err());
|
|
assert!(s.execute("revcloud", &json!({"p1": [0, 0], "p2": [1e300, 1e300], "arcLength": 1e-300})).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn revcloud_command_line() {
|
|
let mut s = Session::new();
|
|
s.cmdline("revcloud a 1").unwrap();
|
|
s.cmdline("0,0 4,2").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert!(matches!(last(&s), EntityKind::LwPolyline(p) if p.vertices.len() == 12));
|
|
s.cmdline("revcloud.polygonal 0,0 4,0 4,3").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(s.running.is_none());
|
|
assert_eq!(kinds(&s), vec!["Polyline", "Polyline"]);
|
|
s.cmdline("revcloud.freehand 0,0 3,0 3,3 0,3").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert_eq!(kinds(&s).len(), 3);
|
|
// Object option converts a closed polyline.
|
|
s.execute("rectang", &json!({"p1": [10, 10], "p2": [12, 12]})).unwrap();
|
|
s.cmdline("revcloud o 10,11").unwrap();
|
|
assert!(s.running.is_none());
|
|
assert_eq!(kinds(&s).len(), 4);
|
|
}
|
|
|
|
// ---------------- BREAKLINE ----------------
|
|
|
|
fn breakline_points(s: &Session) -> Vec<(f64, f64)> {
|
|
match last(s) {
|
|
EntityKind::LwPolyline(p) => {
|
|
assert!(!p.closed);
|
|
p.vertices.iter().map(|v| ((v.p.x * 1e9).round() / 1e9, (v.p.y * 1e9).round() / 1e9)).collect()
|
|
}
|
|
k => panic!("not a polyline: {k:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn breakline_json_symbol_size_extension_and_location() {
|
|
let mut s = Session::new();
|
|
// One polyline: the line to the symbol, the zigzag centred on the midpoint, the rest of the line.
|
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0]})).unwrap();
|
|
assert_eq!(s.doc().unwrap().model.len(), 1);
|
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (4.5, 0.0), (4.75, 0.5), (5.25, -0.5), (5.5, 0.0), (10.0, 0.0)]);
|
|
// Size scales the symbol, Extension runs the line past both points, `at` is projected onto the line.
|
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0], "size": 2, "extension": 1, "at": [2, 3]})).unwrap();
|
|
assert_eq!(breakline_points(&s), vec![(-1.0, 0.0), (1.0, 0.0), (1.5, 1.0), (2.5, -1.0), (3.0, 0.0), (11.0, 0.0)]);
|
|
// A location past the end keeps the symbol on the line; its end then is the line's end.
|
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0], "size": 2, "at": [100, 0]})).unwrap();
|
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (8.0, 0.0), (8.5, 1.0), (9.5, -1.0), (10.0, 0.0)]);
|
|
// The symbol turns with the line.
|
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [0, 10]})).unwrap();
|
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (0.0, 4.5), (-0.5, 4.75), (0.5, 5.25), (0.0, 5.5), (0.0, 10.0)]);
|
|
// DIMSCALE scales the size.
|
|
s.doc_mut().unwrap().header.set_f64("DIMSCALE", 4.0);
|
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0]})).unwrap();
|
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (3.0, 0.0), (4.0, 2.0), (6.0, -2.0), (7.0, 0.0), (10.0, 0.0)]);
|
|
|
|
let n = s.doc().unwrap().model.len();
|
|
for p in [
|
|
json!({"p1": [0, 0], "p2": [3, 0]}),
|
|
json!({"p1": [0, 0], "p2": [0, 0]}),
|
|
json!({"p1": [0, 0], "p2": [10, 0], "size": 0}),
|
|
json!({"p1": [0, 0], "p2": [10, 0], "extension": -1}),
|
|
json!({"p1": [0, 0], "p2": [10, 0], "block": "NOPE"}),
|
|
json!({"p1": [0, 0], "p2": [1e308, 0], "extension": 1e308}),
|
|
] {
|
|
assert!(s.execute("breakline", &p).is_err(), "{p}");
|
|
}
|
|
assert_eq!(s.doc().unwrap().model.len(), n);
|
|
}
|
|
|
|
#[test]
|
|
fn breakline_block_symbol_runs_between_its_defpoints() {
|
|
let mut s = Session::new();
|
|
s.execute("layer.current", &json!({"name": "Defpoints"})).unwrap();
|
|
s.execute("point", &json!({"at": [0, 0]})).unwrap();
|
|
s.execute("point", &json!({"at": [2, 0]})).unwrap();
|
|
s.execute("layer.current", &json!({"name": "0"})).unwrap();
|
|
// Drawn in pieces, one of them backwards: they are chained from one point to the other.
|
|
s.execute("line", &json!({"points": [[2, 0], [1, 1]]})).unwrap();
|
|
s.execute("line", &json!({"points": [[0, 0], [1, 1]]})).unwrap();
|
|
let hs: Vec<String> = s.doc().unwrap().model.iter().map(|e| e.handle.hex()).collect();
|
|
s.execute("block", &json!({"name": "PEAK", "base": [0, 0], "handles": hs, "keep": "delete"})).unwrap();
|
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0], "block": "PEAK"})).unwrap();
|
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (4.0, 0.0), (5.0, 1.0), (6.0, 0.0), (10.0, 0.0)]);
|
|
// Without two Defpoints points the block is refused.
|
|
s.execute("line", &json!({"points": [[0, 5], [1, 5]]})).unwrap();
|
|
let l = s.doc().unwrap().model.last().unwrap().handle.hex();
|
|
s.execute("block", &json!({"name": "BARE", "base": [0, 0], "handles": [l], "keep": "delete"})).unwrap();
|
|
assert!(s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0], "block": "BARE"})).is_err());
|
|
|
|
s.cmdline("breakline b PEAK 0,0 10,0").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(breakline_points(&s).len(), 5);
|
|
s.cmdline("breakline b nope").unwrap();
|
|
assert!(s.prompt_text().contains("block"), "an unknown block asks again: {}", s.prompt_text());
|
|
s.cmdline(".").unwrap();
|
|
s.cmdline("0,0 10,0").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert_eq!(breakline_points(&s).len(), 6, "`.` goes back to the built-in symbol");
|
|
}
|
|
|
|
#[test]
|
|
fn breakline_command_line_prompts_options_and_undo() {
|
|
let mut s = Session::new();
|
|
s.start("breakline").unwrap();
|
|
assert!(s.prompt_text().contains("Specify first point for breakline"), "{}", s.prompt_text());
|
|
assert!(s.prompt_text().contains("Block") && s.prompt_text().contains("Size") && s.prompt_text().contains("Extension"), "{}", s.prompt_text());
|
|
s.cmdline("0,0").unwrap();
|
|
assert!(s.prompt_text().contains("Specify second point for breakline"), "{}", s.prompt_text());
|
|
// Too short for the symbol: asked again.
|
|
s.cmdline("0.5,0").unwrap();
|
|
assert!(s.prompt_text().contains("second point"), "{}", s.prompt_text());
|
|
s.cmdline("10,0").unwrap();
|
|
assert!(s.prompt_text().contains("Specify location for break symbol") && s.prompt_text().contains("Midpoint"), "{}", s.prompt_text());
|
|
assert!(matches!(s.preview(Vec2::new(3.0, 1.0)).first().map(|e| &e.kind), Some(EntityKind::LwPolyline(p)) if p.vertices.len() == 6));
|
|
s.cmdline("").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (4.5, 0.0), (4.75, 0.5), (5.25, -0.5), (5.5, 0.0), (10.0, 0.0)]);
|
|
assert_eq!(s.state().unwrap().undo.len(), 1, "one undo step");
|
|
|
|
// Size and Extension at the first prompt, a picked location; remembered for the next time.
|
|
s.cmdline("breakline s 2 e 1 0,0 10,0 2,3").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(breakline_points(&s), vec![(-1.0, 0.0), (1.0, 0.0), (1.5, 1.0), (2.5, -1.0), (3.0, 0.0), (11.0, 0.0)]);
|
|
assert_eq!(s.state().unwrap().undo.len(), 2);
|
|
s.cmdline("breakline s").unwrap();
|
|
assert!(s.prompt_text().contains("2.0000"), "{}", s.prompt_text());
|
|
s.cmdline("0").unwrap();
|
|
assert!(s.prompt_text().contains("size"), "zero is refused: {}", s.prompt_text());
|
|
s.cancel();
|
|
s.undo().unwrap();
|
|
assert_eq!(s.doc().unwrap().model.len(), 1, "undo removes the whole breakline");
|
|
}
|
|
|
|
// ---------------- WIPEOUT ----------------
|
|
|
|
#[test]
|
|
fn wipeout_json_and_command_line() {
|
|
let mut s = Session::new();
|
|
s.execute("wipeout", &json!({"points": [[0, 0], [2, 0], [2, 2]]})).unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Wipeout(w) if w.boundary.len() == 3));
|
|
let r = h(&s.execute("rectang", &json!({"p1": [5, 5], "p2": [6, 6]})).unwrap());
|
|
s.execute("wipeout", &json!({"handle": r, "erase": true})).unwrap();
|
|
assert_eq!(kinds(&s), vec!["Wipeout", "Wipeout"]);
|
|
s.execute("wipeout", &json!({"frames": "off"})).unwrap();
|
|
assert_eq!(s.doc().unwrap().header.i64("WIPEOUTFRAME", 1), 0);
|
|
assert!(s.execute("wipeout", &json!({"points": [[0, 0], [1, 1]]})).is_err());
|
|
assert!(s.execute("wipeout", &json!({"frames": "sideways"})).is_err());
|
|
|
|
s.cmdline("wipeout 0,0 3,0 3,3").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(s.running.is_none());
|
|
assert_eq!(kinds(&s).len(), 3);
|
|
s.execute("rectang", &json!({"p1": [20, 20], "p2": [22, 22]})).unwrap();
|
|
s.cmdline("wipeout p 20,21 y").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(kinds(&s), vec!["Wipeout"; 4]);
|
|
s.cmdline("wipeout f on").unwrap();
|
|
assert_eq!(s.doc().unwrap().header.i64("WIPEOUTFRAME", 0), 1);
|
|
}
|
|
|
|
// ---------------- XLINE ----------------
|
|
|
|
fn xline_dir(k: &EntityKind) -> (Vec2, Vec2) {
|
|
match k {
|
|
EntityKind::XLine(r) => (r.base.xy(), r.dir.xy()),
|
|
k => panic!("{k:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn xline_options_json() {
|
|
let mut s = Session::new();
|
|
s.execute("xline", &json!({"vertex": [0, 0], "start": [1, 0], "end": [0, 1]})).unwrap();
|
|
let (_, d) = xline_dir(&last(&s));
|
|
assert!(d.near(Vec2::new(1.0, 1.0).normalized(), 1e-9));
|
|
let l = s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap()["handles"][0].as_str().unwrap().to_string();
|
|
s.execute("xline", &json!({"handle": l, "distance": 2, "side": [5, -1]})).unwrap();
|
|
let (b, d) = xline_dir(&last(&s));
|
|
assert!((b.y + 2.0).abs() < 1e-9 && d.near(Vec2::X, 1e-9));
|
|
s.execute("xline", &json!({"handle": l, "through": [3, 7]})).unwrap();
|
|
assert!(near(xline_dir(&last(&s)).0, 3.0, 7.0));
|
|
s.execute("xline", &json!({"base": [1, 1], "ver": true})).unwrap();
|
|
assert!(xline_dir(&last(&s)).1.near(Vec2::Y, 1e-9));
|
|
s.execute("xline", &json!({"base": [1, 1], "angle": 30})).unwrap();
|
|
s.execute("xline", &json!({"base": [1, 1], "through": [2, 2]})).unwrap();
|
|
assert!(s.execute("xline", &json!({"base": [1, 1], "through": [1, 1]})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn xline_options_command_line() {
|
|
let mut s = Session::new();
|
|
s.cmdline("xline b 0,0 1,0 0,1 -1,0").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert_eq!(kinds(&s), vec!["Xline", "Xline"]);
|
|
s.cmdline("xline a 45 3,3").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(xline_dir(&last(&s)).1.near(Vec2::new(1.0, 1.0).normalized(), 1e-9));
|
|
s.execute("line", &json!({"points": [[20, 0], [30, 0]]})).unwrap();
|
|
s.cmdline("xline o 2 25,0 25,5").unwrap();
|
|
s.cmdline("").unwrap();
|
|
let (b, _) = xline_dir(&last(&s));
|
|
assert!((b.y - 2.0).abs() < 1e-9);
|
|
s.cmdline("xline o t 25,0 0,9").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(near(xline_dir(&last(&s)).0, 0.0, 9.0));
|
|
s.cmdline("xline a r 25,0 90 4,4").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(xline_dir(&last(&s)).1.near(Vec2::Y, 1e-9));
|
|
s.cmdline("xline h 0,7").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(s.running.is_none());
|
|
}
|
|
|
|
// ---------------- RECTANG ----------------
|
|
|
|
/// The corners of the rectangle just added, so a test can assert its bounds.
|
|
fn rect_pts(s: &Session) -> Vec<Vec2> {
|
|
match last(s) {
|
|
EntityKind::LwPolyline(p) => p.vertices.iter().map(|v| v.p).collect(),
|
|
k => panic!("{k:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rectang_options_json() {
|
|
let mut s = Session::new();
|
|
s.execute("rectang", &json!({"p1": [0, 0], "p2": [4, 3]})).unwrap();
|
|
assert!(near(rect_pts(&s)[2], 4.0, 3.0));
|
|
s.execute("rectang", &json!({"p1": [0, 0], "dimensions": [3, 2], "p2": [-1, -1]})).unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, -3.0, -2.0) && near(b.max, 0.0, 0.0));
|
|
s.execute("rectang", &json!({"p1": [0, 0], "area": 20, "length": 5})).unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.max, 5.0, 4.0));
|
|
s.execute("rectang", &json!({"p1": [0, 0], "dimensions": [2, 1], "rotation": 90})).unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, -1.0, 0.0) && near(b.max, 0.0, 2.0), "{b:?}");
|
|
s.execute("rectang", &json!({"p1": [0, 0], "p2": [4, 4], "fillet": 1, "width": 0.1, "thickness": 2})).unwrap();
|
|
assert_eq!(rect_pts(&s).len(), 8);
|
|
assert_eq!(s.doc().unwrap().model.last().unwrap().common.thickness, 2.0);
|
|
assert!(s.execute("rectang", &json!({"p1": [0, 0], "area": -1, "length": 1})).is_err());
|
|
assert!(s.execute("rectang", &json!({"p1": [0, 0], "p2": [0, 5]})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rectang_options_command_line() {
|
|
let mut s = Session::new();
|
|
s.cmdline("rectang 0,0 a 20 l 5").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert!(near(cadcraft_geom::Bounds2::from_points(rect_pts(&s)).max, 5.0, 4.0));
|
|
s.cmdline("rectang 0,0 a 12 w 3").unwrap();
|
|
assert!(near(cadcraft_geom::Bounds2::from_points(rect_pts(&s)).max, 4.0, 3.0));
|
|
s.cmdline("rectang 0,0 d 3 2 -1,-1").unwrap();
|
|
assert!(near(cadcraft_geom::Bounds2::from_points(rect_pts(&s)).min, -3.0, -2.0));
|
|
s.cmdline("rectang 0,0 r 90 2,1").unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 0.0, 0.0) && near(b.max, 2.0, 1.0), "{b:?}");
|
|
s.cmdline("rectang 0,0 r p 0,0 1,1 d 2 2 5,5").unwrap();
|
|
let pts = rect_pts(&s);
|
|
assert!(pts.iter().any(|p| near(*p, 0.0, 8f64.sqrt())), "{pts:?}");
|
|
s.cmdline("rectang w 0.5 c 0.5 0.5 0,0 4,4").unwrap();
|
|
match last(&s) {
|
|
EntityKind::LwPolyline(p) => assert!(p.const_width == 0.5 && p.vertices.len() == 8),
|
|
k => panic!("{k:?}"),
|
|
}
|
|
s.cmdline("rectang f 1 e 2 t 3 0,0 4,4").unwrap();
|
|
match &s.doc().unwrap().model.last().unwrap().kind {
|
|
EntityKind::LwPolyline(p) => assert!(p.elevation == 2.0 && p.vertices.len() == 8),
|
|
k => panic!("{k:?}"),
|
|
}
|
|
}
|
|
|
|
/// Guards the JSON `center: true` form for every sizing route: a picked corner mirrored about
|
|
/// the centre, full-size `dimensions`, full `area`, rotation about the centre, that the default
|
|
/// corner mode is untouched, and that centre mode without a size is rejected.
|
|
#[test]
|
|
fn rectang_center_mode_json() {
|
|
let mut s = Session::new();
|
|
// The first point is the centre; the second is a corner mirrored about it.
|
|
s.execute("rectang", &json!({"center": true, "p1": [10, 10], "p2": [14, 16]})).unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// Dimensions are the rectangle's full size, still centred on the first point.
|
|
s.execute("rectang", &json!({"center": true, "p1": [10, 10], "dimensions": [8, 12]})).unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// A second point only picks the quadrant.
|
|
s.execute("rectang", &json!({"center": true, "p1": [10, 10], "dimensions": [8, 12], "p2": [0, 0]})).unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// The area is the full area, centred too.
|
|
s.execute("rectang", &json!({"center": true, "p1": [10, 10], "area": 96, "length": 8})).unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// Rotation is about the centre.
|
|
s.execute("rectang", &json!({"center": true, "p1": [10, 10], "dimensions": [8, 12], "rotation": 90})).unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 4.0, 6.0) && near(b.max, 16.0, 14.0), "{b:?}");
|
|
// The default corner mode is unchanged.
|
|
s.execute("rectang", &json!({"p1": [10, 10], "p2": [14, 16]})).unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 10.0, 10.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// Centre mode still needs a size.
|
|
assert!(s.execute("rectang", &json!({"center": true, "p1": [10, 10]})).is_err());
|
|
}
|
|
|
|
/// Guards that fillet, chamfer, width and thickness still apply in centre mode, and that an
|
|
/// oversized fillet falls back to sharp corners without moving the rectangle off centre.
|
|
#[test]
|
|
fn rectang_center_mode_honours_options() {
|
|
let mut s = Session::new();
|
|
s.execute("rectang", &json!({"center": true, "p1": [10, 10], "p2": [14, 16], "fillet": 1, "width": 0.25, "thickness": 2})).unwrap();
|
|
match &s.doc().unwrap().model.last().unwrap().kind {
|
|
EntityKind::LwPolyline(p) => assert!(p.vertices.len() == 8 && p.const_width == 0.25),
|
|
k => panic!("{k:?}"),
|
|
}
|
|
assert_eq!(s.doc().unwrap().model.last().unwrap().common.thickness, 2.0);
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// Chamfer, still centred.
|
|
s.execute("rectang", &json!({"center": true, "p1": [10, 10], "p2": [14, 16], "chamfer": [1, 1]})).unwrap();
|
|
assert_eq!(rect_pts(&s).len(), 8);
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// A fillet too large for the rectangle falls back to sharp corners, still centred.
|
|
s.execute("rectang", &json!({"center": true, "p1": [10, 10], "p2": [14, 16], "fillet": 5})).unwrap();
|
|
assert_eq!(rect_pts(&s).len(), 4);
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
}
|
|
|
|
/// Guards the `Center` keyword at the command line, before or after other options, with
|
|
/// dimensions and area still centred, the prompt wording and centred rubber band, and that the
|
|
/// default corner mode is unchanged.
|
|
#[test]
|
|
fn rectang_center_mode_command_line() {
|
|
let mut s = Session::new();
|
|
s.cmdline("rectang ce 10,10 14,16").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// Options before the centre point, and the centre picked after them.
|
|
s.cmdline("rectang center f 1 w 0.1 10,10 14,16").unwrap();
|
|
match &s.doc().unwrap().model.last().unwrap().kind {
|
|
EntityKind::LwPolyline(p) => assert!(p.vertices.len() == 8 && p.const_width == 0.1),
|
|
k => panic!("{k:?}"),
|
|
}
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// Dimensions after the centre: the pick only chooses the quadrant.
|
|
s.cmdline("rectang ce 10,10 d 8 12 14,16").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// Area too: the rectangle of that area is centred on the first point.
|
|
s.cmdline("rectang ce 10,10 a 96 l 8").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
// The rubber band is centred while the second point is picked.
|
|
s.start("rectang").unwrap();
|
|
s.cmdline("ce").unwrap();
|
|
assert!(s.prompt_text().contains("Specify center point for rectangle"), "{}", s.prompt_text());
|
|
s.cmdline("10,10").unwrap();
|
|
let pv = s.preview(Vec2::new(14.0, 16.0));
|
|
let pb = match pv.first().map(|e| &e.kind) {
|
|
Some(EntityKind::LwPolyline(p)) => cadcraft_geom::Bounds2::from_points(p.vertices.iter().map(|v| v.p).collect::<Vec<_>>()),
|
|
k => panic!("{k:?}"),
|
|
};
|
|
assert!(near(pb.min, 6.0, 4.0) && near(pb.max, 14.0, 16.0), "{pb:?}");
|
|
s.cancel();
|
|
// The default corner mode is unchanged.
|
|
s.cmdline("rectang 10,10 14,16").unwrap();
|
|
let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));
|
|
assert!(near(b.min, 10.0, 10.0) && near(b.max, 14.0, 16.0), "{b:?}");
|
|
}
|
|
|
|
// ---------------- 3DPOLY / MLINE / HELIX / SPLINE CV ----------------
|
|
|
|
#[test]
|
|
fn poly3d_json_and_command_line() {
|
|
let mut s = Session::new();
|
|
s.execute("3dpoly", &json!({"points": [[0, 0, 0], [1, 0, 2], [1, 1, 3]], "closed": true})).unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Polyline3d(p) if p.points.len() == 3 && p.points[2].z == 3.0 && p.closed));
|
|
assert!(s.execute("3dpoly", &json!({"points": [[0, 0]]})).is_err());
|
|
s.cmdline("3dpoly 0,0 1,0 1,1 u 2,2 c").unwrap();
|
|
assert!(s.running.is_none());
|
|
assert!(matches!(last(&s), EntityKind::Polyline3d(p) if p.points.len() == 3 && p.closed));
|
|
assert_eq!(s.state().unwrap().undo.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn mline_json_and_command_line() {
|
|
// Element offsets from the vertex path, sorted.
|
|
let offs = |k: EntityKind| match k {
|
|
EntityKind::MLine(m) => {
|
|
let mut o = m.offsets();
|
|
o.sort_by(f64::total_cmp);
|
|
o
|
|
}
|
|
k => panic!("{k:?}"),
|
|
};
|
|
let mut s = Session::new();
|
|
s.execute("mline", &json!({"points": [[0, 0], [10, 0], [10, 10]], "scale": 2, "justification": "zero"})).unwrap();
|
|
assert_eq!(offs(last(&s)), vec![-1.0, 1.0]);
|
|
assert!(s.execute("mline", &json!({"points": [[0, 0], [1, 0]], "justification": "middle"})).is_err());
|
|
let mut s = Session::new();
|
|
s.cmdline("mline j b s 3 0,0 10,0 10,5").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(s.running.is_none());
|
|
assert_eq!(offs(last(&s)), vec![0.0, 3.0]);
|
|
s.cmdline("mline 0,20 5,20 5,25 c").unwrap();
|
|
assert!(matches!(last(&s), EntityKind::MLine(m) if m.closed && m.vertices.len() == 3));
|
|
}
|
|
|
|
#[test]
|
|
fn helix_json_and_command_line() {
|
|
let mut s = Session::new();
|
|
s.execute("helix", &json!({"center": [0, 0], "baseRadius": 2, "topRadius": 1, "height": 6, "turns": 2})).unwrap();
|
|
match last(&s) {
|
|
EntityKind::Helix(h) => {
|
|
assert!(near(h.start_point().xy(), 2.0, 0.0));
|
|
let top = h.end_point();
|
|
assert!((top.z - 6.0).abs() < 1e-9 && near(top.xy(), 1.0, 0.0));
|
|
}
|
|
k => panic!("{k:?}"),
|
|
}
|
|
assert!(s.execute("helix", &json!({"center": [0, 0], "baseRadius": 1, "turns": 1e9})).is_err());
|
|
s.cmdline("helix 0,0 1 2 t 4 w cw 5").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
match last(&s) {
|
|
EntityKind::Helix(h) => {
|
|
let top = h.end_point();
|
|
assert!((top.z - 5.0).abs() < 1e-9 && near(top.xy(), 2.0, 0.0));
|
|
assert!(!h.ccw && h.point_at(0.05).y < 0.0, "clockwise twist");
|
|
}
|
|
k => panic!("{k:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn spline_cv_json_and_command_line() {
|
|
let mut s = Session::new();
|
|
s.execute("spline.cv", &json!({"control": [[0, 0], [1, 2], [3, 2], [4, 0]], "degree": 2})).unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Spline(sp) if sp.degree == 2 && sp.fit.is_empty()));
|
|
assert!(s.execute("spline.cv", &json!({"control": [[0, 0]]})).is_err());
|
|
s.cmdline("spline.cv d 3 0,0 1,1 2,0 3,1").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Spline(sp) if sp.control.len() == 4 && sp.degree == 3));
|
|
s.cmdline("spline.cv 0,0 1,1 2,0 c").unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Spline(sp) if sp.closed));
|
|
}
|
|
|
|
// ---------------- CENTER MARK / CENTER LINE ----------------
|
|
|
|
#[test]
|
|
fn centermark_and_centerline() {
|
|
let mut s = Session::new();
|
|
let c = h(&s.execute("circle", &json!({"center": [0, 0], "radius": 2})).unwrap());
|
|
let r = s.execute("centermark", &json!({"handle": c})).unwrap();
|
|
assert_eq!(r["handles"].as_array().unwrap().len(), 2);
|
|
let e = (**s.doc().unwrap().model.last().unwrap()).clone();
|
|
assert_eq!(e.common.linetype, "CENTER");
|
|
assert!(s.doc().unwrap().linetype("CENTER").is_some());
|
|
assert!(matches!(e.kind, EntityKind::Line(l) if (l.b.y - 2.12).abs() < 1e-9));
|
|
let a = s.execute("line", &json!({"points": [[10, 0], [20, 0]]})).unwrap()["handles"][0].as_str().unwrap().to_string();
|
|
let b = s.execute("line", &json!({"points": [[20, 4], [10, 4]]})).unwrap()["handles"][0].as_str().unwrap().to_string();
|
|
s.execute("centerline", &json!({"h1": a, "h2": b})).unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Line(l) if (l.a.y - 2.0).abs() < 1e-9 && (l.a.x - 9.88).abs() < 1e-9));
|
|
assert!(s.execute("centerline", &json!({"h1": a, "h2": c})).is_err());
|
|
assert!(s.execute("centermark", &json!({"handle": a})).is_err());
|
|
|
|
let mut s = Session::new();
|
|
s.execute("circle", &json!({"center": [0, 0], "radius": 2})).unwrap();
|
|
s.execute("arc", &json!({"center": [10, 0], "radius": 1, "start": 0, "end": 90})).unwrap();
|
|
s.cmdline("centermark 2,0 11,0").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert_eq!(kinds(&s).iter().filter(|k| **k == "Line").count(), 4);
|
|
s.execute("line", &json!({"points": [[0, 10], [10, 10]]})).unwrap();
|
|
s.execute("line", &json!({"points": [[0, 12], [10, 12]]})).unwrap();
|
|
s.cmdline("centerline 5,10 5,12").unwrap();
|
|
assert!(s.running.is_none());
|
|
assert!(matches!(last(&s), EntityKind::Line(l) if (l.a.y - 11.0).abs() < 1e-9));
|
|
}
|
|
|
|
#[test]
|
|
fn new_draw_commands_survive_garbage_input() {
|
|
let ids = [
|
|
"arc.sce",
|
|
"arc.sca",
|
|
"arc.scl",
|
|
"arc.sea",
|
|
"arc.sed",
|
|
"arc.ser",
|
|
"arc.cse",
|
|
"arc.csa",
|
|
"arc.csl",
|
|
"arc.continue",
|
|
"circle.cd",
|
|
"circle.2p",
|
|
"circle.3p",
|
|
"circle.ttr",
|
|
"circle.ttt",
|
|
"ellipse.axis",
|
|
"ellipse.arc",
|
|
"divide",
|
|
"measure",
|
|
"revcloud",
|
|
"revcloud.polygonal",
|
|
"revcloud.freehand",
|
|
"wipeout",
|
|
"breakline",
|
|
"3dpoly",
|
|
"mline",
|
|
"helix",
|
|
"spline.cv",
|
|
"centermark",
|
|
"centerline",
|
|
"xline",
|
|
"rectang",
|
|
];
|
|
let mut s = Session::new();
|
|
s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap();
|
|
for id in ids {
|
|
for p in [
|
|
json!({"start": [0, 0], "center": [0, 0], "end": [0, 0], "angle": 1e308, "length": -1e308, "radius": 0, "direction": [0, 0]}),
|
|
json!({"p1": [0, 0], "p2": [0, 0], "p3": [0, 0], "distance": -0.0, "diameter": 1e308, "radius": f64::MAX}),
|
|
json!({"handle": "100", "segments": 1e18, "length": 1e-320, "block": ""}),
|
|
json!({"points": [[0, 0], [1e308, 1e308], [-1e308, 1e308]], "arcLength": 1e-300, "scale": 1e308}),
|
|
json!({"center": [0, 0], "baseRadius": 1e308, "turns": 499, "height": 1e308}),
|
|
json!({"control": [[0, 0], [0, 0]], "degree": 1e9}),
|
|
json!({"p1": [0, 0], "dimensions": [1e308, -1e308], "rotation": 1e308, "area": 1e308, "length": 1e-308}),
|
|
json!({"h1": "100", "p1": [0, 0], "h2": "100", "p2": [0, 0], "h3": "100", "p3": [0, 0], "radius": 1}),
|
|
json!({"vertex": [0, 0], "start": [0, 0], "end": [0, 0]}),
|
|
] {
|
|
let _ = s.execute(id, &p);
|
|
}
|
|
// Interactive: garbage text and points.
|
|
s.start(id).unwrap();
|
|
for t in ["", "x", "1e999", "-5", "0,0", "0,0", "nan", "b", "a", "1", "2,2", ""] {
|
|
if s.running.is_none() {
|
|
break;
|
|
}
|
|
let _ = s.cmdline(t);
|
|
}
|
|
let _ = s.preview(Vec2::new(1e300, -1e300));
|
|
s.cancel();
|
|
let _ = s.input(Input::Cancel);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arc_command_options() {
|
|
for (cmd, center) in [
|
|
("arc 1,0 0,1 -1,0", (0.0, 0.0)),
|
|
("arc c 0,0 1,0 0,1", (0.0, 0.0)),
|
|
("arc c 0,0 1,0 a 90", (0.0, 0.0)),
|
|
("arc c 0,0 1,0 l 1.41421356237", (0.0, 0.0)),
|
|
("arc 1,0 c 0,0 0,1", (0.0, 0.0)),
|
|
("arc 1,0 c 0,0 a 90", (0.0, 0.0)),
|
|
("arc 1,0 e 0,1 0,0", (0.0, 0.0)),
|
|
("arc 1,0 e 0,1 a 90", (0.0, 0.0)),
|
|
("arc 1,0 e 0,1 d 90", (0.0, 0.0)),
|
|
("arc 1,0 e 0,1 r 1", (0.0, 0.0)),
|
|
] {
|
|
let mut s = Session::new();
|
|
s.cmdline(cmd).unwrap();
|
|
assert!(s.running.is_none(), "{cmd}: {}", s.prompt_text());
|
|
let a = as_arc(&last(&s));
|
|
assert!(near(a.center, center.0, center.1) && (a.radius - 1.0).abs() < 1e-6, "{cmd}: {a:?}");
|
|
}
|
|
// Enter at the first prompt continues from the last line.
|
|
let mut s = Session::new();
|
|
s.cmdline("line 0,0 1,0").unwrap();
|
|
s.cmdline("").unwrap();
|
|
s.cmdline("arc").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(s.prompt_text().contains("end point"), "{}", s.prompt_text());
|
|
s.cmdline("2,1").unwrap();
|
|
assert!(near(as_arc(&last(&s)).center, 1.0, 1.0));
|
|
assert!(s.prompt_text() == "Command:");
|
|
}
|