use std::io::sink;
use std::path::Path;
use crate::conversion::{self, ConversionReport, Format, InfillShape, Omission, Options};
use crate::core::{Error, Result, VoxelEstimate, VoxelGrid, boundary_surface};
use super::convert::propose_synthetic_zero;
#[derive(Debug, Clone, PartialEq)]
pub struct PreviewReport {
pub source_format: Format,
pub target_format: Format,
pub source_bytes: u64,
pub points: usize,
pub cells: usize,
pub fields: usize,
pub omissions: Vec<Omission>,
pub voxel_estimate: Option<VoxelEstimate>,
pub complete: bool,
pub limit: Option<String>,
pub mesh_output: Option<ConversionReport>,
}
#[allow(clippy::too_many_lines)] pub fn preview(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
options: &Options,
) -> Result<PreviewReport> {
let input = input.as_ref();
let output = output.as_ref();
let target = Format::from_output_path(output)?;
if options.mesh_output.is_some() && target != Format::Op2 {
return Err(Error::new(
"E_USAGE",
"mesh_output applies only to OP2 output",
));
}
conversion::validate_route_options(target, options)?;
let source_format = options
.input_format
.map_or_else(|| Format::from_input_path(input), Ok)?;
let mut engine_options = options.clone();
if matches!(source_format, Format::Op2 | Format::Pch)
&& options
.mesh
.as_ref()
.is_some_and(|path| Format::from_input_path(path).is_ok_and(|f| f != Format::Bdf))
{
engine_options.assume_basic_frame = true;
}
if target == Format::Op2 {
engine_options.zero_missing_rotations = true;
}
let mut source = conversion::read_path(input, &engine_options)?;
if target == Format::Op2
&& matches!(source.format, Format::Bdf | Format::Inp)
&& source.dataset.fields.is_empty()
{
propose_synthetic_zero(input, &mut source, options.max_bytes)?;
}
let mut result = PreviewReport {
source_format: source.format,
target_format: target,
source_bytes: std::fs::metadata(input)?.len(),
points: source.dataset.mesh.points.len(),
cells: source.dataset.mesh.cells.len(),
fields: source.dataset.fields.len(),
omissions: source.omissions.clone(),
voxel_estimate: None,
complete: false,
limit: None,
mesh_output: None,
};
let mesh_to_grid = matches!(target, Format::Vti | Format::Vox) && source.voxel_grid.is_none();
let surface_to_grid_volume = source
.dataset
.mesh
.cells
.iter()
.all(|cell| cell.kind.dimension() == 2)
&& !source.dataset.mesh.cells.is_empty()
&& !matches!(
target,
Format::Stl | Format::Vti | Format::Vox | Format::Op2 | Format::Pch
)
&& (matches!(
options.infill_shape,
Some(InfillShape::Hex8 | InfillShape::Wedge6 | InfillShape::Pyramid5)
) || options.match_grid.is_some());
if options.match_grid.is_some() && !(mesh_to_grid || surface_to_grid_volume) {
return Err(Error::new(
"E_USAGE",
"--match-grid applies to mesh voxelization only",
));
}
if (mesh_to_grid || surface_to_grid_volume)
&& (options.infill_shape == Some(InfillShape::Hex8)
|| surface_to_grid_volume
|| options.match_grid.is_some())
{
let reference = conversion::load_match_grid(options)?;
let spacing = options
.infill_unit_size
.or_else(|| reference.as_ref().map(|grid| grid.spacing))
.ok_or_else(|| Error::new("E_USAGE", "missing infill unit size"))?;
let mesh = &source.dataset.mesh;
let surface = if mesh.cells.iter().any(|cell| cell.kind.dimension() == 3) {
boundary_surface(mesh)?
} else {
mesh.clone()
};
let estimate = VoxelGrid::estimate_surface(&surface, spacing, reference.as_ref())?;
result.limit = estimate.check_limits().err().map(|error| error.message);
if target == Format::Vox && estimate.dims.iter().any(|&n| n > 256) {
result.limit = Some("VOX dimensions exceed 256 cells on an axis".into());
}
result.voxel_estimate = Some(estimate);
return Ok(result);
}
let mut companion_source = options.mesh_output.as_ref().map(|_| source.clone());
let report = conversion::convert(source, target, &engine_options, &mut sink())?;
result.omissions = report.omissions;
result.complete = true;
if let (Some(companion_path), Some(mut companion_source)) =
(&options.mesh_output, companion_source.take())
{
let format = Format::from_output_path(companion_path)?;
if format == Format::Op2 {
return Err(Error::new(
"E_USAGE",
"mesh_output needs a mesh-bearing format",
));
}
let excluded = companion_source.dataset.fields.len();
companion_source.dataset.fields.clear();
if excluded > 0 {
companion_source.omissions.push(Omission {
stage: conversion::Stage::Destination,
assumption: None,
detail: format!("{excluded} result field(s) excluded from companion mesh"),
});
}
result.mesh_output = Some(conversion::convert(
companion_source,
format,
&engine_options,
&mut sink(),
)?);
}
Ok(result)
}