cadmpeg-ir 0.6.2

CAD data model, validation, diffing, and codec API for cadmpeg.
// SPDX-License-Identifier: Apache-2.0
use super::{
    CodecError, DecodeContext, HarmonicPcurve, HyperbolicPcurve, LinePcurve, PcurveGeometry,
    Point2, Transform2,
};
use crate::geometry::pcurve::{
    CirclePcurve, EllipsePcurve, HyperbolaPcurve, OffsetPcurve, ParabolaPcurve, PcurveNurbs,
    PlacedPcurve, TrimmedPcurve,
};
use cadmpeg_core::decode::{DecodeArena, DecodePolicy};

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

fn nurbs(x: f64, rational: bool) -> PcurveGeometry {
    PcurveGeometry::Nurbs {
        nurbs: PcurveNurbs::from_lanes(
            1,
            vec![0.0, 0.0, 1.0, 1.0],
            vec![Point2::new(x, 1.0); 2],
            rational.then(|| vec![1.0, 2.0]),
            false,
        )
        .expect("curve"),
    }
}

#[test]
fn owned_pcurve_scaling_scales_both_pole_forms() {
    for rational in [false, true] {
        let mut expected = nurbs(1.0, rational);
        expected
            .try_scale_coordinates([2.0, 3.0])
            .expect("reference");
        assert_eq!(
            with_depth(0, |ctx| nurbs(1.0, rational)
                .scaled_coordinates_owned(ctx, [2.0, 3.0]))
            .expect("no copies"),
            Ok(expected)
        );
        assert_eq!(
            with_depth(0, |ctx| nurbs(f64::MAX, rational)
                .scaled_coordinates_owned(ctx, [2.0, 3.0]))
            .expect("borrowed refusal"),
            Err("control_points contains a non-finite point")
        );
    }
}

#[test]
fn owned_pcurve_scaling_refuses_each_wrapper_depth_and_keeps_service_geometry() {
    let wrapped = [
        PcurveGeometry::Trimmed(
            TrimmedPcurve::try_new([0.0, 1.0], true, Box::new(nurbs(1.0, false))).expect("trim"),
        ),
        PcurveGeometry::Offset(
            OffsetPcurve::try_new(2.0, Box::new(nurbs(1.0, false))).expect("offset"),
        ),
        PcurveGeometry::Transformed(
            PlacedPcurve::try_new(Box::new(nurbs(1.0, false)), Transform2::identity())
                .expect("placed"),
        ),
    ];
    for geometry in wrapped {
        assert!(
            matches!(with_depth(0, |ctx| geometry.clone().scaled_coordinates_owned(ctx, [2.0, 2.0])),
            Err(CodecError::ResourceLimit(resource)) if resource.operation == "IR pcurve coordinate scaling nesting")
        );
        let mut expected = geometry.clone();
        expected
            .try_scale_coordinates([2.0, 2.0])
            .expect("reference");
        assert_eq!(
            with_depth(1, |ctx| geometry.scaled_coordinates_owned(ctx, [2.0, 2.0]))
                .expect("no copies"),
            Ok(expected)
        );
    }
}

#[test]
fn owned_pcurve_scaling_preserves_analytic_conversion_and_refusal_order() {
    let p = |u, v| Point2::new(u, v);
    let geometry = [
        PcurveGeometry::Line(LinePcurve::U_AXIS),
        PcurveGeometry::Circle(
            CirclePcurve::try_new(p(1.0, 2.0), p(1.0, 0.0), p(0.0, 1.0), 2.0).expect("circle"),
        ),
        PcurveGeometry::Ellipse(
            EllipsePcurve::try_new(p(1.0, 2.0), p(1.0, 0.0), p(0.0, 1.0), 3.0, 2.0)
                .expect("ellipse"),
        ),
        PcurveGeometry::Parabola(
            ParabolaPcurve::try_new(p(1.0, 2.0), p(1.0, 0.0), p(0.0, 1.0), 2.0).expect("parabola"),
        ),
        PcurveGeometry::Hyperbola(
            HyperbolaPcurve::try_new(p(1.0, 2.0), p(1.0, 0.0), p(0.0, 1.0), 3.0, 2.0)
                .expect("hyperbola"),
        ),
        PcurveGeometry::Harmonic(
            HarmonicPcurve::try_new(p(1.0, 2.0), p(1.0, 0.0), p(0.0, 1.0)).expect("harmonic"),
        ),
        PcurveGeometry::Hyperbolic(
            HyperbolicPcurve::try_new(p(1.0, 2.0), p(1.0, 0.0), p(0.0, 1.0)).expect("hyperbolic"),
        ),
    ];
    for geometry in geometry {
        for scales in [[2.0, 3.0], [2.0, 2.0], [f64::MAX, f64::MAX], [0.0, 0.0]] {
            let mut expected = geometry.clone();
            let expected = expected
                .try_scale_coordinates(scales)
                .map(|()| expected)
                .map_err(|error| error.to_string());
            let actual = with_depth(u64::MAX, |ctx| {
                geometry.clone().scaled_coordinates_owned(ctx, scales)
            })
            .expect("admitted");
            assert_eq!(actual.map_err(str::to_owned), expected);
        }
    }
    for geometry in [
        PcurveGeometry::Offset(
            OffsetPcurve::try_new(f64::MAX, Box::new(PcurveGeometry::Line(LinePcurve::U_AXIS)))
                .expect("offset"),
        ),
        PcurveGeometry::Transformed(
            PlacedPcurve::try_new(
                Box::new(PcurveGeometry::Line(LinePcurve::U_AXIS)),
                Transform2::identity(),
            )
            .expect("placed"),
        ),
    ] {
        for scales in [[2.0, 2.0], [2.0, 3.0], [0.0, 0.0]] {
            let mut expected = geometry.clone();
            let expected = expected
                .try_scale_coordinates(scales)
                .map(|()| expected)
                .map_err(|error| error.to_string());
            let actual = with_depth(u64::MAX, |ctx| {
                geometry.clone().scaled_coordinates_owned(ctx, scales)
            })
            .expect("admitted");
            assert_eq!(actual.map_err(str::to_owned), expected);
        }
    }
}