quadrs 1.0.0

Experimental quad remeshing library and tools based on Instant Meshes
Documentation
use quadrs::{load_obj, remesh, write_obj, MeshReport, RemeshMode, RemeshOptions, RemeshTarget};
use std::env;
use std::error::Error;
use std::path::PathBuf;

fn main() {
    if matches!(env::args().nth(1).as_deref(), Some("-h" | "--help")) {
        println!("{}", usage());
        return;
    }
    if let Err(err) = run() {
        eprintln!("{err}");
        std::process::exit(1);
    }
}

fn run() -> Result<(), Box<dyn Error>> {
    let args = parse_args(env::args().skip(1))?;
    let input = load_obj(&args.input)?;
    let mut options = RemeshOptions::new(args.target);
    options.hierarchy_orientation_iterations = args.hierarchy_orientation_iterations;
    options.hierarchy_position_iterations = args.hierarchy_position_iterations;
    options.orientation_iterations = args.orientation_iterations;
    options.position_iterations = args.position_iterations;
    options.frozen_orientation_iterations = args.frozen_orientation_iterations;
    options.frozen_position_iterations = args.frozen_position_iterations;
    options.seed = args.seed;
    options.mode = args.mode;

    let result = remesh(&input, &options)?;
    print_report("input", &result.input, None);
    eprintln!("seed {}", result.seed);
    write_obj(&args.output, &result.mesh.vertices, &result.mesh.faces)?;
    print_report(
        "output",
        &result.output,
        Some((result.area_ratio, result.volume_ratio)),
    );
    Ok(())
}

#[derive(Clone)]
struct Args {
    input: PathBuf,
    output: PathBuf,
    target: RemeshTarget,
    hierarchy_orientation_iterations: usize,
    hierarchy_position_iterations: usize,
    orientation_iterations: usize,
    position_iterations: usize,
    frozen_orientation_iterations: usize,
    frozen_position_iterations: usize,
    seed: Option<u64>,
    mode: RemeshMode,
}

fn parse_args<I>(args: I) -> Result<Args, Box<dyn Error>>
where
    I: IntoIterator<Item = String>,
{
    let mut input = None;
    let mut output = None;
    let mut edge_length = None;
    let mut target_vertices = None;
    let mut target_faces = None;
    let mut hierarchy_orientation_iterations = 6;
    let mut hierarchy_position_iterations = 6;
    let mut orientation_iterations = 40;
    let mut position_iterations = 80;
    let mut frozen_orientation_iterations = 20;
    let mut frozen_position_iterations = 20;
    let mut seed = None;
    let mut mode = RemeshMode::Intrinsic;
    let mut args = args.into_iter();
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "-o" | "--output" => output = Some(PathBuf::from(next_value(&mut args, &arg)?)),
            "--edge-length" => edge_length = Some(next_value(&mut args, &arg)?.parse()?),
            "--target-vertices" => target_vertices = Some(next_value(&mut args, &arg)?.parse()?),
            "--target-faces" => target_faces = Some(next_value(&mut args, &arg)?.parse()?),
            "--hierarchy-orientation-iters" => {
                hierarchy_orientation_iterations = next_value(&mut args, &arg)?.parse()?
            }
            "--hierarchy-position-iters" => {
                hierarchy_position_iterations = next_value(&mut args, &arg)?.parse()?
            }
            "--orientation-iters" => {
                orientation_iterations = next_value(&mut args, &arg)?.parse()?
            }
            "--position-iters" => position_iterations = next_value(&mut args, &arg)?.parse()?,
            "--frozen-orientation-iters" => {
                frozen_orientation_iterations = next_value(&mut args, &arg)?.parse()?
            }
            "--frozen-position-iters" => {
                frozen_position_iterations = next_value(&mut args, &arg)?.parse()?
            }
            "--seed" => seed = Some(next_value(&mut args, &arg)?.parse()?),
            "--intrinsic" => mode = RemeshMode::Intrinsic,
            "--extrinsic" => mode = RemeshMode::Extrinsic,
            _ if arg.starts_with('-') => {
                return Err(format!("unknown flag: {arg}\n\n{}", usage()).into())
            }
            _ if input.is_none() => input = Some(PathBuf::from(arg)),
            _ => return Err(format!("unexpected argument: {arg}\n\n{}", usage()).into()),
        }
    }

    let Some(target) = target(edge_length, target_vertices, target_faces) else {
        return Err(usage().into());
    };

    match (input, output) {
        (Some(input), Some(output)) => Ok(Args {
            input,
            output,
            target,
            hierarchy_orientation_iterations,
            hierarchy_position_iterations,
            orientation_iterations,
            position_iterations,
            frozen_orientation_iterations,
            frozen_position_iterations,
            seed,
            mode,
        }),
        _ => Err(usage().into()),
    }
}

fn target(
    edge_length: Option<f64>,
    target_vertices: Option<usize>,
    target_faces: Option<usize>,
) -> Option<RemeshTarget> {
    match (edge_length, target_vertices, target_faces) {
        (Some(length), None, None) => Some(RemeshTarget::EdgeLength(length)),
        (None, Some(vertices), None) => Some(RemeshTarget::VertexCount(vertices)),
        (None, None, Some(faces)) => Some(RemeshTarget::FaceCount(faces)),
        _ => None,
    }
}

fn next_value<I>(args: &mut I, flag: &str) -> Result<String, Box<dyn Error>>
where
    I: Iterator<Item = String>,
{
    args.next()
        .ok_or_else(|| format!("missing value for {flag}").into())
}

fn print_report(label: &str, report: &MeshReport, baseline: Option<(Option<f64>, Option<f64>)>) {
    eprintln!(
        "{label}: V={} F={} quads={} non-quads={} area={:.9} abs-volume={:.9}",
        report.vertex_count,
        report.face_count,
        report.quad_face_count,
        report.non_quad_face_count,
        report.area,
        report.abs_volume,
    );
    eprintln!(
        "{label}: boundary edges={} non-manifold edges={} invalid-lt3={} invalid-repeat={} invalid-index={} invalid-quad={} isolated vertices={}",
        report.boundary_edge_count,
        report.nonmanifold_edge_count,
        report.fewer_than_three_face_count,
        report.repeated_vertex_face_count,
        report.invalid_vertex_index_face_count,
        report.invalid_quad_face_count,
        report.isolated_vertex_count,
    );
    if let Some((area_ratio, volume_ratio)) = baseline {
        if let Some(value) = area_ratio {
            eprintln!("{label}: area ratio vs input = {:.3}", value);
        }
        if let Some(value) = volume_ratio {
            eprintln!("{label}: volume ratio vs input = {:.3}", value);
        }
    }
}

fn usage() -> &'static str {
    "usage: remesh <input.obj> -o <output.obj> (--edge-length L | --target-vertices N | --target-faces N) [--hierarchy-orientation-iters N] [--hierarchy-position-iters N] [--orientation-iters N] [--position-iters N] [--frozen-orientation-iters N] [--frozen-position-iters N] [--seed N] [--intrinsic | --extrinsic]"
}