cadmpeg-asm 0.6.2

Parse and write Autodesk Shape Manager record streams.
// SPDX-License-Identifier: Apache-2.0

use crate::kernel_header::RefWidth;
use crate::nurbs::pcurve::tests::curve_block;
use crate::nurbs::pcurve::tests::push_f64;
use crate::nurbs::pcurve::tests::push_ident;
use crate::nurbs::pcurve::tests::push_int;
use crate::nurbs::pcurve::tests::push_position;
use crate::nurbs::pcurve::tests::push_string;
use crate::nurbs::pcurve::tests::push_vector;
use crate::nurbs::pcurve::tests::surface_block;
use crate::nurbs::proc_curve::compound_patch_layout;
use crate::nurbs::proc_curve::extrusion_patch_layout;
use crate::nurbs::proc_curve::helix_patch_layout;
use crate::nurbs::proc_curve::subset_patch_layout;
use crate::nurbs::proc_curve::vector_offset_patch_layout;
use crate::nurbs::proc_surface::DecodedProceduralSurfaceDefinition;
use crate::nurbs::toks::lex_test_span;
use cadmpeg_ir::math::Point3;
use cadmpeg_ir::math::Vector3;

#[test]
fn extrusion_layout_walks_modern_and_legacy_names_at_both_widths() {
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        for name in ["cyl_spl_sur", "cylsur"] {
            let mut bytes = Vec::new();
            push_f64(&mut bytes, 99.0);
            push_vector(&mut bytes, [90.0, 91.0, 92.0]);
            push_position(&mut bytes, [93.0, 94.0, 95.0]);
            bytes.push(0x0f);
            push_ident(&mut bytes, name);
            push_f64(&mut bytes, -2.0);
            push_f64(&mut bytes, 3.0);
            push_vector(&mut bytes, [4.0, 5.0, 6.0]);
            push_position(&mut bytes, [7.0, 8.0, 9.0]);
            bytes.extend_from_slice(&curve_block(int_width));
            bytes.extend_from_slice(&surface_block(int_width));
            bytes.push(0x10);

            let layout = extrusion_patch_layout(&bytes, int_width)
                .unwrap_or_else(|| panic!("extrusion layout {name} at width {int_width}"));
            let interval = layout
                .parameter_interval
                .map(|offset| f64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap()));
            assert_eq!(interval, [-2.0, 3.0]);
            assert_eq!(
                f64::from_le_bytes(
                    bytes[layout.direction..layout.direction + 8]
                        .try_into()
                        .unwrap()
                ),
                4.0
            );
            assert_eq!(
                f64::from_le_bytes(
                    bytes[layout.native_position..layout.native_position + 8]
                        .try_into()
                        .unwrap()
                ),
                7.0
            );
        }
    }
}

#[test]
fn extrusion_definition_decodes_without_a_solved_surface_cache() {
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        let mut bytes = vec![0x0f];
        push_ident(&mut bytes, "cyl_spl_sur");
        push_f64(&mut bytes, -2.0);
        push_f64(&mut bytes, 3.0);
        push_vector(&mut bytes, [4.0, 5.0, 6.0]);
        push_position(&mut bytes, [7.0, 8.0, 9.0]);
        bytes.extend_from_slice(&curve_block(int_width));
        bytes.push(0x10);

        let decoded = crate::nurbs::blend::cyl_spl_sur(
            &lex_test_span(&bytes, int_width).expect("valid single-record byte fixture"),
            None,
        )
        .unwrap_or_else(|| panic!("cache-less extrusion at width {int_width}"));
        assert_eq!(decoded.legacy_cache_fit_tolerance(), None);
        let (definition, _) = decoded.into_parts();
        let DecodedProceduralSurfaceDefinition::Extrusion {
            parameter_interval,
            direction,
            native_position,
            ..
        } = definition
        else {
            panic!("expected extrusion definition")
        };
        assert_eq!(parameter_interval, [-2.0, 3.0]);
        assert_eq!(direction, Vector3::new(40.0, 50.0, 60.0));
        assert_eq!(native_position, Point3::new(70.0, 80.0, 90.0));
    }
}

#[test]
fn helix_layout_walks_optional_range_flags_at_both_widths() {
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        let mut bytes = Vec::new();
        push_f64(&mut bytes, 99.0);
        push_position(&mut bytes, [90.0, 91.0, 92.0]);
        push_vector(&mut bytes, [93.0, 94.0, 95.0]);
        bytes.push(0x0f);
        push_ident(&mut bytes, "helix_int_cur");
        push_int(&mut bytes, 0x04, 23_100, int_width);
        bytes.push(0x0b);
        push_f64(&mut bytes, -1.0);
        push_f64(&mut bytes, 2.0);
        push_position(&mut bytes, [3.0, 4.0, 5.0]);
        push_vector(&mut bytes, [6.0, 7.0, 8.0]);
        push_vector(&mut bytes, [9.0, 10.0, 11.0]);
        push_vector(&mut bytes, [12.0, 13.0, 14.0]);
        push_f64(&mut bytes, 15.0);
        push_vector(&mut bytes, [16.0, 17.0, 18.0]);
        bytes.extend_from_slice(&curve_block(int_width));
        bytes.push(0x10);

        assert!(crate::nurbs::proc_curve::helix_definition(
            &lex_test_span(&bytes, int_width).expect("valid single-record byte fixture")
        )
        .is_some());
        let layout = helix_patch_layout(&bytes, int_width)
            .unwrap_or_else(|| panic!("helix layout at width {int_width}"));
        let range = layout
            .angle_range
            .map(|offset| f64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap()));
        assert_eq!(range, [-1.0, 2.0]);
        assert_eq!(
            f64::from_le_bytes(
                bytes[layout.frame_vectors[0]..layout.frame_vectors[0] + 8]
                    .try_into()
                    .unwrap()
            ),
            3.0
        );
        assert_eq!(
            f64::from_le_bytes(
                bytes[layout.apex_factor..layout.apex_factor + 8]
                    .try_into()
                    .unwrap()
            ),
            15.0
        );
        assert_eq!(
            f64::from_le_bytes(bytes[layout.axis..layout.axis + 8].try_into().unwrap()),
            16.0
        );
    }
}

#[test]
fn decodes_current_cacheless_helix_record() {
    let hex = "0e08696e7463757276650d0563757276650cffffffff04ffffffff0cffffffff0b0f0d0d68656c69785f696e745f637572043c5a00000a067701e4b803dd04400a0605738860695607401338aee5545e6a7e3cbfab714dc0c45b3c13b8e608728f9dbf14930e205da081e83ffbd1d341709ad73f000000000000000014fbd1d341709ad73f930e205da081e8bf00000000000000001400000000000000000000000000000000cdccccccccccf43f0600000000000000001400000000000000000000000000000000000000000000f03f0d0c6e756c6c5f737572666163650d0c6e756c6c5f737572666163650d066e756c6c62730d066e756c6c6273100b0b11";
    let bytes = hex
        .as_bytes()
        .chunks_exact(2)
        .map(|digits| u8::from_str_radix(std::str::from_utf8(digits).unwrap(), 16).unwrap())
        .collect::<Vec<_>>();

    let [record]: [_; 1] = crate::test_support::sab::frame(&bytes, 0, bytes.len(), RefWidth::Four)
        .expect("valid complete helix record")
        .try_into()
        .expect("one helix record");
    let definition = crate::nurbs::proc_curve::helix_definition(&record.tokens)
        .expect("current cache-less helix definition");
    let crate::nurbs::proc_curve::HelixDefinition {
        angle_range,
        pitch,
        apex_factor,
        axis,
        ..
    } = definition;
    assert!(angle_range[0] < angle_range[1]);
    assert_eq!(pitch, Vector3::new(0.0, 0.0, 13.0));
    assert_eq!(apex_factor, 0.0);
    assert_eq!(axis, Vector3::new(0.0, 0.0, 1.0));
}

#[test]
fn decodes_current_cacheless_helix_surface_at_both_widths() {
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        let mut bytes = vec![0x0f];
        push_ident(&mut bytes, "helix_spl_line");
        push_int(&mut bytes, 0x04, 23_100, int_width);
        for value in [-0.5, 0.5, -2.0, 3.0, 0.0, std::f64::consts::TAU] {
            bytes.push(0x0a);
            push_f64(&mut bytes, value);
        }
        push_position(&mut bytes, [1.0, 2.0, 3.0]);
        push_vector(&mut bytes, [2.0, 0.0, 0.0]);
        push_vector(&mut bytes, [0.0, 2.0, 0.0]);
        push_vector(&mut bytes, [0.0, 0.0, 4.0]);
        push_f64(&mut bytes, 0.25);
        push_vector(&mut bytes, [0.0, 0.0, 1.0]);
        for sentinel in ["null_surface", "null_surface", "nullbs", "nullbs"] {
            push_ident(&mut bytes, sentinel);
        }
        push_vector(&mut bytes, [5.0, 6.0, 7.0]);
        bytes.push(0x10);

        let decoded = crate::nurbs::proc_surface::helix_spl_sur(
            &lex_test_span(&bytes, int_width).expect("valid single-record byte fixture"),
        )
        .unwrap_or_else(|| panic!("current helix surface at width {int_width}"));
        let (definition, _) = decoded.into_parts();
        let DecodedProceduralSurfaceDefinition::Helix(construction) = definition else {
            panic!("expected helix surface definition")
        };
        let path = construction.path();
        let profile = construction.profile();
        let pitch = path.pitch();
        assert_eq!(*pitch, Vector3::new(0.0, 0.0, 40.0));
        assert_eq!(
            *profile,
            cadmpeg_ir::geometry::HelixSurfaceProfile::Line(
                cadmpeg_ir::geometry::HelixLineProfile::try_new(Vector3::new(50.0, 60.0, 70.0))
                    .unwrap()
            )
        );
    }
}

#[test]
fn vector_offset_layout_ignores_outer_vectors_at_both_widths() {
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        let mut bytes = Vec::new();
        push_f64(&mut bytes, 99.0);
        push_vector(&mut bytes, [90.0, 91.0, 92.0]);
        bytes.push(0x0f);
        push_ident(&mut bytes, "offset_int_cur");
        bytes.push(0x0b);
        bytes.extend_from_slice(&curve_block(int_width));
        push_f64(&mut bytes, -2.0);
        push_f64(&mut bytes, 5.0);
        push_vector(&mut bytes, [0.5, -1.0, 2.0]);
        push_string(&mut bytes, "source");
        push_int(&mut bytes, 0x04, 7, int_width);
        push_string(&mut bytes, "offset");
        push_int(&mut bytes, 0x04, 9, int_width);
        bytes.extend_from_slice(&curve_block(int_width));
        bytes.push(0x10);

        let layout = vector_offset_patch_layout(&bytes, int_width)
            .unwrap_or_else(|| panic!("vector-offset layout at width {int_width}"));
        let range = layout
            .parameter_range
            .map(|offset| f64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap()));
        assert_eq!(range, [-2.0, 5.0]);
        assert_eq!(
            f64::from_le_bytes(bytes[layout.offset..layout.offset + 8].try_into().unwrap()),
            0.5
        );
    }
}

#[test]
fn subset_layout_ignores_outer_curve_cache_at_both_widths() {
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        let mut bytes = curve_block(int_width);
        push_f64(&mut bytes, 99.0);
        bytes.push(0x0f);
        push_ident(&mut bytes, "subset_int_cur");
        bytes.extend_from_slice(&curve_block(int_width));
        push_f64(&mut bytes, -1.5);
        push_f64(&mut bytes, 3.5);
        bytes.extend_from_slice(&curve_block(int_width));
        bytes.push(0x10);

        let layout = subset_patch_layout(&bytes, int_width)
            .unwrap_or_else(|| panic!("subset layout at width {int_width}"));
        let range = layout
            .parameter_range
            .map(|offset| f64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap()));
        assert_eq!(range, [-1.5, 3.5]);
    }
}

#[test]
fn compound_layout_requires_framed_subtype_at_both_widths() {
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        let mut bytes = Vec::new();
        push_string(&mut bytes, "comp_int_cur");
        push_int(&mut bytes, 0x04, 1, int_width);
        push_f64(&mut bytes, 99.0);
        bytes.push(0x0f);
        push_ident(&mut bytes, "comp_int_cur");
        push_int(&mut bytes, 0x04, 3, int_width);
        for value in [0.0, 0.5, 1.0] {
            push_f64(&mut bytes, value);
        }
        push_int(&mut bytes, 0x04, 2, int_width);
        for value in [-2.0, 4.0] {
            push_f64(&mut bytes, value);
        }
        bytes.push(0x0b);
        bytes.extend_from_slice(&curve_block(int_width));
        bytes.extend_from_slice(&curve_block(int_width));
        bytes.push(0x10);

        let layout = compound_patch_layout(&bytes, int_width)
            .unwrap_or_else(|| panic!("compound layout at width {int_width}"));
        let parameters = layout
            .parameters
            .iter()
            .map(|offset| f64::from_le_bytes(bytes[*offset..*offset + 8].try_into().unwrap()))
            .collect::<Vec<_>>();
        let component_parameters = layout
            .component_parameters
            .iter()
            .map(|offset| f64::from_le_bytes(bytes[*offset..*offset + 8].try_into().unwrap()))
            .collect::<Vec<_>>();
        assert_eq!(parameters, [0.0, 0.5, 1.0]);
        assert_eq!(component_parameters, [-2.0, 4.0]);
    }
}