opensubdiv-petite 0.3.1

Wrapper around parts of Pixar’s OpenSubdiv
//! Wavefront OBJ B-spline surface export functionality
//!
//! This module provides functionality to export OpenSubdiv patches as
//! B-spline surfaces in Wavefront OBJ format. This uses the lesser-known
//! B-spline surface features of the OBJ format.

use crate::far::{PatchTable, PatchType};
use std::io::{self, Write};

/// Error type for OBJ export
#[derive(Debug)]
pub enum ObjExportError {
    /// Unsupported patch type
    UnsupportedPatchType(PatchType),
    /// Invalid control points
    InvalidControlPoints,
    /// IO error
    Io(io::Error),
}

impl From<io::Error> for ObjExportError {
    fn from(err: io::Error) -> Self {
        ObjExportError::Io(err)
    }
}

impl std::fmt::Display for ObjExportError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::UnsupportedPatchType(t) => write!(f, "Unsupported patch type: {t:?}"),
            Self::InvalidControlPoints => write!(f, "Invalid control point configuration"),
            Self::Io(err) => write!(f, "IO error: {err}"),
        }
    }
}

impl std::error::Error for ObjExportError {}

/// Result type for OBJ export
pub type Result<T> = std::result::Result<T, ObjExportError>;

/// Export OpenSubdiv patches as B-spline surfaces to OBJ format
pub fn export_patches_as_bspline_surfaces<W: Write>(
    writer: &mut W,
    patch_table: &PatchTable,
    control_points: &[[f32; 3]],
) -> Result<()> {
    writeln!(writer, "# OpenSubdiv B-spline Surface Export")?;
    writeln!(writer, "# Generated by opensubdiv-petite")?;
    writeln!(writer, "# Number of patches: {}", patch_table.patch_count())?;
    writeln!(writer, "#")?;

    // Write all control points first
    writeln!(writer, "# Control points")?;
    control_points.iter().enumerate().try_for_each(|(i, cp)| {
        writeln!(writer, "v {} {} {}  # vertex {}", cp[0], cp[1], cp[2], i)
    })?;
    writeln!(writer)?;

    // Track global patch index
    let mut patch_global_idx = 0;

    // Process each patch array
    for array_idx in 0..patch_table.patch_array_count() {
        if let Some(desc) = patch_table.patch_array_descriptor(array_idx) {
            if desc.patch_type() != PatchType::Regular {
                // Skip non-regular patches for now
                continue;
            }

            let num_patches = patch_table.patch_array_patch_count(array_idx);
            if let Some(cv_indices) = patch_table.patch_array_vertices(array_idx) {
                const REGULAR_PATCH_SIZE: usize = 16; // 4x4 control points

                for patch_idx in 0..num_patches {
                    writeln!(
                        writer,
                        "# Patch {patch_global_idx} (array {array_idx}, local {patch_idx})"
                    )?;

                    // Write B-spline surface type
                    writeln!(writer, "cstype bspline")?;

                    // Degree 3 in both directions (cubic)
                    writeln!(writer, "deg 3 3")?;

                    // Get control point indices for this patch
                    let start = patch_idx * REGULAR_PATCH_SIZE;
                    let patch_cvs = &cv_indices[start..start + REGULAR_PATCH_SIZE];

                    // Write surface with parameter range [0,1] and control points
                    // Note: OBJ uses 1-based indexing, and negative indices for relative indexing
                    write!(writer, "surf 0.0 1.0 0.0 1.0")?;

                    // Write control point indices in row-major order
                    for cv in patch_cvs.iter().take(16) {
                        let cv_idx = cv.0 as usize;
                        if cv_idx >= control_points.len() {
                            return Err(ObjExportError::InvalidControlPoints);
                        }
                        // Use 1-based index
                        write!(writer, " {}", cv_idx + 1)?;
                    }
                    writeln!(writer)?;

                    // Write knot vectors
                    // Use the same knot vector we fixed for monstertruck: [-3, -2, -1, 0, 1, 2, 3,
                    // 4]
                    writeln!(writer, "parm u -3.0 -2.0 -1.0 0.0 1.0 2.0 3.0 4.0")?;
                    writeln!(writer, "parm v -3.0 -2.0 -1.0 0.0 1.0 2.0 3.0 4.0")?;

                    // End the surface definition
                    writeln!(writer, "end")?;
                    writeln!(writer)?;

                    patch_global_idx += 1;
                }
            }
        }
    }

    Ok(())
}

/// Extension trait for PatchTable to provide OBJ export functionality
pub trait PatchTableObjExt {
    /// Export patches as B-spline surfaces to OBJ format
    fn export_obj_bspline_surfaces<W: Write>(
        &self,
        writer: &mut W,
        control_points: &[[f32; 3]],
    ) -> Result<()>;

    /// Export patches to OBJ file
    fn export_obj_bspline_file(&self, path: &str, control_points: &[[f32; 3]]) -> Result<()>;
}

impl PatchTableObjExt for PatchTable {
    fn export_obj_bspline_surfaces<W: Write>(
        &self,
        writer: &mut W,
        control_points: &[[f32; 3]],
    ) -> Result<()> {
        export_patches_as_bspline_surfaces(writer, self, control_points)
    }

    fn export_obj_bspline_file(&self, path: &str, control_points: &[[f32; 3]]) -> Result<()> {
        let mut file = std::fs::File::create(path)?;
        self.export_obj_bspline_surfaces(&mut file, control_points)
    }
}