csgrs 0.20.1

Constructive solid geometry (CSG) on meshes using BSP trees in Rust
Documentation
//! PLY file format support for Mesh objects
//!
//! This module provides export functionality for Stanford PLY files,
//! a popular format for 3D scanning, research, and mesh processing applications.
use crate::float_types::Real;

use crate::mesh::Mesh;
use crate::sketch::Sketch;
use geo::CoordsIter;
use nalgebra::{Point3, Vector3};
use std::fmt::Debug;
use std::io::Write;

// Helper struct for PLY vertex with normal
#[derive(Clone)]
struct PlyVertex {
    position: Point3<Real>,
    normal: Vector3<Real>,
}

impl<S: Clone + Debug + Send + Sync> Mesh<S> {
    /// Export this Mesh to PLY format as a string
    ///
    /// Creates a Stanford PLY file containing:
    /// 1. All 3D polygons from `self.polygons` (tessellated to triangles)
    /// 2. Any 2D geometry from `self.geometry` (projected to 3D)
    ///
    /// # Arguments
    /// * `comment` - Optional comment to include in PLY header
    ///
    /// # Example
    /// ```
    /// use csgrs::mesh::Mesh;
    /// let csg: Mesh<()> = Mesh::cube(10.0, None);
    /// let ply_content = csg.to_ply("Generated from Mesh operations");
    /// println!("{}", ply_content);
    /// ```
    pub fn to_ply(&self, comment: &str) -> String {
        let mut ply_content = String::new();

        let mut vertices = Vec::new();
        let mut faces = Vec::new();

        // Process 3D polygons
        for poly in &self.polygons {
            // Tessellate polygon to triangles
            let triangles = poly.triangulate();

            for triangle in triangles {
                let mut face_indices = Vec::new();

                for vertex in triangle {
                    let vertex_idx =
                        add_unique_vertex_ply(&mut vertices, vertex.pos, vertex.normal);
                    face_indices.push(vertex_idx);
                }

                if face_indices.len() == 3 {
                    faces.push(face_indices);
                }
            }
        }

        // Write PLY header
        ply_content.push_str("ply\n");
        ply_content.push_str("format ascii 1.0\n");
        ply_content.push_str(&format!("comment {comment}\n"));
        ply_content.push_str("comment Generated by csgrs library\n");
        ply_content.push_str(&format!("element vertex {}\n", vertices.len()));
        ply_content.push_str("property float x\n");
        ply_content.push_str("property float y\n");
        ply_content.push_str("property float z\n");
        ply_content.push_str("property float nx\n");
        ply_content.push_str("property float ny\n");
        ply_content.push_str("property float nz\n");
        ply_content.push_str(&format!("element face {}\n", faces.len()));
        ply_content.push_str("property list uchar int vertex_indices\n");
        ply_content.push_str("end_header\n");

        // Write vertices
        for vertex in &vertices {
            ply_content.push_str(&format!(
                "{:.6} {:.6} {:.6} {:.6} {:.6} {:.6}\n",
                vertex.position.x,
                vertex.position.y,
                vertex.position.z,
                vertex.normal.x,
                vertex.normal.y,
                vertex.normal.z
            ));
        }

        // Write faces (each face is a triangle: 3 v1 v2 v3)
        for face in &faces {
            ply_content.push_str(&format!("3 {} {} {}\n", face[0], face[1], face[2]));
        }

        ply_content
    }

    /// Export this Mesh to a PLY file
    ///
    /// # Arguments
    /// * `writer` - Where to write the PLY data
    /// * `comment` - Comment to include in PLY header
    ///
    /// # Example
    /// ```
    /// use csgrs::mesh::Mesh;
    /// use std::fs::File;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let csg: Mesh<()> = Mesh::cube(10.0, None);
    /// let mut file = File::create("stl/output.ply")?;
    /// csg.write_ply(&mut file, "My Mesh model")?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn write_ply<W: Write>(&self, writer: &mut W, comment: &str) -> std::io::Result<()> {
        let ply_content = self.to_ply(comment);
        writer.write_all(ply_content.as_bytes())
    }
}

impl<S: Clone + Debug + Send + Sync> Sketch<S> {
    /// Export this Sketch to PLY format as a string
    ///
    /// Creates a Stanford PLY file containing:
    /// 1. All 3D polygons from `self.polygons` (tessellated to triangles)
    /// 2. Any 2D geometry from `self.geometry` (projected to 3D)
    ///
    /// # Arguments
    /// * `comment` - Optional comment to include in PLY header
    ///
    /// # Example
    /// ```
    /// use csgrs::mesh::Mesh;
    /// let csg: Mesh<()> = Mesh::cube(10.0, None);
    /// let ply_content = csg.to_ply("Generated from Mesh operations");
    /// println!("{}", ply_content);
    /// ```
    pub fn to_ply(&self, comment: &str) -> String {
        let mut ply_content = String::new();

        let mut vertices = Vec::new();
        let mut faces = Vec::new();

        // Process 2D geometry (project to XY plane at Z=0)
        for geom in &self.geometry.0 {
            match geom {
                geo::Geometry::Polygon(poly2d) => {
                    self.add_2d_polygon_to_ply(poly2d, &mut vertices, &mut faces);
                },
                geo::Geometry::MultiPolygon(mp) => {
                    for poly2d in &mp.0 {
                        self.add_2d_polygon_to_ply(poly2d, &mut vertices, &mut faces);
                    }
                },
                _ => {}, // Skip other geometry types
            }
        }

        // Write PLY header
        ply_content.push_str("ply\n");
        ply_content.push_str("format ascii 1.0\n");
        ply_content.push_str(&format!("comment {comment}\n"));
        ply_content.push_str("comment Generated by csgrs library\n");
        ply_content.push_str(&format!("element vertex {}\n", vertices.len()));
        ply_content.push_str("property float x\n");
        ply_content.push_str("property float y\n");
        ply_content.push_str("property float z\n");
        ply_content.push_str("property float nx\n");
        ply_content.push_str("property float ny\n");
        ply_content.push_str("property float nz\n");
        ply_content.push_str(&format!("element face {}\n", faces.len()));
        ply_content.push_str("property list uchar int vertex_indices\n");
        ply_content.push_str("end_header\n");

        // Write vertices
        for vertex in &vertices {
            ply_content.push_str(&format!(
                "{:.6} {:.6} {:.6} {:.6} {:.6} {:.6}\n",
                vertex.position.x,
                vertex.position.y,
                vertex.position.z,
                vertex.normal.x,
                vertex.normal.y,
                vertex.normal.z
            ));
        }

        // Write faces (each face is a triangle: 3 v1 v2 v3)
        for face in &faces {
            ply_content.push_str(&format!("3 {} {} {}\n", face[0], face[1], face[2]));
        }

        ply_content
    }

    /// Export this Mesh to a PLY file
    ///
    /// # Arguments
    /// * `writer` - Where to write the PLY data
    /// * `comment` - Comment to include in PLY header
    ///
    /// # Example
    /// ```
    /// use csgrs::mesh::Mesh;
    /// use std::fs::File;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let csg: Mesh<()> = Mesh::cube(10.0, None);
    /// let mut file = File::create("stl/output.ply")?;
    /// csg.write_ply(&mut file, "My Mesh model")?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn write_ply<W: Write>(&self, writer: &mut W, comment: &str) -> std::io::Result<()> {
        let ply_content = self.to_ply(comment);
        writer.write_all(ply_content.as_bytes())
    }

    // Helper function to add 2D polygon to PLY data
    fn add_2d_polygon_to_ply(
        &self,
        poly2d: &geo::Polygon<Real>,
        vertices: &mut Vec<PlyVertex>,
        faces: &mut Vec<Vec<usize>>,
    ) {
        // Get the exterior ring
        let exterior: Vec<[Real; 2]> =
            poly2d.exterior().coords_iter().map(|c| [c.x, c.y]).collect();

        // Get holes
        let holes_vec: Vec<Vec<[Real; 2]>> = poly2d
            .interiors()
            .iter()
            .map(|ring| ring.coords_iter().map(|c| [c.x, c.y]).collect())
            .collect();

        let hole_refs: Vec<&[[Real; 2]]> = holes_vec.iter().map(|h| &h[..]).collect();

        // Tessellate the 2D polygon
        let triangles_2d = Self::triangulate_2d(&exterior, &hole_refs);

        // Add triangles with normal pointing up (positive Z)
        let normal = Vector3::new(0.0, 0.0, 1.0);

        for triangle in triangles_2d {
            let mut face_indices = Vec::new();

            for point in triangle {
                let vertex_3d = Point3::new(point.x, point.y, point.z);
                let vertex_idx = add_unique_vertex_ply(vertices, vertex_3d, normal);
                face_indices.push(vertex_idx);
            }

            if face_indices.len() == 3 {
                faces.push(face_indices);
            }
        }
    }
}

// Helper function to add unique vertex with normal for PLY
fn add_unique_vertex_ply(
    vertices: &mut Vec<PlyVertex>,
    position: Point3<Real>,
    normal: Vector3<Real>,
) -> usize {
    const EPSILON: Real = 1e-6;

    // Check if vertex already exists (within tolerance)
    for (i, existing) in vertices.iter().enumerate() {
        if (existing.position.coords - position.coords).norm() < EPSILON
            && (existing.normal - normal).norm() < EPSILON
        {
            return i;
        }
    }

    // Add new vertex
    vertices.push(PlyVertex { position, normal });
    vertices.len() - 1
}