dedup_mesh 0.2.0

Deduplicates vertices in a 3d mesh
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2025 lacklustr@protonmail.com https://github.com/eadf

use crate::IndexType;
use crate::prelude::*;
use crate::scalar::ScalarKernel;

#[derive(PartialEq, Eq)]
pub enum TopologyType {
    TriangleChunks,
    EdgeChunks,
    PointCloud,
    OnlyRemap,
}

pub trait TopologyKernel {
    const VALUE: TopologyType;
    // the average vertices to indices ratio, used for .capacity() estimates
    const VERTICES_TO_INDICES_RATIO: usize;
    const INDICES_MODULUS: usize;

    /// Returns Some((vertices, indices)) if there's insufficient data for normal processing
    /// Returns None if there's enough data to proceed with deduplication
    /// Returns Err if the data is invalid
    fn handle_insufficient_data<T, Index, Vout>(
        indices: &[Index],
        prune_unused: PruneUnusedEnum,
    ) -> Option<(Vec<Vout>, Vec<Index>)>
    where
        T: ScalarKernel,
        Index: IndexType,
        Vout: Into<[T; 3]> + From<[T; 3]> + Clone + Sync;

    fn remap_indices<T: ScalarKernel, Index: IndexType, Vout, const PRUNE_DEGENERATE: bool>(
        new_vertices: Vec<Vout>,
        old_indices: &[Index],
        remap: &[Index],
    ) -> Result<(Vec<Vout>, Vec<Index>), DeDupError>;
    fn fix_last_shunk<T: ScalarKernel, Index: IndexType>(indices: &mut Vec<Index>);
    fn fix_final_shunk<T: ScalarKernel, Index: IndexType>(indices: &mut Vec<Index>);
}

/// Interpret the index buffer as a list of triangles (groups of 3 indices).
///
/// Suitable for fully triangulated meshes, where every three indices form
/// a single triangle in the surface.
///
/// This implementation prunes zero area triangles when [`PruneDegenerate`] is active
///
/// Use with the [`TopologyPolicy`] type parameter in [`dedup`], [`dedup_exact`] and [`dedup_exact_from_iter`].
pub struct Triangulated;
impl TopologyKernel for Triangulated {
    const VALUE: TopologyType = TopologyType::TriangleChunks;
    const VERTICES_TO_INDICES_RATIO: usize = 3;
    const INDICES_MODULUS: usize = 3;

    /// Returns Some((vertices, indices)) if there's insufficient data for normal processing
    /// Returns None if there's enough data to proceed with deduplication
    /// Returns Err if the data is invalid
    fn handle_insufficient_data<T, Index, Vout>(
        indices: &[Index],
        prune_unused: PruneUnusedEnum,
    ) -> Option<(Vec<Vout>, Vec<Index>)>
    where
        T: ScalarKernel,
        Index: IndexType,
        Vout: Into<[T; 3]> + From<[T; 3]> + Clone + Sync,
    {
        if indices.len() < Self::INDICES_MODULUS {
            // we do not have enough indices for one single triangle
            match prune_unused {
                PruneUnused => Some((vec![], vec![])),
                KeepUnused => {
                    // the "too short" indices will be pruned by .chunks_exact(3)
                    None
                }
            }
        } else {
            None
        }
    }

    fn remap_indices<T: Scalar, Index: IndexType, Vout, const PRUNE_DEGENERATE: bool>(
        new_vertices: Vec<Vout>,
        old_indices: &[Index],
        remap: &[Index],
    ) -> Result<(Vec<Vout>, Vec<Index>), DeDupError> {
        let mut new_indices = Vec::with_capacity(old_indices.len());

        // Process triangles in groups of 3
        for triangle in old_indices.chunks_exact(3) {
            // Map old indices to new indices
            let new_a = remap[triangle[0].to_usize()];
            let new_b = remap[triangle[1].to_usize()];
            let new_c = remap[triangle[2].to_usize()];
            debug_assert!(
                new_a.to_usize() < new_vertices.len(),
                "new_a:{new_a:?} {triangle:?}"
            );
            debug_assert!(
                new_b.to_usize() < new_vertices.len(),
                "new_b:{new_b:?} {triangle:?}"
            );
            debug_assert!(
                new_c.to_usize() < new_vertices.len(),
                "new_c:{new_c:?} {triangle:?}"
            );

            if PRUNE_DEGENERATE {
                // Check if triangle is degenerate (any two vertices are the same)
                if new_a != new_b && new_b != new_c && new_a != new_c {
                    // Triangle is valid, add it to the output
                    new_indices.push(new_a);
                    new_indices.push(new_b);
                    new_indices.push(new_c);
                }
            } else {
                new_indices.push(new_a);
                new_indices.push(new_b);
                new_indices.push(new_c);
            }
            // If triangle is degenerate, skip it entirely
        }

        // If we filtered out many degenerate triangles, shrink the vector to reclaim memory
        if PRUNE_DEGENERATE && new_indices.capacity() > new_indices.len() * 2 {
            new_indices.shrink_to_fit();
        }

        Ok((new_vertices, new_indices))
    }

    #[inline(always)]
    fn fix_last_shunk<T: ScalarKernel, Index: IndexType>(indices: &mut Vec<Index>) {
        let last_index = indices.len();
        if last_index >= 3
            && (indices[last_index - 1] == indices[last_index - 2]
                || indices[last_index - 2] == indices[last_index - 3]
                || indices[last_index - 1] == indices[last_index - 3])
        {
            let _ = indices.pop();
            let _ = indices.pop();
            let _ = indices.pop();
        }
    }

    #[inline(always)]
    fn fix_final_shunk<T: ScalarKernel, Index: IndexType>(indices: &mut Vec<Index>) {
        // remove indices that does not match the Topology
        while indices.len() % Self::INDICES_MODULUS != 0 {
            let _ = indices.pop();
        }
    }
}

/// Interpret the index buffer as a flat list of point indices.
///
/// Suitable for de-duplicating unstructured point clouds, where each index
/// refers to a single vertex with no connectivity.
///
/// This implementation prunes nothing when [`PruneDegenerate`] is used
///
/// Use with the [`TopologyPolicy`] type parameter in [`dedup`], [`dedup_exact`] and [`dedup_exact_from_iter`].
pub struct PointCloud;
impl TopologyKernel for PointCloud {
    const VALUE: TopologyType = TopologyType::PointCloud;
    const VERTICES_TO_INDICES_RATIO: usize = 1;
    const INDICES_MODULUS: usize = 1;

    /// Returns Some((vertices, indices)) if there's insufficient data for normal processing
    /// Returns None if there's enough data to proceed with deduplication
    /// Returns Err if the data is invalid
    fn handle_insufficient_data<T, Index, Vout>(
        indices: &[Index],
        prune_unused: PruneUnusedEnum,
    ) -> Option<(Vec<Vout>, Vec<Index>)>
    where
        T: ScalarKernel,
        Index: IndexType,
        Vout: Into<[T; 3]> + From<[T; 3]> + Clone + Sync,
    {
        if indices.len() < Self::INDICES_MODULUS {
            match prune_unused {
                PruneUnused => Some((vec![], vec![])),
                KeepUnused => None,
            }
        } else {
            None
        }
    }

    // this does not remove any geometry for PRUNE_DEGENERATE
    fn remap_indices<T: Scalar, Index: IndexType, Vout, const PRUNE_DEGENERATE: bool>(
        new_vertices: Vec<Vout>,
        old_indices: &[Index],
        remap: &[Index],
    ) -> Result<(Vec<Vout>, Vec<Index>), DeDupError> {
        let new_indices = old_indices
            .iter()
            .map(|old_idx| remap[(*old_idx).to_usize()])
            .collect();
        Ok((new_vertices, new_indices))
    }

    #[inline(always)]
    fn fix_last_shunk<T: ScalarKernel, Index: IndexType>(_indices: &mut Vec<Index>) {}
    #[inline(always)]
    fn fix_final_shunk<T: ScalarKernel, Index: IndexType>(_indices: &mut Vec<Index>) {}
}

/// Interpret the index buffer as a list of vertex pairs representing edges.
///
/// Suitable for edge-based meshes or wireframes, where each pair of indices
/// defines a line segment.
///
/// This implementation prunes zero length edges when [`PruneDegenerate`] is active
///
/// Use with the [`TopologyPolicy`] type parameter in [`dedup`], [`dedup_exact`] and [`dedup_exact_from_iter`].
pub struct Edges;
impl TopologyKernel for Edges {
    const VALUE: TopologyType = TopologyType::EdgeChunks;
    const VERTICES_TO_INDICES_RATIO: usize = 2;
    const INDICES_MODULUS: usize = 2;

    /// Returns Some((vertices, indices)) if there's insufficient data for normal processing
    /// Returns None if there's enough data to proceed with deduplication
    /// Returns Err if the data is invalid
    fn handle_insufficient_data<T, Index, Vout>(
        indices: &[Index],
        prune_unused: PruneUnusedEnum,
    ) -> Option<(Vec<Vout>, Vec<Index>)>
    where
        T: ScalarKernel,
        Index: IndexType,
        Vout: Into<[T; 3]> + From<[T; 3]> + Clone + Sync,
    {
        if indices.len() < Self::INDICES_MODULUS {
            // we do not have enough indices for one single edge
            match prune_unused {
                PruneUnused => Some((vec![], vec![])),
                KeepUnused => {
                    // the "too short" indices will be pruned by .chunks_exact(2)
                    None
                }
            }
        } else {
            None
        }
    }

    // Simple mapping: every old index gets mapped to its new index
    // This preserves edge structure even if some edges become degenerate
    fn remap_indices<T: Scalar, Index: IndexType, Vout, const PRUNE_DEGENERATE: bool>(
        new_vertices: Vec<Vout>,
        old_indices: &[Index],
        remap: &[Index],
    ) -> Result<(Vec<Vout>, Vec<Index>), DeDupError> {
        let mut new_indices = Vec::with_capacity(old_indices.len());

        // Process edges in groups of 2
        for edge in old_indices.chunks_exact(2) {
            // Map old indices to new indices
            let new_a = remap[edge[0].to_usize()];
            let new_b = remap[edge[1].to_usize()];

            if PRUNE_DEGENERATE {
                // Check if edge is degenerate (the vertices are the same)
                if new_a != new_b {
                    // Edge is valid, add it to the output
                    new_indices.push(new_a);
                    new_indices.push(new_b);
                }
                // If edge is degenerate, skip it entirely
            } else {
                new_indices.push(new_a);
                new_indices.push(new_b);
            }
        }
        // If we filtered out many degenerate edges, shrink the vector to reclaim memory
        if PRUNE_DEGENERATE && new_indices.capacity() > new_indices.len() * 2 {
            new_indices.shrink_to_fit();
        }

        Ok((new_vertices, new_indices))
    }

    #[inline(always)]
    fn fix_last_shunk<T: ScalarKernel, Index: IndexType>(_indices: &mut Vec<Index>) {}
    #[inline(always)]
    fn fix_final_shunk<T: ScalarKernel, Index: IndexType>(_indices: &mut Vec<Index>) {}
}

impl<T: TopologyKernel> TopologyPolicy for T {}