#![allow(clippy::unwrap_used)]
use cadmpeg_test_support::edit;
use super::{
check_pcurve_surface_consistency, check_procedural_support_consistency,
edge_pcurve_parameter_ranges, mapped_pcurve_parameter_near_point, pcurve_parameter_domain,
pcurve_parameter_ranges, pcurve_parameter_seeds_on_surface, SurfacePcurveContext,
};
use crate::document::CadIr;
use crate::examples::unit_cube;
use crate::geometry::{
nurbs::NurbsSurface,
pcurve::{Pcurve, PcurveGeometry, PcurveMetadata},
Curve, CurveGeometry, DirectedParameterRange, IntcurveSupportContext, IntcurveSupportSide,
ProceduralCurve, ProceduralCurveDefinition, ProceduralSurface, ProceduralSurfaceDefinition,
SolvedCurveGeometry, SolvedSurfaceGeometry, SupportPcurve, Surface, SurfaceCurveFamily,
SurfaceGeometry,
};
use crate::ids::{CurveId, ProceduralCurveId, ProceduralSurfaceId, SurfaceId};
use crate::math::{Point2, Point3, Vector3};
use crate::report::{check::Check, Severity};
use crate::topology::{Coedge, Edge, Face, Loop, PcurveUse, Sense, Vertex};
use crate::validate::validate_neutral;
#[test]
fn wide_pcurve_domain_keeps_its_finite_midpoint_seed() {
let pcurve = Pcurve {
id: "test:model:pcurve#wide-domain".try_into().unwrap(),
geometry: PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::new(
1,
vec![-f64::MAX, -f64::MAX, f64::MAX * 0.5, f64::MAX * 0.5],
crate::geometry::pcurve::PcurveNurbsPoles::Polynomial {
points: vec![Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)],
},
false,
)
.unwrap(),
},
metadata: PcurveMetadata::default(),
};
assert!(super::pcurve_parameter_seeds(
&cadmpeg_test_support::service_decode_context(),
&pcurve
)
.unwrap()
.contains(&(-f64::MAX * 0.25)));
}
#[test]
fn large_surface_domain_keeps_its_finite_midpoint_pcurve_seed() {
let surface = SurfaceGeometry::Solved(SolvedSurfaceGeometry::Nurbs(
NurbsSurface::from_lanes(
crate::geometry::nurbs::NurbsSurfaceAxis::new(
1,
vec![f64::MAX * 0.75, f64::MAX * 0.75, f64::MAX, f64::MAX],
false,
),
crate::geometry::nurbs::NurbsSurfaceAxis::new(1, vec![0.0, 0.0, 1.0, 1.0], false),
crate::geometry::nurbs::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(),
));
let surface_id = SurfaceId::mint("test:model:surface#large-domain").unwrap();
let mut ir = CadIr::empty();
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: surface.clone(),
source_object: None,
});
let index = crate::index::ModelIndex::build(&ir);
let pcurve = Pcurve {
id: "test:model:pcurve#large-surface-domain".try_into().unwrap(),
geometry: PcurveGeometry::Line(
crate::geometry::pcurve::LinePcurve::try_new(
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
)
.unwrap(),
),
metadata: PcurveMetadata::default(),
};
let context = SurfacePcurveContext {
model: crate::validate::geometry_consistency::PassEvaluator {
index: &index,
pass: &crate::eval::admission::ModelEvaluationPass::new(),
},
surface_id: &surface_id,
geometry: &surface,
};
let midpoint = (f64::MAX * 0.75).midpoint(f64::MAX);
assert!(pcurve_parameter_seeds_on_surface(
&cadmpeg_test_support::service_decode_context(),
&context,
&pcurve
)
.unwrap()
.iter()
.any(|seed| seed.get() == midpoint));
}
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)
}};
}
fn mapped_surface_curve(mapping: [f64; 2]) -> CadIr {
mapped_surface_curve_with_pcurve(
PcurveGeometry::Line(
crate::geometry::pcurve::LinePcurve::try_new(
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
)
.unwrap(),
),
mapping,
)
}
fn mapped_surface_curve_with_pcurve(pcurve: PcurveGeometry, mapping: [f64; 2]) -> CadIr {
let mut ir = CadIr::empty();
let curve = CurveId::mint("test:model:curve#curve".to_string()).expect("valid identity");
let surface =
SurfaceId::mint("test:model:surface#surface".to_string()).expect("valid identity");
ir.model.curves.push(Curve {
parameter_range: None,
id: curve.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(2.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
)),
source_object: None,
});
ir.model.surfaces.push(Surface {
id: surface.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,
});
let construction = procedural_curve! {
id: ProceduralCurveId::mint("test:model:entity#surface-curve".to_string()).expect("valid identity"),
definition: ProceduralCurveDefinition::SurfaceCurve {
family: SurfaceCurveFamily::Parametric {
context: IntcurveSupportContext::try_new([
IntcurveSupportSide {
surface: Some(surface),
pcurve: Some(SupportPcurve::new(
pcurve,
Some(DirectedParameterRange::new(mapping).unwrap()),
)),
},
IntcurveSupportSide {
surface: None,
pcurve: None,
},
], [0.0, 1.0], std::array::from_fn(|_| Vec::new())).unwrap(),
tail: None,
},
},
cache_fit_tolerance: None,
};
ir.model.add_procedural_curve(&curve, construction).unwrap();
ir
}
fn mapped_surface_offset() -> CadIr {
let mut ir = mapped_surface_curve([2.0, 3.0]);
let base = CurveId::mint("test:model:entity#base".to_string()).expect("valid identity");
*ir.model.curves[0]
.geometry
.solved_cache_mut()
.expect("mapped curve has a solved cache") = SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(2.0, 0.0, 25.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
);
ir.model.curves.push(Curve {
parameter_range: None,
id: base.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 ProceduralCurveDefinition::SurfaceCurve { family } =
ir.model.procedural_curves[0].definition()
else {
unreachable!();
};
let context = family.context().clone();
ir.model.procedural_curves[0].replace_definition(ProceduralCurveDefinition::SurfaceOffset(
crate::geometry::curve_payloads::SurfaceOffsetCurveConstruction::try_new(
context,
false,
[[0.0, 1.0], [0.0, 1.0]],
(base, [2.0, 3.0], [Some(2.0), Some(3.0)]),
crate::geometry::CacheContract::from_form(None),
25.0,
[0.0, 1.0],
)
.unwrap(),
));
ir
}
fn untrimmed_surface_curve() -> CadIr {
let mut ir = CadIr::empty();
ir.model.points.extend([
crate::topology::Point::new(
"test:model:point#point-start"
.try_into()
.expect("valid identity"),
crate::features::FinitePoint3::new(Point3::new(0.0, 1.0, 0.0))
.expect("a finite position is a point"),
None,
),
crate::topology::Point::new(
"test:model:point#point-end"
.try_into()
.expect("valid identity"),
crate::features::FinitePoint3::new(Point3::new(-1.0, 0.0, 0.0))
.expect("a finite position is a point"),
None,
),
]);
ir.model.vertices.extend([
Vertex {
id: "test:model:vertex#vertex-start"
.try_into()
.expect("valid identity"),
point: "test:model:point#point-start"
.try_into()
.expect("valid identity"),
tolerance: None,
},
Vertex {
id: "test:model:vertex#vertex-end"
.try_into()
.expect("valid identity"),
point: "test:model:point#point-end"
.try_into()
.expect("valid identity"),
tolerance: None,
},
]);
ir.model.curves.push(Curve {
parameter_range: None,
id: "test:model:curve#curve".try_into().expect("valid identity"),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Circle(
crate::geometry::analytic::CircleCurve::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),
1.0,
)
.unwrap(),
)),
source_object: None,
});
ir.model.edges.push(Edge {
id: "test:model:edge#edge".try_into().expect("valid identity"),
carrier: crate::topology::EdgeCarrier::unbounded(Some(
"test:model:curve#curve".try_into().expect("valid identity"),
)),
start: "test:model:vertex#vertex-start"
.try_into()
.expect("valid identity"),
end: "test:model:vertex#vertex-end"
.try_into()
.expect("valid identity"),
tolerance: None,
});
ir.model.surfaces.push(Surface {
id: "test:model:surface#surface"
.try_into()
.expect("valid identity"),
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,
});
ir.model.pcurves.push(Pcurve {
id: "test:model:pcurve#pcurve"
.try_into()
.expect("valid identity"),
geometry: PcurveGeometry::Circle(
crate::geometry::pcurve::CirclePcurve::try_new(
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(0.0, 1.0),
1.0,
)
.unwrap(),
),
metadata: PcurveMetadata::default(),
});
ir.model.coedges.push(Coedge {
id: "test:model:coedge#coedge"
.try_into()
.expect("valid identity"),
owner_loop: "test:model:loop#loop".try_into().expect("valid identity"),
edge: "test:model:edge#edge".try_into().expect("valid identity"),
radial_next: "test:model:coedge#coedge"
.try_into()
.expect("valid identity"),
sense: Sense::Forward,
pcurves: vec![PcurveUse {
pcurve: "test:model:pcurve#pcurve"
.try_into()
.expect("valid identity"),
isoparametric: None,
parameter_range: None,
}],
use_curve: None,
});
ir.model.loops.push(Loop {
id: "test:model:loop#loop".try_into().expect("valid identity"),
face: "test:model:face#face".try_into().expect("valid identity"),
boundary: crate::topology::LoopBoundary::Ring(
crate::topology::LoopRing::new(
vec!["test:model:coedge#coedge"
.try_into()
.expect("valid identity")],
Vec::new(),
)
.expect("valid loop ring"),
),
});
ir.model.faces.push(Face {
id: "test:model:face#face".try_into().expect("valid identity"),
shell: "test:model:shell#shell".try_into().expect("valid identity"),
surface: "test:model:surface#surface"
.try_into()
.expect("valid identity"),
sense: Sense::Forward,
loops: crate::topology::FaceLoops::unspecified(vec!["test:model:loop#loop"
.try_into()
.expect("valid identity")]),
name: None,
color: None,
tolerance: None,
});
ir
}
#[test]
fn procedural_support_endpoints_honor_the_per_side_parameter_mapping() {
let mut findings = Vec::new();
check_procedural_support_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&mapped_surface_curve([2.0, 3.0])),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the procedural support is consistent");
assert!(findings.is_empty());
check_procedural_support_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&mapped_surface_curve([3.0, 2.0])),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the procedural support is consistent");
assert_eq!(findings.len(), 1);
assert!(findings[0].message.contains("support side 0"));
}
#[test]
fn surface_offset_support_constrains_the_embedded_base_curve() {
let mut findings = Vec::new();
check_procedural_support_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&mapped_surface_offset()),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the procedural support is consistent");
assert!(findings.is_empty());
let mut context_first = mapped_surface_offset();
context_first.model.procedural_curves[0].edit_definition(|definition| {
let ProceduralCurveDefinition::SurfaceOffset(definition_payload) = definition else {
unreachable!();
};
let restored_cache = definition_payload.legacy_cache();
*definition_payload =
crate::geometry::curve_payloads::SurfaceOffsetCurveConstruction::try_new(
definition_payload.context().clone(),
*definition_payload.discontinuity_flag(),
[
definition_payload.base_u_range().endpoints(),
definition_payload.base_v_range().endpoints(),
],
(
definition_payload.base().clone(),
definition_payload.base_range().endpoints(),
[None, None],
),
crate::geometry::CacheContract::from_form(
definition_payload
.cache_first()
.map(crate::geometry::CacheFirstCurveForm::to_raw),
),
definition_payload.distance().get(),
[
definition_payload.shift().get(),
definition_payload.scale().get(),
],
)
.unwrap();
match restored_cache {
Some(cache) => definition
.set_legacy_cache(cache)
.expect("a context-first surface offset states a legacy cache slot"),
None => definition.clear_legacy_cache(),
}
});
check_procedural_support_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&context_first),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the procedural support is consistent");
assert!(findings.is_empty());
let mut ir = mapped_surface_offset();
let CurveGeometry::Solved(SolvedCurveGeometry::Line(line_curve)) =
&mut ir.model.curves[1].geometry
else {
unreachable!();
};
let direction = line_curve.direction();
let mut origin = line_curve.origin().get();
origin.y = 2.0;
let origin = crate::features::FinitePoint3::new(origin).unwrap();
*line_curve = crate::geometry::analytic::LineCurve::new(origin, direction);
check_procedural_support_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&ir),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the procedural support is consistent");
assert_eq!(findings.len(), 2);
assert!(findings
.iter()
.any(|finding| finding.message.contains("base offset distance")));
assert!(findings
.iter()
.any(|finding| finding.message.contains("support side 0")));
}
#[test]
fn untrimmed_pcurve_uses_a_vertex_derived_parameter_interval() {
let ir = untrimmed_surface_curve();
let mut findings = Vec::new();
super::check_pcurve_surface_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&ir),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the pcurve is consistent with the surface");
assert!(findings.is_empty(), "{findings:#?}");
let mut mismatched = ir;
let PcurveGeometry::Circle(circle_pcurve) = &mut mismatched.model.pcurves[0].geometry else {
unreachable!();
};
let center = circle_pcurve.center();
let x_axis = circle_pcurve.x_axis();
let y_axis = circle_pcurve.y_axis();
*circle_pcurve = crate::geometry::pcurve::CirclePcurve::try_new(
center.get(),
x_axis.get(),
y_axis.get(),
2.0,
)
.unwrap();
super::check_pcurve_surface_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&mismatched),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the pcurve is consistent with the surface");
assert_eq!(findings.len(), 1);
assert!(findings[0].message.contains("pcurve mapped through"));
}
#[test]
fn trimmed_surface_pcurve_uses_the_local_parameterization_for_validation() {
let mut ir = untrimmed_surface_curve();
let base_id = SurfaceId::mint("test:model:entity#base-surface").expect("valid identity");
let base_geometry = ir.model.surfaces[0].geometry.clone();
ir.model.surfaces[0].id = base_id.clone();
ir.model.surfaces.push(Surface {
id: "test:model:surface#surface"
.try_into()
.expect("valid identity"),
geometry: base_geometry,
source_object: None,
});
let construction = procedural_surface! {
id: ProceduralSurfaceId::mint("test:model:entity#trimmed-surface").expect("valid identity"),
definition: ProceduralSurfaceDefinition::Subset(crate::geometry::surface_payloads::SubsetSurfaceConstruction::try_new(base_id, [[2.0, 0.0], [0.0, 2.0]], Some(false), Some(true), None).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
};
ir.model
.add_procedural_surface(
&SurfaceId::mint("test:model:surface#surface").expect("valid identity"),
construction,
)
.unwrap();
ir.model.points[0].set_position(
crate::features::FinitePoint3::new(Point3::new(1.0, 2.0, 0.0))
.expect("a finite position is a point"),
);
ir.model.points[1].set_position(
crate::features::FinitePoint3::new(Point3::new(2.0, 1.0, 0.0))
.expect("a finite position is a point"),
);
ir.model.curves[0].geometry = CurveGeometry::Solved(SolvedCurveGeometry::Circle(
crate::geometry::analytic::CircleCurve::try_new(
Point3::new(1.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
1.0,
)
.unwrap(),
));
ir.model.pcurves[0].geometry = PcurveGeometry::Circle(
crate::geometry::pcurve::CirclePcurve::try_new(
Point2::new(1.0, 1.0),
Point2::new(1.0, 0.0),
Point2::new(0.0, 1.0),
1.0,
)
.unwrap(),
);
let PcurveMetadata::General { form: metadata } = &mut ir.model.pcurves[0].metadata else {
panic!("fixture uses general pcurve metadata")
};
{
let replacement = Some([0.0, std::f64::consts::PI]);
edit::replace(metadata, |previous| {
crate::geometry::pcurve::PcurveGeneralForm::try_new(
previous.wrapper_reversed,
replacement,
previous
.fit_tolerance()
.map(crate::geometry::FitTolerance::get),
)
})
}
.unwrap();
let mut findings = Vec::new();
super::check_pcurve_surface_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&ir),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the pcurve is consistent with the surface");
assert!(findings.is_empty(), "{findings:#?}");
}
#[test]
fn untrimmed_nurbs_pcurve_uses_its_own_endpoint_parameters() {
let mut ir = untrimmed_surface_curve();
ir.model.pcurves[0].geometry = PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::from_lanes(
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![
Point2::new(0.0, 1.0),
Point2::new(-1.0, 1.0),
Point2::new(-1.0, 0.0),
],
Some(vec![1.0, 2.0_f64.sqrt() / 2.0, 1.0]),
false,
)
.unwrap(),
};
let mut findings = Vec::new();
super::check_pcurve_surface_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&ir),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the pcurve is consistent with the surface");
assert!(findings.is_empty(), "{findings:#?}");
}
#[test]
fn stale_trimmed_pcurve_range_can_use_a_vertex_derived_interval() {
let mut ir = untrimmed_surface_curve();
ir.model.points[0].set_position(
crate::features::FinitePoint3::new(Point3::new(0.25, 0.0, 0.0))
.expect("a finite position is a point"),
);
ir.model.points[1].set_position(
crate::features::FinitePoint3::new(Point3::new(0.0, 0.0, 0.0))
.expect("a finite position is a point"),
);
ir.model.pcurves[0].geometry = PcurveGeometry::Trimmed(
crate::geometry::pcurve::TrimmedPcurve::try_new(
[0.0, 1.0],
true,
Box::new(PcurveGeometry::Line(
crate::geometry::pcurve::LinePcurve::try_new(
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
)
.unwrap(),
)),
)
.unwrap(),
);
let mut findings = Vec::new();
super::check_pcurve_surface_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&ir),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the pcurve is consistent with the surface");
assert!(findings.is_empty(), "{findings:#?}");
}
#[test]
fn raw_nurbs_domain_is_not_treated_as_edge_trim() {
let pcurve = Pcurve {
id: "test:model:pcurve#pcurve"
.try_into()
.expect("valid identity"),
geometry: PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::from_lanes(
2,
vec![-1.0, 0.0, 0.0, 1.0, 1.0, 2.0],
vec![
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(2.0, 0.0),
],
None,
false,
)
.unwrap(),
},
metadata: PcurveMetadata::default(),
};
assert_eq!(
pcurve_parameter_domain(
&cadmpeg_test_support::service_decode_context(),
&pcurve.geometry
)
.unwrap()
.map(crate::topology::IncreasingParameterInterval::endpoints),
Some([0.0, 1.0])
);
assert!(pcurve_parameter_ranges(
&cadmpeg_test_support::service_decode_context(),
&pcurve,
None,
None
)
.unwrap()
.is_none());
}
#[test]
fn collapsed_trimmed_pcurve_falls_back_to_its_basis_domain() {
let geometry = PcurveGeometry::Trimmed(
crate::geometry::pcurve::TrimmedPcurve::try_new(
[1.0, 1.0],
true,
Box::new(PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::from_lanes(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)],
None,
true,
)
.unwrap(),
}),
)
.unwrap(),
);
assert_eq!(
pcurve_parameter_domain(&cadmpeg_test_support::service_decode_context(), &geometry)
.unwrap()
.map(crate::topology::IncreasingParameterInterval::endpoints),
Some([0.0, 1.0])
);
}
#[test]
fn line_pcurve_recovers_vertices_from_nurbs_surface_domain_seeds() {
let surface = SurfaceGeometry::Solved(SolvedSurfaceGeometry::Nurbs(
NurbsSurface::from_lanes(
crate::geometry::nurbs::NurbsSurfaceAxis::new(1, vec![0.0, 0.0, 1.0, 1.0], false),
crate::geometry::nurbs::NurbsSurfaceAxis::new(
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
false,
),
crate::geometry::nurbs::NurbsSurfaceLanes::new(
vec![
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 1.0),
],
vec![
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 1.0),
],
],
None,
),
false,
)
.unwrap(),
));
let surface_id =
SurfaceId::mint("test:model:surface#surface".to_string()).expect("valid identity");
let mut ir = CadIr::empty();
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: surface.clone(),
source_object: None,
});
let index = crate::index::ModelIndex::build(&ir);
let pcurve = Pcurve {
id: "test:model:pcurve#pcurve"
.try_into()
.expect("valid identity"),
geometry: PcurveGeometry::Line(
crate::geometry::pcurve::LinePcurve::try_new(
Point2::new(1.0, 0.0),
Point2::new(0.0, 1.0),
)
.unwrap(),
),
metadata: PcurveMetadata::default(),
};
let context = SurfacePcurveContext {
model: crate::validate::geometry_consistency::PassEvaluator {
index: &index,
pass: &crate::eval::admission::ModelEvaluationPass::new(),
},
surface_id: &surface_id,
geometry: &surface,
};
let ctx = cadmpeg_test_support::service_decode_context();
let seeds = pcurve_parameter_seeds_on_surface(&ctx, &context, &pcurve).unwrap();
assert!(seeds.iter().any(|seed| seed.get() == 0.5));
let ranges = edge_pcurve_parameter_ranges(
&ctx,
&context,
None,
[Point3::new(1.0, 0.0, 0.5625), Point3::new(1.0, 0.0, 0.0625)],
&pcurve,
&pcurve,
1.0e-12,
)
.expect("compute the edge pcurve parameter ranges")
.expect("finite NURBS surface domain should provide recovery seeds");
assert!(
ranges.iter().any(|[start, end]| {
(start - 0.75).abs() <= 1.0e-10 && (end - 0.25).abs() <= 1.0e-10
}),
"{ranges:?}"
);
}
#[test]
fn procedural_surface_carrier_requires_its_exact_owner() {
let mut ir = unit_cube().expect("valid unit cube fixture");
let construction =
ProceduralSurfaceId::mint("synthetic:cube:procedural-surface#0").expect("valid identity");
ir.model.surfaces[0].geometry = SurfaceGeometry::Procedural {
construction: construction.clone(),
cache: None,
};
ir.model.procedural_surfaces.push(procedural_surface! {
id: construction.clone(),
definition: ProceduralSurfaceDefinition::Exact(crate::geometry::surface_payloads::ExactSurfacePayload::try_new(crate::geometry::ExactSpline::Legacy {
ranges: [[0.0, 1.0], [0.0, 1.0]],
extension: 0,
cache: None,
}).unwrap()),
cache_fit_tolerance: None,
record_bounds: None,
});
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report.is_ok(), "{:?}", report.findings);
ir.model.surfaces[0].geometry = SurfaceGeometry::Procedural {
construction: ProceduralSurfaceId::mint("test:model:entity#synthetic:missing")
.expect("valid identity"),
cache: None,
};
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| {
finding
.message
.contains("references missing procedural surface construction")
}));
}
#[test]
fn procedural_curve_carrier_requires_its_exact_owner() {
let mut ir = unit_cube().expect("valid unit cube fixture");
let construction =
ProceduralCurveId::mint("synthetic:cube:procedural-curve#0").expect("valid identity");
ir.model.curves[0].geometry = CurveGeometry::Procedural {
construction: construction.clone(),
cache: None,
};
ir.model.procedural_curves.push(procedural_curve! {
id: construction.clone(),
definition: ProceduralCurveDefinition::Helix(crate::geometry::HelixCurveConstruction::try_new([0.0, std::f64::consts::TAU], crate::geometry::HelixFrame { center: Point3::new(0.0, 0.0, 0.0), major: Vector3::new(1.0, 0.0, 0.0), minor: Vector3::new(0.0, 1.0, 0.0), pitch: Vector3::new(0.0, 0.0, 1.0), axis: Vector3::new(0.0, 0.0, 1.0) }, 0.0, None).unwrap()),
cache_fit_tolerance: None,
});
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report.is_ok(), "{:?}", report.findings);
ir.model.curves[0].geometry = CurveGeometry::Procedural {
construction: ProceduralCurveId::mint("test:model:entity#synthetic:missing")
.expect("valid identity"),
cache: None,
};
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| {
finding
.message
.contains("references missing procedural curve construction")
}));
}
#[test]
fn self_referential_composite_curve_is_invalid() {
use crate::geometry::{CompositeCurveSegment, CompositeCurveTransition};
let mut ir = unit_cube().expect("valid unit cube fixture");
let id = CurveId::mint("synthetic:test:curve#recursive").expect("valid identity");
ir.model.curves.push(Curve {
parameter_range: None,
id: id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Composite {
segments: crate::geometry::CompositeCurveSegments::try_from(vec![
CompositeCurveSegment {
curve: id,
same_sense: true,
transition: CompositeCurveTransition::Continuous,
},
])
.unwrap(),
self_intersect: Some(false),
}),
source_object: None,
});
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.check == Check::ReferentialIntegrity));
}
#[test]
fn edge_endpoint_mismatch_is_flagged() {
let mut ir = unit_cube().expect("valid unit cube fixture");
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(
!report
.findings
.iter()
.any(|f| f.check == Check::GeometricConsistency),
"worked cube must be geometrically consistent, got: {:?}",
report.findings
);
let mut source_tolerant = unit_cube().expect("valid unit cube fixture");
let moved = source_tolerant.model.points[0].position().get();
source_tolerant.model.points[0].set_position(
crate::features::FinitePoint3::new(crate::math::Point3::new(
moved.x,
moved.y,
moved.z + 0.015,
))
.expect("a finite position is a point"),
);
source_tolerant.tolerances.linear =
crate::scalar::PositiveLength::new(0.02).expect("positive finite tolerance");
let report =
validate_neutral(&source_tolerant, Vec::new()).expect("validate the neutral model");
assert!(
!report
.findings
.iter()
.any(|finding| finding.check == Check::GeometricConsistency),
"document tolerance must qualify a small endpoint mismatch: {:?}",
report.findings
);
let moved = ir.model.points[0].position().get();
ir.model.points[0].set_position(
crate::features::FinitePoint3::new(crate::math::Point3::new(
moved.x,
moved.y,
moved.z + 1.0,
))
.expect("a finite position is a point"),
);
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(
report
.findings
.iter()
.any(|f| f.check == Check::GeometricConsistency
&& f.severity == Severity::Error
&& f.entity.as_deref().is_some_and(|e| e.contains("edge"))),
"displaced vertex must fail edge endpoint consistency, got: {:?}",
report.findings
);
let curve = ir.model.edges[0].curve().cloned().expect("cube edge curve");
let procedural = procedural_curve! {
id: ProceduralCurveId::mint("synthetic:cube:curve-cache#0").expect("valid identity"),
definition: ProceduralCurveDefinition::Intersection {
context: crate::geometry::IntcurveSupportContext::try_new(std::array::from_fn(|_| crate::geometry::IntcurveSupportSide {
surface: None,
pcurve: None,
}), ir.model.edges[0].param_range().expect("cube edge range").get(), std::array::from_fn(|_| Vec::new())).unwrap(),
discontinuity_flag: false,
cache: None,
},
cache_fit_tolerance: Some(0.99),
};
ir.model.add_procedural_curve(&curve, procedural).unwrap();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(
report
.findings
.iter()
.any(|f| f.check == Check::GeometricConsistency
&& f.entity.as_deref() == Some("synthetic:cube:edge#0")),
"cache tolerance below the endpoint mismatch must still fail"
);
ir.model.procedural_curves[0]
.set_cache_fit_tolerance(Some(1.0))
.unwrap();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(
!report
.findings
.iter()
.any(|f| f.check == Check::GeometricConsistency
&& f.entity.as_deref() == Some("synthetic:cube:edge#0")),
"curve mismatch within its cache fit tolerance must validate, got: {:?}",
report.findings
);
}
#[test]
fn pcurve_surface_mismatch_is_flagged() {
let checked = |u_end: f64, v_end: f64, fit_tolerance: Option<f64>| {
let mut ir = unit_cube().expect("valid unit cube fixture");
ir.model.pcurves.push(crate::geometry::pcurve::Pcurve {
id: crate::ids::PcurveId::mint("synthetic:cube:pcurve#0").expect("valid identity"),
geometry: crate::geometry::pcurve::PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::from_lanes(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![
crate::math::Point2::new(0.0, 0.0),
crate::math::Point2::new(u_end, v_end),
],
None,
false,
)
.unwrap(),
},
metadata: PcurveMetadata::general(
None,
None,
fit_tolerance.map(|value| crate::geometry::FitTolerance::try_new(value).unwrap()),
),
});
let coedge = ir
.model
.coedges
.iter_mut()
.find(|coedge| {
coedge.id.as_str().contains("bottom")
&& coedge.edge.as_str() == "synthetic:cube:edge#0"
})
.expect("bottom face uses edge #0");
coedge.pcurves = vec![crate::topology::PcurveUse {
pcurve: crate::ids::PcurveId::mint("synthetic:cube:pcurve#0").expect("valid identity"),
isoparametric: None,
parameter_range: None,
}];
validate_neutral(&ir, Vec::new()).expect("validate the neutral model")
};
let consistent = checked(10.0, 0.0, None);
assert!(
!consistent
.findings
.iter()
.any(|f| f.check == Check::GeometricConsistency),
"matching pcurve must validate, got: {:?}",
consistent.findings
);
let inconsistent = checked(10.0, 5.0, Some(4.99));
assert!(
inconsistent
.findings
.iter()
.any(|f| f.check == Check::GeometricConsistency
&& f.entity.as_deref().is_some_and(|e| e.contains("coedge"))),
"off-surface-image pcurve must be flagged, got: {:?}",
inconsistent.findings
);
let tolerance_qualified = checked(10.0, 5.0, Some(5.0));
assert!(
!tolerance_qualified
.findings
.iter()
.any(|f| f.check == Check::GeometricConsistency),
"pcurve mismatch within its fit tolerance must validate, got: {:?}",
tolerance_qualified.findings
);
let mut procedural = unit_cube().expect("valid unit cube fixture");
procedural
.model
.pcurves
.push(crate::geometry::pcurve::Pcurve {
id: crate::ids::PcurveId::mint("synthetic:cube:pcurve#procedural")
.expect("valid identity"),
geometry: crate::geometry::pcurve::PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::from_lanes(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![
crate::math::Point2::new(0.0, 0.0),
crate::math::Point2::new(10.0, 5.0),
],
None,
false,
)
.unwrap(),
},
metadata: PcurveMetadata::default(),
});
let coedge = procedural
.model
.coedges
.iter_mut()
.find(|coedge| {
coedge.id.as_str().contains("bottom") && coedge.edge.as_str() == "synthetic:cube:edge#0"
})
.expect("bottom face uses edge #0");
coedge.pcurves = vec![crate::topology::PcurveUse {
pcurve: crate::ids::PcurveId::mint("synthetic:cube:pcurve#procedural")
.expect("valid identity"),
isoparametric: None,
parameter_range: None,
}];
let owner_loop = coedge.owner_loop.clone();
let surface = procedural
.model
.loops
.iter()
.find(|lp| lp.id == owner_loop)
.and_then(|lp| {
procedural
.model
.faces
.iter()
.find(|face| face.id == lp.face)
.map(|face| face.surface.clone())
})
.expect("coedge owner face");
let construction = procedural_surface! {
id: ProceduralSurfaceId::mint("synthetic:cube:procedural-surface#0").expect("valid identity"),
definition: ProceduralSurfaceDefinition::Revolution(crate::geometry::surface_payloads::RevolutionSurfaceConstruction::try_new(procedural.model.curves[0].id.clone(), (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: Some(0.01),
record_bounds: None,
};
procedural
.model
.add_procedural_surface(&surface, construction)
.unwrap();
let procedural_report =
validate_neutral(&procedural, Vec::new()).expect("validate the neutral model");
assert!(
!procedural_report
.findings
.iter()
.any(|finding| finding.check == Check::GeometricConsistency),
"procedural UVs must not be evaluated on the solved cache, got: {:?}",
procedural_report.findings
);
procedural.model.procedural_surfaces[0].edit_definition(|definition| {
*definition = ProceduralSurfaceDefinition::Exact(
crate::geometry::surface_payloads::ExactSurfacePayload::try_new(
crate::geometry::ExactSpline::Legacy {
ranges: [[0.0, 1.0], [0.0, 1.0]],
extension: 0,
cache: None,
},
)
.unwrap(),
);
});
let exact_report =
validate_neutral(&procedural, Vec::new()).expect("validate the neutral model");
assert!(
!exact_report
.findings
.iter()
.any(|finding| finding.check == Check::GeometricConsistency),
"exact procedural UVs must not be evaluated on the solved cache, got: {:?}",
exact_report.findings
);
let mut negative_parameterization = unit_cube().expect("valid unit cube fixture");
negative_parameterization
.model
.pcurves
.push(crate::geometry::pcurve::Pcurve {
id: crate::ids::PcurveId::mint("synthetic:cube:pcurve#negative")
.expect("valid identity"),
geometry: crate::geometry::pcurve::PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::from_lanes(
1,
vec![-10.0, -10.0, 0.0, 0.0],
vec![
crate::math::Point2::new(10.0, 0.0),
crate::math::Point2::new(0.0, 0.0),
],
None,
false,
)
.unwrap(),
},
metadata: PcurveMetadata::default(),
});
let coedge = negative_parameterization
.model
.coedges
.iter_mut()
.find(|coedge| {
coedge.id.as_str().contains("bottom") && coedge.edge.as_str() == "synthetic:cube:edge#0"
})
.expect("bottom face uses edge #0");
coedge.pcurves = vec![crate::topology::PcurveUse {
pcurve: crate::ids::PcurveId::mint("synthetic:cube:pcurve#negative")
.expect("valid identity"),
isoparametric: None,
parameter_range: Some(crate::geometry::DirectedParameterRange::new([-10.0, 0.0]).unwrap()),
}];
let ranged_coedge_id = coedge.id.clone();
let negative = validate_neutral(&negative_parameterization, Vec::new())
.expect("validate the neutral model");
assert!(
!negative
.findings
.iter()
.any(|finding| finding.check == Check::GeometricConsistency),
"opposite-sign pcurve parameterization must validate, got: {:?}",
negative.findings
);
let pcurve_use = &mut negative_parameterization
.model
.coedges
.iter_mut()
.find(|coedge| coedge.id == ranged_coedge_id)
.expect("ranged coedge")
.pcurves[0];
pcurve_use.parameter_range =
Some(crate::geometry::DirectedParameterRange::new([-11.0, 0.0]).unwrap());
let invalid_range = validate_neutral(&negative_parameterization, Vec::new())
.expect("validate the neutral model");
assert!(invalid_range.findings.iter().any(|finding| {
finding.check == Check::ParameterDomain && finding.message.contains("coedge pcurve range")
}));
}
fn nurbs_surface_leaf() -> SolvedSurfaceGeometry {
SolvedSurfaceGeometry::Nurbs(
NurbsSurface::from_lanes(
crate::geometry::nurbs::NurbsSurfaceAxis::new(1, vec![0.0, 0.0, 1.0, 1.0], false),
crate::geometry::nurbs::NurbsSurfaceAxis::new(1, vec![0.0, 0.0, 1.0, 1.0], false),
crate::geometry::nurbs::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(),
)
}
fn placed_nurbs_surface(placements: usize) -> Result<SolvedSurfaceGeometry, &'static str> {
let mut geometry = nurbs_surface_leaf();
for _ in 0..placements {
geometry = SolvedSurfaceGeometry::Transformed(crate::geometry::PlacedSurface::try_new(
Box::new(geometry),
crate::transform::Transform::identity(),
)?);
}
Ok(geometry)
}
fn nurbs_pcurve_leaf() -> PcurveGeometry {
PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::from_lanes(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)],
None,
false,
)
.unwrap(),
}
}
fn placed_pcurve(placements: usize, leaf: PcurveGeometry) -> Result<PcurveGeometry, &'static str> {
let mut geometry = leaf;
for _ in 0..placements {
geometry = PcurveGeometry::Transformed(crate::geometry::pcurve::PlacedPcurve::try_new(
Box::new(geometry),
crate::transform::Transform2::identity(),
)?);
}
Ok(geometry)
}
#[test]
fn surface_parameter_domains_stop_at_the_admitted_nesting_depth() {
let arena = cadmpeg_core::decode::DecodeArena::new();
let mut policy = cadmpeg_core::decode::DecodePolicy::service();
policy.limits.max_recursion_depth =
cadmpeg_core::decode::u64_from_index(crate::geometry::MAX_GEOMETRY_NESTING + 1);
let (ctx, _) =
cadmpeg_core::decode::DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let accepted =
placed_nurbs_surface(crate::geometry::MAX_GEOMETRY_NESTING).expect("admitted nesting");
assert!(super::solved_surface_parameter_domains(&ctx, &accepted)
.unwrap()
.is_some());
assert_eq!(
placed_nurbs_surface(crate::geometry::MAX_GEOMETRY_NESTING + 1),
Err("PlacedSurface.basis nests past the admitted inline basis depth")
);
}
#[test]
fn pcurve_trim_range_stops_at_the_admitted_nesting_depth() {
let arena = cadmpeg_core::decode::DecodeArena::new();
let mut policy = cadmpeg_core::decode::DecodePolicy::service();
policy.limits.max_recursion_depth =
cadmpeg_core::decode::u64_from_index(crate::geometry::MAX_GEOMETRY_NESTING + 1);
let (ctx, _) =
cadmpeg_core::decode::DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let trimmed = PcurveGeometry::Trimmed(
crate::geometry::pcurve::TrimmedPcurve::try_new(
[0.25, 0.75],
true,
Box::new(nurbs_pcurve_leaf()),
)
.unwrap(),
);
let accepted = placed_pcurve(crate::geometry::MAX_GEOMETRY_NESTING - 1, trimmed.clone())
.expect("admitted nesting");
assert_eq!(
super::pcurve_geometry_trim_range(&ctx, &accepted).unwrap(),
Some([0.25, 0.75])
);
assert_eq!(
placed_pcurve(crate::geometry::MAX_GEOMETRY_NESTING, trimmed),
Err("PlacedPcurve.basis nests past the admitted inline basis depth")
);
}
mod finding_admission;
mod index_admission;
mod inverse_admission;
mod overflowing_arms;
mod parameter_scaling;
mod parameter_storage;
fn overflowing_plane() -> SurfaceGeometry {
SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(
crate::geometry::analytic::PlaneSurface::try_new(
Point3::new(f64::MAX, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
))
}
#[test]
fn a_support_side_whose_points_overflow_misses_its_contract_by_nan() {
let mut ir = mapped_surface_curve([1.0e300, 2.0e300]);
ir.model.surfaces[0].geometry = overflowing_plane();
let mut findings = Vec::new();
check_procedural_support_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&ir),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the procedural support is consistent");
assert_eq!(findings.len(), 1, "{findings:?}");
assert_eq!(
findings[0].message,
"procedural support side 0 misses its endpoint distance contract by NaN"
);
}
#[test]
fn a_coedge_pcurve_whose_mapped_points_overflow_misses_the_vertices_by_nan() {
let mut ir = unit_cube().expect("valid unit cube fixture");
ir.model.pcurves.push(Pcurve {
id: crate::ids::PcurveId::mint("synthetic:cube:pcurve#overflow").expect("valid identity"),
geometry: PcurveGeometry::Offset(
crate::geometry::pcurve::OffsetPcurve::try_new(
-f64::MAX,
Box::new(PcurveGeometry::Line(
crate::geometry::pcurve::LinePcurve::try_new(
Point2::new(f64::MAX, 0.0),
Point2::new(0.0, 1.0),
)
.unwrap(),
)),
)
.unwrap(),
),
metadata: PcurveMetadata::default(),
});
let coedge = ir
.model
.coedges
.iter_mut()
.find(|coedge| {
coedge.id.as_str().contains("bottom") && coedge.edge.as_str() == "synthetic:cube:edge#0"
})
.expect("bottom face uses edge #0");
coedge.pcurves = vec![PcurveUse {
pcurve: crate::ids::PcurveId::mint("synthetic:cube:pcurve#overflow")
.expect("valid identity"),
isoparametric: None,
parameter_range: None,
}];
let coedge_id = coedge.id.as_str().to_owned();
let mut findings = Vec::new();
check_pcurve_surface_consistency(
&cadmpeg_test_support::service_decode_context(),
&crate::index::ModelIndex::new_model_only(&ir),
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.expect("check that the pcurve is consistent with the surface");
assert!(
findings.iter().any(|finding| {
finding.entity.as_deref() == Some(coedge_id.as_str())
&& finding.message
== "pcurve mapped through the face surface misses the edge's vertex positions \
by NaN"
}),
"{findings:?}"
);
}
#[test]
fn the_mapped_pcurve_search_halves_a_step_whose_point_overflows() {
let surface = SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(
crate::geometry::analytic::PlaneSurface::try_new(
Point3::new(1.78e308, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
));
let surface_id =
SurfaceId::mint("test:model:surface#overflow".to_string()).expect("valid identity");
let mut ir = CadIr::empty();
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: surface.clone(),
source_object: None,
});
let index = crate::index::ModelIndex::build(&ir);
let context = SurfacePcurveContext {
model: crate::validate::geometry_consistency::PassEvaluator {
index: &index,
pass: &crate::eval::admission::ModelEvaluationPass::new(),
},
surface_id: &surface_id,
geometry: &surface,
};
let parabola = PcurveGeometry::Parabola(
crate::geometry::pcurve::ParabolaPcurve::try_new(
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(0.0, 1.0),
0.25,
)
.unwrap(),
);
let target_parameter = 1.2e153;
let target = Point3::new(
1.78e308 + target_parameter * target_parameter,
target_parameter,
0.0,
);
let seed = crate::scalar::FiniteReal::new(1.0e152).expect("finite seed");
let parameter = mapped_pcurve_parameter_near_point(
&cadmpeg_test_support::service_decode_context(),
&context,
¶bola,
target,
seed,
1.0e300,
)
.expect("compute the mapped pcurve parameter near point")
.expect("the halved step reaches the target")
.get();
assert!(
(parameter / target_parameter - 1.0).abs() < 1.0e-6,
"{parameter}"
);
}
#[test]
fn the_mapped_pcurve_search_without_a_domain_ends_where_a_step_leaves_the_finite_range() {
let surface = 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(),
));
let surface_id =
SurfaceId::mint("test:model:surface#far-line".to_string()).expect("valid identity");
let mut ir = CadIr::empty();
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: surface.clone(),
source_object: None,
});
let index = crate::index::ModelIndex::build(&ir);
let context = SurfacePcurveContext {
model: crate::validate::geometry_consistency::PassEvaluator {
index: &index,
pass: &crate::eval::admission::ModelEvaluationPass::new(),
},
surface_id: &surface_id,
geometry: &surface,
};
let line = PcurveGeometry::Line(
crate::geometry::pcurve::LinePcurve::try_new(Point2::new(0.0, 0.0), Point2::new(0.5, 0.0))
.unwrap(),
);
let seed = crate::scalar::FiniteReal::new(-1.0e308).expect("finite seed");
assert_eq!(
mapped_pcurve_parameter_near_point(
&cadmpeg_test_support::service_decode_context(),
&context,
&line,
Point3::new(-1.0e308, 0.0, 0.0),
seed,
1.0
)
.expect("compute the mapped pcurve parameter near point"),
None
);
assert_eq!(
mapped_pcurve_parameter_near_point(
&cadmpeg_test_support::service_decode_context(),
&context,
&line,
Point3::new(-4.0e307, 0.0, 0.0),
seed,
1.0
)
.expect("compute the mapped pcurve parameter near point")
.map(crate::scalar::FiniteReal::get),
Some(-8.0e307)
);
}
#[test]
fn the_mapped_pcurve_search_accepts_a_matching_seed_whose_pcurve_has_no_tangent() {
let surface = 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(),
));
let surface_id =
SurfaceId::mint("test:model:surface#plane".to_string()).expect("valid identity");
let mut ir = CadIr::empty();
ir.model.surfaces.push(Surface {
id: surface_id.clone(),
geometry: surface.clone(),
source_object: None,
});
let index = crate::index::ModelIndex::build(&ir);
let context = SurfacePcurveContext {
model: crate::validate::geometry_consistency::PassEvaluator {
index: &index,
pass: &crate::eval::admission::ModelEvaluationPass::new(),
},
surface_id: &surface_id,
geometry: &surface,
};
let offset = |basis| {
PcurveGeometry::Offset(
crate::geometry::pcurve::OffsetPcurve::try_new(1.0, Box::new(basis)).unwrap(),
)
};
let pcurve = offset(offset(PcurveGeometry::Line(
crate::geometry::pcurve::LinePcurve::try_new(Point2::new(5.0, 0.0), Point2::new(0.0, 1.0))
.unwrap(),
)));
assert!(crate::eval::pcurve_tangent(
crate::eval::admission::EvaluationAdmission::Standard,
&pcurve,
0.5
)
.is_err());
let seed = crate::scalar::FiniteReal::new(0.5).expect("finite seed");
assert_eq!(
mapped_pcurve_parameter_near_point(
&cadmpeg_test_support::service_decode_context(),
&context,
&pcurve,
Point3::new(3.0, 0.5, 0.0),
seed,
0.0
)
.expect("compute the mapped pcurve parameter near point"),
Some(seed)
);
}
#[test]
fn solved_surface_parameter_domains_preserves_session_depth_refusals() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let geometry = placed_nurbs_surface(2).unwrap();
for (cap, held_frame) in [(0, false), (2, false), (2, true)] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = cap;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let guard = held_frame.then(|| ctx.enter_nested_limit("caller frame").unwrap());
let limit = super::solved_surface_parameter_domains(&ctx, &geometry).unwrap_err();
assert_eq!(limit.dimension, ResourceDimension::RecursionDepth);
assert_eq!(limit.limit, cap);
assert_eq!(limit.used, cap);
assert_eq!(limit.additional, 1);
assert_eq!(limit.operation, "surface parameter domain nesting");
drop(guard);
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(original)) if original == limit)
);
}
}
#[test]
fn pcurve_geometry_trim_range_preserves_session_depth_refusals() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let geometry = placed_pcurve(2, nurbs_pcurve_leaf()).unwrap();
for (cap, held_frame) in [(0, false), (2, false), (2, true)] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = cap;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let guard = held_frame.then(|| ctx.enter_nested_limit("caller frame").unwrap());
let limit = super::pcurve_geometry_trim_range(&ctx, &geometry).unwrap_err();
assert_eq!(limit.dimension, ResourceDimension::RecursionDepth);
assert_eq!(limit.limit, cap);
assert_eq!(limit.used, cap);
assert_eq!(limit.additional, 1);
assert_eq!(limit.operation, "pcurve trim range nesting");
drop(guard);
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(original)) if original == limit)
);
}
}
#[test]
fn a_verified_pcurve_interval_over_dense_knots_has_no_findings() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
let mut ir = untrimmed_surface_curve();
for (point, x) in ir.model.points.iter_mut().zip([0.0, 1.0]) {
point.set_position(crate::features::FinitePoint3::new(Point3::new(x, 0.0, 0.0)).unwrap());
}
ir.model.curves[0].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(),
));
let mut knots = vec![0.0, 0.0];
knots.extend((1..1_000).map(|i| f64::from(i) / 1_000.0));
knots.extend([1.0, 1.0]);
ir.model.pcurves[0].geometry = PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::from_lanes(
1,
knots,
(0..=1_000)
.map(|i| Point2::new(f64::from(i) / 1_000.0, 0.0))
.collect(),
None,
false,
)
.unwrap(),
};
ir.model.coedges[0].pcurves[0].parameter_range =
Some(DirectedParameterRange::new([0.0, 1.0]).unwrap());
let index = crate::index::ModelIndex::new_model_only(&ir);
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let mut findings = Vec::new();
check_pcurve_surface_consistency(
&ctx,
&index,
&crate::eval::admission::ModelEvaluationPass::new(),
&mut findings,
)
.unwrap();
assert!(findings.is_empty(), "{findings:?}");
assert!(ctx.finish_session().is_ok());
}