Skip to main content

oxihuman_export/
las_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! LAS lidar point cloud format export stub.
6
7pub const LAS_MAGIC: &[u8; 4] = b"LASF";
8
9/// LAS point data format 0 (basic XYZ + intensity).
10#[allow(dead_code)]
11pub struct LasPointV2 {
12    pub x: i32,
13    pub y: i32,
14    pub z: i32,
15    pub intensity: u16,
16    pub return_number: u8,
17    pub classification: u8,
18}
19
20/// LAS 1.4 header stub.
21#[allow(dead_code)]
22pub struct LasHeaderV2 {
23    pub version_major: u8,
24    pub version_minor: u8,
25    pub point_data_format: u8,
26    pub point_count: u64,
27    pub scale: [f64; 3],
28    pub offset: [f64; 3],
29}
30
31/// LAS export bundle.
32#[allow(dead_code)]
33pub struct LasExport {
34    pub header: LasHeaderV2,
35    pub points: Vec<LasPointV2>,
36}
37
38/// Create a new LAS export.
39#[allow(dead_code)]
40pub fn new_las_export(scale: f64) -> LasExport {
41    LasExport {
42        header: LasHeaderV2 {
43            version_major: 1,
44            version_minor: 4,
45            point_data_format: 0,
46            point_count: 0,
47            scale: [scale; 3],
48            offset: [0.0; 3],
49        },
50        points: Vec::new(),
51    }
52}
53
54/// Add a point (world coordinates converted to integer).
55#[allow(dead_code)]
56pub fn add_las_point(export: &mut LasExport, x: f64, y: f64, z: f64, intensity: u16) {
57    let xi = ((x - export.header.offset[0]) / export.header.scale[0]) as i32;
58    let yi = ((y - export.header.offset[1]) / export.header.scale[1]) as i32;
59    let zi = ((z - export.header.offset[2]) / export.header.scale[2]) as i32;
60    export.points.push(LasPointV2 {
61        x: xi,
62        y: yi,
63        z: zi,
64        intensity,
65        return_number: 1,
66        classification: 0,
67    });
68    export.header.point_count += 1;
69}
70
71/// Point count.
72#[allow(dead_code)]
73pub fn las_point_count_v2(export: &LasExport) -> u64 {
74    export.header.point_count
75}
76
77/// Build LAS header bytes (simplified).
78#[allow(dead_code)]
79pub fn build_las_header_bytes(export: &LasExport) -> Vec<u8> {
80    let mut buf = LAS_MAGIC.to_vec();
81    buf.extend_from_slice(&[export.header.version_major, export.header.version_minor]);
82    buf.extend_from_slice(&export.header.point_count.to_le_bytes());
83    buf
84}
85
86/// Validate LAS export.
87#[allow(dead_code)]
88pub fn validate_las(export: &LasExport) -> bool {
89    export.header.point_count == export.points.len() as u64
90        && export.header.scale.iter().all(|&s| s > 0.0)
91}
92
93/// Estimate file size.
94#[allow(dead_code)]
95pub fn las_file_size_estimate_v2(point_count: u64) -> usize {
96    375 + point_count as usize * 20
97}
98
99/// Load from f32 positions.
100#[allow(dead_code)]
101pub fn las_from_positions(positions: &[[f32; 3]], scale: f64) -> LasExport {
102    let mut e = new_las_export(scale);
103    for &p in positions {
104        add_las_point(&mut e, p[0] as f64, p[1] as f64, p[2] as f64, 0);
105    }
106    e
107}
108
109/// Get world X from integer record.
110#[allow(dead_code)]
111pub fn las_world_x(point: &LasPointV2, header: &LasHeaderV2) -> f64 {
112    point.x as f64 * header.scale[0] + header.offset[0]
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn magic_correct() {
121        assert_eq!(&LAS_MAGIC[..], b"LASF");
122    }
123
124    #[test]
125    fn new_export_zero_points() {
126        let e = new_las_export(0.001);
127        assert_eq!(las_point_count_v2(&e), 0);
128    }
129
130    #[test]
131    fn add_point_increments_count() {
132        let mut e = new_las_export(0.001);
133        add_las_point(&mut e, 1.0, 2.0, 3.0, 100);
134        assert_eq!(las_point_count_v2(&e), 1);
135    }
136
137    #[test]
138    fn validate_passes() {
139        let mut e = new_las_export(0.001);
140        add_las_point(&mut e, 0.0, 0.0, 0.0, 0);
141        assert!(validate_las(&e));
142    }
143
144    #[test]
145    fn header_bytes_start_with_magic() {
146        let e = new_las_export(0.001);
147        let bytes = build_las_header_bytes(&e);
148        assert_eq!(&bytes[..4], b"LASF");
149    }
150
151    #[test]
152    fn file_size_grows() {
153        assert!(las_file_size_estimate_v2(1000) > las_file_size_estimate_v2(100));
154    }
155
156    #[test]
157    fn from_positions_count() {
158        let pos = vec![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]];
159        let e = las_from_positions(&pos, 0.001);
160        assert_eq!(las_point_count_v2(&e), 2);
161    }
162
163    #[test]
164    fn world_x_round_trip() {
165        let mut e = new_las_export(0.001);
166        add_las_point(&mut e, 1.0, 0.0, 0.0, 0);
167        let wx = las_world_x(&e.points[0], &e.header);
168        assert!((wx - 1.0).abs() < 0.01);
169    }
170
171    #[test]
172    fn version_correct() {
173        let e = new_las_export(0.001);
174        assert_eq!(e.header.version_major, 1);
175        assert_eq!(e.header.version_minor, 4);
176    }
177}