Files
OneCAD-Draft/tests/ucs_face_plane.rs
T

200 lines
7.6 KiB
Rust

//! `UCS FACE` turns a pick on a solid into a drawing plane. The plane is only
//! as good as the face lookup underneath it, so this covers that half: given a
//! body and a point on one of its faces, does the right face come back, and is
//! its outward normal the one a sketch should be built on?
//!
//! The axis construction on top of that normal is unit-tested beside
//! `ucs_from_normal` in `src/app/helpers.rs`, which is `pub(crate)` and so out
//! of reach from here.
use codec::{entities::Circle, types::Vector3, EntityType};
use OpenCADStudio::scene::model::{presspull_model, solid_model};
/// Extrude a disc into a puck 10 units tall, centred on the origin.
fn puck() -> kernel::brep::Body {
let entity = EntityType::Circle(Circle::from_center_radius(
Vector3::new(0.0, 0.0, 0.0),
100.0,
));
let body = presspull_model::extrusion_body(&entity, [0.0, 0.0, 10.0])
.expect("a closed circular profile extrudes");
assert!(
body.validate().is_empty(),
"extrusion produced a valid body"
);
body
}
/// The two flat ends point away from the material, in opposite directions.
/// Getting this wrong is what would make a sketch land inside the solid, or
/// extrude back into it.
#[test]
fn opposite_faces_report_opposite_outward_normals() {
let body = puck();
let top =
solid_model::nearest_planar_face(&body, [0.0, 0.0, 10.0]).expect("the top cap is planar");
let bottom =
solid_model::nearest_planar_face(&body, [0.0, 0.0, 0.0]).expect("the bottom cap is planar");
assert_ne!(top, bottom, "the caps are distinct faces");
let top_normal = solid_model::planar_face_normal(&body, top).expect("top cap has a normal");
let bottom_normal =
solid_model::planar_face_normal(&body, bottom).expect("bottom cap has a normal");
assert!(
(top_normal[2] - 1.0).abs() < 1e-9,
"top cap should point +Z, got {top_normal:?}"
);
assert!(
(bottom_normal[2] + 1.0).abs() < 1e-9,
"bottom cap should point -Z, got {bottom_normal:?}"
);
}
/// A pick anywhere on a cap resolves to that cap, not merely to the one
/// nearest the origin. A sketch is started by clicking somewhere on a face,
/// rarely at its centre.
#[test]
fn an_off_centre_pick_still_lands_on_its_own_face() {
let body = puck();
let centre = solid_model::nearest_planar_face(&body, [0.0, 0.0, 10.0]).unwrap();
let off_centre = solid_model::nearest_planar_face(&body, [60.0, -25.0, 10.0]).unwrap();
assert_eq!(centre, off_centre, "both picks are on the top cap");
}
/// The curved side is not a plane, so it cannot become a sketch plane. The
/// lookup has to decline rather than hand back a nonsense normal.
#[test]
fn the_curved_side_is_not_offered_as_a_plane() {
let body = puck();
// Halfway up the barrel, on its surface.
let face = solid_model::nearest_planar_face(&body, [100.0, 0.0, 5.0]);
if let Some(face) = face {
// A planar cap may still be the nearest planar face; what must never
// happen is the barrel itself being reported as planar.
let normal = solid_model::planar_face_normal(&body, face)
.expect("whatever came back claims to be planar, so it has a normal");
assert!(
normal[2].abs() > 1e-9,
"a cap normal has Z; a barrel wall wrongly called planar would not"
);
}
}
#[test]
fn ucs_pick_command_enables_solid_face_picking() {
use glam::DVec3;
use OpenCADStudio::command::{CadCommand, CmdResult, UcsPickCommand};
let cmd_face = UcsPickCommand { face: true };
assert!(cmd_face.needs_entity_pick(), "UCS FACE needs entity pick");
assert!(
cmd_face.entity_pick_includes_fills(),
"UCS FACE must include fills to raycast solid faces"
);
assert!(
cmd_face.entity_pick_uses_surface_point(),
"UCS FACE must use 3D surface point"
);
assert!(
cmd_face.entity_pick_highlights_hover(),
"UCS FACE must highlight hovered solid face"
);
let cmd_obj = UcsPickCommand { face: false };
assert!(cmd_obj.needs_entity_pick());
assert!(
!cmd_obj.entity_pick_includes_fills(),
"UCS OBJECT operates on curves, not fills"
);
assert!(!cmd_obj.entity_pick_uses_surface_point());
let handle = codec::Handle::new(0x2A);
let pt = DVec3::new(10.5, 20.25, 30.125);
let mut cmd = cmd_face;
match cmd.on_entity_pick(handle, pt) {
CmdResult::Dispatch(cmd_str) => {
assert_eq!(cmd_str, "UCS FACE 2A 10.5,20.25,30.125");
}
_ => panic!("Expected CmdResult::Dispatch"),
}
}
#[test]
fn face_ucs_snaps_origin_and_aligns_axes_on_box() {
let box_body = solid_model::box_solid([0.0, 0.0, 0.0], 10.0, 20.0, 30.0)
.expect("box_solid constructs successfully");
// Top face is at Z = +15, bounds X in [-5, 5], Y in [-10, 10].
// Pick near corner (5, 10, 15), closer to the edge along -X than the edge along -Y.
let face =
solid_model::nearest_planar_face(&box_body, [3.0, 9.0, 15.0]).expect("top face found");
let ucs = solid_model::planar_face_ucs(&box_body, face, [3.0, 9.0, 15.0])
.expect("planar_face_ucs constructed");
// Origin must snap to the nearest corner (5, 10, 15) instead of the pick coordinate.
assert!((ucs.origin.x - 5.0).abs() < 1e-6);
assert!((ucs.origin.y - 10.0).abs() < 1e-6);
assert!((ucs.origin.z - 15.0).abs() < 1e-6);
// Z axis must be outward normal (0, 0, 1).
let z = glam::DVec3::new(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z).cross(glam::DVec3::new(
ucs.y_axis.x,
ucs.y_axis.y,
ucs.y_axis.z,
));
assert!((z.z - 1.0).abs() < 1e-6);
// Closer to edge along -X: X axis must point along (-1, 0, 0).
assert!((ucs.x_axis.x - (-1.0)).abs() < 1e-6);
assert!(ucs.x_axis.y.abs() < 1e-6);
assert!(ucs.x_axis.z.abs() < 1e-6);
// Y axis = Z x X = (0, 0, 1) x (-1, 0, 0) = (0, -1, 0).
assert!(ucs.y_axis.x.abs() < 1e-6);
assert!((ucs.y_axis.y - (-1.0)).abs() < 1e-6);
assert!(ucs.y_axis.z.abs() < 1e-6);
// Now pick near the same corner (5, 10, 15), but closer to the edge along -Y.
let ucs_y_edge = solid_model::planar_face_ucs(&box_body, face, [4.5, 7.0, 15.0])
.expect("planar_face_ucs constructed");
// Origin is still the same corner (5, 10, 15).
assert!((ucs_y_edge.origin.x - 5.0).abs() < 1e-6);
assert!((ucs_y_edge.origin.y - 10.0).abs() < 1e-6);
assert!((ucs_y_edge.origin.z - 15.0).abs() < 1e-6);
// Closer to edge along -Y: X axis must point along (0, -1, 0).
assert!(ucs_y_edge.x_axis.x.abs() < 1e-6);
assert!((ucs_y_edge.x_axis.y - (-1.0)).abs() < 1e-6);
assert!(ucs_y_edge.x_axis.z.abs() < 1e-6);
// Y axis = Z x X = (0, 0, 1) x (0, -1, 0) = (1, 0, 0).
assert!((ucs_y_edge.y_axis.x - 1.0).abs() < 1e-6);
assert!(ucs_y_edge.y_axis.y.abs() < 1e-6);
assert!(ucs_y_edge.y_axis.z.abs() < 1e-6);
}
#[test]
fn face_ucs_on_puck() {
let body = puck();
let top_face = solid_model::nearest_planar_face(&body, [60.0, -25.0, 10.0]).unwrap();
let ucs = solid_model::planar_face_ucs(&body, top_face, [60.0, -25.0, 10.0]).unwrap();
// Normal Z = (0, 0, 1).
let z = glam::DVec3::new(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z).cross(glam::DVec3::new(
ucs.y_axis.x,
ucs.y_axis.y,
ucs.y_axis.z,
));
assert!((z.z - 1.0).abs() < 1e-6);
// Orthonormality of X and Y.
let x = glam::DVec3::new(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z);
let y = glam::DVec3::new(ucs.y_axis.x, ucs.y_axis.y, ucs.y_axis.z);
assert!((x.length() - 1.0).abs() < 1e-6);
assert!((y.length() - 1.0).abs() < 1e-6);
assert!(x.dot(y).abs() < 1e-6);
}