cadmpeg-codec-step 0.6.8

Read and write ISO 10303-21 STEP Part 21 files for AP203, AP214, and AP242.
Documentation
// SPDX-License-Identifier: Apache-2.0
//! Helix curve and surface carriers at STEP export.
#![allow(clippy::unwrap_used)]

use super::reports::edgeless_doc;
use crate::{export::write_step, loss::StepLossCode, StepSchema, StepWriteOptions};
use cadmpeg_ir::{
    geometry::{
        Curve, CurveGeometry, HelixCircleProfile, HelixCurveConstruction, HelixFrame,
        HelixPathConstruction, HelixSurfaceConstruction, HelixSurfaceProfile, ProceduralCurve,
        ProceduralCurveDefinition, ProceduralSurface, ProceduralSurfaceDefinition, SurfaceGeometry,
    },
    ids::{CurveId, ProceduralCurveId, ProceduralSurfaceId},
    math::{Point3, Vector3},
    topology::{EdgeCarrier, Point},
    CadIr,
};

/// The edgeless document with its one edge closed on a full turn of a
/// pitchless helix in the face plane, from and to `(1, 0, 0)`.
fn helix_edge_doc(cache: bool) -> CadIr {
    let mut ir = edgeless_doc();
    let curve_id = CurveId::mint("test:model:curve#helix").unwrap();
    let construction_id = ProceduralCurveId::mint("test:model:procedural_curve#helix").unwrap();
    let helix = HelixCurveConstruction::try_new(
        [0.0, std::f64::consts::TAU],
        HelixFrame {
            center: Point3::new(0.0, 0.0, 0.0),
            major: Vector3::new(1.0, 0.0, 0.0),
            minor: Vector3::new(0.0, 1.0, 0.0),
            pitch: Vector3::new(0.0, 0.0, 0.0),
            axis: Vector3::new(0.0, 0.0, 1.0),
        },
        0.0,
        None,
    )
    .unwrap();
    ir.model.points[0] = Point::new(
        ir.model.points[0].id.clone(),
        cadmpeg_ir::features::FinitePoint3::new(Point3::new(1.0, 0.0, 0.0)).unwrap(),
        None,
    );
    ir.model.edges[0].end = ir.model.edges[0].start.clone();
    let fitted = cadmpeg_ir::eval::helix_nurbs::helix_nurbs(&helix, ir.tolerances.linear).unwrap();
    ir.model.curves.push(Curve {
        parameter_range: None,
        id: curve_id.clone(),
        geometry: CurveGeometry::Procedural {
            construction: construction_id.clone(),
            cache: cache.then_some(cadmpeg_ir::geometry::SolvedCurveGeometry::Nurbs(fitted)),
        },
        source_object: None,
    });
    ir.model.procedural_curves.push(ProceduralCurve::new(
        construction_id,
        ProceduralCurveDefinition::Helix(helix),
    ));
    ir.model.edges[0].carrier = EdgeCarrier::unbounded(Some(curve_id));
    ir
}

fn entity_count(bytes: &[u8], name: &str) -> usize {
    let (exchange, _) =
        crate::test_support::with_service_context(bytes, crate::parse::parse_inner).unwrap();
    exchange
        .records()
        .values()
        .flat_map(|record| &record.partials)
        .filter(|partial| partial.name == name)
        .count()
}

#[test]
fn an_edge_on_an_uncached_helix_is_written_on_a_fitted_bspline() {
    let ir = helix_edge_doc(false);
    let mut bytes = Vec::new();
    let report = write_step(
        &ir,
        &mut bytes,
        StepSchema::Ap214,
        &StepWriteOptions::default(),
    )
    .unwrap();

    assert_eq!(entity_count(&bytes, "EDGE_CURVE"), 1);
    assert_eq!(entity_count(&bytes, "B_SPLINE_CURVE_WITH_KNOTS"), 1);
    let codes: Vec<_> = report.losses.iter().map(|loss| loss.code.clone()).collect();
    assert!(codes.contains(&StepLossCode::HelixFittedToBspline.kind()));
    assert!(!codes.contains(&StepLossCode::EdgeCurveUnsupported.kind()));
    assert!(!codes.contains(&StepLossCode::ProceduralReducedToCarrier.kind()));
    assert!(!codes.contains(&StepLossCode::ProceduralDefinitionNotWritten.kind()));
}

#[test]
fn an_edge_on_a_subset_of_an_uncached_helix_is_written_on_a_trimmed_fitted_bspline() {
    let mut ir = helix_edge_doc(false);
    let helix_id = ir.model.edges[0].curve().unwrap().clone();
    let subset_id = CurveId::mint("test:model:curve#helix-subset").unwrap();
    let construction_id =
        ProceduralCurveId::mint("test:model:procedural_curve#helix-subset").unwrap();
    ir.model.curves.push(Curve {
        parameter_range: None,
        id: subset_id.clone(),
        geometry: CurveGeometry::Procedural {
            construction: construction_id.clone(),
            cache: None,
        },
        source_object: None,
    });
    ir.model.procedural_curves.push(ProceduralCurve::new(
        construction_id,
        ProceduralCurveDefinition::Subset(
            cadmpeg_ir::geometry::curve_payloads::SubsetCurveConstruction::try_new(
                helix_id,
                [0.0, std::f64::consts::TAU],
                true,
                None,
            )
            .unwrap(),
        ),
    ));
    ir.model.edges[0].carrier = EdgeCarrier::unbounded(Some(subset_id));
    let mut bytes = Vec::new();
    let report = write_step(
        &ir,
        &mut bytes,
        StepSchema::Ap214,
        &StepWriteOptions::default(),
    )
    .unwrap();

    assert_eq!(entity_count(&bytes, "EDGE_CURVE"), 1);
    assert_eq!(entity_count(&bytes, "TRIMMED_CURVE"), 1);
    assert_eq!(entity_count(&bytes, "B_SPLINE_CURVE_WITH_KNOTS"), 1);
    let codes: Vec<_> = report.losses.iter().map(|loss| loss.code.clone()).collect();
    assert!(codes.contains(&StepLossCode::HelixFittedToBspline.kind()));
    assert!(!codes.contains(&StepLossCode::EdgeCurveUnsupported.kind()));
}

#[test]
fn an_edge_on_a_cached_helix_is_written_on_its_cache() {
    let ir = helix_edge_doc(true);
    let mut bytes = Vec::new();
    let report = write_step(
        &ir,
        &mut bytes,
        StepSchema::Ap214,
        &StepWriteOptions::default(),
    )
    .unwrap();

    assert_eq!(entity_count(&bytes, "EDGE_CURVE"), 1);
    let codes: Vec<_> = report.losses.iter().map(|loss| loss.code.clone()).collect();
    assert!(!codes.contains(&StepLossCode::HelixFittedToBspline.kind()));
    assert!(codes.contains(&StepLossCode::ProceduralReducedToCarrier.kind()));
}

/// The uncached helix edge document with its face on an uncached
/// spring-wire surface.
fn helix_surface_doc() -> CadIr {
    let mut ir = helix_edge_doc(false);
    let construction = HelixSurfaceConstruction::try_new(
        [-std::f64::consts::PI, std::f64::consts::PI],
        [0.0, std::f64::consts::TAU],
        HelixPathConstruction::try_new(
            [0.0, std::f64::consts::TAU],
            HelixFrame {
                center: Point3::new(0.0, 0.0, 0.0),
                major: Vector3::new(6.0, 0.0, 0.0),
                minor: Vector3::new(0.0, 6.0, 0.0),
                pitch: Vector3::new(0.0, 0.0, 2.0),
                axis: Vector3::new(0.0, 0.0, 1.0),
            },
            0.0,
        )
        .unwrap(),
        HelixSurfaceProfile::Circle(HelixCircleProfile::try_new(0.0, 0.5).unwrap()),
    )
    .unwrap();
    let owner = ir.model.surfaces[0].id.clone();
    ir.model
        .add_procedural_surface(
            &owner,
            ProceduralSurface::new(
                ProceduralSurfaceId::mint("test:model:procedural_surface#wire").unwrap(),
                ProceduralSurfaceDefinition::Helix {
                    construction: Box::new(construction),
                },
                None,
            ),
        )
        .unwrap();
    let SurfaceGeometry::Procedural { cache, .. } = &mut ir.model.surfaces[0].geometry else {
        panic!("construction owner")
    };
    *cache = None;
    ir
}

#[test]
fn a_face_on_an_uncached_helix_surface_is_written_on_a_fitted_bspline_surface() {
    let ir = helix_surface_doc();
    let mut bytes = Vec::new();
    let report = write_step(
        &ir,
        &mut bytes,
        StepSchema::Ap214,
        &StepWriteOptions::default(),
    )
    .unwrap();

    assert_eq!(entity_count(&bytes, "ADVANCED_FACE"), 1);
    assert_eq!(entity_count(&bytes, "B_SPLINE_SURFACE_WITH_KNOTS"), 1);
    assert_eq!(entity_count(&bytes, "RATIONAL_B_SPLINE_SURFACE"), 1);
    let codes: Vec<_> = report.losses.iter().map(|loss| loss.code.clone()).collect();
    assert!(codes.contains(&StepLossCode::HelixFittedToBspline.kind()));
    assert!(!codes.contains(&StepLossCode::UnknownSurfaceFaceOmitted.kind()));
    assert!(!codes.contains(&StepLossCode::ProceduralReducedToCarrier.kind()));
    assert!(!codes.contains(&StepLossCode::ProceduralSampledToBspline.kind()));
    assert!(!codes.contains(&StepLossCode::ProceduralDefinitionNotWritten.kind()));
}

#[test]
fn a_helix_without_a_bspline_within_the_tolerance_reports_its_failed_fit() {
    let mut ir = helix_edge_doc(false);
    ir.tolerances.linear = cadmpeg_ir::scalar::PositiveLength::new(f64::MIN_POSITIVE).unwrap();
    let mut bytes = Vec::new();
    let report = write_step(
        &ir,
        &mut bytes,
        StepSchema::Ap214,
        &StepWriteOptions::default(),
    )
    .unwrap();

    let failed: Vec<_> = report
        .losses
        .iter()
        .filter(|loss| loss.code == StepLossCode::HelixFitFailed.kind())
        .collect();
    assert_eq!(failed.len(), 1, "{:?}", report.losses);
    assert!(
        failed[0]
            .message
            .contains("1 beyond the linear tolerance at the segment bound"),
        "{}",
        failed[0].message
    );
    let codes: Vec<_> = report.losses.iter().map(|loss| loss.code.clone()).collect();
    assert!(!codes.contains(&StepLossCode::HelixFittedToBspline.kind()));
    assert!(!codes.contains(&StepLossCode::ProceduralSampledToBspline.kind()));
    assert!(!codes.contains(&StepLossCode::ProceduralDefinitionNotWritten.kind()));
}