#![allow(clippy::unwrap_used)]
use crate::geometry::nurbs::{NurbsSurfaceAxis, NurbsSurfaceLanes};
use cadmpeg_test_support::edit;
const EPS_CURVE_INVERSE_WITNESS: f64 = 1.0e-12;
use crate::eval::model_curve_differential_by_id;
use crate::eval::model_curve_point_by_id;
use crate::eval::model_surface_partials_by_id;
use crate::eval::model_surface_point;
use crate::eval::model_surface_point_by_id;
use crate::eval::model_surface_second_partials_by_id;
use crate::eval::nurbs_curve_parameter_near_point;
use crate::eval::nurbs_curve_speed_bound;
use crate::eval::nurbs_surface_isoline;
use crate::eval::nurbs_surface_parameter_near_point;
use crate::eval::nurbs_surface_parameter_segment_chord_bound;
use crate::eval::nurbs_surface_parameter_within_tolerance;
use crate::eval::nurbs_surface_parameter_within_tolerance_with_budget;
use crate::eval::rolling_ball_jet_point;
use crate::eval::IsolineDirection;
use crate::geometry::{
nurbs::{NurbsCurve, NurbsSurface},
sampled::{PolylineCurve, PolylineSamples, PolylineVertex},
Curve, CurveGeometry, LawExpression, LegacyExtensionFlags, OffsetExtension, ProceduralSurface,
ProceduralSurfaceDefinition, RecordBounds, RevisionCacheForm, RevisionSurfaceForm,
RevisionSurfaceParameterization, RollingBallJetDerivative, RollingBallJetSite,
SolvedCurveGeometry, SolvedSurfaceGeometry, Surface, SurfaceGeometry,
};
use crate::ids::{CurveId, EdgeId, PointId, ProceduralSurfaceId, SurfaceId, VertexId};
use crate::math::{Point2, Point3, Vector3};
use crate::topology::{Edge, Point, Vertex};
use crate::transform::Transform;
use crate::CadIr;
use cadmpeg_core::decode::WorkBudget;
macro_rules! procedural_surface {
(
id: $id:expr,
definition: $definition:expr,
cache_fit_tolerance: $cache_fit_tolerance:expr,
record_bounds: $record_bounds:expr $(,)?
) => {{
let mut definition = $definition;
definition
.set_legacy_cache($cache_fit_tolerance.map(|value: f64| {
crate::geometry::LegacyCache::try_new(value)
.expect("admissible fit tolerance fixture")
}))
.expect("valid procedural surface cache fixture");
ProceduralSurface::new($id, definition, $record_bounds)
}};
}
macro_rules! procedural_curve {
(
id: $id:expr,
definition: $definition:expr,
cache_fit_tolerance: $cache_fit_tolerance:expr $(,)?
) => {{
let mut definition = $definition;
let cache_fit_tolerance: Option<f64> = $cache_fit_tolerance;
match cache_fit_tolerance {
Some(value) => definition
.set_legacy_cache(
crate::geometry::LegacyCache::try_new(value)
.expect("admissible fit tolerance fixture"),
)
.expect("valid procedural curve cache fixture"),
None => definition.clear_legacy_cache(),
}
ProceduralCurve::new($id, definition)
}};
}
mod helix;
mod law_sweep;
mod model_index;
mod overflowing_arms;
mod overflowing_curve_arms;
mod pcurves;
mod procedural_curves;
mod ruled_sum;
mod surface_derivative_context;
mod surface_parameter_bounds;
mod variable_blend;
const EPS_DEGREE_ZERO_SURFACE_BOUND: f64 = 1.0e-12;
fn contact_track(point: Point3, tangent: Vector3) -> crate::eval::ContactTrack {
let finite = |vector| crate::features::FiniteVector3::new(vector).unwrap();
crate::eval::ContactTrack {
support: crate::eval::SurfaceFirstOrder {
point: crate::features::FinitePoint3::new(point).unwrap(),
first: Ok([finite(tangent), finite(Vector3::new(0.0, 0.0, 0.0))]),
},
uv_tangent: Ok(
crate::units::FinitePoint2::new(crate::math::Point2::new(1.0, 0.0)).unwrap(),
),
normal_derivative: Err(crate::eval::EvaluationFailure::NoValue),
}
}
fn law_operand(value: f64, derivative: f64) -> crate::eval::sweep_law::ScalarSweepDifferential {
crate::eval::sweep_law::ScalarSweepDifferential {
value: crate::scalar::FiniteReal::new(value).unwrap(),
derivative: Ok(crate::scalar::FiniteReal::new(derivative).unwrap()),
}
}
fn bilinear_surface() -> NurbsSurface {
NurbsSurface::from_lanes(
NurbsSurfaceAxis::new(1, vec![0.0, 0.0, 1.0, 1.0], false),
NurbsSurfaceAxis::new(1, vec![0.0, 0.0, 1.0, 1.0], false),
NurbsSurfaceLanes::new(
vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)],
vec![Point3::new(1.0, 0.0, 0.0), Point3::new(1.0, 1.0, 0.0)],
],
None,
),
false,
)
.unwrap()
}
#[test]
fn periodic_nurbs_surface_coordinates_reduce_into_the_knot_domain() {
let mut surface = bilinear_surface();
{
let replacement = true;
edit::replace(&mut surface, |previous| {
NurbsSurface::new(
NurbsSurfaceAxis::new(
previous.u_degree(),
previous.u_knots().to_vec(),
replacement,
),
NurbsSurfaceAxis::new(
previous.v_degree(),
previous.v_knots().to_vec(),
previous.v_periodic(),
),
previous.pole_grid().clone(),
previous.normal_reversed(),
)
})
.unwrap();
};
let expected = crate::eval::decode::nurbs_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
0.25,
0.75,
)
.expect("in-domain surface point");
assert_eq!(
crate::eval::decode::nurbs_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
1.25,
0.75
),
Ok(expected)
);
assert_eq!(
crate::eval::decode::nurbs_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
-0.75,
0.75
),
Ok(expected)
);
{
let replacement = false;
edit::replace(&mut surface, |previous| {
NurbsSurface::new(
NurbsSurfaceAxis::new(
previous.u_degree(),
previous.u_knots().to_vec(),
replacement,
),
NurbsSurfaceAxis::new(
previous.v_degree(),
previous.v_knots().to_vec(),
previous.v_periodic(),
),
previous.pole_grid().clone(),
previous.normal_reversed(),
)
})
.unwrap();
};
assert_ne!(
crate::eval::decode::nurbs_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
1.25,
0.75
),
Ok(expected)
);
}
#[test]
fn rolling_ball_jet_evaluation_interpolates_spine_and_sweeps_arc() {
const TEST_TOLERANCE: f64 = 1e-12;
let derivative = RollingBallJetDerivative {
first_limit: Vector3::new(1.0 / 3.0, 0.0, 0.0),
second_limit: Vector3::new(1.0 / 3.0, 0.0, 0.0),
center: Vector3::new(1.0 / 3.0, 0.0, 0.0),
angle: 0.0,
};
let definition = ProceduralSurfaceDefinition::RollingBallJet(
crate::geometry::RollingBallJetStations::try_new(
5,
vec![
crate::geometry::RollingBallJetStation {
knot: 2.0,
multiplicity: 6,
site: RollingBallJetSite {
first_limit: Point3::new(2.0, 0.0, 0.0),
second_limit: Point3::new(0.0, 2.0, 0.0),
center: Point3::new(0.0, 0.0, 0.0),
angle: std::f64::consts::FRAC_PI_2,
first_derivative: derivative,
second_derivative: RollingBallJetDerivative {
first_limit: Vector3::new(0.0, 0.0, 0.0),
second_limit: Vector3::new(0.0, 0.0, 0.0),
center: Vector3::new(0.0, 0.0, 0.0),
angle: 0.0,
},
},
},
crate::geometry::RollingBallJetStation {
knot: 5.0,
multiplicity: 6,
site: RollingBallJetSite {
first_limit: Point3::new(3.0, 0.0, 0.0),
second_limit: Point3::new(1.0, 2.0, 0.0),
center: Point3::new(1.0, 0.0, 0.0),
angle: std::f64::consts::FRAC_PI_2,
first_derivative: derivative,
second_derivative: RollingBallJetDerivative {
first_limit: Vector3::new(0.0, 0.0, 0.0),
second_limit: Vector3::new(0.0, 0.0, 0.0),
center: Vector3::new(0.0, 0.0, 0.0),
angle: 0.0,
},
},
},
],
)
.unwrap(),
);
let point = rolling_ball_jet_point(&definition, 3.5, 0.5).expect("jet point");
let expected = Point3::new(0.5 + 2.0_f64.sqrt(), 2.0_f64.sqrt(), 0.0);
assert!((point.x - expected.x).abs() <= TEST_TOLERANCE);
assert!((point.y - expected.y).abs() <= TEST_TOLERANCE);
assert!((point.z - expected.z).abs() <= TEST_TOLERANCE);
let start = rolling_ball_jet_point(&definition, 2.0, 0.0).expect("jet start");
assert!((start.x - 2.0).abs() <= TEST_TOLERANCE);
assert!(start.y.abs() <= TEST_TOLERANCE);
assert!(start.z.abs() <= TEST_TOLERANCE);
assert!(rolling_ball_jet_point(&definition, 5.0, 1.0).is_ok());
assert_eq!(
rolling_ball_jet_point(&definition, 1.0, 0.5),
Err(crate::eval::EvaluationFailure::NoValue)
);
}
#[test]
fn rolling_ball_jet_evaluation_uses_fixed_radius_frame() {
const TEST_TOLERANCE: f64 = 1e-12;
let zero = RollingBallJetDerivative {
first_limit: Vector3::new(0.0, 0.0, 0.0),
second_limit: Vector3::new(0.0, 0.0, 0.0),
center: Vector3::new(0.0, 0.0, 0.0),
angle: 0.0,
};
let definition = ProceduralSurfaceDefinition::RollingBallJet(
crate::geometry::RollingBallJetStations::try_new(
5,
vec![
crate::geometry::RollingBallJetStation {
knot: 2.0,
multiplicity: 6,
site: RollingBallJetSite {
first_limit: Point3::new(2.0, 0.0, 0.0),
second_limit: Point3::new(0.0, 2.0, 0.0),
center: Point3::new(0.0, 0.0, 0.0),
angle: std::f64::consts::FRAC_PI_2,
first_derivative: zero,
second_derivative: zero,
},
},
crate::geometry::RollingBallJetStation {
knot: 5.0,
multiplicity: 6,
site: RollingBallJetSite {
first_limit: Point3::new(0.0, 2.0, 0.0),
second_limit: Point3::new(-2.0, 0.0, 0.0),
center: Point3::new(0.0, 0.0, 0.0),
angle: std::f64::consts::FRAC_PI_2,
first_derivative: zero,
second_derivative: zero,
},
},
],
)
.unwrap(),
);
let point = rolling_ball_jet_point(&definition, 3.5, 0.5).expect("jet point");
let root_two = 2.0_f64.sqrt();
let expected = Point3::new(root_two / 2.0 - 1.0, root_two / 2.0 + 1.0, 0.0);
assert!((point.x - expected.x).abs() <= TEST_TOLERANCE);
assert!((point.y - expected.y).abs() <= TEST_TOLERANCE);
assert!((point.z - expected.z).abs() <= TEST_TOLERANCE);
}
#[test]
fn budgeted_nurbs_surface_evaluation_charges_degree_work() {
let surface = bilinear_surface();
let budget = WorkBudget::new(3);
assert!(crate::eval::admission::EvaluationAdmission::Standard
.within_work_slice(&budget, |admission| {
crate::eval::decode::nurbs_surface_point(admission, &surface, 0.25, 0.75)
})
.is_err());
assert!(budget.exhausted());
let budget = WorkBudget::new(12);
assert!(crate::eval::admission::EvaluationAdmission::Standard
.within_work_slice(&budget, |admission| {
crate::eval::decode::nurbs_surface_point(admission, &surface, 0.25, 0.75)
})
.is_ok());
assert_eq!(budget.consumed(), 12);
let budget = WorkBudget::new(27);
assert!(crate::eval::admission::EvaluationAdmission::Standard
.within_work_slice(&budget, |admission| crate::eval::nurbs_surface_partials(
admission, &surface, 0.25, 0.75
))
.is_err());
assert!(budget.exhausted());
let budget = WorkBudget::new(28);
assert!(crate::eval::admission::EvaluationAdmission::Standard
.within_work_slice(&budget, |admission| crate::eval::nurbs_surface_partials(
admission, &surface, 0.25, 0.75
))
.is_ok());
assert_eq!(budget.consumed(), 28);
let transformed = SolvedSurfaceGeometry::Transformed(
crate::geometry::PlacedSurface::try_new(
Box::new(SolvedSurfaceGeometry::Nurbs(surface)),
Transform::affine([
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 1.0],
])
.expect("affine transform"),
)
.expect("placed surface"),
);
let budget = WorkBudget::new(12);
assert!(crate::eval::admission::EvaluationAdmission::Standard
.within_work_slice(&budget, |admission| crate::eval::decode::surface_point(
admission,
&SurfaceGeometry::Solved(transformed.clone()),
0.25,
0.75
))
.is_err_and(|failure| failure == crate::eval::EvaluationFailure::WorkExhausted));
assert!(budget.exhausted());
let budget = WorkBudget::new(13);
assert_eq!(
crate::eval::admission::EvaluationAdmission::Standard
.within_work_slice(&budget, |admission| crate::eval::decode::surface_point(
admission,
&SurfaceGeometry::Solved(transformed.clone()),
0.25,
0.75
))
.map(crate::features::FinitePoint3::get),
Ok(Point3::new(0.25, 0.75, 1.0))
);
assert_eq!(budget.consumed(), 13);
}
#[test]
fn budgeted_model_surface_charges_nurbs_directrix_work() {
let directrix_id =
CurveId::mint("test:model:entity#budgeted-directrix").expect("valid identity");
let surface_id = SurfaceId::mint("test:model:entity#budgeted-sweep").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: directrix_id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(
NurbsCurve::from_lanes(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
None,
false,
)
.unwrap(),
)),
source_object: None,
});
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Unknown { record: None }),
source_object: None,
});
ir.model
.add_procedural_surface(&surface_id, procedural_surface! {
id: ProceduralSurfaceId::mint("test:model:entity#budgeted-sweep-construction").expect("valid identity"),
definition: ProceduralSurfaceDefinition::LinearSweep(
crate::geometry::surface_payloads::LinearSweepSurfaceConstruction::try_new(
directrix_id,
Vector3::new(0.0, 0.0, 1.0),
)
.expect("valid linear sweep"),
),
cache_fit_tolerance: None,
record_bounds: None,
}).unwrap();
let index = crate::index::ModelIndex::build(&ir);
let budget = WorkBudget::new(5);
assert_eq!(
crate::eval::admission::EvaluationAdmission::Standard.within_work_slice(
&budget,
|admission| model_surface_point_by_id(admission, &index, &surface_id, 0.25, 2.0)
),
Err(crate::eval::EvaluationFailure::WorkExhausted)
);
assert!(budget.exhausted());
let budget = WorkBudget::new(6);
assert_eq!(
crate::eval::admission::EvaluationAdmission::Standard
.within_work_slice(&budget, |admission| model_surface_point_by_id(
admission,
&index,
&surface_id,
0.25,
2.0
))
.map(crate::features::FinitePoint3::get),
Ok(Point3::new(0.25, 0.0, 2.0))
);
assert_eq!(budget.consumed(), 6);
}
#[test]
fn degree_zero_nurbs_surface_has_an_exact_parameter_segment_bound() {
let surface = NurbsSurface::from_lanes(
NurbsSurfaceAxis::new(0, vec![0.0, 1.0], false),
NurbsSurfaceAxis::new(0, vec![0.0, 1.0], false),
NurbsSurfaceLanes::new(vec![vec![Point3::new(1.0, 2.0, 3.0)]], None),
false,
)
.unwrap();
let point = Point3::new(1.0, 2.0, 3.0);
assert_eq!(
crate::eval::decode::nurbs_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
0.25,
0.75
)
.ok()
.map(crate::features::FinitePoint3::get),
Some(point)
);
let bound = nurbs_surface_parameter_segment_chord_bound(
&cadmpeg_test_support::service_decode_context(),
&surface,
[Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)],
[point, point],
)
.expect("bound the NURBS surface segment's chord deviation")
.expect("degree-zero surface bound");
assert!(bound <= EPS_DEGREE_ZERO_SURFACE_BOUND, "{bound}");
}
#[test]
fn degree_zero_nurbs_surface_patch_spans_use_their_matching_poles() {
let surface = NurbsSurface::from_lanes(
NurbsSurfaceAxis::new(0, vec![0.0, 1.0, 2.0], false),
NurbsSurfaceAxis::new(0, vec![0.0, 1.0], false),
NurbsSurfaceLanes::new(
vec![
vec![Point3::new(1.0, 2.0, 3.0)],
vec![Point3::new(4.0, 5.0, 6.0)],
],
None,
),
false,
)
.unwrap();
let poles = [Point3::new(1.0, 2.0, 3.0), Point3::new(4.0, 5.0, 6.0)];
for (range, pole) in [([0.0, 1.0], poles[0]), ([1.0, 2.0], poles[1])] {
let bound = nurbs_surface_parameter_segment_chord_bound(
&cadmpeg_test_support::service_decode_context(),
&surface,
[Point2::new(range[0], 0.0), Point2::new(range[1], 1.0)],
[pole, pole],
)
.expect("bound the NURBS surface segment's chord deviation")
.expect("degree-zero patch span bound");
assert!(bound <= EPS_DEGREE_ZERO_SURFACE_BOUND, "{bound}");
}
}
#[test]
fn nurbs_surface_parameter_segment_bound_splits_internal_knots() {
let surface = NurbsSurface::from_lanes(
NurbsSurfaceAxis::new(1, vec![0.0, 0.0, 0.5, 1.0, 1.0], false),
NurbsSurfaceAxis::new(1, vec![0.0, 0.0, 1.0, 1.0], false),
NurbsSurfaceLanes::new(
vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)],
vec![Point3::new(0.5, 0.0, 0.25), Point3::new(0.5, 1.0, 0.25)],
vec![Point3::new(1.0, 0.0, 0.0), Point3::new(1.0, 1.0, 0.0)],
],
Some(vec![1.0, 1.0, 0.5, 0.5, 1.0, 1.0])
.map(|values| values.chunks(2_usize).map(<[_]>::to_vec).collect()),
),
false,
)
.unwrap();
let parameters = [Point2::new(0.1, 0.2), Point2::new(0.9, 0.8)];
let endpoints = parameters.map(|point| {
crate::eval::decode::nurbs_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
point.u,
point.v,
)
.expect("surface endpoint")
.get()
});
let bound = nurbs_surface_parameter_segment_chord_bound(
&cadmpeg_test_support::service_decode_context(),
&surface,
parameters,
endpoints,
)
.expect("bound the NURBS surface segment's chord deviation")
.expect("multi-span rational Bézier residual bound");
for index in 0..=100 {
let parameter = f64::from(index) / 100.0;
let uv = Point2::new(
parameters[0].u + parameter * (parameters[1].u - parameters[0].u),
parameters[0].v + parameter * (parameters[1].v - parameters[0].v),
);
let point = crate::eval::decode::nurbs_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
uv.u,
uv.v,
)
.expect("surface point");
let target = Point3::new(
endpoints[0].x + parameter * (endpoints[1].x - endpoints[0].x),
endpoints[0].y + parameter * (endpoints[1].y - endpoints[0].y),
endpoints[0].z + parameter * (endpoints[1].z - endpoints[0].z),
);
let distance = (point.x - target.x)
.hypot(point.y - target.y)
.hypot(point.z - target.z);
assert!(distance <= bound);
}
}
#[test]
fn direct_analytic_curve_inverses_preserve_native_parameters() {
let geometries = [
SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(1.0, 2.0, 3.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
),
SolvedCurveGeometry::Circle(
crate::geometry::analytic::CircleCurve::try_new(
Point3::new(1.0, 2.0, 3.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
4.0,
)
.unwrap(),
),
SolvedCurveGeometry::Ellipse(
crate::geometry::analytic::EllipseCurve::try_new(
Point3::new(1.0, 2.0, 3.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
4.0,
2.0,
)
.unwrap(),
),
SolvedCurveGeometry::Parabola(
crate::geometry::analytic::ParabolaCurve::try_new(
Point3::new(1.0, 2.0, 3.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
2.0,
)
.unwrap(),
),
SolvedCurveGeometry::Hyperbola(
crate::geometry::analytic::HyperbolaCurve::try_new(
Point3::new(1.0, 2.0, 3.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
4.0,
2.0,
)
.unwrap(),
),
];
for (index, geometry) in geometries.into_iter().enumerate() {
let parameter = if matches!(
&geometry,
SolvedCurveGeometry::Circle(_) | SolvedCurveGeometry::Ellipse(_)
) {
0.7 + std::f64::consts::TAU
} else {
0.7
};
let point = crate::eval::decode::curve_point(
crate::eval::admission::EvaluationAdmission::Standard,
&CurveGeometry::Solved(geometry.clone()),
parameter,
)
.expect("analytic curve evaluates");
let id = CurveId::mint(format!("test:inverse:curve#{index}")).expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: id.clone(),
geometry: CurveGeometry::Solved(geometry.clone()),
source_object: None,
});
let inverse = crate::eval::model_curve_parameter_near_point_in_index(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::build(&ir),
&id,
point.get(),
parameter,
)
.expect("find the model curve parameter near the point")
.expect("direct analytic inverse")
.get();
assert!((inverse - parameter).abs() < 1.0e-12);
}
}
#[test]
fn polyline_inverse_searches_every_segment_in_native_parameter_space() {
let cases = [
(
SolvedCurveGeometry::Polyline(
PolylineCurve::new(
PolylineSamples::Unparameterized {
points: vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
]
.try_into()
.expect("nonempty polyline fixture"),
},
0.0,
)
.unwrap(),
),
Point3::new(0.5, 0.0, 0.0),
0.5,
0.5,
),
(
SolvedCurveGeometry::Polyline(
PolylineCurve::new(
PolylineSamples::Parameterized {
vertices: vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
]
.into_iter()
.zip(vec![4.0, 2.0, 0.0])
.map(|(point, parameter)| PolylineVertex { parameter, point })
.collect::<Vec<_>>()
.try_into()
.expect("nonempty polyline fixture"),
},
0.0,
)
.unwrap(),
),
Point3::new(1.0, 0.5, 0.0),
1.0,
1.0,
),
(
SolvedCurveGeometry::Polyline(
PolylineCurve::new(
PolylineSamples::Parameterized {
vertices: vec![
Point3::new(2.0, 3.0, 4.0),
Point3::new(2.0, 3.0, 4.0),
Point3::new(5.0, 3.0, 4.0),
]
.into_iter()
.zip(vec![0.0, 1.0, 2.0])
.map(|(point, parameter)| PolylineVertex { parameter, point })
.collect::<Vec<_>>()
.try_into()
.expect("nonempty polyline fixture"),
},
0.0,
)
.unwrap(),
),
Point3::new(2.0, 3.0, 4.0),
0.7,
0.7,
),
];
for (index, (geometry, point, seed, expected)) in cases.into_iter().enumerate() {
let id =
CurveId::mint(format!("test:polyline-inverse:curve#{index}")).expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: id.clone(),
geometry: CurveGeometry::Solved(geometry),
source_object: None,
});
let inverse = crate::eval::model_curve_parameter_near_point_in_index(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::build(&ir),
&id,
point,
seed,
)
.expect("find the model curve parameter near the point")
.expect("polyline inverse")
.get();
assert!((inverse - expected).abs() < 1.0e-12);
}
}
#[test]
fn indexed_curve_inverse_uses_the_caller_tolerance() {
let id = CurveId::mint("test:model:entity#test:inverse-tolerance").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
)),
source_object: None,
});
let index = crate::index::ModelIndex::build(&ir);
let point = Point3::new(0.5, 0.005, 0.0);
assert!(super::model_curve_parameter_near_point_in_index(
&cadmpeg_test_support::service_decode_context(),
&index,
&id,
point,
0.5
)
.expect("find the model curve parameter near the point")
.is_none());
let inverse = super::model_curve_parameter_near_point_in_index_with_tolerance(
&cadmpeg_test_support::service_decode_context(),
&index,
&id,
point,
0.5,
0.01,
)
.expect("find the model curve parameter near the point")
.expect("caller tolerance admits the bounded residual")
.get();
assert!((inverse - 0.5).abs() < 1.0e-12);
}
#[test]
fn transformed_curve_inverse_uses_the_basis_parameterization() {
let basis = SolvedCurveGeometry::Circle(
crate::geometry::analytic::CircleCurve::try_new(
Point3::new(1.0, 2.0, 3.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
4.0,
)
.unwrap(),
);
let transform = Transform::affine([
[-2.0, 0.0, 0.0, 1.0e6],
[0.0, 0.5, 0.0, -2.0e6],
[0.0, 0.0, 3.0, 3.0e6],
])
.expect("affine transform");
let geometry = SolvedCurveGeometry::Transformed(
crate::geometry::PlacedCurve::try_new(Box::new(basis.clone()), transform)
.expect("placed curve"),
);
let parameter = 0.7 + std::f64::consts::TAU;
let point = crate::eval::decode::curve_point(
crate::eval::admission::EvaluationAdmission::Standard,
&CurveGeometry::Solved(geometry.clone()),
parameter,
)
.expect("transformed curve evaluates");
let id = CurveId::mint("test:model:entity#test:transformed-inverse").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: id.clone(),
geometry: CurveGeometry::Solved(geometry.clone()),
source_object: None,
});
let inverse = crate::eval::model_curve_parameter_near_point_in_index(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::build(&ir),
&id,
point.get(),
parameter,
)
.expect("find the model curve parameter near the point")
.expect("transformed inverse")
.get();
assert!((inverse - parameter).abs() < 1.0e-10);
ir.model.curves[0].geometry = CurveGeometry::Solved(SolvedCurveGeometry::Transformed(
crate::geometry::PlacedCurve::try_new(
Box::new(basis),
Transform::affine([
[0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
])
.expect("affine transform"),
)
.expect("placed curve"),
));
assert!(crate::eval::model_curve_parameter_near_point_in_index(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::build(&ir),
&id,
Point3::new(0.0, 0.0, 0.0),
0.0
)
.expect("find the model curve parameter near the point")
.is_none());
}
#[test]
fn degenerate_curve_inverse_preserves_the_selected_parameter() {
let point = Point3::new(2.0, 3.0, 4.0);
let id = CurveId::mint("test:model:entity#test:degenerate-inverse").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Degenerate(
crate::geometry::analytic::DegenerateCurve::try_new(point).unwrap(),
)),
source_object: None,
});
let seed = 123.5;
assert_eq!(
crate::eval::model_curve_parameter_near_point_in_index(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::build(&ir),
&id,
point,
seed
)
.expect("find the model curve parameter near the point")
.map(crate::scalar::FiniteReal::get),
Some(seed)
);
assert!(crate::eval::model_curve_parameter_near_point_in_index(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::build(&ir),
&id,
Point3::new(2.0, 3.0, 5.0),
seed
)
.expect("find the model curve parameter near the point")
.is_none());
}
#[test]
fn a_surface_isoline_reproduces_the_surface_along_its_free_parameter() {
let surface = NurbsSurface::from_lanes(
NurbsSurfaceAxis::new(2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], false),
NurbsSurfaceAxis::new(1, vec![-2.0, -2.0, 3.0, 3.0], false),
NurbsSurfaceLanes::new(
vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 4.0)],
vec![Point3::new(1.0, 2.0, 0.5), Point3::new(1.0, 2.0, 4.5)],
vec![Point3::new(3.0, -1.0, 1.0), Point3::new(3.0, -1.0, 5.0)],
],
Some(vec![1.0, 2.0, 0.5, 1.5, 3.0, 0.25])
.map(|values| values.chunks(2_usize).map(<[_]>::to_vec).collect()),
),
false,
)
.unwrap();
for (direction, at, samples) in [
(IsolineDirection::ConstantU, 0.4, [-2.0, 0.75, 3.0]),
(IsolineDirection::ConstantV, 1.25, [0.0, 0.6, 1.0]),
] {
let curve = nurbs_surface_isoline(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
direction,
at,
)
.expect("compute the NURBS surface isoline")
.expect("isoline");
for sample in samples {
let (u, v) = match direction {
IsolineDirection::ConstantU => (at, sample),
IsolineDirection::ConstantV => (sample, at),
};
let expected = crate::eval::decode::nurbs_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
u,
v,
)
.expect("surface point");
let actual = crate::eval::decode::nurbs_curve_point_at(
crate::eval::admission::EvaluationAdmission::Standard,
&curve,
sample,
)
.expect("curve point");
for (left, right) in [
(actual.x, expected.x),
(actual.y, expected.y),
(actual.z, expected.z),
] {
assert!((left - right).abs() <= 1.0e-12, "{left} vs {right}");
}
}
}
}
#[test]
fn recursive_offsets_use_exact_support_normals_at_large_parameters() {
let support_id = SurfaceId::mint("test:model:entity#support").expect("valid identity");
let first_id = SurfaceId::mint("test:model:entity#first-offset").expect("valid identity");
let second_id = SurfaceId::mint("test:model:entity#second-offset").expect("valid identity");
let first_construction =
ProceduralSurfaceId::mint("test:model:entity#first-construction").expect("valid identity");
let second_construction =
ProceduralSurfaceId::mint("test:model:entity#second-construction").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.surfaces = vec![
Surface {
id: support_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(
crate::geometry::analytic::PlaneSurface::try_new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
)),
source_object: None,
},
Surface {
id: first_id.clone(),
geometry: SurfaceGeometry::Procedural {
construction: first_construction.clone(),
cache: None,
},
source_object: None,
},
Surface {
id: second_id.clone(),
geometry: SurfaceGeometry::Procedural {
construction: second_construction.clone(),
cache: None,
},
source_object: None,
},
];
ir.model.procedural_surfaces = vec![
procedural_surface! {
id: first_construction,
definition: ProceduralSurfaceDefinition::Offset(crate::geometry::surface_payloads::OffsetSurfaceConstruction::try_new(support_id, 2.0, None, None, false, OffsetExtension::Legacy { flags: LegacyExtensionFlags::Absent {}, cache: None }).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
},
procedural_surface! {
id: second_construction,
definition: ProceduralSurfaceDefinition::Offset(crate::geometry::surface_payloads::OffsetSurfaceConstruction::try_new(first_id, -5.0, None, None, false, OffsetExtension::Legacy { flags: LegacyExtensionFlags::Absent {}, cache: None }).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
},
];
let index = crate::index::ModelIndex::build(&ir);
assert_eq!(
model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&second_id,
1.0e16,
-1.0e16
)
.map(crate::features::FinitePoint3::get),
Ok(Point3::new(1.0e16, -1.0e16, -3.0))
);
let budget = WorkBudget::new(2);
assert_eq!(
crate::eval::admission::EvaluationAdmission::Standard
.within_work_slice(&budget, |admission| model_surface_point_by_id(
admission, &index, &second_id, 1.0e16, -1.0e16
)),
Err(crate::eval::EvaluationFailure::WorkExhausted)
);
assert!(budget.exhausted());
let budget = WorkBudget::new(3);
assert_eq!(
crate::eval::admission::EvaluationAdmission::Standard
.within_work_slice(&budget, |admission| model_surface_point_by_id(
admission, &index, &second_id, 1.0e16, -1.0e16
))
.map(crate::features::FinitePoint3::get),
Ok(Point3::new(1.0e16, -1.0e16, -3.0))
);
assert_eq!(budget.consumed(), 3);
let partials = model_surface_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&second_id,
1.0e16,
-1.0e16,
)
.expect("transformed plane evaluates");
assert_eq!(partials.point, Point3::new(1.0e16, -1.0e16, -3.0));
assert_eq!(partials.du, Vector3::new(1.0, 0.0, 0.0));
assert_eq!(partials.dv, Vector3::new(0.0, 1.0, 0.0));
}
#[test]
fn linear_offset_support_extension_uses_the_boundary_tangent_plane() {
let support_id = SurfaceId::mint("test:model:entity#support").expect("valid identity");
let offset_id = SurfaceId::mint("test:model:entity#offset").expect("valid identity");
let construction =
ProceduralSurfaceId::mint("test:model:entity#offset-construction").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.surfaces = vec![
Surface {
id: support_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Nurbs(
NurbsSurface::from_lanes(
NurbsSurfaceAxis::new(1, vec![0.0, 0.0, 1.0, 1.0], false),
NurbsSurfaceAxis::new(2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], false),
NurbsSurfaceLanes::new(
[0.0, 1.0]
.into_iter()
.map(|u| {
[(0.0, 0.0), (0.5, 0.0), (1.0, 1.0)]
.into_iter()
.map(|(v, z)| Point3::new(u, v, z))
.collect()
})
.collect(),
None,
),
false,
)
.unwrap(),
)),
source_object: None,
},
Surface {
id: offset_id.clone(),
geometry: SurfaceGeometry::Procedural {
construction: construction.clone(),
cache: None,
},
source_object: None,
},
];
ir.model.procedural_surfaces.push(procedural_surface! {
id: construction,
definition: ProceduralSurfaceDefinition::Offset(crate::geometry::surface_payloads::OffsetSurfaceConstruction::try_new(support_id, 0.0, None, None, true, OffsetExtension::Legacy { flags: LegacyExtensionFlags::Absent {}, cache: None }).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
});
let index = crate::index::ModelIndex::build(&ir);
let point = model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&offset_id,
0.25,
1.2,
)
.expect("linearly extended offset")
.get();
let epsilon = 64.0 * f64::EPSILON;
assert!((point.x - 0.25).abs() <= epsilon);
assert!((point.y - 1.2).abs() <= epsilon);
assert!((point.z - 1.4).abs() <= epsilon);
}
#[test]
fn offset_uses_the_nurbs_carrier_normal_orientation() {
let support_id = SurfaceId::mint("test:model:entity#support").expect("valid identity");
let offset_id = SurfaceId::mint("test:model:entity#offset").expect("valid identity");
let construction =
ProceduralSurfaceId::mint("test:model:entity#offset-construction").expect("valid identity");
let mut support = bilinear_surface();
{
let replacement = true;
edit::replace(&mut support, |previous| {
NurbsSurface::new(
NurbsSurfaceAxis::new(
previous.u_degree(),
previous.u_knots().to_vec(),
previous.u_periodic(),
),
NurbsSurfaceAxis::new(
previous.v_degree(),
previous.v_knots().to_vec(),
previous.v_periodic(),
),
previous.pole_grid().clone(),
replacement,
)
})
.unwrap();
};
let mut ir = CadIr::empty();
ir.model.surfaces = vec![
Surface {
id: support_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Nurbs(support)),
source_object: None,
},
Surface {
id: offset_id.clone(),
geometry: SurfaceGeometry::Procedural {
construction: construction.clone(),
cache: None,
},
source_object: None,
},
];
ir.model.procedural_surfaces.push(procedural_surface! {
id: construction,
definition: ProceduralSurfaceDefinition::Offset(crate::geometry::surface_payloads::OffsetSurfaceConstruction::try_new(support_id, 2.0, None, None, false, OffsetExtension::Legacy { flags: LegacyExtensionFlags::Absent {}, cache: None }).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
});
let point = model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&crate::index::ModelIndex::build(&ir),
&offset_id,
0.2,
0.3,
)
.expect("oriented offset point")
.get();
let expected = Point3::new(0.2, 0.3, -2.0);
let epsilon = 64.0 * f64::EPSILON;
assert!((point.x - expected.x).abs() <= epsilon);
assert!((point.y - expected.y).abs() <= epsilon);
assert!((point.z - expected.z).abs() <= epsilon);
}
#[test]
fn offset_of_reversed_subset_uses_the_local_surface_normal() {
let base_id = SurfaceId::mint("test:model:entity#base").expect("valid identity");
let subset_id = SurfaceId::mint("test:model:entity#subset").expect("valid identity");
let offset_id = SurfaceId::mint("test:model:entity#offset").expect("valid identity");
let subset_construction =
ProceduralSurfaceId::mint("test:model:entity#subset-construction").expect("valid identity");
let offset_construction =
ProceduralSurfaceId::mint("test:model:entity#offset-construction").expect("valid identity");
let plane = SolvedSurfaceGeometry::Plane(
crate::geometry::analytic::PlaneSurface::try_new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
);
let mut ir = CadIr::empty();
ir.model.surfaces = vec![
Surface {
id: base_id.clone(),
geometry: SurfaceGeometry::Solved(plane.clone()),
source_object: None,
},
Surface {
id: subset_id.clone(),
geometry: SurfaceGeometry::Solved(plane),
source_object: None,
},
Surface {
id: offset_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Unknown { record: None }),
source_object: None,
},
];
ir.model
.add_procedural_surface(&subset_id, procedural_surface! {
id: subset_construction,
definition: ProceduralSurfaceDefinition::Subset(crate::geometry::surface_payloads::SubsetSurfaceConstruction::try_new(base_id, [[0.0, 1.0], [0.0, 1.0]], Some(false), Some(true), None).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
})
.expect("subset surface exists and has no procedural construction");
ir.model
.add_procedural_surface(&offset_id, procedural_surface! {
id: offset_construction,
definition: ProceduralSurfaceDefinition::Offset(crate::geometry::surface_payloads::OffsetSurfaceConstruction::try_new(subset_id, 2.0, None, None, false, OffsetExtension::Legacy { flags: LegacyExtensionFlags::Absent {}, cache: None }).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
})
.expect("offset surface exists and has no procedural construction");
let index = crate::index::ModelIndex::build(&ir);
assert_eq!(
model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&offset_id,
0.25,
0.5
)
.map(crate::features::FinitePoint3::get),
Ok(Point3::new(-0.25, 0.5, -2.0))
);
let partials = model_surface_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&offset_id,
0.25,
0.5,
)
.expect("offset of a reversed subset evaluates");
assert_eq!(partials.point, Point3::new(-0.25, 0.5, -2.0));
assert_eq!(partials.du, Vector3::new(-1.0, 0.0, 0.0));
assert_eq!(partials.dv, Vector3::new(0.0, 1.0, 0.0));
}
#[test]
fn curve_bounded_surface_delegates_evaluation_to_its_support() {
let support_id =
SurfaceId::mint("test:model:entity#curve-bounded-support").expect("valid identity");
let bounded_id = SurfaceId::mint("test:model:entity#curve-bounded").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.surfaces = vec![
Surface {
id: support_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(
crate::geometry::analytic::PlaneSurface::try_new(
Point3::new(1.0, 2.0, 3.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
)),
source_object: None,
},
Surface {
id: bounded_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Unknown { record: None }),
source_object: None,
},
];
ir.model
.add_procedural_surface(&bounded_id, procedural_surface! {
id: ProceduralSurfaceId::mint("test:model:entity#curve-bounded-construction").expect("valid identity"),
definition: ProceduralSurfaceDefinition::CurveBounded {
support: support_id,
boundaries: Vec::new(),
boundary_pcurves: Vec::new(),
implicit_outer: true,
},
cache_fit_tolerance: None,
record_bounds: None,
}).unwrap();
let index = crate::index::ModelIndex::build(&ir);
assert_eq!(
model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&bounded_id,
0.25,
0.75
)
.map(crate::features::FinitePoint3::get),
Ok(Point3::new(1.25, 2.75, 3.0))
);
let partials = model_surface_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&bounded_id,
0.25,
0.75,
)
.expect("curve-bounded support evaluates");
assert_eq!(partials.point, Point3::new(1.25, 2.75, 3.0));
assert_eq!(partials.du, Vector3::new(1.0, 0.0, 0.0));
assert_eq!(partials.dv, Vector3::new(0.0, 1.0, 0.0));
}
#[test]
fn linear_sweep_surface_evaluation_uses_directrix_and_sweep_parameters() {
let directrix_id = CurveId::mint("test:model:entity#directrix").expect("valid identity");
let surface_id = SurfaceId::mint("test:model:entity#sweep").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: directrix_id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Transformed(
crate::geometry::PlacedCurve::try_new(
Box::new(SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
)),
crate::transform::Transform::affine([
[2.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 2.0],
[0.0, 0.0, 1.0, 3.0],
])
.unwrap(),
)
.expect("placed curve"),
)),
source_object: None,
});
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Unknown { record: None }),
source_object: None,
});
ir.model
.add_procedural_surface(&surface_id, procedural_surface! {
id: ProceduralSurfaceId::mint("test:model:entity#sweep-construction").expect("valid identity"),
definition: ProceduralSurfaceDefinition::LinearSweep(
crate::geometry::surface_payloads::LinearSweepSurfaceConstruction::try_new(
directrix_id,
Vector3::new(0.0, 0.0, 1.0),
)
.expect("valid linear sweep"),
),
cache_fit_tolerance: None,
record_bounds: None,
}).unwrap();
let index = crate::index::ModelIndex::build(&ir);
let point = model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
0.5,
4.0,
)
.expect("linear sweep point")
.get();
assert_eq!(point, Point3::new(2.0, 2.0, 7.0));
let partials = model_surface_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
0.5,
4.0,
)
.expect("linear sweep partials");
assert_eq!(partials.point, point);
assert_eq!(partials.du, Vector3::new(2.0, 0.0, 0.0));
assert_eq!(partials.dv, Vector3::new(0.0, 0.0, 1.0));
let second_partials = model_surface_second_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
0.5,
4.0,
)
.expect("linear sweep second partials");
assert_eq!(second_partials.duu, Vector3::new(0.0, 0.0, 0.0));
assert_eq!(second_partials.duv, Vector3::new(0.0, 0.0, 0.0));
assert_eq!(second_partials.dvv, Vector3::new(0.0, 0.0, 0.0));
}
#[test]
fn cacheless_revision_extrusion_uses_the_directrix_sense_chart() {
let directrix_id =
CurveId::mint("test:model:entity#reversed-directrix").expect("valid identity");
let surface_id =
SurfaceId::mint("test:model:entity#cacheless-extrusion").expect("valid identity");
let construction_id =
ProceduralSurfaceId::mint("test:model:entity#cacheless-extrusion-construction")
.expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: directrix_id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(
NurbsCurve::from_lanes(
1,
vec![0.0, 0.0, 2.0, 2.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 0.0, 0.0)],
None,
false,
)
.unwrap(),
)),
source_object: None,
});
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: SurfaceGeometry::Procedural {
construction: construction_id.clone(),
cache: None,
},
source_object: None,
});
ir.model.procedural_surfaces.push(procedural_surface! {
id: construction_id,
definition: ProceduralSurfaceDefinition::Extrusion(crate::geometry::surface_payloads::ExtrusionSurfaceConstruction::try_new(directrix_id, Some([-2.0, 0.0]), Vector3::new(0.0, 0.0, 1.0), Some(Point3::new(0.0, 0.0, 0.0)), crate::geometry::CacheContract::from_form(Some(RevisionSurfaceForm {
revision: crate::scalar::PositiveI64::new(1).expect("positive revision"),
support_bounds: [None; 4],
reference_endpoints: [None; 2],
second_endpoints: [None; 2],
flags: vec![true],
cache: RevisionCacheForm::Parameterization(
RevisionSurfaceParameterization::default(),
),
discontinuities: Default::default(),
tail_flag: false,
trailing_flags: Vec::new(),
}))).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
});
let index = crate::index::ModelIndex::build(&ir);
let partials = model_surface_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
-0.5,
3.0,
)
.expect("cacheless reversed extrusion point");
assert_eq!(partials.point, Point3::new(0.5, 0.0, 3.0));
assert_eq!(partials.du, Vector3::new(-1.0, 0.0, 0.0));
assert_eq!(partials.dv, Vector3::new(0.0, 0.0, 1.0));
}
#[test]
fn cacheless_law_sweep_evaluation_uses_text_law_and_identity_rail() {
let (ir, surface_id) = law_sweep::law_sweep_model(LawExpression::Text {
value: cadmpeg_core::nonblank_literal!("2.0*X"),
});
let index = crate::index::ModelIndex::build(&ir);
let expected = Point3::new(0.5, -0.5, 0.25);
assert_eq!(
model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
0.5,
0.25
)
.map(crate::features::FinitePoint3::get),
Ok(expected)
);
assert_eq!(
model_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&ir,
&ir.model.surfaces[0].geometry,
0.5,
0.25
)
.map(crate::features::FinitePoint3::get),
Ok(expected)
);
let partials = model_surface_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
0.5,
0.25,
)
.expect("cacheless sweep partials");
assert_eq!(partials.point, expected);
assert_eq!(partials.du, Vector3::new(1.0, 0.0, 0.0));
assert_eq!(partials.dv, Vector3::new(0.0, -2.0, 1.0));
}
#[test]
fn axis_revolution_surface_evaluation_rotates_the_profile_parameterization() {
let directrix_id = CurveId::mint("test:model:entity#profile").expect("valid identity");
let surface_id = SurfaceId::mint("test:model:entity#revolution").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: directrix_id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Transformed(
crate::geometry::PlacedCurve::try_new(
Box::new(SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(2.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
)
.unwrap(),
)),
Transform::identity(),
)
.expect("placed curve"),
)),
source_object: None,
});
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Unknown { record: None }),
source_object: None,
});
ir.model
.add_procedural_surface(&surface_id, procedural_surface! {
id: ProceduralSurfaceId::mint("test:model:entity#revolution-construction").expect("valid identity"),
definition: ProceduralSurfaceDefinition::AxisRevolution(
crate::geometry::surface_payloads::AxisRevolutionSurfaceConstruction::try_new(
directrix_id,
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
)
.expect("valid axis revolution"),
),
cache_fit_tolerance: None,
record_bounds: None,
}).unwrap();
let index = crate::index::ModelIndex::build(&ir);
let point = model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
std::f64::consts::FRAC_PI_2,
1.5,
)
.expect("axis revolution point")
.get();
assert!(point.x.abs() < 1.0e-12);
assert!((point.y - 2.0).abs() < 1.0e-12);
assert!((point.z - 1.5).abs() < 1.0e-12);
let partials = model_surface_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
std::f64::consts::FRAC_PI_2,
1.5,
)
.expect("axis revolution partials");
assert!((partials.du.x + 2.0).abs() < 1.0e-12);
assert!(partials.du.y.abs() < 1.0e-12);
assert_eq!(partials.dv, Vector3::new(0.0, 0.0, 1.0));
let second_partials = model_surface_second_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
std::f64::consts::FRAC_PI_2,
1.5,
)
.expect("axis revolution second partials");
assert!((second_partials.duu.y + 2.0).abs() < 1.0e-12);
assert!(second_partials.duv.norm() < 1.0e-12);
assert!(second_partials.dvv.norm() < 1.0e-12);
}
#[test]
fn revolution_surface_maps_its_angular_parameter_interval() {
let directrix_id = CurveId::mint("test:model:entity#mapped-profile").expect("valid identity");
let surface_id =
SurfaceId::mint("test:model:entity#mapped-revolution").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: directrix_id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(2.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
)
.unwrap(),
)),
source_object: None,
});
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Unknown { record: None }),
source_object: None,
});
ir.model
.add_procedural_surface(&surface_id, procedural_surface! {
id: ProceduralSurfaceId::mint("test:model:entity#mapped-revolution-construction").expect("valid identity"),
definition: ProceduralSurfaceDefinition::Revolution(crate::geometry::surface_payloads::RevolutionSurfaceConstruction::try_new(directrix_id, (crate::features::FinitePoint3::ZERO, crate::units::UnitVector3::Z_AXIS), [0.0, std::f64::consts::PI], Some([10.0, 14.0]), None, false, crate::geometry::CacheContract::from_form(None)).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
}).unwrap();
let index = crate::index::ModelIndex::build(&ir);
let partials = model_surface_second_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
1.5,
12.0,
)
.expect("mapped revolution partials");
assert!(partials.point.x.abs() < 1.0e-12);
assert!((partials.point.y - 2.0).abs() < 1.0e-12);
assert!((partials.point.z - 1.5).abs() < 1.0e-12);
assert_eq!(partials.du, Vector3::new(0.0, 0.0, 1.0));
assert!((partials.dv.x + std::f64::consts::FRAC_PI_2).abs() < 1.0e-12);
assert!(partials.dv.y.abs() < 1.0e-12);
assert!(partials.dvv.x.abs() < 1.0e-12);
assert!((partials.dvv.y + std::f64::consts::PI.powi(2) / 8.0).abs() < 1.0e-12);
assert!(partials.duv.norm() < 1.0e-12);
}
#[test]
fn revolution_over_wide_angular_parameter_interval_maps_interior_angle() {
let directrix_id =
CurveId::mint("test:model:entity#wide-angle-profile").expect("valid identity");
let surface_id =
SurfaceId::mint("test:model:entity#wide-angle-revolution").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: directrix_id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(2.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
)
.unwrap(),
)),
source_object: None,
});
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Unknown { record: None }),
source_object: None,
});
ir.model.add_procedural_surface(&surface_id, procedural_surface! {
id: ProceduralSurfaceId::mint("test:model:entity#wide-angle-construction").expect("valid identity"),
definition: ProceduralSurfaceDefinition::Revolution(
crate::geometry::surface_payloads::RevolutionSurfaceConstruction::try_new(
directrix_id,
(crate::features::FinitePoint3::ZERO, crate::units::UnitVector3::Z_AXIS),
[0.0, std::f64::consts::PI],
Some([-f64::MAX, f64::MAX]),
None,
false,
crate::geometry::CacheContract::from_form(None),
).unwrap()
),
cache_fit_tolerance: None,
record_bounds: None,
}).unwrap();
let index = crate::index::ModelIndex::build(&ir);
let point = model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
0.0,
0.0,
)
.expect("wide mapped revolution point")
.get();
assert!(point.x.abs() < 1.0e-12);
assert!((point.y - 2.0).abs() < 1.0e-12);
assert_eq!(point.z, 0.0);
}
#[test]
fn revolution_surface_maps_a_normalized_line_domain_to_its_distance_carrier() {
let directrix_id =
CurveId::mint("test:model:entity#normalized-profile").expect("valid identity");
let surface_id =
SurfaceId::mint("test:model:entity#normalized-revolution").expect("valid identity");
let start_point_id =
PointId::mint("test:model:entity#normalized-profile-start-point").expect("valid identity");
let end_point_id =
PointId::mint("test:model:entity#normalized-profile-end-point").expect("valid identity");
let start_vertex_id = VertexId::mint("test:model:entity#normalized-profile-start-vertex")
.expect("valid identity");
let end_vertex_id =
VertexId::mint("test:model:entity#normalized-profile-end-vertex").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.curves.push(Curve {
parameter_range: None,
id: directrix_id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(2.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
)
.unwrap(),
)),
source_object: None,
});
ir.model.points.extend([
Point::new(
start_point_id.clone(),
crate::features::FinitePoint3::new(Point3::new(2.0, 0.0, 0.0))
.expect("a finite position is a point"),
None,
),
Point::new(
end_point_id.clone(),
crate::features::FinitePoint3::new(Point3::new(2.0, 0.0, 10.0))
.expect("a finite position is a point"),
None,
),
]);
ir.model.vertices.extend([
Vertex {
id: start_vertex_id.clone(),
point: start_point_id,
tolerance: None,
},
Vertex {
id: end_vertex_id.clone(),
point: end_point_id,
tolerance: None,
},
]);
ir.model.edges.push(Edge {
id: EdgeId::mint("test:model:entity#normalized-profile-edge").expect("valid identity"),
carrier: crate::topology::EdgeCarrier::new(Some(directrix_id.clone()), Some([0.0, 10.0]))
.unwrap(),
start: start_vertex_id,
end: end_vertex_id,
tolerance: None,
});
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Unknown { record: None }),
source_object: None,
});
ir.model
.add_procedural_surface(&surface_id, procedural_surface! {
id: ProceduralSurfaceId::mint("test:model:entity#normalized-revolution-construction").expect("valid identity"),
definition: ProceduralSurfaceDefinition::Revolution(crate::geometry::surface_payloads::RevolutionSurfaceConstruction::try_new(directrix_id, (crate::features::FinitePoint3::ZERO, crate::units::UnitVector3::Z_AXIS), [0.0, std::f64::consts::TAU], None, Some([0.0, 1.0]), false, crate::geometry::CacheContract::from_form(None)).unwrap()),
cache_fit_tolerance: None,
record_bounds: Some(RecordBounds::try_new([
Some(0.0),
Some(10.0),
None,
None,
])
.expect("finite record bounds")),
}).unwrap();
let index = crate::index::ModelIndex::build(&ir);
let point = model_surface_point_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
5.0,
0.0,
)
.expect("normalized line domain maps to distance carrier")
.get();
assert_eq!(point, Point3::new(2.0, 0.0, 5.0));
let partials = model_surface_partials_by_id(
crate::eval::admission::EvaluationAdmission::Standard,
&index,
&surface_id,
5.0,
0.0,
)
.expect("normalized line domain partials");
assert_eq!(partials.du, Vector3::new(0.0, 0.0, 1.0));
}
#[test]
fn nurbs_curve_inverse_uses_the_seed_to_select_an_ambiguous_witness() {
let curve = NurbsCurve::from_lanes(
1,
vec![0.0, 0.0, 0.5, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 0.0),
],
None,
false,
)
.unwrap();
let point = Point3::new(0.5, 0.0, 0.0);
assert_eq!(
nurbs_curve_parameter_near_point(
&cadmpeg_test_support::service_decode_context(),
&curve,
point,
EPS_CURVE_INVERSE_WITNESS,
0.1
)
.expect("find the NURBS curve parameter near the point")
.map(crate::scalar::FiniteReal::get),
Some(0.25)
);
assert_eq!(
nurbs_curve_parameter_near_point(
&cadmpeg_test_support::service_decode_context(),
&curve,
point,
EPS_CURVE_INVERSE_WITNESS,
0.9
)
.expect("find the NURBS curve parameter near the point")
.map(crate::scalar::FiniteReal::get),
Some(0.75)
);
assert_eq!(
nurbs_curve_parameter_near_point(
&cadmpeg_test_support::service_decode_context(),
&curve,
Point3::new(0.5, 1.0, 0.0),
EPS_CURVE_INVERSE_WITNESS,
0.5,
)
.expect("find the NURBS curve parameter near the point"),
None
);
assert!(nurbs_curve_speed_bound(&curve).is_some_and(|bound| bound.get() >= 2.0));
}
mod bounded_nurbs;
mod periodic_and_analytic;
mod numerical_audit;
mod numerical_followup;
mod construction_parameters;
mod analytic_ranges;
mod surface_inversion;
mod offset_frames;
mod isocurves;
mod admission_derivatives;
mod differentials;