cadmpeg-ir 0.6.1

CAD data model, validation, diffing, and codec API for cadmpeg.
// SPDX-License-Identifier: Apache-2.0

use crate::geometry::nurbs::{NurbsCurve, NurbsSurface, NurbsSurfaceAxis, NurbsSurfaceLanes};
use crate::math::Point3;
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
use cadmpeg_core::CodecError;

fn with_limit<T>(cap: u64, f: impl FnOnce(&DecodeContext<'_>) -> T) -> T {
    let arena = DecodeArena::new();
    let mut policy = DecodePolicy::service();
    policy.limits.max_collection_items = cap;
    let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
    f(&ctx)
}

#[test]
fn raw_curve_constructor_refuses_pairing_and_admission() {
    for (cap, operation) in [(1, "IR NURBS paired poles"), (3, "IR NURBS admitted poles")] {
        let result = with_limit(cap, |ctx| {
            NurbsCurve::from_lanes(
                ctx,
                1,
                vec![0.0, 0.0, 1.0, 1.0],
                vec![Point3::new(0.0, 0.0, 0.0); 2],
                Some(vec![1.0; 2]),
                false,
            )
        });
        assert!(
            matches!(result, Err(CodecError::ResourceLimit(resource)) if resource.operation == operation)
        );
    }
    let points = vec![Point3::new(0.0, 0.0, 0.0); 2];
    let expected = NurbsCurve::from_lanes(
        &cadmpeg_test_support::service_decode_context(),
        1,
        vec![0.0, 0.0, 1.0, 1.0],
        points.clone(),
        Some(vec![1.0; 2]),
        false,
    )
    .expect("fixture constructor admission");
    assert_eq!(
        with_limit(4, |ctx| NurbsCurve::from_lanes(
            ctx,
            1,
            vec![0.0, 0.0, 1.0, 1.0],
            points,
            Some(vec![1.0; 2]),
            false
        ))
        .expect("exact cap"),
        expected
    );
}

#[test]
fn raw_surface_constructor_refuses_each_nested_collection() {
    let make = |ctx: &DecodeContext<'_>| {
        NurbsSurface::from_lanes(
            ctx,
            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); 2]; 2],
                Some(vec![vec![1.0; 2]; 2]),
            ),
            false,
        )
    };
    for (cap, operation) in [
        (0, "IR NURBS paired grid rows"),
        (2, "IR NURBS paired poles"),
        (3, "IR NURBS paired grid rows"),
        (5, "IR NURBS paired poles"),
        (6, "IR NURBS admitted grid rows"),
        (8, "IR NURBS admitted poles"),
        (9, "IR NURBS admitted grid rows"),
        (11, "IR NURBS admitted poles"),
    ] {
        assert!(
            matches!(with_limit(cap, make), Err(CodecError::ResourceLimit(resource)) if resource.operation == operation)
        );
    }
    let expected = NurbsSurface::from_lanes(
        &cadmpeg_test_support::service_decode_context(),
        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); 2]; 2],
            Some(vec![vec![1.0; 2]; 2]),
        ),
        false,
    )
    .expect("fixture constructor admission");
    assert_eq!(with_limit(12, make).expect("exact cap"), expected);
}

#[test]
fn raw_constructor_refusal_text_is_admitted_and_keeps_order() {
    for (degree, knots, points, weights) in [
        (
            1,
            vec![f64::NAN],
            vec![Point3::new(f64::NAN, 0.0, 0.0)],
            Some(vec![]),
        ),
        (
            1,
            vec![f64::NAN],
            vec![Point3::new(f64::NAN, 0.0, 0.0)],
            Some(vec![0.0]),
        ),
        (
            1,
            vec![f64::NAN],
            vec![Point3::new(f64::NAN, 0.0, 0.0)],
            None,
        ),
        (
            1,
            vec![f64::NAN; 4],
            vec![Point3::new(f64::NAN, 0.0, 0.0); 2],
            None,
        ),
        (
            1,
            vec![f64::NAN; 4],
            vec![Point3::new(0.0, 0.0, 0.0); 2],
            None,
        ),
        (
            1,
            vec![0.0, 1.0, 0.0, 1.0],
            vec![Point3::new(0.0, 0.0, 0.0); 2],
            None,
        ),
    ] {
        let expected = NurbsCurve::from_lanes(
            &cadmpeg_test_support::service_decode_context(),
            degree,
            knots.clone(),
            points.clone(),
            weights.clone(),
            false,
        )
        .expect("fixture constructor admission")
        .expect_err("invalid lanes")
        .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).expect("root");
        assert!(
            matches!(NurbsCurve::from_lanes(&ctx, degree, knots.clone(), points.clone(), weights.clone(), false),
            Err(CodecError::ResourceLimit(resource)) if resource.dimension == cadmpeg_core::decode::ResourceDimension::RetainedBytes)
        );
        let actual = with_limit(u64::MAX, |ctx| {
            NurbsCurve::from_lanes(ctx, degree, knots, points, weights, false)
        })
        .expect("service refusal text")
        .expect_err("same geometry refusal")
        .to_string();
        assert_eq!(actual, expected);
    }
}

#[test]
fn raw_curve_constructor_retains_converted_poles_and_scopes_pairing() {
    let arena = DecodeArena::new();
    let mut policy = DecodePolicy::service();
    let output_bytes =
        2 * std::mem::size_of::<super::super::WeightedPole3<crate::features::FinitePoint3>>();
    let temporary_bytes = 4 * std::mem::size_of::<super::super::WeightedPole3<Point3>>();
    policy.limits.max_retained_bytes = u64::try_from(output_bytes).unwrap();
    policy.limits.max_materialized_bytes = u64::try_from(temporary_bytes).unwrap();
    let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
    let curve = NurbsCurve::from_lanes(
        &ctx,
        1,
        vec![0.0, 0.0, 1.0, 1.0],
        vec![Point3::new(0.0, 0.0, 0.0); 2],
        Some(vec![1.0; 2]),
        false,
    )
    .unwrap()
    .unwrap();
    assert_eq!(curve.pole_count(), 2);
    let reservation = ctx
        .reserve_scoped(
            u64::try_from(temporary_bytes).unwrap(),
            "reuse paired storage",
        )
        .unwrap();
    drop(reservation);
}

#[test]
fn admitted_curve_constructor_keeps_polynomial_pole_storage() {
    let arena = DecodeArena::new();
    let mut policy = DecodePolicy::service();
    policy.limits.max_retained_bytes = 0;
    policy.limits.max_materialized_bytes = 0;
    policy.limits.max_collection_items = 0;
    let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
    let points = vec![crate::features::FinitePoint3::new(Point3::new(0.0, 0.0, 0.0)).unwrap(); 2];
    let address = points.as_ptr();
    let curve = NurbsCurve::from_lanes(&ctx, 1, vec![0.0, 0.0, 1.0, 1.0], points, None, false)
        .unwrap()
        .unwrap();
    let super::super::NurbsPoles3::Polynomial { points } = curve.into_parts().2 else {
        panic!("polynomial poles");
    };
    assert_eq!(points.as_ptr(), address);
}