cadmpeg-ir 0.6.1

CAD data model, validation, diffing, and codec API for cadmpeg.
// SPDX-License-Identifier: Apache-2.0
use crate::geometry::{
    ProceduralSurfaceDefinition, RollingBallJetDerivative, RollingBallJetSite,
    RollingBallJetStation,
};
use crate::math::{Point3, Vector3};
use serde_json::json;

fn station(knot: f64, multiplicity: u32) -> RollingBallJetStation {
    let derivative = RollingBallJetDerivative {
        first_limit: Vector3::new(0.0, 0.0, 0.0),
        second_limit: Vector3::new(0.0, 0.0, 0.0),
        center: Vector3::new(0.0, 0.0, 0.0),
        angle: 0.0,
    };
    RollingBallJetStation {
        knot,
        multiplicity,
        site: RollingBallJetSite {
            first_limit: Point3::new(1.0, 0.0, knot),
            second_limit: Point3::new(0.0, 1.0, knot),
            center: Point3::new(0.0, 0.0, knot),
            angle: std::f64::consts::FRAC_PI_2,
            first_derivative: derivative,
            second_derivative: derivative,
        },
    }
}

#[test]
fn rolling_ball_jet_station_rows_preserve_the_flat_wire() {
    let definition = ProceduralSurfaceDefinition::RollingBallJet(
        crate::geometry::RollingBallJetStations::try_new(
            5,
            vec![station(2.0, 6), station(4.0, 3), station(8.0, 6)],
            &cadmpeg_test_support::service_decode_context(),
        )
        .expect("fixture rolling-ball admission")
        .unwrap(),
    );
    let wire = serde_json::to_value(&definition).unwrap();
    assert_eq!(wire["stations"][0]["knot"], json!(2.0));
    assert_eq!(wire["stations"][1]["multiplicity"], json!(3));
    assert_eq!(
        wire["stations"][1]["site"]["center"],
        json!({"x": 0.0, "y": 0.0, "z": 4.0})
    );
    // The three columns travel as one row each, so a knot without its
    // multiplicity or its site has no spelling.
    assert!(wire.get("knots").is_none());
    assert!(wire.get("multiplicities").is_none());
    assert!(wire.get("sites").is_none());
    assert_eq!(
        serde_json::from_value::<ProceduralSurfaceDefinition>(wire.clone()).unwrap(),
        definition
    );
    for column in ["knot", "multiplicity", "site"] {
        let mut malformed = wire.clone();
        malformed["stations"][1]
            .as_object_mut()
            .unwrap()
            .remove(column);
        let error = serde_json::from_value::<ProceduralSurfaceDefinition>(malformed).unwrap_err();
        assert!(error.to_string().contains(column), "{error}");
    }
}

#[test]
fn rolling_ball_jet_admits_only_clamped_finite_station_payloads() {
    use crate::geometry::RollingBallJetStations;

    let valid = || vec![station(2.0, 6), station(8.0, 6)];
    for degree in [0, u32::MAX] {
        assert!(RollingBallJetStations::try_new(
            degree,
            valid(),
            &cadmpeg_test_support::service_decode_context()
        )
        .expect("fixture rolling-ball admission")
        .is_err());
    }
    for stations in [
        Vec::new(),
        vec![station(2.0, 6)],
        vec![station(8.0, 6), station(2.0, 6)],
        vec![station(2.0, 6), station(2.0, 6)],
        vec![station(f64::NAN, 6), station(8.0, 6)],
        vec![station(2.0, 5), station(8.0, 6)],
        vec![station(2.0, 6), station(8.0, 5)],
        vec![station(2.0, 6), station(4.0, 0), station(8.0, 6)],
        vec![station(2.0, 6), station(4.0, 7), station(8.0, 6)],
    ] {
        assert!(RollingBallJetStations::try_new(
            5,
            stations,
            &cadmpeg_test_support::service_decode_context()
        )
        .expect("fixture rolling-ball admission")
        .is_err());
    }
    let mut stations = valid();
    stations[0].site.first_derivative.angle = f64::INFINITY;
    assert!(RollingBallJetStations::try_new(
        5,
        stations,
        &cadmpeg_test_support::service_decode_context()
    )
    .expect("fixture rolling-ball admission")
    .is_err());
    let mut stations = valid();
    stations[0].site.center.x = f64::NAN;
    assert!(RollingBallJetStations::try_new(
        5,
        stations,
        &cadmpeg_test_support::service_decode_context()
    )
    .expect("fixture rolling-ball admission")
    .is_err());
    let mut stations = valid();
    stations[0].site.first_limit = stations[0].site.center;
    assert!(RollingBallJetStations::try_new(
        5,
        stations,
        &cadmpeg_test_support::service_decode_context()
    )
    .expect("fixture rolling-ball admission")
    .is_err());
    let mut stations = valid();
    stations[0].site.first_limit.x = 2.0;
    assert!(RollingBallJetStations::try_new(
        5,
        stations,
        &cadmpeg_test_support::service_decode_context()
    )
    .expect("fixture rolling-ball admission")
    .is_err());

    assert!(RollingBallJetStations::try_new(
        1,
        vec![station(2.0, 2), station(8.0, 2)],
        &cadmpeg_test_support::service_decode_context()
    )
    .expect("fixture rolling-ball admission")
    .is_ok());
    assert!(RollingBallJetStations::try_new(
        u32::MAX - 1,
        vec![station(2.0, u32::MAX), station(8.0, u32::MAX)],
        &cadmpeg_test_support::service_decode_context()
    )
    .expect("fixture rolling-ball admission")
    .is_ok());
    let mut varying_radius = valid();
    varying_radius[1].site.first_limit.x = 2.0;
    varying_radius[1].site.second_limit.y = 2.0;
    assert!(RollingBallJetStations::try_new(
        5,
        varying_radius,
        &cadmpeg_test_support::service_decode_context()
    )
    .expect("fixture rolling-ball admission")
    .is_ok());

    let definition = ProceduralSurfaceDefinition::RollingBallJet(
        RollingBallJetStations::try_new(
            5,
            valid(),
            &cadmpeg_test_support::service_decode_context(),
        )
        .expect("fixture rolling-ball admission")
        .unwrap(),
    );
    let wire = serde_json::to_value(definition).unwrap();
    for (field, value) in [("degree", json!(0)), ("degree", json!(u32::MAX))] {
        let mut malformed = wire.clone();
        malformed[field] = value;
        assert!(serde_json::from_value::<ProceduralSurfaceDefinition>(malformed).is_err());
    }
    for (path, value) in [
        (["stations", "0", "knot"], json!(8.0)),
        (["stations", "1", "knot"], json!(2.0)),
        (["stations", "0", "multiplicity"], json!(5)),
    ] {
        let mut malformed = wire.clone();
        malformed[path[0]][path[1].parse::<usize>().unwrap()][path[2]] = value;
        assert!(serde_json::from_value::<ProceduralSurfaceDefinition>(malformed).is_err());
    }
    let mut malformed = wire;
    malformed["stations"][0]["site"]["first_limit"]["x"] = json!(2.0);
    assert!(serde_json::from_value::<ProceduralSurfaceDefinition>(malformed).is_err());
}

#[test]
fn admitted_rolling_ball_stations_keep_the_refusals_of_raw_stations() {
    use crate::geometry::RollingBallJetStations;
    use crate::scalar::FiniteReal;

    let admit = |stations: Vec<RollingBallJetStation>| {
        stations
            .into_iter()
            .map(|station| RollingBallJetStation {
                knot: FiniteReal::new(station.knot).unwrap(),
                multiplicity: station.multiplicity,
                site: station.site.admit().unwrap(),
            })
            .collect::<Vec<_>>()
    };
    let mut collapsed_radius = vec![station(2.0, 6), station(8.0, 6)];
    collapsed_radius[0].site.first_limit = collapsed_radius[0].site.center;
    let mut unequal_radii = vec![station(2.0, 6), station(8.0, 6)];
    unequal_radii[0].site.first_limit.x = 2.0;
    for (degree, stations) in [
        (5, vec![station(2.0, 6), station(4.0, 3), station(8.0, 6)]),
        (0, vec![station(2.0, 6), station(8.0, 6)]),
        (5, vec![station(2.0, 6)]),
        (5, vec![station(8.0, 6), station(2.0, 6)]),
        (5, vec![station(2.0, 6), station(2.0, 6)]),
        (5, vec![station(2.0, 5), station(8.0, 6)]),
        (5, vec![station(2.0, 6), station(4.0, 7), station(8.0, 6)]),
        (5, collapsed_radius),
        (5, unequal_radii),
    ] {
        assert_eq!(
            RollingBallJetStations::from_parts(
                degree,
                admit(stations.clone()),
                &cadmpeg_test_support::service_decode_context()
            )
            .expect("fixture rolling-ball admission"),
            RollingBallJetStations::try_new(
                degree,
                stations,
                &cadmpeg_test_support::service_decode_context()
            )
            .expect("fixture rolling-ball admission")
        );
    }
}

#[test]
fn rolling_ball_jet_decode_refuses_work_and_retained_rows() {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
    for dimension in [
        ResourceDimension::WorkUnits,
        ResourceDimension::RetainedBytes,
    ] {
        let arena = DecodeArena::new();
        let mut policy = DecodePolicy::service();
        if dimension == ResourceDimension::WorkUnits {
            policy.limits.max_work_units = 0;
        } else {
            policy.limits.max_retained_bytes = 0;
        }
        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
        let result = crate::geometry::RollingBallJetStations::try_new(
            5,
            vec![station(2.0, 6), station(8.0, 6)],
            &ctx,
        );
        assert!(
            matches!(result, Err(cadmpeg_core::CodecError::ResourceLimit(limit)) if limit.dimension == dimension)
        );
    }
}

#[test]
fn rolling_ball_constructors_preserve_first_and_later_caller_work_refusals() {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
    use cadmpeg_core::CodecError;
    for typed in [false, true] {
        for (cap, operation) in [
            (0, "rolling-ball jet multiplicities"),
            (1, "rolling-ball jet multiplicities"),
            (2, "rolling-ball jet knots"),
            (3, "rolling-ball jet knots"),
            (4, "rolling-ball jet station controls"),
            (5, "rolling-ball jet station controls"),
        ] {
            let raw = vec![station(2.0, 6), station(8.0, 6)];
            let arena = DecodeArena::new();
            let mut policy = DecodePolicy::service();
            policy.limits.max_work_units = cap;
            let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
            let result = if typed {
                let rows = raw
                    .into_iter()
                    .map(|row| RollingBallJetStation {
                        knot: crate::scalar::FiniteReal::new(row.knot).unwrap(),
                        multiplicity: row.multiplicity,
                        site: row.site.admit().unwrap(),
                    })
                    .collect();
                crate::geometry::RollingBallJetStations::from_parts(5, rows, &ctx)
            } else {
                crate::geometry::RollingBallJetStations::try_new(5, raw, &ctx)
            };
            let Err(CodecError::ResourceLimit(limit)) = result else {
                panic!("station admission must refuse");
            };
            assert_eq!(limit.dimension, ResourceDimension::WorkUnits);
            assert_eq!(limit.operation, operation);
            assert!(
                matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(original)) if original == limit)
            );
        }
    }
}

#[test]
fn rolling_ball_typed_constructor_moves_owned_rows_without_allocation() {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
    let rows = [station(2.0, 6), station(8.0, 6)]
        .into_iter()
        .map(|row| RollingBallJetStation {
            knot: crate::scalar::FiniteReal::new(row.knot).unwrap(),
            multiplicity: row.multiplicity,
            site: row.site.admit().unwrap(),
        })
        .collect::<Vec<_>>();
    let pointer = rows.as_ptr();
    let expected = rows.clone();
    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 jet = crate::geometry::RollingBallJetStations::from_parts(5, rows, &ctx)
        .unwrap()
        .unwrap();
    assert_eq!(jet.stations().as_ptr(), pointer);
    assert_eq!(jet.stations(), expected);
    ctx.finish_session().unwrap();
    let wire = serde_json::to_value(&jet).unwrap();
    assert_eq!(
        serde_json::from_value::<crate::geometry::RollingBallJetStations>(wire).unwrap(),
        jet
    );
}