cadmpeg-ir 0.6.2

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

use super::*;

const CHORD_BOUND_TOLERANCE: f64 = 1e-10;
use crate::geometry::nurbs::{NurbsSurfaceAxis, NurbsSurfaceLanes};
use crate::geometry::{
    Curve, ProceduralSurface, RevisionSurfaceParameterization, Surface, SweepRevisionForm,
    SweepSurfaceConstruction,
};
use crate::ids::{CurveId, ProceduralSurfaceId, SurfaceId};

fn bilinear(domain: [f64; 2], scale: f64) -> NurbsSurface {
    NurbsSurface::from_lanes(
        NurbsSurfaceAxis::new(1, vec![domain[0], domain[0], domain[1], domain[1]], false),
        NurbsSurfaceAxis::new(1, vec![0., 0., 1., 1.], false),
        NurbsSurfaceLanes::new(
            vec![
                vec![Point3::new(0., 0., 0.), Point3::new(0., scale, 0.)],
                vec![Point3::new(scale, 0., 0.), Point3::new(scale, scale, 0.)],
            ],
            None,
        ),
        false,
    )
    .unwrap()
}
fn fixture(scale: f64) -> (crate::CadIr, SurfaceId) {
    let profile_id =
        CurveId::mint("test:model:entity#profile-frame-profile").expect("valid identity");
    let spine_id = CurveId::mint("test:model:entity#profile-frame-spine").expect("valid identity");
    let surface_id =
        SurfaceId::mint("test:model:entity#profile-frame-sweep").expect("valid identity");
    let construction_id = ProceduralSurfaceId::mint("test:model:entity#profile-frame-construction")
        .expect("valid identity");
    let mut ir = CadIr::empty();
    ir.model.curves = vec![
        Curve {
            parameter_range: None,
            id: profile_id.clone(),
            geometry: CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(
                NurbsCurve::from_lanes(
                    1,
                    vec![0.0, 0.0, 1.0, 1.0],
                    vec![Point3::new(1.0, 0.0, 0.0), Point3::new(2.0, 0.0, 0.0)],
                    None,
                    false,
                )
                .unwrap(),
            )),
            source_object: None,
        },
        Curve {
            parameter_range: None,
            id: spine_id.clone(),
            geometry: CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(
                NurbsCurve::from_lanes(
                    1,
                    vec![0.0, 0.0, 1.0, 1.0],
                    vec![Point3::new(4.0, 5.0, 6.0), Point3::new(4.0, 5.0, 7.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(ProceduralSurface::new(
        construction_id,
        ProceduralSurfaceDefinition::Sweep(
            crate::geometry::surface_payloads::SweepSurfacePayload::try_new(
                profile_id,
                spine_id,
                Some(Box::new(SweepSurfaceConstruction {
                    primary_kind: 0,
                    cache: crate::geometry::CacheContract::Revision {
                        form: SweepRevisionForm {
                            revision: crate::scalar::PositiveI64::new(22601)
                                .expect("positive revision"),
                            primary_flag: true,
                            profile_endpoints: [None, None],
                            path_endpoints: [None, None],
                            cache: crate::geometry::RevisionCacheForm::Parameterization(
                                RevisionSurfaceParameterization::default(),
                            ),
                        },
                    },
                    layout: SweepSurfaceLayout::LawDriven {
                        mode: -2,
                        profile_range: [-scale, 0.0],
                        profile_frame: Some((
                            Point3::new(2.0, 0.0, 0.0),
                            Vector3::new(0.0, 0.0, -1.0),
                        )),
                        origin: Point3::new(0.0, 0.0, 0.0),
                        directions: [
                            Vector3::new(1.0, 0.0, 0.0),
                            Vector3::new(0.0, 1.0, 0.0),
                            Vector3::new(0.0, 0.0, 1.0),
                        ],
                        first_law: Box::new(LawExpression::Text {
                            value: cadmpeg_core::nonblank_literal!("2.0*X"),
                        }),
                        first_mode: 0,
                        first_range: [0.0, 1.0],
                        law_direction: Vector3::new(0.0, 0.0, 1.0),
                        path_mode: 1,
                        path_flag: true,
                        path_range: [0.0, 1.0],
                        path_parameter: 0.0,
                        second_law_flag: true,
                        second_law: Box::new(LawExpression::Text {
                            value: cadmpeg_core::nonblank_literal!("VEC(2,1,1)"),
                        }),
                        formula_mode: 0,
                        formula: LawFormula::Named {
                            name: cadmpeg_core::nonblank_literal!(
                                "ROTATE(DOMAIN(VEC(1,0,0),0,1),TRANS1)"
                            ),
                            variables: vec![LawExpression::TransformVec {
                                vectors: [
                                    Vector3::new(0.0, 1.0, 0.0),
                                    Vector3::new(-1.0, 0.0, 0.0),
                                    Vector3::new(0.0, 0.0, 1.0),
                                    Vector3::new(0.0, 0.0, 0.0),
                                ],
                                scale: 1.0,
                                flags: [true, false, false],
                            }],
                        },
                        trailing_flag: false,
                    },
                    discontinuities: std::array::from_fn(|_| Vec::new()),
                    discontinuity_flag: false,
                })),
            )
            .unwrap(),
        ),
        None,
    ));

    (ir, surface_id)
}
#[test]
fn numerical_0922_wide_surface_chart_keeps_inverse_and_bound() {
    for d in [[0., 1.], [-1e308, 1e308]] {
        let s = bilinear(d, 1.);
        let endpoints = [Point2::new(d[0], 0.), Point2::new(d[1], 1.)];
        let bound = crate::eval::nurbs_surface_parameter_segment_chord_bound(
            &cadmpeg_test_support::service_decode_context(),
            &s,
            endpoints,
            [Point3::new(0., 0., 0.), Point3::new(1., 1., 0.)],
        )
        .expect("bound the NURBS surface segment's chord deviation");
        let inverse = crate::eval::nurbs_surface_parameter_near_point(
            crate::eval::admission::EvaluationAdmission::Standard,
            &s,
            Point3::new(0.5, 0.5, 0.),
            None,
        )
        .expect("compute the NURBS surface parameter near point");
        println!("IR plane domain {d:?}: chord bound={bound:?}, inverse={inverse:?}");
        assert!(bound.unwrap() < CHORD_BOUND_TOLERANCE);
        let expected_u = d[0].midpoint(d[1]);
        assert_eq!(
            inverse.map(crate::units::FinitePoint2::get),
            Some(Point2::new(expected_u, 0.5))
        );
    }
}
#[test]
fn numerical_0922_far_surface_query_keeps_inverse() {
    let s = bilinear([0., 1.], 1.);
    for z in [1., 1e200] {
        let budget = cadmpeg_core::decode::WorkBudget::new(100_000);
        let result = crate::eval::nurbs_surface_closest_parameter_with_budget(
            &cadmpeg_test_support::service_decode_context(),
            &s,
            Point3::new(0.5, 0.5, z),
            None,
            &budget,
        )
        .expect("find the closest NURBS surface parameters");
        println!(
            "IR unit plane, query z={z:e}: {result:?}, budget {}",
            budget.consumed()
        );
        assert_eq!(
            result.map(crate::units::FinitePoint2::get),
            Some(Point2::new(0.5, 0.5))
        );
    }
}
#[test]
fn numerical_0922_reflection_rejects_unrelated_points() {
    for scale in [1., 1e200] {
        let a = Point3::new(0., scale, 0.);
        let b = Point3::new(scale, scale, 0.);
        let result = crate::eval::spatial_points_are_reflections(
            a,
            b,
            Point3::new(0., 0., 0.),
            Point3::new(scale, 0., 0.),
        );
        println!("IR non-reflections scale{scale:e}: {result}");
        assert!(!result);
        assert!(spatial_points_are_reflections(
            a,
            Point3::new(0., -scale, 0.),
            Point3::new(0., 0., 0.),
            Point3::new(scale, 0., 0.)
        ));
    }
}
#[test]
fn numerical_0922_sweep_evaluation_ignores_profile_units() {
    for scale in [1., 1e18] {
        let (ir, id) = fixture(scale);
        let index = crate::index::ModelIndex::build(&ir);
        let r = crate::eval::model_surface_point_by_id(
            crate::eval::admission::EvaluationAdmission::Standard,
            &index,
            &id,
            -0.25 * scale,
            0.25,
        );
        println!("IR same law sweep profile chart scale{scale:e}: {r:?}");
        assert!(r.unwrap().distance(Point3::new(-0.5, 0.5, 0.25)) < 1e-14);
    }
}

#[test]
fn numerical_0922b_finite_chord_bound() {
    for s in [1., 1e200] {
        let surf = NurbsSurface::from_lanes(
            NurbsSurfaceAxis::new(1, vec![0., 0., 1., 1.], false),
            NurbsSurfaceAxis::new(1, vec![0., 0., 1., 1.], false),
            NurbsSurfaceLanes::new(
                vec![
                    vec![Point3::new(0., 0., 0.), Point3::new(0., s, 0.)],
                    vec![Point3::new(s, 0., 0.), Point3::new(s, s, s)],
                ],
                None,
            ),
            false,
        )
        .unwrap();
        let r = nurbs_surface_parameter_segment_chord_bound(
            &cadmpeg_test_support::service_decode_context(),
            &surf,
            [Point2::new(0., 0.), Point2::new(1., 1.)],
            [Point3::new(0., 0., 0.), Point3::new(s, s, s)],
        )
        .expect("bound the NURBS surface segment's chord deviation");
        println!("IR curved diagonal scale{s:e}:bound{r:?}");
        let bound = r.unwrap() / s;
        assert!((0.25..0.34).contains(&bound));
    }
}

#[test]
fn numerical_audit_membership_and_inverse_keep_wide_knot_domains() {
    for [a, b] in [[0., 1.], [-1e308, 1e308]] {
        let knots = [a, a, b, b];
        let points = [Point2::new(0., 0.), Point2::new(1., 0.)];
        assert_eq!(
            nurbs_pcurve_contains_point(
                1,
                &knots,
                &points,
                None,
                Point2::new(0.5, 0.),
                CHORD_BOUND_TOLERANCE,
            ),
            Some(true)
        );
        assert_eq!(
            nurbs_pcurve_contains_point(
                1,
                &knots,
                &points,
                None,
                Point2::new(0.5, 2.),
                CHORD_BOUND_TOLERANCE,
            ),
            Some(false)
        );
    }
}
#[test]
fn numerical_audit_plane_inverse_projects_before_displacement_overflow() {
    let k = std::f64::consts::FRAC_1_SQRT_2;
    let plane = SolvedSurfaceGeometry::Plane(
        crate::geometry::analytic::PlaneSurface::try_new(
            Point3::new(-1e308, 0., 0.),
            Vector3::new(0., 0., 1.),
            Vector3::new(k, k, 0.),
        )
        .unwrap(),
    );
    let uv = analytic_surface_parameters_solved(&plane, Point3::new(1e308, 0., 0.)).unwrap();
    assert!((uv.as_raw().u / 1e308 - std::f64::consts::SQRT_2).abs() < 8. * f64::EPSILON);
    assert!((uv.as_raw().v / 1e308 + std::f64::consts::SQRT_2).abs() < 8. * f64::EPSILON);
}