cadmpeg-ir 0.6.2

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

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::geometry::Curve;
use crate::geometry::CurveGeometry;
use crate::geometry::ProceduralSurface;
use crate::geometry::ProceduralSurfaceDefinition;
use crate::geometry::SolvedCurveGeometry;
use crate::geometry::Surface;
use crate::geometry::SurfaceGeometry;
use crate::ids::CurveId;
use crate::ids::ProceduralSurfaceId;
use crate::ids::SurfaceId;
use crate::math::Point3;
use crate::math::Vector3;
use crate::CadIr;

fn direct_surface_fixture(
    definition: ProceduralSurfaceDefinition,
    surface_name: &str,
) -> (CadIr, SurfaceId) {
    let construction_id =
        ProceduralSurfaceId::mint(format!("test:model:construction#{surface_name}"))
            .expect("valid identity");
    let surface_id =
        SurfaceId::mint(format!("test:model:surface#{surface_name}")).expect("valid identity");
    let mut ir = CadIr::empty();
    ir.model.curves = vec![
        Curve {
            parameter_range: None,
            id: CurveId::mint("test:model:entity#first").expect("valid identity"),
            geometry: CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(
                crate::geometry::nurbs::NurbsCurve::from_lanes(
                    1,
                    vec![0.0, 0.0, 1.0, 1.0],
                    vec![Point3::new(1.0, 2.0, 3.0), Point3::new(3.0, 2.0, 3.0)],
                    None,
                    false,
                )
                .unwrap(),
            )),
            source_object: None,
        },
        Curve {
            parameter_range: None,
            id: CurveId::mint("test:model:entity#second").expect("valid identity"),
            geometry: CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(
                crate::geometry::nurbs::NurbsCurve::from_lanes(
                    1,
                    vec![0.0, 0.0, 1.0, 1.0],
                    vec![Point3::new(5.0, 10.0, 13.0), Point3::new(5.0, 13.0, 13.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: definition,
        cache_fit_tolerance: None,
        record_bounds: None,
    });
    (ir, surface_id)
}

#[test]
fn cacheless_ruled_surface_interpolates_profiles_and_partials() {
    let (ir, surface_id) = direct_surface_fixture(
        ProceduralSurfaceDefinition::Ruled {
            first: CurveId::mint("test:model:entity#first").expect("valid identity"),
            second: CurveId::mint("test:model:entity#second").expect("valid identity"),
            cache: None,
        },
        "ruled",
    );
    let index = crate::index::ModelIndex::build(&ir);
    let point = model_surface_point_by_id(
        crate::eval::admission::EvaluationAdmission::Standard,
        &index,
        &surface_id,
        0.25,
        0.5,
    )
    .expect("cacheless ruled point")
    .get();
    assert_eq!(point, Point3::new(3.25, 6.375, 8.0));
    assert_eq!(
        model_surface_point(
            crate::eval::admission::EvaluationAdmission::Standard,
            &ir,
            &ir.model.surfaces[0].geometry,
            0.25,
            0.5
        )
        .map(crate::features::FinitePoint3::get),
        Ok(point)
    );
    let partials = model_surface_second_partials_by_id(
        crate::eval::admission::EvaluationAdmission::Standard,
        &index,
        &surface_id,
        0.25,
        0.5,
    )
    .expect("cacheless ruled second partials");
    assert_eq!(partials.point, point);
    assert_eq!(partials.du, Vector3::new(1.0, 1.5, 0.0));
    assert_eq!(partials.dv, Vector3::new(3.5, 8.75, 10.0));
    assert_eq!(partials.duu, Vector3::new(0.0, 0.0, 0.0));
    assert_eq!(partials.duv, Vector3::new(-2.0, 3.0, 0.0));
    assert_eq!(partials.dvv, Vector3::new(0.0, 0.0, 0.0));
}

#[test]
fn cacheless_sum_surface_adds_independent_curve_parameters() {
    let (ir, surface_id) = direct_surface_fixture(
        ProceduralSurfaceDefinition::Sum(
            crate::geometry::surface_payloads::SumSurfaceConstruction::try_new(
                CurveId::mint("test:model:entity#first").expect("valid identity"),
                CurveId::mint("test:model:entity#second").expect("valid identity"),
                Vector3::new(0.5, 1.0, 2.0),
                crate::geometry::CacheContract::from_form(None),
            )
            .expect("valid sum"),
        ),
        "sum",
    );
    let index = crate::index::ModelIndex::build(&ir);
    let point = model_surface_point_by_id(
        crate::eval::admission::EvaluationAdmission::Standard,
        &index,
        &surface_id,
        0.25,
        0.5,
    )
    .expect("cacheless sum point")
    .get();
    assert_eq!(point, Point3::new(6.0, 12.5, 14.0));
    let partials = model_surface_partials_by_id(
        crate::eval::admission::EvaluationAdmission::Standard,
        &index,
        &surface_id,
        0.25,
        0.5,
    )
    .expect("cacheless sum 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, 3.0, 0.0));
}

#[test]
fn a_ruled_surface_whose_point_overflows_reports_the_point_it_reached() {
    // At v = MAX the displacement between the profiles carries every
    // coordinate past the finite range.
    let (ir, surface_id) = direct_surface_fixture(
        ProceduralSurfaceDefinition::Ruled {
            first: CurveId::mint("test:model:entity#first").expect("valid identity"),
            second: CurveId::mint("test:model:entity#second").expect("valid identity"),
            cache: None,
        },
        "ruled",
    );
    let index = crate::index::ModelIndex::build(&ir);
    let reached =
        |point: Result<crate::features::FinitePoint3, crate::eval::EvaluationFailure<Point3>>| {
            matches!(point, Err(crate::eval::EvaluationFailure::NonFinite(point))
            if point.x.is_nan() && point.y.is_nan() && point.z.is_nan())
        };
    assert!(reached(model_surface_point_by_id(
        crate::eval::admission::EvaluationAdmission::Standard,
        &index,
        &surface_id,
        0.25,
        f64::MAX
    )));
    assert!(reached(model_surface_point(
        crate::eval::admission::EvaluationAdmission::Standard,
        &ir,
        &ir.model.surfaces[0].geometry,
        0.25,
        f64::MAX
    )));
}

#[test]
fn a_sum_surface_whose_point_overflows_reports_the_point_it_reached() {
    let (mut ir, surface_id) = direct_surface_fixture(
        ProceduralSurfaceDefinition::Sum(
            crate::geometry::surface_payloads::SumSurfaceConstruction::try_new(
                CurveId::mint("test:model:entity#first").expect("valid identity"),
                CurveId::mint("test:model:entity#second").expect("valid identity"),
                Vector3::new(0.5, 1.0, 2.0),
                crate::geometry::CacheContract::from_form(None),
            )
            .expect("valid sum"),
        ),
        "sum",
    );
    // Both profiles lie at x = MAX, so their sum leaves the finite range in
    // x only.
    for (curve, poles) in ir.model.curves.iter_mut().zip([
        [
            Point3::new(f64::MAX, 2.0, 3.0),
            Point3::new(f64::MAX, 4.0, 3.0),
        ],
        [
            Point3::new(f64::MAX, 10.0, 13.0),
            Point3::new(f64::MAX, 13.0, 13.0),
        ],
    ]) {
        curve.geometry = CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(
            crate::geometry::nurbs::NurbsCurve::from_lanes(
                1,
                vec![0.0, 0.0, 1.0, 1.0],
                poles.to_vec(),
                None,
                false,
            )
            .unwrap(),
        ));
    }
    let index = crate::index::ModelIndex::build(&ir);
    assert_eq!(
        model_surface_point_by_id(
            crate::eval::admission::EvaluationAdmission::Standard,
            &index,
            &surface_id,
            0.25,
            0.5
        ),
        Err(crate::eval::EvaluationFailure::NonFinite(Point3::new(
            f64::INFINITY,
            13.0,
            14.0
        )))
    );
}