Skip to main content

oxihuman_export/
pointcloud_viewer_export.rs

1//! Point cloud format export for external viewers (LAS stub, E57 stub).
2
3#[allow(dead_code)]
4pub struct LasHeader {
5    pub version_major: u8,
6    pub version_minor: u8,
7    pub point_count: u32,
8    pub x_scale: f64,
9    pub y_scale: f64,
10    pub z_scale: f64,
11    pub x_offset: f64,
12    pub y_offset: f64,
13    pub z_offset: f64,
14}
15
16#[allow(dead_code)]
17pub struct LasPoint {
18    pub x: i32, // quantized
19    pub y: i32,
20    pub z: i32,
21    pub intensity: u16,
22    pub classification: u8,
23}
24
25#[allow(dead_code)]
26pub struct LasFile {
27    pub header: LasHeader,
28    pub points: Vec<LasPoint>,
29}
30
31#[allow(dead_code)]
32pub struct E57Stub {
33    pub point_count: usize,
34    pub has_color: bool,
35    pub xml_header: String,
36}
37
38#[allow(dead_code)]
39pub fn new_las_header(point_count: u32, scale: f64) -> LasHeader {
40    LasHeader {
41        version_major: 1,
42        version_minor: 4,
43        point_count,
44        x_scale: scale,
45        y_scale: scale,
46        z_scale: scale,
47        x_offset: 0.0,
48        y_offset: 0.0,
49        z_offset: 0.0,
50    }
51}
52
53#[allow(dead_code)]
54pub fn positions_to_las(positions: &[[f32; 3]], scale: f64) -> LasFile {
55    let header = new_las_header(positions.len() as u32, scale);
56    let points: Vec<LasPoint> = positions
57        .iter()
58        .map(|p| LasPoint {
59            x: (p[0] as f64 / scale).round() as i32,
60            y: (p[1] as f64 / scale).round() as i32,
61            z: (p[2] as f64 / scale).round() as i32,
62            intensity: 0,
63            classification: 0,
64        })
65        .collect();
66    LasFile { header, points }
67}
68
69#[allow(dead_code)]
70pub fn las_point_to_world(point: &LasPoint, header: &LasHeader) -> [f64; 3] {
71    [
72        point.x as f64 * header.x_scale + header.x_offset,
73        point.y as f64 * header.y_scale + header.y_offset,
74        point.z as f64 * header.z_scale + header.z_offset,
75    ]
76}
77
78#[allow(dead_code)]
79pub fn las_file_size_estimate(las: &LasFile) -> usize {
80    // LAS header ~375 bytes + 20 bytes per point (simplified)
81    375 + las.points.len() * 20
82}
83
84#[allow(dead_code)]
85pub fn export_las_binary_stub(las: &LasFile) -> Vec<u8> {
86    // Simplified stub: file signature + point count + scale values
87    let mut bytes = Vec::new();
88    // "LASF" signature
89    bytes.extend_from_slice(b"LASF");
90    // version major / minor
91    bytes.push(las.header.version_major);
92    bytes.push(las.header.version_minor);
93    // point count (little-endian u32)
94    bytes.extend_from_slice(&las.header.point_count.to_le_bytes());
95    // scale (x,y,z) as little-endian f64
96    bytes.extend_from_slice(&las.header.x_scale.to_le_bytes());
97    bytes.extend_from_slice(&las.header.y_scale.to_le_bytes());
98    bytes.extend_from_slice(&las.header.z_scale.to_le_bytes());
99    // simplified point data: x,y,z as i32
100    for p in &las.points {
101        bytes.extend_from_slice(&p.x.to_le_bytes());
102        bytes.extend_from_slice(&p.y.to_le_bytes());
103        bytes.extend_from_slice(&p.z.to_le_bytes());
104        bytes.extend_from_slice(&p.intensity.to_le_bytes());
105        bytes.push(p.classification);
106    }
107    bytes
108}
109
110#[allow(dead_code)]
111pub fn new_e57_stub(point_count: usize, has_color: bool) -> E57Stub {
112    let xml_header = format!(
113        "<?xml version=\"1.0\"?><e57Root pointCount=\"{}\" hasColor=\"{}\"/>",
114        point_count, has_color
115    );
116    E57Stub {
117        point_count,
118        has_color,
119        xml_header,
120    }
121}
122
123#[allow(dead_code)]
124pub fn e57_xml_header(stub: &E57Stub) -> String {
125    stub.xml_header.clone()
126}
127
128/// Returns (min, max) in quantized coordinates.
129#[allow(dead_code)]
130pub fn las_bounds(las: &LasFile) -> ([i32; 3], [i32; 3]) {
131    if las.points.is_empty() {
132        return ([0, 0, 0], [0, 0, 0]);
133    }
134    let mut min = [i32::MAX; 3];
135    let mut max = [i32::MIN; 3];
136    for p in &las.points {
137        let coords = [p.x, p.y, p.z];
138        for k in 0..3 {
139            if coords[k] < min[k] {
140                min[k] = coords[k];
141            }
142            if coords[k] > max[k] {
143                max[k] = coords[k];
144            }
145        }
146    }
147    (min, max)
148}
149
150#[allow(dead_code)]
151pub fn las_point_count(las: &LasFile) -> usize {
152    las.points.len()
153}
154
155#[allow(dead_code)]
156pub fn filter_las_by_classification(las: &LasFile, class: u8) -> Vec<&LasPoint> {
157    las.points
158        .iter()
159        .filter(|p| p.classification == class)
160        .collect()
161}
162
163#[allow(dead_code)]
164pub fn las_to_positions(las: &LasFile) -> Vec<[f32; 3]> {
165    las.points
166        .iter()
167        .map(|p| {
168            let w = las_point_to_world(p, &las.header);
169            [w[0] as f32, w[1] as f32, w[2] as f32]
170        })
171        .collect()
172}
173
174#[allow(dead_code)]
175pub fn decimate_las(las: &LasFile, keep_every: usize) -> LasFile {
176    let keep = keep_every.max(1);
177    let points: Vec<LasPoint> = las
178        .points
179        .iter()
180        .enumerate()
181        .filter_map(|(i, p)| {
182            if i % keep == 0 {
183                Some(LasPoint {
184                    x: p.x,
185                    y: p.y,
186                    z: p.z,
187                    intensity: p.intensity,
188                    classification: p.classification,
189                })
190            } else {
191                None
192            }
193        })
194        .collect();
195    let new_count = points.len() as u32;
196    let header = LasHeader {
197        version_major: las.header.version_major,
198        version_minor: las.header.version_minor,
199        point_count: new_count,
200        x_scale: las.header.x_scale,
201        y_scale: las.header.y_scale,
202        z_scale: las.header.z_scale,
203        x_offset: las.header.x_offset,
204        y_offset: las.header.y_offset,
205        z_offset: las.header.z_offset,
206    };
207    LasFile { header, points }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    fn sample_positions() -> Vec<[f32; 3]> {
215        vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]
216    }
217
218    #[test]
219    fn test_new_las_header() {
220        let h = new_las_header(100, 0.001);
221        assert_eq!(h.version_major, 1);
222        assert_eq!(h.version_minor, 4);
223        assert_eq!(h.point_count, 100);
224        assert!((h.x_scale - 0.001).abs() < 1e-9);
225    }
226
227    #[test]
228    fn test_positions_to_las() {
229        let positions = sample_positions();
230        let las = positions_to_las(&positions, 0.001);
231        assert_eq!(las.points.len(), 3);
232        assert_eq!(las.header.point_count, 3);
233    }
234
235    #[test]
236    fn test_las_point_to_world_round_trip() {
237        let positions = vec![[1.5_f32, 2.5, 3.5]];
238        let scale = 0.001;
239        let las = positions_to_las(&positions, scale);
240        let world = las_point_to_world(&las.points[0], &las.header);
241        assert!((world[0] - 1.5).abs() < 0.01);
242        assert!((world[1] - 2.5).abs() < 0.01);
243        assert!((world[2] - 3.5).abs() < 0.01);
244    }
245
246    #[test]
247    fn test_las_point_count() {
248        let las = positions_to_las(&sample_positions(), 0.001);
249        assert_eq!(las_point_count(&las), 3);
250    }
251
252    #[test]
253    fn test_las_bounds() {
254        let las = positions_to_las(&sample_positions(), 1.0);
255        let (mn, mx) = las_bounds(&las);
256        assert!(mn[0] <= mx[0]);
257        assert!(mn[1] <= mx[1]);
258        assert!(mn[2] <= mx[2]);
259    }
260
261    #[test]
262    fn test_las_bounds_empty() {
263        let header = new_las_header(0, 0.001);
264        let las = LasFile {
265            header,
266            points: vec![],
267        };
268        let (mn, mx) = las_bounds(&las);
269        assert_eq!(mn, [0, 0, 0]);
270        assert_eq!(mx, [0, 0, 0]);
271    }
272
273    #[test]
274    fn test_filter_by_classification() {
275        let mut las = positions_to_las(&sample_positions(), 0.001);
276        las.points[0].classification = 1;
277        las.points[1].classification = 2;
278        las.points[2].classification = 1;
279        let filtered = filter_las_by_classification(&las, 1);
280        assert_eq!(filtered.len(), 2);
281    }
282
283    #[test]
284    fn test_las_to_positions() {
285        let positions = sample_positions();
286        let las = positions_to_las(&positions, 0.001);
287        let back = las_to_positions(&las);
288        assert_eq!(back.len(), 3);
289        assert!((back[0][0] - 1.0).abs() < 0.01);
290    }
291
292    #[test]
293    fn test_decimate_las() {
294        let positions: Vec<[f32; 3]> = (0..10).map(|i| [i as f32, 0.0, 0.0]).collect();
295        let las = positions_to_las(&positions, 0.001);
296        let dec = decimate_las(&las, 2);
297        assert_eq!(dec.points.len(), 5);
298    }
299
300    #[test]
301    fn test_decimate_las_keep_all() {
302        let positions = sample_positions();
303        let las = positions_to_las(&positions, 0.001);
304        let dec = decimate_las(&las, 1);
305        assert_eq!(dec.points.len(), 3);
306    }
307
308    #[test]
309    fn test_e57_stub() {
310        let stub = new_e57_stub(500, true);
311        assert_eq!(stub.point_count, 500);
312        assert!(stub.has_color);
313        let xml = e57_xml_header(&stub);
314        assert!(xml.contains("500"));
315    }
316
317    #[test]
318    fn test_binary_stub_non_empty() {
319        let las = positions_to_las(&sample_positions(), 0.001);
320        let bytes = export_las_binary_stub(&las);
321        assert!(!bytes.is_empty());
322        assert!(bytes.starts_with(b"LASF"));
323    }
324
325    #[test]
326    fn test_las_file_size_estimate() {
327        let las = positions_to_las(&sample_positions(), 0.001);
328        let size = las_file_size_estimate(&las);
329        assert!(size > 375);
330    }
331
332    #[test]
333    fn test_positions_to_las_empty() {
334        let las = positions_to_las(&[], 0.001);
335        assert_eq!(las.points.len(), 0);
336        assert_eq!(las.header.point_count, 0);
337    }
338}