hypercurve 0.3.0

Hyperreal-backed planar curves, contours, and regions for CAD topology
Documentation
use hypercurve::{
    BulgeVertex2, Classification, Contour2, ContourPointLocation, CurveError, CurvePolicy,
    FillRule, Real, Segment2, UncertaintyReason,
};

fn s(value: i32) -> Real {
    value.into()
}

fn p(x: i32, y: i32) -> hypercurve::Point2 {
    hypercurve::Point2::new(s(x), s(y))
}

fn vertex(x: i32, y: i32, bulge: i32) -> BulgeVertex2 {
    BulgeVertex2::new(p(x, y), s(bulge))
}

fn policy() -> CurvePolicy {
    CurvePolicy::certified()
}

fn rectangle() -> Contour2 {
    Contour2::from_bulge_vertices(&[
        vertex(0, 0, 0),
        vertex(4, 0, 0),
        vertex(4, 4, 0),
        vertex(0, 4, 0),
    ])
    .unwrap()
}

fn rotated_rectangle() -> Contour2 {
    Contour2::from_bulge_vertices(&[
        vertex(4, 4, 0),
        vertex(0, 4, 0),
        vertex(0, 0, 0),
        vertex(4, 0, 0),
    ])
    .unwrap()
}

fn reversed_rectangle() -> Contour2 {
    Contour2::from_bulge_vertices(&[
        vertex(0, 0, 0),
        vertex(0, 4, 0),
        vertex(4, 4, 0),
        vertex(4, 0, 0),
    ])
    .unwrap()
}

#[test]
fn contour_builds_closed_bulge_loop() {
    let contour = rectangle();

    assert_eq!(contour.len(), 4);
    assert_eq!(contour.fill_rule(), FillRule::NonZero);
    assert!(
        contour
            .segments()
            .iter()
            .all(|segment| matches!(segment, Segment2::Line(_)))
    );
}

#[test]
fn contour_rejects_open_segment_chain() {
    let segments = vec![
        vertex(0, 0, 0).segment_to(&vertex(1, 0, 0)).unwrap(),
        vertex(1, 0, 0).segment_to(&vertex(2, 0, 0)).unwrap(),
    ];

    let err = Contour2::try_new(segments).expect_err("open chain is not a contour");
    assert_eq!(err, CurveError::DisconnectedCurveString);
}

#[test]
fn rectangle_classifies_inside_outside_and_boundary() {
    let contour = rectangle();

    assert_eq!(
        contour.classify_point(&p(1, 1), &policy()),
        Classification::Decided(ContourPointLocation::Inside)
    );
    assert_eq!(
        contour.classify_point(&p(-1, 1), &policy()),
        Classification::Decided(ContourPointLocation::Outside)
    );
    assert_eq!(
        contour.classify_point(&p(4, 2), &policy()),
        Classification::Decided(ContourPointLocation::Boundary)
    );
    assert_eq!(
        contour.classify_point(&p(0, 0), &policy()),
        Classification::Decided(ContourPointLocation::Boundary)
    );
}

#[test]
fn prepared_contour_classification_matches_plain_contour() {
    let contour = rectangle();
    let policy = policy();
    let prepared = contour.prepare_topology_queries(&policy);

    assert_eq!(prepared.contour(), &contour);
    assert!(prepared.contour_box().is_some());
    assert_eq!(prepared.segment_boxes().len(), contour.segments().len());

    for point in [p(1, 1), p(-1, 1), p(4, 2), p(0, 0), p(9, 2)] {
        assert_eq!(
            prepared.point_on_boundary(&point, &policy),
            contour.point_on_boundary(&point, &policy)
        );
        assert_eq!(
            prepared.winding_number(&point, &policy),
            contour.winding_number(&point, &policy)
        );
        assert_eq!(
            prepared.classify_point(&point, &policy),
            contour.classify_point(&point, &policy)
        );
    }
}

#[test]
fn contour_aabb_miss_classifies_outside_and_zero_winding() {
    let contour = rectangle();

    assert_eq!(
        contour.point_on_boundary(&p(9, 2), &policy()),
        Classification::Decided(false)
    );
    assert_eq!(
        contour.winding_number(&p(9, 2), &policy()),
        Classification::Decided(0)
    );
    assert_eq!(
        contour.classify_point(&p(9, 2), &policy()),
        Classification::Decided(ContourPointLocation::Outside)
    );
}

#[test]
fn contour_aabb_edge_hit_still_checks_boundary() {
    let contour = rectangle();

    assert_eq!(
        contour.point_on_boundary(&p(4, 2), &policy()),
        Classification::Decided(true)
    );
    assert_eq!(
        contour.classify_point(&p(4, 2), &policy()),
        Classification::Decided(ContourPointLocation::Boundary)
    );
}

#[test]
fn rectangle_winding_is_positive_inside_and_boundary_is_explicit() {
    let contour = rectangle();

    assert_eq!(
        contour.winding_number(&p(2, 2), &policy()),
        Classification::Decided(1)
    );
    assert_eq!(
        contour.winding_number(&p(4, 2), &policy()),
        Classification::Uncertain(UncertaintyReason::Boundary)
    );
}

#[test]
fn exact_boundary_equality_ignores_closed_start_and_direction() {
    let contour = rectangle();
    let rotated = rotated_rectangle();
    let reversed = reversed_rectangle();
    let even_odd = Contour2::from_bulge_vertices_with_fill_rule(
        &[
            vertex(0, 0, 0),
            vertex(4, 0, 0),
            vertex(4, 4, 0),
            vertex(0, 4, 0),
        ],
        FillRule::EvenOdd,
    )
    .unwrap();
    let different = Contour2::from_bulge_vertices(&[
        vertex(0, 0, 0),
        vertex(5, 0, 0),
        vertex(5, 4, 0),
        vertex(0, 4, 0),
    ])
    .unwrap();

    assert!(contour.has_same_exact_boundary(&rotated));
    assert!(contour.has_same_exact_boundary(&reversed));
    assert!(!contour.has_same_exact_boundary(&even_odd));
    assert!(!contour.has_same_exact_boundary(&different));
}

#[test]
fn even_odd_fill_uses_winding_parity() {
    let twice = Contour2::from_bulge_vertices_with_fill_rule(
        &[
            vertex(0, 0, 1),
            vertex(2, 0, 1),
            vertex(0, 0, 1),
            vertex(2, 0, 1),
        ],
        FillRule::EvenOdd,
    )
    .unwrap();

    assert_eq!(
        twice.winding_number(&p(1, 0), &policy()),
        Classification::Decided(2)
    );
    assert_eq!(
        twice.classify_point(&p(1, 0), &policy()),
        Classification::Decided(ContourPointLocation::Outside)
    );

    let policy = policy();
    let prepared = twice.prepare_topology_queries(&policy);
    assert_eq!(
        prepared.winding_number(&p(1, 0), &policy),
        Classification::Decided(2)
    );
    assert_eq!(
        prepared.classify_point(&p(1, 0), &policy),
        Classification::Decided(ContourPointLocation::Outside)
    );
}

#[test]
fn circular_contour_winds_positive_semicircle_counter_clockwise() {
    let contour = Contour2::from_bulge_vertices(&[vertex(0, 0, 1), vertex(2, 0, 1)]).unwrap();

    assert_eq!(
        contour.winding_number(&p(1, 0), &policy()),
        Classification::Decided(1)
    );
    assert_eq!(
        contour.classify_point(&p(3, 0), &policy()),
        Classification::Decided(ContourPointLocation::Outside)
    );

    let reversed = Contour2::from_bulge_vertices(&[vertex(2, 0, -1), vertex(0, 0, -1)]).unwrap();
    assert_eq!(
        reversed.winding_number(&p(1, 0), &policy()),
        Classification::Decided(-1)
    );
}