document-svg 2.0.2

Convert PDF, Word, Excel, PowerPoint, diagram and CAD files into one SVG per page, locally — a Rust library and the docsvg command
Documentation
//! SVG to Stanford PLY 3D model extrusion reverse converter.
//!
//! Converts 2D SVG vector shapes into an extruded 3D PLY model
//! with top, bottom, and side faces.

use std::io::Write;

use crate::cad::svg_reader::{Point2D, SvgElement, parse_svg_elements};
use crate::error::Result;

#[derive(Clone, Copy, Debug)]
struct Pt3 {
    x: f64,
    y: f64,
    z: f64,
}

/// Converts SVG shapes into a 3D PLY model by extruding contours along the Z-axis.
pub fn write_svg_to_ply<W: Write>(svg_content: &str, mut writer: W) -> Result<()> {
    let doc = parse_svg_elements(svg_content)?;

    let mut contours: Vec<Vec<Point2D>> = Vec::new();

    for elem in doc.elements {
        match elem {
            SvgElement::Rect {
                x,
                y,
                width,
                height,
                ..
            } => {
                contours.push(vec![
                    Point2D::new(x, y),
                    Point2D::new(x + width, y),
                    Point2D::new(x + width, y + height),
                    Point2D::new(x, y + height),
                ]);
            }
            SvgElement::Circle { center, radius, .. } => {
                let segments = crate::cad::DEFAULT_CIRCLE_SEGMENTS;
                let mut pts = Vec::with_capacity(segments);
                for i in 0..segments {
                    let theta = (i as f64) * std::f64::consts::TAU / (segments as f64);
                    pts.push(Point2D::new(
                        center.x + radius * theta.cos(),
                        center.y + radius * theta.sin(),
                    ));
                }
                contours.push(pts);
            }
            SvgElement::Polyline { points, .. } => {
                if points.len() >= 3 {
                    contours.push(points);
                }
            }
            _ => {}
        }
    }

    // Default fallback square contour if no shapes were parsed
    if contours.is_empty() {
        contours.push(vec![
            Point2D::new(0.0, 0.0),
            Point2D::new(100.0, 0.0),
            Point2D::new(100.0, 100.0),
            Point2D::new(0.0, 100.0),
        ]);
    }

    let extrusion_height = 20.0;
    let mut vertices: Vec<Pt3> = Vec::new();
    let mut faces: Vec<Vec<usize>> = Vec::new();

    for contour in contours {
        let n = contour.len();
        if n < 3 {
            continue;
        }
        let base_idx = vertices.len();

        // Bottom vertices (z = 0)
        for pt in &contour {
            vertices.push(Pt3 {
                x: pt.x,
                y: -pt.y,
                z: 0.0,
            });
        }
        // Top vertices (z = extrusion_height)
        for pt in &contour {
            vertices.push(Pt3 {
                x: pt.x,
                y: -pt.y,
                z: extrusion_height,
            });
        }

        // Bottom face (fan triangulation)
        for i in 1..n - 1 {
            faces.push(vec![base_idx, base_idx + i + 1, base_idx + i]);
        }

        // Top face (fan triangulation)
        for i in 1..n - 1 {
            faces.push(vec![base_idx + n, base_idx + n + i, base_idx + n + i + 1]);
        }

        // Side walls (quads / 2 triangles each)
        for i in 0..n {
            let next = (i + 1) % n;
            let b0 = base_idx + i;
            let b1 = base_idx + next;
            let t0 = base_idx + n + i;
            let t1 = base_idx + n + next;

            faces.push(vec![b0, b1, t1]);
            faces.push(vec![b0, t1, t0]);
        }
    }

    // Write ASCII PLY
    writeln!(writer, "ply")?;
    writeln!(writer, "format ascii 1.0")?;
    writeln!(writer, "comment Generated by docsvg")?;
    writeln!(writer, "element vertex {}", vertices.len())?;
    writeln!(writer, "property float x")?;
    writeln!(writer, "property float y")?;
    writeln!(writer, "property float z")?;
    writeln!(writer, "element face {}", faces.len())?;
    writeln!(writer, "property list uchar int vertex_indices")?;
    writeln!(writer, "end_header")?;

    for v in &vertices {
        writeln!(writer, "{:.4} {:.4} {:.4}", v.x, v.y, v.z)?;
    }

    for f in &faces {
        write!(writer, "{}", f.len())?;
        for idx in f {
            write!(writer, " {}", idx)?;
        }
        writeln!(writer)?;
    }

    Ok(())
}