cadmpeg-asm 0.6.6

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

use super::{decode_pcurve_cache, final_pcurve_patch_layout, pcurve_fit_tolerance};
use crate::kernel_header::RefWidth;
use crate::nurbs::reader::NUBS_MARKER;

fn push_int(out: &mut Vec<u8>, tag: u8, value: i64, int_width: RefWidth) {
    out.push(tag);
    if int_width == RefWidth::Four {
        out.extend_from_slice(
            &i32::try_from(value)
                .expect("test value fits i32")
                .to_le_bytes(),
        );
    } else {
        out.extend_from_slice(&value.to_le_bytes());
    }
}

fn push_f64(out: &mut Vec<u8>, value: f64) {
    out.push(0x06);
    out.extend_from_slice(&value.to_le_bytes());
}

fn push_ident(out: &mut Vec<u8>, value: &str) {
    out.extend_from_slice(&[0x0d, u8::try_from(value.len()).expect("test value fits")]);
    out.extend_from_slice(value.as_bytes());
}

fn push_string(out: &mut Vec<u8>, value: &str) {
    out.extend_from_slice(&[0x07, u8::try_from(value.len()).expect("test value fits")]);
    out.extend_from_slice(value.as_bytes());
}

fn push_position(out: &mut Vec<u8>, values: [f64; 3]) {
    out.push(0x13);
    for value in values {
        out.extend_from_slice(&value.to_le_bytes());
    }
}

fn push_vector(out: &mut Vec<u8>, values: [f64; 3]) {
    out.push(0x14);
    for value in values {
        out.extend_from_slice(&value.to_le_bytes());
    }
}

/// A degree-1 two-pole 3D `nubs` curve block over `[0, 1]`.
fn curve_block_with_endpoint(int_width: RefWidth, endpoint: [f64; 3]) -> Vec<u8> {
    let mut b = NUBS_MARKER.to_vec();
    push_int(&mut b, 0x04, 1, int_width); // degree
    push_int(&mut b, 0x15, 0, int_width); // open closure
    push_int(&mut b, 0x04, 2, int_width); // unique knot count
    push_f64(&mut b, 0.0);
    push_int(&mut b, 0x04, 1, int_width);
    push_f64(&mut b, 1.0);
    push_int(&mut b, 0x04, 1, int_width);
    for component in [0.0, 0.0, 0.0, endpoint[0], endpoint[1], endpoint[2]] {
        push_f64(&mut b, component);
    }
    b
}

fn curve_block(int_width: RefWidth) -> Vec<u8> {
    curve_block_with_endpoint(int_width, [1.0, 2.0, 3.0])
}

/// A degree-1 2×2-pole `nubs` surface block over `[0, 1]²`.
fn surface_block_with_x_offset(int_width: RefWidth, x_offset: f64) -> Vec<u8> {
    let mut b = NUBS_MARKER.to_vec();
    push_int(&mut b, 0x04, 1, int_width); // u degree
    push_int(&mut b, 0x04, 1, int_width); // v degree
    for _ in 0..4 {
        push_int(&mut b, 0x15, 0, int_width); // periodic/singularity enums
    }
    push_int(&mut b, 0x04, 2, int_width); // unique u knots
    push_int(&mut b, 0x04, 2, int_width); // unique v knots
    for _ in 0..2 {
        push_f64(&mut b, 0.0);
        push_int(&mut b, 0x04, 1, int_width);
        push_f64(&mut b, 1.0);
        push_int(&mut b, 0x04, 1, int_width);
    }
    for pole in 0..4 {
        push_f64(&mut b, x_offset + f64::from(pole));
        push_f64(&mut b, 0.0);
        push_f64(&mut b, 0.0);
    }
    b
}

fn surface_block(int_width: RefWidth) -> Vec<u8> {
    surface_block_with_x_offset(int_width, 0.0)
}

fn pcurve_block(int_width: RefWidth) -> Vec<u8> {
    let mut b = NUBS_MARKER.to_vec();
    push_int(&mut b, 0x04, 1, int_width);
    push_int(&mut b, 0x15, 0, int_width);
    push_int(&mut b, 0x04, 2, int_width);
    for knot in [0.0, 1.0] {
        push_f64(&mut b, knot);
        push_int(&mut b, 0x04, 1, int_width);
    }
    for component in [0.0, 0.0, 1.0, 1.0] {
        push_f64(&mut b, component);
    }
    b
}

/// A revision-gated exact curve with one plane support and one paired BS2
/// pcurve in the shared cache-first context.
fn exact_cache_first_curve(int_width: RefWidth) -> Vec<u8> {
    let mut bytes = vec![0x0f];
    push_ident(&mut bytes, "exact_int_cur");
    push_int(&mut bytes, 0x04, 23_100, int_width);
    push_int(&mut bytes, 0x15, 0, int_width);
    bytes.extend_from_slice(&curve_block(int_width));
    push_f64(&mut bytes, 0.0);

    // Slot 1 is an inline plane support with one paired UV curve.
    push_ident(&mut bytes, "plane");
    push_position(&mut bytes, [0.0, 0.0, 0.0]);
    push_vector(&mut bytes, [0.0, 0.0, 1.0]);
    push_vector(&mut bytes, [1.0, 0.0, 0.0]);
    bytes.extend_from_slice(&[0x0b; 5]);
    push_ident(&mut bytes, "null_surface");

    // Slot 1 has one paired UV curve; slot 2 has pcurve absence.
    bytes.extend_from_slice(&pcurve_block(int_width));
    push_ident(&mut bytes, "nullbs");
    bytes.extend_from_slice(&[0x0b; 2]);
    for _ in 0..3 {
        push_int(&mut bytes, 0x04, 0, int_width);
    }
    push_int(&mut bytes, 0x04, 0, int_width);

    // Exact-curve fields after the shared cache-first context.
    bytes.push(0x0a);
    push_f64(&mut bytes, 1.0);
    bytes.push(0x0a);
    push_f64(&mut bytes, 0.0);
    push_int(&mut bytes, 0x15, 0, int_width);
    push_int(&mut bytes, 0x15, 0, int_width);
    bytes.push(0x10);
    bytes
}

#[test]
fn intcurve_selector_uses_the_serialized_direct_slot() {
    let asm_decode_arena = cadmpeg_core::decode::DecodeArena::new();
    let (asm_decode_ctx, _) = cadmpeg_core::decode::DecodeContext::from_root_bytes(
        &[],
        &asm_decode_arena,
        &cadmpeg_core::decode::DecodePolicy::default(),
    )
    .expect("test decode context");
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        let mut bytes = curve_block(int_width);
        bytes.extend_from_slice(&pcurve_block(int_width));
        let toks = crate::nurbs::toks::lex_test_span(&bytes, int_width)
            .expect("valid single-record byte fixture");
        let table = crate::nurbs::toks::test_table(&bytes, int_width)
            .expect("valid single-record byte fixture");

        assert!(crate::nurbs::proc_curve::pcurve_for_selector_with_chart(
            &asm_decode_ctx,
            &toks,
            2,
            &table
        )
        .transpose()
        .expect("decode the selected pcurve with its chart")
        .is_some());
        assert!(crate::nurbs::proc_curve::pcurve_for_selector_with_chart(
            &asm_decode_ctx,
            &toks,
            -2,
            &table
        )
        .transpose()
        .expect("decode the selected pcurve with its chart")
        .is_some());
        assert!(crate::nurbs::proc_curve::pcurve_for_selector_with_chart(
            &asm_decode_ctx,
            &toks,
            1,
            &table
        )
        .transpose()
        .expect("decode the selected pcurve with its chart")
        .is_none());
    }
}

#[test]
fn exact_curve_selector_uses_its_cache_first_support_slot() {
    let asm_decode_arena = cadmpeg_core::decode::DecodeArena::new();
    let (asm_decode_ctx, _) = cadmpeg_core::decode::DecodeContext::from_root_bytes(
        &[],
        &asm_decode_arena,
        &cadmpeg_core::decode::DecodePolicy::default(),
    )
    .expect("test decode context");
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        let bytes = exact_cache_first_curve(int_width);

        let toks = crate::nurbs::toks::lex_test_span(&bytes, int_width)
            .expect("valid single-record byte fixture");
        let table = crate::nurbs::toks::test_table(&bytes, int_width)
            .expect("valid single-record byte fixture");
        let decoded = crate::nurbs::proc_curve::procedural_curve_resolving_refs(
            &asm_decode_ctx,
            &toks,
            &table,
        )
        .transpose()
        .expect("decode the procedural curve")
        .unwrap_or_else(|| panic!("exact curve at width {int_width}"));
        assert!(matches!(
            decoded.construction,
            crate::nurbs::proc_curve::ProceduralCurveConstruction::Exact
        ));
        let (pcurve, _) = crate::nurbs::proc_curve::pcurve_for_selector_with_chart(
            &asm_decode_ctx,
            &toks,
            1,
            &table,
        )
        .transpose()
        .expect("decode the selected pcurve with its chart")
        .unwrap_or_else(|| panic!("exact curve pcurve at width {int_width}"));
        // A `forward_v` plane stores native lengths along `u_dir` and
        // `normal × u_dir`.
        assert_eq!(
            pcurve.control_points()[1],
            cadmpeg_ir::math::Point2::new(10.0, 10.0)
        );
        assert!(crate::nurbs::proc_curve::pcurve_for_selector_with_chart(
            &asm_decode_ctx,
            &toks,
            -1,
            &table
        )
        .transpose()
        .expect("decode the selected pcurve with its chart")
        .is_some());
        assert!(crate::nurbs::proc_curve::pcurve_for_selector_with_chart(
            &asm_decode_ctx,
            &toks,
            2,
            &table
        )
        .transpose()
        .expect("decode the selected pcurve with its chart")
        .is_none());
    }
}

#[test]
fn exact_curve_selector_follows_subtype_reference() {
    let asm_decode_arena = cadmpeg_core::decode::DecodeArena::new();
    let (asm_decode_ctx, _) = cadmpeg_core::decode::DecodeContext::from_root_bytes(
        &[],
        &asm_decode_arena,
        &cadmpeg_core::decode::DecodePolicy::default(),
    )
    .expect("test decode context");
    for int_width in [RefWidth::Four, RefWidth::Eight] {
        let active = exact_cache_first_curve(int_width);
        for named in [false, true] {
            let mut wrapper = vec![0x0f];
            if named {
                push_ident(&mut wrapper, "ref");
            }
            push_int(&mut wrapper, 0x04, 0, int_width);
            wrapper.push(0x10);
            let toks = crate::nurbs::toks::lex_test_span(&wrapper, int_width)
                .expect("valid single-record byte fixture");
            let table = crate::nurbs::toks::test_table(&active, int_width)
                .expect("valid single-record byte fixture");
            assert!(crate::nurbs::proc_curve::pcurve_for_selector_with_chart(
                &asm_decode_ctx,
                &toks,
                -1,
                &table
            )
            .transpose()
            .expect("decode the selected pcurve with its chart")
            .is_some());
            assert!(crate::nurbs::proc_curve::pcurve_for_selector_with_chart(
                &asm_decode_ctx,
                &toks,
                2,
                &table
            )
            .transpose()
            .expect("decode the selected pcurve with its chart")
            .is_none());
        }
    }
}

fn rolling_ball_side(int_width: RefWidth, label: &str) -> Vec<u8> {
    let mut bytes = Vec::new();
    push_string(
        &mut bytes,
        if label == "left" {
            "blend_support_surface"
        } else {
            "blend_support_curve"
        },
    );
    push_ident(&mut bytes, "null_surface");
    bytes.extend_from_slice(&curve_block(int_width));
    bytes.extend_from_slice(&[0x0b, 0x0b]);
    bytes.extend_from_slice(&pcurve_block(int_width));
    push_position(&mut bytes, [7.0, 8.0, 9.0]);
    push_ident(&mut bytes, "nullbs");
    push_int(&mut bytes, 0x04, 0, int_width);
    push_ident(&mut bytes, "nullbs");
    bytes
}

fn variable_blend_side(int_width: RefWidth, name: &str, extension: Option<i64>) -> Vec<u8> {
    let mut bytes = Vec::new();
    push_string(&mut bytes, name);
    push_ident(&mut bytes, "null_surface");
    push_ident(&mut bytes, "null_curve");
    bytes.extend_from_slice(&pcurve_block(int_width));
    push_position(&mut bytes, [1.0, 2.0, 3.0]);
    push_ident(&mut bytes, "nullbs");
    if let Some(extension) = extension {
        push_int(&mut bytes, 0x04, extension, int_width);
        push_ident(&mut bytes, "nullbs");
    }
    bytes.push(0x10);
    bytes
}

mod blend_laws;
mod cache_selection;
mod procedural_curves;
mod procedural_surfaces;
mod side_charts;