forked from 1b-agent/opencadstudio-upstream
Cover the face-to-plane path with tests
UCS FACE had no test at all: it was only known to compile. The plane it builds has two halves and they sit either side of a visibility boundary, so the coverage splits to match. tests/ucs_face_plane.rs takes the public route — extrude a disc, then check the caps come back as distinct planar faces with opposite outward normals, that an off-centre pick lands on the same face as a central one, and that the curved wall is never offered as a plane. The axis construction is pub(crate), so it is unit-tested next to ucs_from_normal instead: Z is the normal it was handed, the axes stay orthonormal, a normal along world Z still yields a plane rather than crossing with itself, the same normal is reproducible, and a zero vector is refused rather than silently producing a degenerate UCS.
This commit is contained in:
2 files changed
+155
No files matched your search
@@ -611,3 +611,78 @@ pub(super) fn build_window_icon() -> Option<Vec<u8>> {
|
|||||||
resvg::render(&tree, transform, &mut pixmap.as_mut());
|
resvg::render(&tree, transform, &mut pixmap.as_mut());
|
||||||
Some(pixmap.take())
|
Some(pixmap.take())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod ucs_from_normal_tests {
|
||||||
|
use super::ucs_from_normal;
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
fn axes(ucs: &acadrust::tables::Ucs) -> (DVec3, DVec3, DVec3) {
|
||||||
|
let x = DVec3::new(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z);
|
||||||
|
let y = DVec3::new(ucs.y_axis.x, ucs.y_axis.y, ucs.y_axis.z);
|
||||||
|
(x, y, x.cross(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contract `UCS FACE` depends on: the plane's Z is the face normal it
|
||||||
|
/// was handed. If this drifts, sketches tilt off the face they were
|
||||||
|
/// started on.
|
||||||
|
#[test]
|
||||||
|
fn z_axis_is_the_supplied_normal() {
|
||||||
|
for normal in [
|
||||||
|
DVec3::Z,
|
||||||
|
DVec3::NEG_Z,
|
||||||
|
DVec3::X,
|
||||||
|
DVec3::Y,
|
||||||
|
DVec3::new(1.0, 2.0, 3.0).normalize(),
|
||||||
|
] {
|
||||||
|
let ucs = ucs_from_normal(DVec3::ZERO, normal).expect("a normal defines a plane");
|
||||||
|
let (_, _, z) = axes(&ucs);
|
||||||
|
assert!(
|
||||||
|
(z - normal).length() < 1e-9,
|
||||||
|
"expected Z {normal:?}, got {z:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Right-handed and unit length, or every coordinate read off the plane is
|
||||||
|
/// skewed.
|
||||||
|
#[test]
|
||||||
|
fn axes_are_orthonormal() {
|
||||||
|
for normal in [DVec3::Z, DVec3::new(-2.0, 0.5, 1.0).normalize()] {
|
||||||
|
let ucs = ucs_from_normal(DVec3::ZERO, normal).unwrap();
|
||||||
|
let (x, y, z) = axes(&ucs);
|
||||||
|
for (name, axis) in [("X", x), ("Y", y), ("Z", z)] {
|
||||||
|
assert!((axis.length() - 1.0).abs() < 1e-9, "{name} is not unit");
|
||||||
|
}
|
||||||
|
assert!(x.dot(y).abs() < 1e-9, "X and Y are not perpendicular");
|
||||||
|
assert!(x.dot(z).abs() < 1e-9, "X and Z are not perpendicular");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The 1/64 branch exists so a normal along world Z does not cross-product
|
||||||
|
/// with itself. Both sides of that branch must still produce a usable
|
||||||
|
/// plane.
|
||||||
|
#[test]
|
||||||
|
fn a_normal_along_world_z_still_yields_a_plane() {
|
||||||
|
let ucs = ucs_from_normal(DVec3::ZERO, DVec3::Z).expect("world Z is a valid normal");
|
||||||
|
let (x, _, _) = axes(&ucs);
|
||||||
|
assert!(x.length() > 0.5, "X degenerated next to the world Z axis");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same face must always give the same X axis, or a sketch would spin
|
||||||
|
/// each time the UCS was rebuilt from it.
|
||||||
|
#[test]
|
||||||
|
fn the_same_normal_is_reproducible() {
|
||||||
|
let normal = DVec3::new(0.3, -0.7, 0.2).normalize();
|
||||||
|
let first = axes(&ucs_from_normal(DVec3::ZERO, normal).unwrap());
|
||||||
|
let second = axes(&ucs_from_normal(DVec3::ZERO, normal).unwrap());
|
||||||
|
assert!((first.0 - second.0).length() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A zero vector defines no plane; it must be refused rather than
|
||||||
|
/// producing a degenerate UCS that silently misplaces geometry.
|
||||||
|
#[test]
|
||||||
|
fn a_degenerate_normal_is_rejected() {
|
||||||
|
assert!(ucs_from_normal(DVec3::ZERO, DVec3::ZERO).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
//! `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 acadrust::{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() -> cadkernel::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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user