hronn 0.7.0

An experimental CNC toolpath generator
Documentation
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) 2023 lacklustr@protonmail.com https://github.com/eadf
// This file is part of the hronn crate.

use super::tapered_end_to_edge::tapered_end_to_edge_collision;
use crate::geo::ConvertTo;
use crate::meshanalyzer::SearchResult;
use crate::prelude::MaximumTracker;
use crate::probe::xy_distance_to_line_squared;
use crate::probe::{PlaneMetadata, QueryParameters, TriangleMetadata};
use crate::{HronnError, is_inside_2d_triangle};
use vector_traits::prelude::{GenericVector3, HasXYZ};

#[derive(Debug, Clone, Copy)]
/// Represents the geometric properties of a cone.
///
/// A cone is defined by its base radius, height, and the tangent of half its apex angle.
/// This struct is generic over `T`, which must implement the `GenericVector3` trait,
/// allowing it to work with different scalar types (e.g., `f32`, `f64`).
///
/// # Fields
/// - `radius`: The radius of the base of the cone.
/// - `r_sq`: The square of the radius, precomputed for efficiency.
/// - `tan_half_angle`: The tangent of half the apex angle of the cone.
/// - `height`: The height of the cone.
///
/// # Examples
/// ```rust,ignore
/// use some_crate::GenericVector3;
/// use some_crate::ConeProperties;
///
/// let cone = ConeProperties {
///     radius: 2.0,
///     r_sq: 4.0,
///     tan_half_angle: 0.5,
///     height: 4.0,
/// };
///
/// assert_eq!(cone.radius, 2.0);
/// assert_eq!(cone.height, 4.0);
/// ```
pub(crate) struct ConeProperties<T: GenericVector3> {
    /// The radius of the base of the cone.
    ///
    /// This is the distance from the center of the base to its edge.
    /// It is used to define the size of the cone's base.
    pub radius: T::Scalar,

    /// The height of the cone.
    ///
    /// This is the perpendicular distance from the base of the cone to its tip.
    /// It is used to define the overall size and proportions of the cone.
    pub height: T::Scalar,

    /// The square of the radius, precomputed for efficiency.
    ///
    /// This value is often used in geometric calculations to avoid repeatedly
    /// computing `radius * radius`. It is stored as a field for performance reasons.
    pub r_sq: T::Scalar,

    /// The precomputed slope of the sides dz/dxy = height/radius
    pub slope: T::Scalar,
}

impl<T: GenericVector3> ConeProperties<T> {
    pub fn new(radius: T::Scalar, height: T::Scalar) -> Self {
        Self {
            radius,
            height,
            slope: height / radius,
            r_sq: radius * radius,
        }
    }
}

pub(crate) fn shared_tapered_precompute_logic<T: GenericVector3, MESH: HasXYZ + ConvertTo<T>>(
    vertices: &[MESH],
    indices: &[u32],
    probe_radius: T::Scalar,
    slope: T::Scalar,
) -> Result<Vec<TriangleMetadata<T, MESH>>, HronnError> {
    indices
        .as_ref()
        .chunks(3)
        .map(|triangle| {
            TriangleMetadata::<T, MESH>::new_for_tapered_nose(
                probe_radius,
                slope,
                vertices[triangle[0] as usize].to(),
                vertices[triangle[1] as usize].to(),
                vertices[triangle[2] as usize].to(),
            )
        })
        .collect::<Result<Vec<_>, HronnError>>()
}

// this function must use the CollisionFn signature
//
// General problem domain:
// * If the center of the cone is inside the triangle and the slope of the triangle is flatter than the cone:
//  - use the center collision point. No edge detection is necessary.
// * Center of Cone Outside Triangle:
//  - If the center of the cone is outside the triangle and the slope of the triangle is flatter than the cone, defer to edge detection.
// * Slope of Triangle is Steeper Than Cone:
//  - If the slope of the triangle is steeper than the cone, use the collision point as high up on the cone as possible. Also, run edge detection.
//
pub(crate) fn tapered_compute_collision<T: GenericVector3 + Sized, MESH: HasXYZ + ConvertTo<T>>(
    qp: &QueryParameters<'_, T, MESH>,
    site_index: u32,
    center: T::Vector2,
    mt: &mut MaximumTracker<SearchResult<T>>,
) {
    let cone_prop = ConeProperties::new(qp.probe_radius, qp.probe_height);

    //if center.is_abs_diff_eq(T::Vector2::new_2d((-0.5).into(), (0.0).into()), 0.01.into()) {
    //    println!("center:{:?}", center);
    //}

    let triangle_index = site_index * 3;
    let p0 = qp.vertices[qp.indices[triangle_index as usize] as usize].to();
    let p1 = qp.vertices[qp.indices[(triangle_index + 1) as usize] as usize].to();
    let p2 = qp.vertices[qp.indices[(triangle_index + 2) as usize] as usize].to();

    let plane_meta = &qp.meta_data[site_index as usize].plane;

    if let Some(PlaneMetadata {
        pft,
        translated_triangle: [p0_t, p1_t, p2_t],
        ..
    }) = plane_meta
    {
        if is_inside_2d_triangle(center, *p0_t, *p1_t, *p2_t) {
            // if the cone center is inside the pre-computed triangle, we know that the plane detection
            // will result in the highest point, so there is no need to test any of the edges.
            mt.insert(SearchResult::new(site_index, pft.compute_z(center)));
            return;
        }
    }

    if true {
        tapered_end_to_edge_collision::<T>(
            xy_distance_to_line_squared(center, p0, p1),
            cone_prop,
            site_index,
            mt,
        );
        tapered_end_to_edge_collision::<T>(
            xy_distance_to_line_squared(center, p1, p2),
            cone_prop,
            site_index,
            mt,
        );
        tapered_end_to_edge_collision::<T>(
            xy_distance_to_line_squared(center, p2, p0),
            cone_prop,
            site_index,
            mt,
        );
    }
}