use super::bilinear_surface;
use crate::eval::nurbs_surface_parameter_segment_chord_bound;
use crate::eval::rational_patch_parameter_segment;
use crate::eval::RationalBezierSurfacePatch;
use crate::math::{Point2, Point3};
use crate::topology::IncreasingParameterInterval;
use crate::units::FinitePoint2;
#[test]
fn rational_patch_rejects_out_of_domain_endpoint_instead_of_moving_it() {
let ctx = cadmpeg_test_support::service_decode_context();
let patch = RationalBezierSurfacePatch {
u_domain: IncreasingParameterInterval::new([0.0, 1.0]).unwrap(),
v_domain: IncreasingParameterInterval::new([0.0, 1.0]).unwrap(),
u_degree: 1,
v_degree: 1,
controls: [[0.0, 0.0, 0.0, 1.0]; 4].to_vec(),
_scratch: ctx.reserve_scoped_limit(0, "test patch controls").unwrap(),
};
let start = FinitePoint2::new(Point2::new(-0.5, 0.0)).unwrap();
let end = FinitePoint2::new(Point2::new(0.5, 1.0)).unwrap();
assert!(rational_patch_parameter_segment(
&cadmpeg_test_support::service_decode_context(),
&patch,
start,
end
)
.expect("resource allocation did not fail")
.is_none());
}
#[test]
fn nurbs_surface_parameter_segment_bound_contains_curved_diagonal() {
let mut surface = bilinear_surface();
surface
.try_map_control_points(
|index, point| {
let mut mapped = point.get();
if index == 3 {
mapped.z = 1.0;
}
crate::features::FinitePoint3::new(mapped).ok_or(())
},
&cadmpeg_test_support::service_decode_context(),
)
.expect("pole edit admission")
.unwrap();
let parameters = [Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)];
let chord = [Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 1.0)];
let bound = nurbs_surface_parameter_segment_chord_bound(
&cadmpeg_test_support::service_decode_context(),
&surface,
parameters,
chord,
)
.expect("resource allocation did not fail")
.expect("rational Bézier residual bound");
assert!(bound >= 1.0 / 3.0);
assert!(bound < 1.0 / 3.0 + 1.0e-12);
let reverse_bound = nurbs_surface_parameter_segment_chord_bound(
&cadmpeg_test_support::service_decode_context(),
&surface,
[parameters[1], parameters[0]],
[chord[1], chord[0]],
)
.expect("resource allocation did not fail")
.expect("reversed rational Bézier residual bound");
assert!((reverse_bound - bound).abs() < 1.0e-12);
for index in 0..=100 {
let parameter = f64::from(index) / 100.0;
let point = crate::eval::decode::nurbs_surface_point(
crate::eval::admission::EvaluationAdmission::Standard,
&surface,
parameter,
parameter,
)
.expect("surface point");
let target = Point3::new(parameter, parameter, parameter);
let distance = (point.x - target.x)
.hypot(point.y - target.y)
.hypot(point.z - target.z);
assert!(distance <= bound);
}
}
#[test]
fn surface_segment_bound_preserves_session_and_local_refusals() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let surface = bilinear_surface();
let parameters = [Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)];
let chord = [Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 0.0)];
for dimension in [
ResourceDimension::MaterializedBytes,
ResourceDimension::CollectionItems,
ResourceDimension::WorkUnits,
ResourceDimension::RecursionDepth,
] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
match dimension {
ResourceDimension::MaterializedBytes => policy.limits.max_materialized_bytes = 0,
ResourceDimension::CollectionItems => policy.limits.max_collection_items = 0,
ResourceDimension::WorkUnits => policy.limits.max_work_units = 0,
ResourceDimension::RecursionDepth => policy.limits.max_recursion_depth = 0,
_ => unreachable!(),
}
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let error = nurbs_surface_parameter_segment_chord_bound(&ctx, &surface, parameters, chord)
.unwrap_err();
assert!(matches!(&error, CodecError::ResourceLimit(limit) if limit.dimension == dimension));
assert_eq!(
ctx.finish_session().unwrap_err().to_string(),
error.to_string()
);
}
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_retained_bytes = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
assert!(
nurbs_surface_parameter_segment_chord_bound(&ctx, &surface, parameters, chord)
.unwrap()
.is_some()
);
ctx.finish_session().unwrap();
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let budget = ctx.work_budget(0);
let error = crate::eval::nurbs_surface_parameter_segment_chord_bound_with_budget(
&ctx, &surface, parameters, chord, &budget,
)
.unwrap_err();
assert!(
matches!(&error, CodecError::ResourceLimit(limit) if limit.operation == "IR surface segment work")
);
assert_eq!(
ctx.finish_session().unwrap_err().to_string(),
error.to_string()
);
}
#[test]
fn surface_patch_extraction_preserves_refusals_and_scoped_output_storage() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let surface = bilinear_surface();
for dimension in [
ResourceDimension::MaterializedBytes,
ResourceDimension::CollectionItems,
ResourceDimension::WorkUnits,
] {
let mut policy = DecodePolicy::service();
match dimension {
ResourceDimension::MaterializedBytes => policy.limits.max_materialized_bytes = 0,
ResourceDimension::CollectionItems => policy.limits.max_collection_items = 0,
ResourceDimension::WorkUnits => policy.limits.max_work_units = 0,
_ => unreachable!("tested extraction dimensions"),
}
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let budget = ctx.work_budget(1_000_000);
let limit = crate::eval::rational_surface_patches_with_budget(&ctx, &surface, &budget)
.expect_err("extraction admission");
assert_eq!(limit.dimension, dimension);
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == limit)
);
}
let mut policy = DecodePolicy::service();
policy.limits.max_materialized_bytes = 65536;
policy.limits.max_retained_bytes = 0;
policy.limits.max_recursion_depth = 0;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let budget = ctx.work_budget(1_000_000);
let output = crate::eval::rational_surface_patches_with_budget(&ctx, &surface, &budget)
.expect("extraction")
.expect("active patch");
assert_eq!(output.rows.len(), 1);
assert_eq!(output.rows[0].u_domain.endpoints(), [0.0, 1.0]);
assert_eq!(output.rows[0].v_domain.endpoints(), [0.0, 1.0]);
assert_eq!(
output.rows[0].controls,
[
[0.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 1.0],
[1.0, 0.0, 0.0, 1.0],
[1.0, 1.0, 0.0, 1.0],
]
);
let bytes = output.rows.capacity() * std::mem::size_of::<RationalBezierSurfacePatch>()
+ output
.rows
.iter()
.map(|patch| patch.controls.capacity() * std::mem::size_of::<[f64; 4]>())
.sum::<usize>();
let bytes = u64::try_from(bytes).expect("test allocation bytes fit");
let spare = ctx
.reserve_scoped_limit(65536 - bytes, "test surface extraction scratch released")
.expect("only output headers and controls remain");
drop(spare);
drop(output);
let reuse = ctx
.reserve_scoped_limit(65536, "test surface patches released")
.expect("all bytes reusable");
drop(reuse);
ctx.finish_session()
.expect("no retained extraction storage");
let ctx = cadmpeg_test_support::service_decode_context();
let budget = cadmpeg_core::decode::WorkBudget::new(0);
assert!(
crate::eval::rational_surface_patches_with_budget(&ctx, &surface, &budget)
.expect("independent local exhaustion")
.is_none()
);
ctx.finish_session()
.expect("local exhaustion is not a session refusal");
}
#[test]
fn surface_patch_split_preserves_refusals_and_both_tensor_directions() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let surface = bilinear_surface();
let source_ctx = cadmpeg_test_support::service_decode_context();
let source_budget = source_ctx.work_budget(1_000_000);
let source =
crate::eval::rational_surface_patches_with_budget(&source_ctx, &surface, &source_budget)
.expect("fixture extraction")
.expect("active patch");
for split_u in [false, true] {
for dimension in [
ResourceDimension::MaterializedBytes,
ResourceDimension::CollectionItems,
ResourceDimension::WorkUnits,
] {
let mut policy = DecodePolicy::service();
match dimension {
ResourceDimension::MaterializedBytes => policy.limits.max_materialized_bytes = 0,
ResourceDimension::CollectionItems => policy.limits.max_collection_items = 0,
ResourceDimension::WorkUnits => policy.limits.max_work_units = 0,
_ => unreachable!("tested split dimensions"),
}
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let budget = ctx.work_budget(1_000_000);
let limit =
crate::eval::split_rational_surface_patch(&ctx, &source.rows[0], split_u, &budget)
.expect_err("split admission");
assert_eq!(limit.dimension, dimension);
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == limit)
);
}
let mut policy = DecodePolicy::service();
policy.limits.max_materialized_bytes = 8192;
policy.limits.max_retained_bytes = 0;
policy.limits.max_recursion_depth = 0;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let budget = ctx.work_budget(1_000_000);
let children =
crate::eval::split_rational_surface_patch(&ctx, &source.rows[0], split_u, &budget)
.expect("split")
.expect("divisible patch");
for (side, child) in children.iter().enumerate() {
let range = if side == 0 { [0.0, 0.5] } else { [0.5, 1.0] };
assert_eq!(
child.u_domain.endpoints(),
if split_u { range } else { [0.0, 1.0] }
);
assert_eq!(
child.v_domain.endpoints(),
if split_u { [0.0, 1.0] } else { range }
);
let u = child.u_domain.endpoints();
let v = child.v_domain.endpoints();
for (index, control) in child.controls.iter().enumerate() {
assert_eq!(*control, [u[index / 2], v[index % 2], 0.0, 1.0]);
}
}
let bytes = children
.iter()
.map(|patch| patch.controls.capacity() * std::mem::size_of::<[f64; 4]>())
.sum::<usize>();
let bytes = u64::try_from(bytes).expect("test bytes fit");
let spare = ctx
.reserve_scoped_limit(8192 - bytes, "test split surface scratch released")
.expect("only child controls remain");
drop(spare);
drop(children);
let reuse = ctx
.reserve_scoped_limit(8192, "test split surface outputs released")
.expect("all bytes reusable");
drop(reuse);
ctx.finish_session().expect("no retained split storage");
}
}
#[test]
fn surface_distance_bounds_preserve_each_control_scan_refusal() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let surface = bilinear_surface();
let source_ctx = cadmpeg_test_support::service_decode_context();
let source_budget = source_ctx.work_budget(1_000_000);
let source =
crate::eval::rational_surface_patches_with_budget(&source_ctx, &surface, &source_budget)
.expect("fixture extraction")
.expect("active patch");
for cap in 0..5 {
let mut policy = DecodePolicy::service();
policy.limits.max_work_units = cap;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let budget = ctx.work_budget(1_000_000);
let limit =
crate::eval::rational_patch_distance_bounds_with_budget(&ctx, &source.rows[0], &budget)
.expect_err("each visit needs work");
assert_eq!(limit.dimension, ResourceDimension::WorkUnits);
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == limit)
);
}
let mut policy = DecodePolicy::service();
policy.limits.max_work_units = 5;
policy.limits.max_materialized_bytes = 0;
policy.limits.max_retained_bytes = 0;
policy.limits.max_collection_items = 0;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let budget = ctx.work_budget(1_000_000);
let (lower, diameter) =
crate::eval::rational_patch_distance_bounds_with_budget(&ctx, &source.rows[0], &budget)
.expect("five visits")
.expect("finite hull");
assert_eq!(lower, 0.0);
assert!((diameter - 2.0_f64.sqrt()).abs() <= 4.0 * f64::EPSILON);
ctx.finish_session()
.expect("no scratch allocation for bounds");
}
#[test]
fn surface_start_search_keeps_only_output_storage_after_both_contracts() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
let surface = bilinear_surface();
for tolerance in [None, Some(0.0)] {
let mut policy = DecodePolicy::service();
policy.limits.max_materialized_bytes = 8192;
policy.limits.max_retained_bytes = 0;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let budget = ctx.work_budget(1_000_000);
let point = Point3::new(0.3, 0.7, if tolerance.is_some() { 0.0 } else { 0.2 });
let starts = crate::eval::complete_nurbs_surface_starts(
&ctx, &surface, point, None, tolerance, &budget,
)
.expect("actual scoped workspace fits")
.expect("surface starts");
assert!(!starts.is_empty());
for start in starts.iter() {
assert!((start.u - 0.3).abs() <= 64.0 * f64::EPSILON);
assert!((start.v - 0.7).abs() <= 64.0 * f64::EPSILON);
}
let bytes = u64::try_from(starts.capacity() * std::mem::size_of::<FinitePoint2>())
.expect("test allocation bytes fit");
let spare = ctx
.reserve_scoped_limit(8192 - bytes, "test surface start workspace released")
.expect("only output starts remain reserved");
drop(spare);
drop(starts);
let reuse = ctx
.reserve_scoped_limit(8192, "test surface starts released")
.expect("all bytes reusable");
drop(reuse);
ctx.finish_session().expect("no retained search storage");
}
}