Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
260 lines
12 KiB
Rust
260 lines
12 KiB
Rust
//! Construction axes, points and planes (SOLID ▸ CONSTRUCT), one test per type, against a
|
||
//! 40 × 30 × 20 box at the origin, a cylinder, a sphere and a torus.
|
||
|
||
use serde_json::{Value, json};
|
||
use solvecraft_geom::{Plane, Vec3};
|
||
|
||
use crate::Session;
|
||
use crate::view::{construction_axes, construction_planes, construction_points};
|
||
|
||
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 part() -> Session {
|
||
let mut s = Session::default();
|
||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "body_name": "Box"}));
|
||
run(&mut s, "solid.cylinder", json!({"base": [60, 0, 0], "radius": 5, "height": 10, "body_name": "Cyl"}));
|
||
run(&mut s, "solid.sphere", json!({"center": [0, 0, 60], "radius": 5, "body_name": "Ball"}));
|
||
run(&mut s, "solid.torus", json!({"center": [0, 60, 0], "major": 10, "minor": 2, "body_name": "Ring"}));
|
||
s
|
||
}
|
||
|
||
fn near(a: Vec3, b: Vec3) -> bool {
|
||
a.dist(b) < 1e-3
|
||
}
|
||
|
||
fn parallel(a: Vec3, b: Vec3) -> bool {
|
||
a.normalized().zip(b.normalized()).is_some_and(|(a, b)| a.cross(b).len() < 1e-6)
|
||
}
|
||
|
||
fn axis(s: &Session, name: &str) -> (Vec3, Vec3) {
|
||
construction_axes(s).into_iter().find(|a| a.1 == name).map(|a| (a.2, a.3)).unwrap_or_else(|| panic!("no axis {name}"))
|
||
}
|
||
|
||
fn point(s: &Session, name: &str) -> Vec3 {
|
||
construction_points(s).into_iter().find(|p| p.1 == name).map(|p| p.2).unwrap_or_else(|| panic!("no point {name}"))
|
||
}
|
||
|
||
fn plane(s: &Session, name: &str) -> Plane {
|
||
construction_planes(s).into_iter().find(|p| p.1 == name).map(|p| p.2).unwrap_or_else(|| panic!("no plane {name}"))
|
||
}
|
||
|
||
/// The line through `o` along `d` contains `p`.
|
||
fn on_line(o: Vec3, d: Vec3, p: Vec3) -> bool {
|
||
let d = d.normalized().unwrap_or(Vec3::Z);
|
||
(p - o - d * (p - o).dot(d)).len() < 1e-3
|
||
}
|
||
|
||
fn on_plane(pl: &Plane, p: Vec3) -> bool {
|
||
(p - pl.origin).dot(pl.normal()).abs() < 1e-3
|
||
}
|
||
|
||
#[test]
|
||
fn axis_through_cylinder_cone_or_torus() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.axis.through_cylinder", json!({"face": {"face": [65, 0, 5]}, "name": "A"}));
|
||
let (o, d) = axis(&s, "A");
|
||
assert!(parallel(d, Vec3::Z) && on_line(o, d, Vec3::new(60.0, 0.0, 0.0)), "{o:?} {d:?}");
|
||
run(&mut s, "construct.axis.through_cylinder", json!({"face": {"face": [12, 60, 0]}, "name": "T"}));
|
||
let (o, d) = axis(&s, "T");
|
||
assert!(parallel(d, Vec3::Z) && on_line(o, d, Vec3::new(0.0, 60.0, 0.0)), "{o:?} {d:?}");
|
||
// A flat face has no axis.
|
||
assert!(s.execute("construct.axis.through_cylinder", &json!({"face": {"face": [20, 15, 20]}})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn axis_perpendicular_to_a_face_at_a_point() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.axis.normal_to_face", json!({"face": {"face": [10, 10, 20]}, "point": {"point": [10, 10, 25]}, "name": "N"}));
|
||
let (o, d) = axis(&s, "N");
|
||
assert!(near(o, Vec3::new(10.0, 10.0, 20.0)) && parallel(d, Vec3::Z), "{o:?} {d:?}");
|
||
// On the cylinder: radial through the point.
|
||
run(&mut s, "construct.axis.normal_to_face", json!({"face": {"face": [65, 0, 5]}, "point": {"point": [70, 0, 5]}, "name": "R"}));
|
||
let (o, d) = axis(&s, "R");
|
||
assert!(near(o, Vec3::new(65.0, 0.0, 5.0)) && parallel(d, Vec3::X), "{o:?} {d:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn axis_through_two_planes() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.axis.two_planes", json!({"a": {"plane": "XZ"}, "b": {"face": [40, 15, 10]}, "name": "L"}));
|
||
let (o, d) = axis(&s, "L");
|
||
assert!(parallel(d, Vec3::Z) && on_line(o, d, Vec3::new(40.0, 0.0, 0.0)), "{o:?} {d:?}");
|
||
assert!(s.execute("construct.axis.two_planes", &json!({"a": "XY", "b": {"face": [20, 15, 20]}})).is_err(), "parallel");
|
||
}
|
||
|
||
#[test]
|
||
fn axis_through_two_points() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.axis.two_points", json!({"a": {"vertex": [0.5, 0.5, 0.5]}, "b": {"vertex": [39, 29, 19]}, "name": "D"}));
|
||
let (o, d) = axis(&s, "D");
|
||
assert!(on_line(o, d, Vec3::ZERO) && parallel(d, Vec3::new(40.0, 30.0, 20.0)), "{o:?} {d:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn axis_through_an_edge() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.axis.edge", json!({"edge": {"edge": [20, 0, 0]}, "name": "E"}));
|
||
let (o, d) = axis(&s, "E");
|
||
assert!(parallel(d, Vec3::X) && on_line(o, d, Vec3::ZERO), "{o:?} {d:?}");
|
||
// A circular edge is not straight.
|
||
assert!(s.execute("construct.axis.edge", &json!({"edge": {"edge": [65, 0, 10]}})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn point_at_a_vertex() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.point.vertex", json!({"point": {"vertex": [39, 29, 19]}, "name": "P"}));
|
||
assert!(near(point(&s, "P"), Vec3::new(40.0, 30.0, 20.0)));
|
||
}
|
||
|
||
#[test]
|
||
fn point_through_two_edges() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.point.two_edges", json!({"a": {"edge": [20, 0, 0]}, "b": {"edge": [40, 15, 0]}, "name": "P"}));
|
||
assert!(near(point(&s, "P"), Vec3::new(40.0, 0.0, 0.0)), "{:?}", point(&s, "P"));
|
||
}
|
||
|
||
#[test]
|
||
fn point_through_three_planes() {
|
||
let mut s = part();
|
||
run(
|
||
&mut s,
|
||
"construct.point.three_planes",
|
||
json!({"a": {"face": [40, 15, 10]}, "b": {"face": [20, 30, 10]}, "c": {"face": [20, 15, 20]}, "name": "P"}),
|
||
);
|
||
assert!(near(point(&s, "P"), Vec3::new(40.0, 30.0, 20.0)), "{:?}", point(&s, "P"));
|
||
}
|
||
|
||
#[test]
|
||
fn point_at_the_centre_of_a_circle_sphere_or_torus() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.point.center", json!({"of": {"edge": [65, 0, 10]}, "name": "C"}));
|
||
assert!(near(point(&s, "C"), Vec3::new(60.0, 0.0, 10.0)), "{:?}", point(&s, "C"));
|
||
run(&mut s, "construct.point.center", json!({"of": {"face": [5, 0, 60]}, "name": "S"}));
|
||
assert!(near(point(&s, "S"), Vec3::new(0.0, 0.0, 60.0)), "{:?}", point(&s, "S"));
|
||
run(&mut s, "construct.point.center", json!({"of": {"face": [12, 60, 0]}, "name": "T"}));
|
||
assert!(near(point(&s, "T"), Vec3::new(0.0, 60.0, 0.0)), "{:?}", point(&s, "T"));
|
||
}
|
||
|
||
#[test]
|
||
fn point_at_edge_and_plane() {
|
||
let mut s = part();
|
||
run(
|
||
&mut s,
|
||
"construct.point.edge_plane",
|
||
json!({"edge": {"edge": [20, 0, 0]}, "plane": {"plane": {"origin": [12, 0, 0], "normal": [1, 0, 0]}}, "name": "P"}),
|
||
);
|
||
assert!(near(point(&s, "P"), Vec3::new(12.0, 0.0, 0.0)), "{:?}", point(&s, "P"));
|
||
}
|
||
|
||
#[test]
|
||
fn point_along_a_path() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.point.along_path", json!({"path": {"edge": [20, 0, 0]}, "t": 0.25, "name": "P"}));
|
||
let p = point(&s, "P");
|
||
assert!(near(p, Vec3::new(10.0, 0.0, 0.0)) || near(p, Vec3::new(30.0, 0.0, 0.0)), "{p:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn midplane_between_parallel_and_meeting_faces() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.plane.midplane", json!({"a": "XY", "b": {"face": [20, 15, 20]}, "name": "M"}));
|
||
let m = plane(&s, "M");
|
||
assert!(parallel(m.normal(), Vec3::Z) && on_plane(&m, Vec3::new(5.0, 5.0, 10.0)), "{m:?}");
|
||
// Two sides meeting at the corner (40, 30): the diagonal plane through it.
|
||
run(&mut s, "construct.plane.midplane", json!({"a": {"face": [40, 15, 10]}, "b": {"face": [20, 30, 10]}, "name": "Q"}));
|
||
let q = plane(&s, "Q");
|
||
assert!(on_plane(&q, Vec3::new(40.0, 30.0, 0.0)) && on_plane(&q, Vec3::new(41.0, 31.0, 5.0)), "{q:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn plane_through_two_edges() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.plane.two_edges", json!({"a": {"edge": [20, 0, 0]}, "b": {"edge": [20, 30, 20]}, "name": "E"}));
|
||
let e = plane(&s, "E");
|
||
// The front-bottom and back-top edges: the slanted plane through both.
|
||
assert!(on_plane(&e, Vec3::new(5.0, 0.0, 0.0)) && on_plane(&e, Vec3::new(5.0, 30.0, 20.0)), "{e:?}");
|
||
assert!(s.execute("construct.plane.two_edges", &json!({"a": {"edge": [20, 0, 0]}, "b": {"edge": [40, 15, 20]}})).is_err(), "skew");
|
||
}
|
||
|
||
#[test]
|
||
fn plane_through_three_points() {
|
||
let mut s = part();
|
||
run(
|
||
&mut s,
|
||
"construct.plane.three_points",
|
||
json!({"a": {"vertex": [0, 0, 20]}, "b": {"vertex": [40, 0, 20]}, "c": {"vertex": [40, 30, 0]}, "name": "T"}),
|
||
);
|
||
let t = plane(&s, "T");
|
||
assert!(on_plane(&t, Vec3::new(0.0, 0.0, 20.0)) && on_plane(&t, Vec3::new(40.0, 30.0, 0.0)) && on_plane(&t, Vec3::new(0.0, 30.0, 0.0)), "{t:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn plane_tangent_to_a_cylinder() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.plane.tangent", json!({"face": {"face": [65, 0, 5]}, "name": "T"}));
|
||
let t = plane(&s, "T");
|
||
assert!(parallel(t.normal(), Vec3::X) && on_plane(&t, Vec3::new(65.0, 3.0, 9.0)), "{t:?}");
|
||
// And to the sphere, where the pick lies over it.
|
||
run(&mut s, "construct.plane.tangent", json!({"face": {"face": [0, 0, 65]}, "name": "U"}));
|
||
let u = plane(&s, "U");
|
||
assert!(parallel(u.normal(), Vec3::Z) && on_plane(&u, Vec3::new(3.0, 3.0, 65.0)), "{u:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn plane_along_a_path() {
|
||
let mut s = part();
|
||
run(&mut s, "construct.plane.along_path", json!({"path": {"edge": [20, 0, 0]}, "t": 0.5, "name": "P"}));
|
||
let p = plane(&s, "P");
|
||
assert!(parallel(p.normal(), Vec3::X) && on_plane(&p, Vec3::new(20.0, 7.0, 3.0)), "{p:?}");
|
||
// A sketch can be placed on it.
|
||
run(&mut s, "sketch.create", json!({"plane": "P", "name": "OnPath"}));
|
||
}
|
||
|
||
/// Built from model geometry, they follow when the model changes: the box gets taller.
|
||
#[test]
|
||
fn construction_follows_the_model() {
|
||
let mut s = Session::default();
|
||
run(&mut s, "parameters.add", json!({"name": "h", "expression": "20 mm"}));
|
||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": "h", "body_name": "Box"}));
|
||
run(
|
||
&mut s,
|
||
"construct.point.three_planes",
|
||
json!({"a": {"face": [40, 15, 10]}, "b": {"face": [20, 30, 10]}, "c": {"face": [20, 15, 20]}, "name": "Corner"}),
|
||
);
|
||
run(&mut s, "construct.axis.edge", json!({"edge": {"edge": [20, 30, 20]}, "name": "TopBack"}));
|
||
run(&mut s, "construct.plane.midplane", json!({"a": "XY", "b": {"face": [20, 15, 20]}, "name": "Mid"}));
|
||
run(&mut s, "parameters.change", json!({"name": "h", "expression": "50 mm"}));
|
||
assert!(near(point(&s, "Corner"), Vec3::new(40.0, 30.0, 50.0)), "{:?}", point(&s, "Corner"));
|
||
let (o, d) = axis(&s, "TopBack");
|
||
assert!(parallel(d, Vec3::X) && on_line(o, d, Vec3::new(0.0, 30.0, 50.0)), "{o:?} {d:?}");
|
||
assert!(on_plane(&plane(&s, "Mid"), Vec3::new(1.0, 1.0, 25.0)));
|
||
// Construction from construction: an axis through two construction points.
|
||
run(&mut s, "construct.point.vertex", json!({"point": {"vertex": [0, 0, 0]}, "name": "Origin"}));
|
||
run(&mut s, "construct.axis.two_points", json!({"a": {"construct_point": "Origin"}, "b": {"construct_point": "Corner"}, "name": "Diag"}));
|
||
let (o, d) = axis(&s, "Diag");
|
||
assert!(on_line(o, d, Vec3::new(40.0, 30.0, 50.0)) && on_line(o, d, Vec3::ZERO));
|
||
}
|
||
|
||
/// In a component moved 50 mm along X, picks given in the world land on its geometry, and the
|
||
/// axis and point move with the occurrence.
|
||
#[test]
|
||
fn construction_lives_in_the_component_frame() {
|
||
let mut s = Session::default();
|
||
run(&mut s, "component.create", json!({"name": "Part"}));
|
||
run(&mut s, "solid.cylinder", json!({"radius": 5, "height": 10, "body_name": "Pin"}));
|
||
run(&mut s, "occurrence.move", json!({"occurrence": "Part:1", "translate": [50, 0, 0]}));
|
||
run(&mut s, "construct.axis.through_cylinder", json!({"face": {"face": [55, 0, 5]}, "name": "PinAxis"}));
|
||
run(&mut s, "construct.point.center", json!({"of": {"edge": [55, 0, 10]}, "name": "PinTop"}));
|
||
let (o, d) = axis(&s, "PinAxis");
|
||
assert!(parallel(d, Vec3::Z) && on_line(o, d, Vec3::new(50.0, 0.0, 0.0)), "{o:?} {d:?}");
|
||
assert!(near(point(&s, "PinTop"), Vec3::new(50.0, 0.0, 10.0)), "{:?}", point(&s, "PinTop"));
|
||
run(&mut s, "occurrence.move", json!({"occurrence": "Part:1", "translate": [0, 20, 0]}));
|
||
assert!(near(point(&s, "PinTop"), Vec3::new(50.0, 20.0, 10.0)), "{:?}", point(&s, "PinTop"));
|
||
}
|