t-spline-commands 0.1.0

operations to perform on t-spline data structures
Documentation
/*
 * Copyright (C) 2026 Dominick Schroer
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use rayon::prelude::*;
use t_spline::tmesh::bounds::Bounds;
use t_spline::tmesh::ids::VertID;
use t_spline::tmesh::{LocalKnots, TMesh};
use t_spline::{Command, Numeric, Point3};

/// Calculate points evenly across the surface
pub struct Tessellate {
    pub resolution: usize,
}

impl<T: Numeric + Send + Sync + 'static> Command<T> for Tessellate {
    type Result = Vec<Point3<T>>;

    fn execute(&mut self, mesh: &TMesh<T>) -> Self::Result {
        let mut bounds = Bounds::default();
        bounds.add_mesh(mesh);

        let knot_cache: Vec<_> = Self::knot_vectors(mesh);
        Self::tessellate(self.resolution, bounds, mesh, &knot_cache)
    }
}

impl Tessellate {
    pub fn tessellate<T: Numeric + Send + Sync + 'static>(
        resolution: usize,
        bounds: Bounds<T>,
        mesh: &TMesh<T>,
        knot_cache: &[LocalKnots<T>],
    ) -> Vec<Point3<T>> {
        (0..resolution * resolution)
            .into_par_iter()
            .map(|i| mesh.subs(bounds.interpolate(i, resolution), knot_cache))
            .filter_map(|p| p)
            .collect()
    }

    pub fn knot_vectors<T: Numeric + Send + Sync>(mesh: &TMesh<T>) -> Vec<LocalKnots<T>> {
        (0..mesh.vertices.len())
            .into_par_iter()
            .map(|v| mesh.infer_local_knots(VertID(v)))
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use t_spline::TSpline;
    use t_spline::tmesh::ids::FaceID;

    #[test]
    pub fn it_can_evaluate_points_on_square() {
        let square = TSpline::new_unit_square();
        let knots = Tessellate::knot_vectors(square.mesh());

        assert_eq!(
            Some(Point3::new(0., 0., 0.)),
            square.mesh().subs((0.0, 0.0), &knots)
        );
        assert_eq!(
            Some(Point3::new(1., 0., 0.)),
            square.mesh().subs((1.0, 0.0), &knots)
        );
        assert_eq!(
            Some(Point3::new(0., 1., 0.)),
            square.mesh().subs((0.0, 1.0), &knots)
        );
        assert_eq!(
            Some(Point3::new(1., 1., 0.)),
            square.mesh().subs((1.0, 1.0), &knots)
        );
    }

    #[test]
    pub fn it_can_tessellate_a_square() {
        let square = TSpline::new_unit_square();
        let points = square.apply(&mut Tessellate { resolution: 2 });

        assert_eq!(4, points.len());

        assert_eq!(Point3::new(0., 0., 0.), points[0]);
        assert_eq!(Point3::new(1., 0., 0.), points[1]);
        assert_eq!(Point3::new(0., 1., 0.), points[2]);
        assert_eq!(Point3::new(1., 1., 0.), points[3]);
    }

    #[test]
    pub fn it_can_evaluate_center() {
        let square = TSpline::new_unit_square();
        let knots = Tessellate::knot_vectors(square.mesh());
        let center = square.mesh().subs((0.5, 0.5), &knots).unwrap();

        // Check components with epsilon tolerance
        let expected = Point3::new(0.5, 0.5, 0.0);
        let diff = center - expected;
        assert_eq!(0., diff.x);
        assert_eq!(0., diff.y);
        assert_eq!(0., diff.z);
    }

    #[test]
    pub fn it_can_tessellate_square() {
        let square = TSpline::<f64>::new_unit_square();
        let resolution = 10;
        let points = square.apply(&mut Tessellate { resolution });

        assert_eq!(points.len(), resolution * resolution);

        // Verify bounds of tessellated points
        for p in points {
            assert!(p.x >= -1e-9 && p.x <= 1.0 + 1e-9);
            assert!(p.y >= -1e-9 && p.y <= 1.0 + 1e-9);
            assert!((p.z - 0.0).abs() < 1e-9);
        }
    }

    #[test]
    pub fn it_can_create_and_evaluate_t_junction_mesh() {
        let mut t_mesh = TSpline::new_t_junction();

        // lift center t-junction,
        // spline should still be symmetrical
        t_mesh.apply_mut(&mut |m: &mut TMesh<f64>| {
            let j = m.vertices.iter_mut().find(|v| v.is_t_junction).unwrap();
            j.geometry.z = 0.5;
        });

        let knots = Tessellate::knot_vectors(t_mesh.mesh());

        let mut f1_bounds = Bounds::default();
        f1_bounds.add_face(&t_mesh.mesh(), FaceID(1));
        assert_eq!(1., f1_bounds.area());
        let f1_center = t_mesh.mesh().subs(f1_bounds.center(), &knots).unwrap();

        let mut f2_bounds = Bounds::default();
        f2_bounds.add_face(&t_mesh.mesh(), FaceID(2));
        assert_eq!(1., f2_bounds.area());
        let f2_center = t_mesh.mesh().subs(f2_bounds.center(), &knots).unwrap();

        assert_eq!(f1_center.z, f2_center.z, "t-spline is not symmetrical");
    }
}