* WIP deep rename (not for push) * WIP2 * WIP3 * WIP4 wxs fix * rustfmt * Rename the remaining identifiers in merged crash-report code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
286 lines
16 KiB
Rust
286 lines
16 KiB
Rust
use serde_json::{Value, json};
|
|
use solidcraft_geom::Vec2;
|
|
|
|
use crate::Session;
|
|
|
|
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
|
match s.execute(id, &p) {
|
|
Ok(v) => v,
|
|
Err(e) => panic!("{id} {p}: {e}"),
|
|
}
|
|
}
|
|
|
|
fn ids(v: &Value) -> Vec<String> {
|
|
v.as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default()
|
|
}
|
|
|
|
fn new_sketch() -> Session {
|
|
let mut s = Session::default();
|
|
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
|
s
|
|
}
|
|
|
|
fn sketch(s: &Session) -> solidcraft_sketch::Sketch {
|
|
s.model.state().sketch(s.active_sketch.unwrap()).unwrap().sketch.clone()
|
|
}
|
|
|
|
#[test]
|
|
fn redundant_constraints_and_dimensions_are_refused_driven_ones_measure() {
|
|
let mut s = new_sketch();
|
|
let l = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]], "infer": true}))["curves"])[0].clone();
|
|
// Already horizontal (inferred).
|
|
let e = s.execute("sketch.constraint.horizontal_vertical", &json!({"line": l})).unwrap_err().to_string();
|
|
assert!(e.contains("over-constrain"), "{e}");
|
|
run(&mut s, "sketch.constraint.coincident", json!({"a": format!("{l}.start"), "b": "origin"}));
|
|
run(&mut s, "sketch.dimension", json!({"entities": [l], "value": 10}));
|
|
let si = run(&mut s, "sketch.inspect", json!({}));
|
|
assert_eq!(si["dof"], 0, "{si}");
|
|
// A second length on it over-constrains: refused, unless driven.
|
|
assert!(s.execute("sketch.dimension", &json!({"entities": [format!("{l}.start"), format!("{l}.end")]})).is_err());
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": [format!("{l}.start"), format!("{l}.end")], "driven": true}));
|
|
assert_eq!(d["driven"], true);
|
|
// The driven dimension follows the driving one.
|
|
let pname = si["constraints"].as_array().unwrap().iter().find_map(|c| c["param"].as_str().map(str::to_string)).unwrap();
|
|
run(&mut s, "parameters.change", json!({"name": pname, "expression": "25 mm"}));
|
|
let g = run(&mut s, "sketch.glyphs", json!({}));
|
|
let driven = g["glyphs"].as_array().unwrap().iter().find(|x| x["driven"] == true).unwrap().clone();
|
|
assert!((driven["value"].as_f64().unwrap() - 25.0).abs() < 1e-7, "{driven}");
|
|
assert_eq!(g["fully_constrained"], true);
|
|
}
|
|
|
|
/// `measured` is the dimension on the solved sketch: a trough drawn 99.84 long and dimensioned
|
|
/// 100 reports 100, not the length it had before solving.
|
|
#[test]
|
|
fn a_dimension_reports_the_solved_value() {
|
|
let mut s = new_sketch();
|
|
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [99.84, 40.2]}));
|
|
run(&mut s, "sketch.constraint.coincident", json!({"a": "p1", "b": "origin"}));
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 100}));
|
|
assert!((d["measured"].as_f64().unwrap() - 100.0).abs() < 1e-6, "{d}");
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l2"], "value": "40 mm"}));
|
|
assert!((d["measured"].as_f64().unwrap() - 40.0).abs() < 1e-6, "{d}");
|
|
// A driven dimension measures the solved geometry too.
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["p1", "p3"], "driven": true}));
|
|
assert!((d["measured"].as_f64().unwrap() - 100.0_f64.hypot(40.0)).abs() < 1e-6, "{d}");
|
|
let c = run(&mut s, "sketch.circle.center", json!({"center": [50, 20], "radius": 7.3}));
|
|
let c = ids(&c["curves"])[0].clone();
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": [c], "value": 15}));
|
|
assert!((d["measured"].as_f64().unwrap() - 15.0).abs() < 1e-6, "{d}");
|
|
}
|
|
|
|
#[test]
|
|
fn curvature_polygon_arc_length_linear_diameter() {
|
|
let mut s = new_sketch();
|
|
// A horizontal line ending where a control-point spline starts: G2 keeps the spline's
|
|
// start straight (zero curvature) and tangent.
|
|
let l = ids(&run(&mut s, "sketch.line", json!({"points": [[-10, 0], [0, 0]], "infer": true}))["curves"])[0].clone();
|
|
let sp =
|
|
ids(&run(&mut s, "sketch.spline.control_point", json!({"points": [format!("{l}.end"), [5, 2], [10, 6], [15, 15]]}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.constraint.curvature", json!({"a": l, "b": sp}));
|
|
let sk = sketch(&s);
|
|
let pts: Vec<Vec2> = sk.curves[sk.curve_index(&sp).unwrap()].kind.point_ids().iter().map(|i| sk.point(*i).unwrap()).collect();
|
|
// Tangent: first leg along +x; zero curvature: the second leg too (collinear first three).
|
|
assert!((pts[1] - pts[0]).y.abs() < 1e-5, "{pts:?}");
|
|
assert!((pts[2] - pts[1]).cross(pts[1] - pts[0]).abs() < 1e-3, "{pts:?}");
|
|
|
|
// Polygon constraint on a rough quadrilateral: becomes a square-ish regular polygon.
|
|
let q = run(&mut s, "sketch.line", json!({"points": [[100, 0], [111, 1], [110, 9], [99, 10]], "closed": true}));
|
|
let ql = ids(&q["curves"]);
|
|
run(&mut s, "sketch.constraint.polygon", json!({"lines": ql}));
|
|
let sk = sketch(&s);
|
|
let lens: Vec<f64> = ql.iter().map(|i| sk.polyline(sk.curve_index(i).unwrap())).map(|p| p[0].dist(p[p.len() - 1])).collect();
|
|
assert!(lens.iter().all(|x| (x - lens[0]).abs() < 1e-6), "{lens:?}");
|
|
|
|
// Arc length.
|
|
let a = ids(&run(&mut s, "sketch.arc.center_point", json!({"center": [0, 50], "start": [10, 50], "end": [0, 60]}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.dimension", json!({"entities": [a], "type": "arc_length", "value": 20}));
|
|
let sk = sketch(&s);
|
|
let ai = sk.curve_index(&a).unwrap();
|
|
let len = match sk.segs(ai)[0] {
|
|
solidcraft_geom::Seg2::Arc { radius, sweep, .. } => radius * sweep,
|
|
_ => 0.0,
|
|
};
|
|
assert!((len - 20.0).abs() < 1e-6, "{len}");
|
|
|
|
// Linear diameter about a centerline.
|
|
let cl = ids(&run(&mut s, "sketch.line", json!({"points": [[200, 0], [200, 50]]}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.centerline", json!({"curves": [cl]}));
|
|
run(&mut s, "sketch.point", json!({"point": [207, 10], "id": "rim"}));
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["rim", cl], "value": 30}));
|
|
assert_eq!(d["type"], "LinearDiameterDimension", "{d}");
|
|
let sk = sketch(&s);
|
|
let rim = sk.point(sk.point_index("rim").unwrap()).unwrap();
|
|
let line = sk.polyline(sk.curve_index(&cl).unwrap());
|
|
let (a, b) = (line[0], line[line.len() - 1]);
|
|
let dist = (b - a).normalized().unwrap().cross(rim - a).abs();
|
|
assert!((dist - 15.0).abs() < 1e-6, "{rim:?} {dist}");
|
|
}
|
|
|
|
#[test]
|
|
fn glyphs_and_snaps() {
|
|
let mut s = new_sketch();
|
|
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10], "connect": false}));
|
|
let l = ids(&r["curves"]);
|
|
let g = run(&mut s, "sketch.glyphs", json!({}));
|
|
assert_eq!(g["glyphs"].as_array().unwrap().len(), 4);
|
|
assert!(g["glyphs"][0]["at"].is_array());
|
|
let sn = run(&mut s, "sketch.snap", json!({"at": [20.3, 0.2], "radius": 1}));
|
|
assert_eq!(sn["snaps"][0]["type"], "coincident", "{sn}");
|
|
let sn = run(&mut s, "sketch.snap", json!({"at": [10.2, 0.3], "radius": 1}));
|
|
assert_eq!(sn["snaps"][0]["type"], "midpoint", "{sn}");
|
|
assert_eq!(sn["snaps"][0]["curve"], json!(l[0]));
|
|
let sn = run(&mut s, "sketch.snap", json!({"at": [40, 10.4], "from": [30, 10], "radius": 1}));
|
|
assert!(sn["snaps"].as_array().unwrap().iter().any(|x| x["type"] == "horizontal"), "{sn}");
|
|
}
|
|
|
|
#[test]
|
|
fn auto_constrain_fully_constrains_a_rough_sketch() {
|
|
let mut s = new_sketch();
|
|
// A slightly crooked closed rectangle and a circle inside.
|
|
run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0.004], [40.003, 30], [0, 30.002]], "closed": true}));
|
|
run(&mut s, "sketch.circle.center", json!({"center": [20, 15], "radius": 5}));
|
|
let r = run(&mut s, "sketch.auto_constrain", json!({}));
|
|
assert_eq!(r["dof"], 0, "{r}");
|
|
let si = run(&mut s, "sketch.inspect", json!({}));
|
|
assert_eq!(si["fully_constrained"], true, "{si}");
|
|
let names: Vec<String> = si["constraints"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap().to_string()).collect();
|
|
assert!(names.iter().filter(|n| *n == "Horizontal").count() >= 2, "{names:?}");
|
|
assert!(names.iter().any(|n| n == "DiameterDimension"), "{names:?}");
|
|
// From a datum, and finishing.
|
|
let mut s = new_sketch();
|
|
run(&mut s, "sketch.point", json!({"point": [5, 5], "id": "d"}));
|
|
run(&mut s, "sketch.line", json!({"points": [[10, 10], [20, 10]]}));
|
|
let r = run(&mut s, "sketch.finish_auto_constrain", json!({"datum": "d"}));
|
|
assert!(s.active_sketch.is_none());
|
|
assert!(r["added"].as_array().unwrap().len() >= 3, "{r}");
|
|
}
|
|
|
|
#[test]
|
|
fn drawing_infers_tangent_and_perpendicular() {
|
|
let mut s = new_sketch();
|
|
let l1 = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 10]], "infer": true}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.line", json!({"points": [format!("{l1}.end"), [20, 0]], "infer": true}));
|
|
assert_eq!(r["constraints"].as_array().unwrap().len(), 1, "{r}");
|
|
let a = ids(&run(&mut s, "sketch.arc.center_point", json!({"center": [50, 0], "start": [60, 0], "end": [50, 10]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.line", json!({"points": [format!("{a}.end"), [30, 10]], "infer": true}));
|
|
let si = run(&mut s, "sketch.inspect", json!({}));
|
|
let names: Vec<&str> = si["constraints"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap()).collect();
|
|
assert!(names.contains(&"Perpendicular") && names.contains(&"Tangent"), "{names:?} {r}");
|
|
}
|
|
|
|
#[test]
|
|
fn smart_constrain_and_driven_toggle() {
|
|
let mut s = new_sketch();
|
|
let a = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0.5]]}))["curves"])[0].clone();
|
|
let b = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 5], [10, 6]]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.constrainer", json!({"entities": [a]}));
|
|
assert_eq!(r["type"], "Horizontal");
|
|
let r = run(&mut s, "sketch.constrainer", json!({"entities": [a, b]}));
|
|
assert_eq!(r["type"], "Parallel");
|
|
let c1 = ids(&run(&mut s, "sketch.circle.center", json!({"center": [50, 0], "radius": 5}))["curves"])[0].clone();
|
|
let c2 = ids(&run(&mut s, "sketch.circle.center", json!({"center": [50.3, 0.2], "radius": 3}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.constrainer", json!({"entities": [c1, c2]}));
|
|
assert_eq!(r["type"], "Concentric");
|
|
// Driven toggle.
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": [c1], "value": 10}));
|
|
let g = run(&mut s, "sketch.glyphs", json!({}));
|
|
let did = g["glyphs"].as_array().unwrap().iter().find(|x| x["param"] == d["param"]).unwrap()["id"].as_str().unwrap().to_string();
|
|
let r = run(&mut s, "sketch.toggle_driven", json!({"constraint": did}));
|
|
assert_eq!(r["driven"], true);
|
|
assert!(s.doc.param(d["param"].as_str().unwrap()).is_none(), "the parameter goes");
|
|
let r = run(&mut s, "sketch.toggle_driven", json!({"constraint": did}));
|
|
assert_eq!(r["driven"], false);
|
|
}
|
|
|
|
#[test]
|
|
fn drawing_from_a_line_midpoint_keeps_it_there() {
|
|
let mut s = new_sketch();
|
|
let l = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.line", json!({"points": [format!("mid:{l}"), [10, 15]]}));
|
|
let si = run(&mut s, "sketch.inspect", json!({}));
|
|
assert!(si["constraints"].as_array().unwrap().iter().any(|c| c["name"] == "MidPoint"), "{si}");
|
|
// Lengthen the first line: the second one's start follows the middle.
|
|
run(&mut s, "sketch.move_point", json!({"point": format!("{l}.end"), "to": [40, 0]}));
|
|
let sk = sketch(&s);
|
|
let n = ids(&r["curves"])[0].clone();
|
|
let st = sk.resolve_point(&format!("{n}.start")).and_then(|i| sk.point(i)).unwrap();
|
|
let a = sk.resolve_point(&format!("{l}.start")).and_then(|i| sk.point(i)).unwrap();
|
|
let b = sk.resolve_point(&format!("{l}.end")).and_then(|i| sk.point(i)).unwrap();
|
|
assert!(st.dist((a + b) * 0.5) < 1e-7, "{st:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn drawing_onto_a_curve_keeps_the_point_on_it() {
|
|
let mut s = new_sketch();
|
|
let c = ids(&run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.line", json!({"points": [format!("on:{c}:7.2,7.1"), [30, 30]]}));
|
|
let n = ids(&r["curves"])[0].clone();
|
|
run(&mut s, "sketch.dimension", json!({"entities": [c], "value": 16}));
|
|
let sk = sketch(&s);
|
|
let st = sk.resolve_point(&format!("{n}.start")).and_then(|i| sk.point(i)).unwrap();
|
|
let cc = sk.center(sk.curve_index(&c).unwrap()).unwrap();
|
|
assert!((st.dist(cc) - 8.0).abs() < 1e-7, "{st:?} {cc:?}");
|
|
}
|
|
|
|
/// A faucet spout path: a vertical riser, a horizontal top and an outlet sloping down, joined
|
|
/// by fillets. The riser points up and the outlet down-left, so their directions are 150° apart
|
|
/// while the lines meet at 30°.
|
|
fn spout() -> Session {
|
|
let mut s = new_sketch();
|
|
run(&mut s, "sketch.line", json!({"points": [[795, 955], [795, 1068], [689, 1068], [661, 1020]], "ids": ["l1", "l2", "l3"]}));
|
|
run(&mut s, "sketch.fillet", json!({"point": "l1.end", "radius": 50}));
|
|
run(&mut s, "sketch.fillet", json!({"point": "l2.end", "radius": 35}));
|
|
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l1", "mode": "vertical"}));
|
|
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l2", "mode": "horizontal"}));
|
|
run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l1.start"], "type": "horizontal", "value": 795}));
|
|
run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l1.start"], "type": "vertical", "value": 955}));
|
|
run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l2.start"], "type": "vertical", "value": 1068}));
|
|
run(&mut s, "sketch.dimension", json!({"entities": ["l3.end", "l1.start"], "type": "horizontal", "value": 134}));
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l3.end"], "type": "vertical", "value": 1020}));
|
|
assert_eq!(d["sketch"]["dof"], 1, "{d}");
|
|
s
|
|
}
|
|
|
|
/// The angle (degrees, 0..=90) between two lines of the active sketch.
|
|
fn line_angle(s: &Session, a: &str, b: &str) -> f64 {
|
|
let sk = sketch(s);
|
|
let dir = |l: &str| {
|
|
let c = sk.curve_index(l).unwrap();
|
|
let (p, q) = crate::cmd::sketch::line_pts(&sk, c).unwrap();
|
|
q - p
|
|
};
|
|
let (u, v) = (dir(a), dir(b));
|
|
let t = u.cross(v).abs().atan2(u.dot(v)).to_degrees();
|
|
t.min(180.0 - t)
|
|
}
|
|
|
|
#[test]
|
|
fn an_angle_between_lines_takes_the_reading_nearest_its_value_and_reports_degrees() {
|
|
// The riser and the outlet are not adjacent and meet at 30°; their directions are 150°
|
|
// apart. Asking for 30° used to swing the outlet round and was refused as over-constraining.
|
|
for ents in [["l1", "l3"], ["l3", "l1"]] {
|
|
let mut s = spout();
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ents, "type": "angle", "value": "30 deg"}));
|
|
assert_eq!(d["sketch"]["dof"], 0, "{d}");
|
|
assert_eq!(d["unit"], "deg", "{d}");
|
|
assert!((d["measured"].as_f64().unwrap() - 30.0).abs() < 1e-6, "{d}");
|
|
assert!((line_angle(&s, "l1", "l3") - 30.0).abs() < 1e-6);
|
|
}
|
|
// The other reading still works, and the adjacent top line measures 60° the same way.
|
|
let mut s = spout();
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l1", "l3"], "value": "150 deg"}));
|
|
assert_eq!(d["sketch"]["dof"], 0, "{d}");
|
|
assert!((line_angle(&s, "l1", "l3") - 30.0).abs() < 1e-6);
|
|
let mut s = spout();
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l2", "l3"], "value": "60 deg"}));
|
|
assert!((d["measured"].as_f64().unwrap() - 60.0).abs() < 1e-6, "{d}");
|
|
// Without a value the acute angle is kept as it is.
|
|
let mut s = spout();
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l1", "l3"]}));
|
|
let e = d["expression"].as_str().unwrap();
|
|
assert!(e.ends_with(" deg") && (e.trim_end_matches(" deg").parse::<f64>().unwrap() - 30.3).abs() < 0.1, "{d}");
|
|
// Lengths report millimetres.
|
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l2"], "driven": true}));
|
|
assert_eq!(d["unit"], "mm", "{d}");
|
|
}
|