cadmpeg-ir 0.6.1

CAD data model, validation, diffing, and codec API for cadmpeg.
// SPDX-License-Identifier: Apache-2.0
//! Admission of stored derivatives and placed surface partials.

use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;

use crate::eval::{
    curve_second_derivative, curve_second_derivative_solved, curve_tangent_solved,
    surface_partials, surface_second_partials, EvaluationFailure,
};
use crate::geometry::nurbs::NurbsCurve;
use crate::geometry::{CurveGeometry, SolvedCurveGeometry, SolvedSurfaceGeometry, SurfaceGeometry};
use crate::math::{Point3, Vector3};

#[test]
fn stored_curve_derivatives_admit_actual_scratch_and_work() {
    let fixture = cadmpeg_test_support::service_decode_context();
    let solved = SolvedCurveGeometry::Nurbs(
        NurbsCurve::from_lanes(
            &fixture,
            2,
            vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
            vec![
                Point3::new(0.0, 0.0, 0.0),
                Point3::new(0.5, 0.0, 0.0),
                Point3::new(1.0, 0.0, 0.0),
            ],
            None,
            false,
        )
        .unwrap()
        .unwrap(),
    );
    let geometry = CurveGeometry::Solved(solved.clone());
    for trigger in 0..4 {
        for order in 0..3 {
            let mut policy = DecodePolicy::service();
            let dimension = match trigger {
                0 => {
                    policy.limits.max_materialized_bytes = 0;
                    ResourceDimension::MaterializedBytes
                }
                1 => {
                    policy.limits.max_collection_items = 0;
                    ResourceDimension::CollectionItems
                }
                2 => {
                    policy.limits.max_work_units = 0;
                    ResourceDimension::WorkUnits
                }
                _ => {
                    policy.limits.max_retained_bytes = 0;
                    ResourceDimension::RetainedBytes
                }
            };
            let arena = DecodeArena::new();
            let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
            let result = match order {
                0 => curve_tangent_solved(&ctx, &solved, 0.5),
                1 => curve_second_derivative_solved(&ctx, &solved, 0.5),
                _ => curve_second_derivative(&ctx, &geometry, 0.5),
            };
            if trigger == 3 {
                let expected = if order == 0 {
                    Vector3::new(1.0, 0.0, 0.0)
                } else {
                    Vector3::new(0.0, 0.0, 0.0)
                };
                assert_eq!(result.unwrap().get(), expected);
                assert!(ctx.finish_session().is_ok());
            } else {
                let EvaluationFailure::ResourceLimit(first) = result.unwrap_err() else {
                    panic!("refusal must remain a resource error");
                };
                assert_eq!(first.dimension, dimension);
                assert_eq!((first.limit, first.used), (0, 0));
                assert!(first.additional > 0);
                assert_eq!(
                    curve_second_derivative(&ctx, &geometry, f64::NAN),
                    Err(EvaluationFailure::ResourceLimit(first))
                );
                assert!(
                    matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == first)
                );
            }
        }
    }
}

#[test]
fn stored_surface_partial_entries_admit_actual_scratch_and_work() {
    let surface = SurfaceGeometry::Solved(SolvedSurfaceGeometry::Nurbs(
        crate::eval::test_support::high_degree_plane(),
    ));
    for trigger in 0..4 {
        for second in [false, true] {
            let mut policy = DecodePolicy::service();
            let dimension = match trigger {
                0 => {
                    policy.limits.max_materialized_bytes = 0;
                    ResourceDimension::MaterializedBytes
                }
                1 => {
                    policy.limits.max_collection_items = 0;
                    ResourceDimension::CollectionItems
                }
                2 => {
                    policy.limits.max_work_units = 0;
                    ResourceDimension::WorkUnits
                }
                _ => {
                    policy.limits.max_retained_bytes = 0;
                    ResourceDimension::RetainedBytes
                }
            };
            let arena = DecodeArena::new();
            let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
            let result = if second {
                surface_second_partials(&ctx, &surface, 0.5, 0.5).map(|value| value.point)
            } else {
                surface_partials(&ctx, &surface, 0.5, 0.5).map(|value| value.point)
            };
            if trigger == 3 {
                assert_eq!(result.unwrap().get(), Point3::new(0.5, 0.5, 0.0));
                assert!(ctx.finish_session().is_ok());
            } else {
                let EvaluationFailure::ResourceLimit(first) = result.unwrap_err() else {
                    panic!("refusal must remain a resource error");
                };
                assert_eq!(first.dimension, dimension);
                assert_eq!((first.limit, first.used), (0, 0));
                assert!(first.additional > 0);
                assert_eq!(
                    surface_second_partials(&ctx, &surface, f64::NAN, 0.5),
                    Err(EvaluationFailure::ResourceLimit(first))
                );
                assert!(
                    matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == first)
                );
            }
        }
    }
}

#[test]
fn placed_surface_partials_preserve_work_and_depth_refusals() {
    let surface = SurfaceGeometry::Solved(SolvedSurfaceGeometry::Transformed(
        crate::geometry::PlacedSurface::try_new(
            Box::new(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(),
            )),
            crate::transform::Transform::affine([
                [2.0, 0.0, 0.0, 0.0],
                [0.0, 2.0, 0.0, 0.0],
                [0.0, 0.0, 2.0, 0.0],
            ])
            .unwrap(),
        )
        .unwrap(),
    ));
    for depth in [false, true] {
        for second in [false, true] {
            let mut policy = DecodePolicy::service();
            let dimension = if depth {
                policy.limits.max_recursion_depth = 0;
                ResourceDimension::RecursionDepth
            } else {
                policy.limits.max_work_units = 0;
                ResourceDimension::WorkUnits
            };
            let arena = DecodeArena::new();
            let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
            let result = if second {
                surface_second_partials(&ctx, &surface, 0.5, 0.5).map(|value| value.point)
            } else {
                surface_partials(&ctx, &surface, 0.5, 0.5).map(|value| value.point)
            };
            let EvaluationFailure::ResourceLimit(first) = result.unwrap_err() else {
                panic!("placed step must refuse");
            };
            assert_eq!(first.dimension, dimension);
            assert_eq!((first.limit, first.used, first.additional), (0, 0, 1));
            assert_eq!(
                surface_partials(&ctx, &surface, f64::NAN, 0.5),
                Err(EvaluationFailure::ResourceLimit(first))
            );
            assert!(
                matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == first)
            );
        }
    }
}