Skip to main content

point_formats/
convert.rs

1use crate::error::{Error, Result};
2use crate::format::Format;
3use crate::io::NativeOptions;
4use crate::types::{Geometry, PointCloud};
5use std::path::Path;
6
7/// How the conversion pipeline should treat formats that can contain either
8/// vertices-only point data or triangle meshes.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum GeometryPolicy {
11    /// Preserve mesh faces when the destination supports them; otherwise require
12    /// `allow_lossy` before dropping faces.
13    #[default]
14    Auto,
15    /// Force point-cloud output. Mesh inputs drop faces only when `allow_lossy` is true.
16    PointsOnly,
17    /// Require mesh geometry.
18    MeshOnly,
19}
20
21/// Conversion options. Defaults prioritize preservation and explicit errors over
22/// silent lossy behavior.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ConvertOptions {
25    pub input_format: Option<Format>,
26    pub output_format: Option<Format>,
27    pub allow_lossy: bool,
28    pub geometry_policy: GeometryPolicy,
29    pub native: NativeOptions,
30}
31
32impl Default for ConvertOptions {
33    fn default() -> Self {
34        Self {
35            input_format: None,
36            output_format: None,
37            allow_lossy: false,
38            geometry_policy: GeometryPolicy::Auto,
39            native: NativeOptions::default(),
40        }
41    }
42}
43
44/// Summary of a completed conversion.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct ConversionReport {
47    pub input_format: Format,
48    pub output_format: Format,
49    pub points_read: usize,
50    pub points_written: usize,
51    pub faces_read: usize,
52    pub faces_written: usize,
53    pub warnings: Vec<String>,
54}
55
56/// Converts an input file to an output file using built-in native codecs.
57///
58/// Heavyweight formats such as LAS/LAZ/COPC/E57 are represented by [`Format`]
59/// but require adapter codecs. The built-in path returns explicit
60/// [`Error::UnsupportedFormat`] for those formats.
61pub fn convert_path(
62    input: impl AsRef<Path>,
63    output: impl AsRef<Path>,
64    options: &ConvertOptions,
65) -> Result<ConversionReport> {
66    let input = input.as_ref();
67    let output = output.as_ref();
68    let input_format = options
69        .input_format
70        .map(Ok)
71        .unwrap_or_else(|| Format::from_path(input))?;
72    let output_format = options
73        .output_format
74        .map(Ok)
75        .unwrap_or_else(|| Format::from_path(output))?;
76
77    let mut geometry = crate::io::read_path(input, input_format, &options.native)?;
78    geometry.metadata_mut().source_format = Some(input_format);
79    let points_read = geometry.point_count();
80    let faces_read = geometry.face_count();
81
82    geometry = apply_geometry_policy(geometry, output_format, options)?;
83
84    let points_written = geometry.point_count();
85    let faces_written = geometry.face_count();
86    let warnings = geometry.metadata().warnings.clone();
87
88    crate::io::write_path(output, output_format, &geometry, &options.native)?;
89
90    Ok(ConversionReport {
91        input_format,
92        output_format,
93        points_read,
94        points_written,
95        faces_read,
96        faces_written,
97        warnings,
98    })
99}
100
101fn apply_geometry_policy(
102    geometry: Geometry,
103    output_format: Format,
104    options: &ConvertOptions,
105) -> Result<Geometry> {
106    match options.geometry_policy {
107        GeometryPolicy::Auto => coerce_for_output(geometry, output_format, options.allow_lossy),
108        GeometryPolicy::PointsOnly => force_points(geometry, output_format, options.allow_lossy),
109        GeometryPolicy::MeshOnly => match geometry {
110            Geometry::Mesh(mesh) => Ok(Geometry::Mesh(mesh)),
111            Geometry::PointCloud(_) => Err(Error::LossyConversionBlocked {
112                from: "point cloud",
113                to: output_format,
114                reason: "mesh output was requested, but no meshing algorithm is configured"
115                    .to_string(),
116            }),
117        },
118    }
119}
120
121fn coerce_for_output(
122    geometry: Geometry,
123    output_format: Format,
124    allow_lossy: bool,
125) -> Result<Geometry> {
126    match (&geometry, output_format) {
127        (
128            Geometry::Mesh(_),
129            Format::Xyz | Format::Txt | Format::Csv | Format::Pts | Format::Ptx | Format::Pcd,
130        ) => force_points(geometry, output_format, allow_lossy),
131        _ => Ok(geometry),
132    }
133}
134
135fn force_points(geometry: Geometry, output_format: Format, allow_lossy: bool) -> Result<Geometry> {
136    match geometry {
137        Geometry::PointCloud(cloud) => Ok(Geometry::PointCloud(cloud)),
138        Geometry::Mesh(mesh) => {
139            if !allow_lossy {
140                return Err(Error::LossyConversionBlocked {
141                    from: "mesh",
142                    to: output_format,
143                    reason: "faces would be discarded".to_string(),
144                });
145            }
146            Ok(Geometry::PointCloud(mesh.vertex_cloud()))
147        }
148    }
149}
150
151/// Converts geometry already in memory to a point cloud, dropping faces only when
152/// `allow_lossy` is true.
153pub fn geometry_to_point_cloud(
154    geometry: Geometry,
155    destination: Format,
156    allow_lossy: bool,
157) -> Result<PointCloud> {
158    match force_points(geometry, destination, allow_lossy)? {
159        Geometry::PointCloud(cloud) => Ok(cloud),
160        Geometry::Mesh(_) => Err(Error::invalid(
161            "internal conversion error: mesh remained after force_points",
162        )),
163    }
164}