oxihuman_export/
las_export.rs1#![allow(dead_code)]
4
5pub const LAS_MAGIC: &[u8; 4] = b"LASF";
8
9#[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#[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#[allow(dead_code)]
33pub struct LasExport {
34 pub header: LasHeaderV2,
35 pub points: Vec<LasPointV2>,
36}
37
38#[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#[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#[allow(dead_code)]
73pub fn las_point_count_v2(export: &LasExport) -> u64 {
74 export.header.point_count
75}
76
77#[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#[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#[allow(dead_code)]
95pub fn las_file_size_estimate_v2(point_count: u64) -> usize {
96 375 + point_count as usize * 20
97}
98
99#[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#[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}