1use crate::format::Format;
2use std::collections::BTreeMap;
3
4#[derive(Debug, Clone, Copy, PartialEq)]
6pub struct Vec3 {
7 pub x: f64,
8 pub y: f64,
9 pub z: f64,
10}
11
12impl Vec3 {
13 pub const ZERO: Self = Self {
14 x: 0.0,
15 y: 0.0,
16 z: 0.0,
17 };
18
19 #[inline]
20 pub const fn new(x: f64, y: f64, z: f64) -> Self {
21 Self { x, y, z }
22 }
23
24 #[inline]
25 pub fn is_finite(self) -> bool {
26 self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
27 }
28
29 #[inline]
30 #[allow(clippy::should_implement_trait)]
31 pub fn sub(self, other: Self) -> Self {
32 Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
33 }
34
35 #[inline]
36 pub fn cross(self, other: Self) -> Self {
37 Self::new(
38 self.y * other.z - self.z * other.y,
39 self.z * other.x - self.x * other.z,
40 self.x * other.y - self.y * other.x,
41 )
42 }
43
44 #[inline]
45 pub fn dot(self, other: Self) -> f64 {
46 self.x * other.x + self.y * other.y + self.z * other.z
47 }
48
49 #[inline]
50 pub fn norm(self) -> f64 {
51 self.dot(self).sqrt()
52 }
53
54 #[inline]
55 pub fn normalized(self) -> Option<Self> {
56 let norm = self.norm();
57 if norm == 0.0 || !norm.is_finite() {
58 None
59 } else {
60 Some(Self::new(self.x / norm, self.y / norm, self.z / norm))
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct Color {
69 pub red: u16,
70 pub green: u16,
71 pub blue: u16,
72}
73
74impl Color {
75 #[inline]
76 pub const fn new(red: u16, green: u16, blue: u16) -> Self {
77 Self { red, green, blue }
78 }
79
80 #[inline]
81 pub const fn from_u8(red: u8, green: u8, blue: u8) -> Self {
82 Self {
83 red: red as u16,
84 green: green as u16,
85 blue: blue as u16,
86 }
87 }
88
89 #[inline]
90 pub fn to_u8_lossy(self) -> [u8; 3] {
91 [
92 self.red.min(255) as u8,
93 self.green.min(255) as u8,
94 self.blue.min(255) as u8,
95 ]
96 }
97
98 #[inline]
99 pub fn to_unit_rgb(self) -> [f64; 3] {
100 [
101 self.red as f64 / u16::MAX as f64,
102 self.green as f64 / u16::MAX as f64,
103 self.blue as f64 / u16::MAX as f64,
104 ]
105 }
106
107 #[inline]
108 pub fn from_unit_rgb(red: f64, green: f64, blue: f64) -> Option<Self> {
109 fn component(v: f64) -> Option<u16> {
110 if !v.is_finite() || !(0.0..=1.0).contains(&v) {
111 return None;
112 }
113 Some((v * u16::MAX as f64).round() as u16)
114 }
115 Some(Self::new(
116 component(red)?,
117 component(green)?,
118 component(blue)?,
119 ))
120 }
121}
122
123#[derive(Debug, Clone, PartialEq)]
126pub enum AttributeValue {
127 Int(i64),
128 UInt(u64),
129 Float(f64),
130 Text(String),
131}
132
133#[derive(Debug, Default, PartialEq)]
135pub struct PointAttributes(pub Option<Box<BTreeMap<String, AttributeValue>>>);
136
137impl Clone for PointAttributes {
138 #[inline]
139 fn clone(&self) -> Self {
140 Self(self.0.as_ref().map(|map| Box::new((**map).clone())))
141 }
142}
143
144impl std::ops::Deref for PointAttributes {
145 type Target = BTreeMap<String, AttributeValue>;
146
147 #[inline]
148 fn deref(&self) -> &Self::Target {
149 static EMPTY: std::sync::OnceLock<BTreeMap<String, AttributeValue>> =
150 std::sync::OnceLock::new();
151 let empty = EMPTY.get_or_init(BTreeMap::new);
152 match &self.0 {
153 Some(map) => map,
154 None => empty,
155 }
156 }
157}
158
159impl std::ops::DerefMut for PointAttributes {
160 #[inline]
161 fn deref_mut(&mut self) -> &mut Self::Target {
162 if self.0.is_none() {
163 self.0 = Some(Box::default());
164 }
165 self.0.as_mut().unwrap()
166 }
167}
168
169impl From<BTreeMap<String, AttributeValue>> for PointAttributes {
170 #[inline]
171 fn from(map: BTreeMap<String, AttributeValue>) -> Self {
172 if map.is_empty() {
173 Self(None)
174 } else {
175 Self(Some(Box::new(map)))
176 }
177 }
178}
179
180impl From<PointAttributes> for BTreeMap<String, AttributeValue> {
181 #[inline]
182 fn from(attrs: PointAttributes) -> Self {
183 match attrs.0 {
184 Some(boxed) => *boxed,
185 None => BTreeMap::new(),
186 }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq)]
192pub struct Point {
193 pub position: Vec3,
194 pub intensity: Option<f32>,
195 pub color: Option<Color>,
196 pub classification: Option<u8>,
197 pub return_number: Option<u8>,
198 pub number_of_returns: Option<u8>,
199 pub gps_time: Option<f64>,
200 pub scan_angle: Option<f32>,
201 pub normal: Option<Vec3>,
202 pub attributes: PointAttributes,
203}
204
205impl Point {
206 #[inline]
207 pub fn new(x: f64, y: f64, z: f64) -> Self {
208 Self {
209 position: Vec3::new(x, y, z),
210 intensity: None,
211 color: None,
212 classification: None,
213 return_number: None,
214 number_of_returns: None,
215 gps_time: None,
216 scan_angle: None,
217 normal: None,
218 attributes: PointAttributes::default(),
219 }
220 }
221
222 #[inline]
223 pub fn with_intensity(mut self, intensity: f32) -> Self {
224 self.intensity = Some(intensity);
225 self
226 }
227
228 #[inline]
229 pub fn with_color(mut self, color: Color) -> Self {
230 self.color = Some(color);
231 self
232 }
233
234 #[inline]
235 pub fn with_classification(mut self, classification: u8) -> Self {
236 self.classification = Some(classification);
237 self
238 }
239
240 #[inline]
241 pub fn with_normal(mut self, normal: Vec3) -> Self {
242 self.normal = Some(normal);
243 self
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq)]
249pub struct Bounds3 {
250 pub min: Vec3,
251 pub max: Vec3,
252}
253
254impl Bounds3 {
255 #[inline]
256 pub fn empty() -> Self {
257 Self {
258 min: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
259 max: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
260 }
261 }
262
263 pub fn from_points<'a>(points: impl IntoIterator<Item = &'a Point>) -> Option<Self> {
264 let mut bounds = Self::empty();
265 let mut any = false;
266 for point in points {
267 bounds.include(point.position);
268 any = true;
269 }
270 any.then_some(bounds)
271 }
272
273 pub fn from_vertices<'a>(vertices: impl IntoIterator<Item = &'a Vertex>) -> Option<Self> {
274 let mut bounds = Self::empty();
275 let mut any = false;
276 for vertex in vertices {
277 bounds.include(vertex.position);
278 any = true;
279 }
280 any.then_some(bounds)
281 }
282
283 #[inline]
284 pub fn include(&mut self, p: Vec3) {
285 self.min.x = self.min.x.min(p.x);
286 self.min.y = self.min.y.min(p.y);
287 self.min.z = self.min.z.min(p.z);
288 self.max.x = self.max.x.max(p.x);
289 self.max.y = self.max.y.max(p.y);
290 self.max.z = self.max.z.max(p.z);
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Default)]
297pub struct Metadata {
298 pub source_format: Option<Format>,
299 pub point_count_hint: Option<usize>,
300 pub crs_wkt: Option<String>,
301 pub scanner_transform: Option<[[f64; 4]; 4]>,
302 pub comments: Vec<String>,
303 pub warnings: Vec<String>,
304 pub attributes: BTreeMap<String, AttributeValue>,
305}
306
307#[derive(Debug, Clone, PartialEq)]
311pub struct PointCloud {
312 pub points: Vec<Point>,
313 pub metadata: Metadata,
314}
315
316impl PointCloud {
317 pub fn new(points: Vec<Point>) -> Self {
318 Self {
319 points,
320 metadata: Metadata::default(),
321 }
322 }
323
324 pub fn empty() -> Self {
325 Self::new(Vec::new())
326 }
327
328 pub fn len(&self) -> usize {
329 self.points.len()
330 }
331
332 pub fn is_empty(&self) -> bool {
333 self.points.is_empty()
334 }
335
336 pub fn bounds(&self) -> Option<Bounds3> {
337 Bounds3::from_points(&self.points)
338 }
339
340 pub fn has_color(&self) -> bool {
341 self.points.iter().any(|p| p.color.is_some())
342 }
343
344 pub fn has_intensity(&self) -> bool {
345 self.points.iter().any(|p| p.intensity.is_some())
346 }
347
348 pub fn has_classification(&self) -> bool {
349 self.points.iter().any(|p| p.classification.is_some())
350 }
351
352 pub fn has_gps_time(&self) -> bool {
353 self.points.iter().any(|p| p.gps_time.is_some())
354 }
355
356 pub fn has_normals(&self) -> bool {
357 self.points.iter().any(|p| p.normal.is_some())
358 }
359}
360
361#[derive(Debug, Clone, PartialEq)]
364pub struct Vertex {
365 pub position: Vec3,
366 pub normal: Option<Vec3>,
367 pub color: Option<Color>,
368}
369
370impl Vertex {
371 #[inline]
372 pub fn new(position: Vec3) -> Self {
373 Self {
374 position,
375 normal: None,
376 color: None,
377 }
378 }
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383pub struct Face {
384 pub indices: [usize; 3],
385}
386
387impl Face {
388 #[inline]
389 pub const fn new(a: usize, b: usize, c: usize) -> Self {
390 Self { indices: [a, b, c] }
391 }
392}
393
394#[derive(Debug, Clone, PartialEq)]
396pub struct Mesh {
397 pub vertices: Vec<Vertex>,
398 pub faces: Vec<Face>,
399 pub metadata: Metadata,
400}
401
402impl Mesh {
403 #[inline]
404 pub fn new(vertices: Vec<Vertex>, faces: Vec<Face>) -> Self {
405 Self {
406 vertices,
407 faces,
408 metadata: Metadata::default(),
409 }
410 }
411
412 #[inline]
413 pub fn bounds(&self) -> Option<Bounds3> {
414 Bounds3::from_vertices(&self.vertices)
415 }
416
417 pub fn vertex_cloud(&self) -> PointCloud {
418 let points = self
419 .vertices
420 .iter()
421 .map(|vertex| {
422 let mut point = Point::new(vertex.position.x, vertex.position.y, vertex.position.z);
423 point.normal = vertex.normal;
424 point.color = vertex.color;
425 point
426 })
427 .collect();
428 let mut metadata = self.metadata.clone();
429 metadata.warnings.push(
430 "mesh faces were discarded while converting vertices to a point cloud".to_string(),
431 );
432 PointCloud { points, metadata }
433 }
434}
435
436#[derive(Debug, Clone, PartialEq)]
439pub enum Geometry {
440 PointCloud(PointCloud),
441 Mesh(Mesh),
442}
443
444impl Geometry {
445 pub fn point_count(&self) -> usize {
446 match self {
447 Self::PointCloud(cloud) => cloud.points.len(),
448 Self::Mesh(mesh) => mesh.vertices.len(),
449 }
450 }
451
452 pub fn face_count(&self) -> usize {
453 match self {
454 Self::PointCloud(_) => 0,
455 Self::Mesh(mesh) => mesh.faces.len(),
456 }
457 }
458
459 pub fn metadata(&self) -> &Metadata {
460 match self {
461 Self::PointCloud(cloud) => &cloud.metadata,
462 Self::Mesh(mesh) => &mesh.metadata,
463 }
464 }
465
466 pub fn metadata_mut(&mut self) -> &mut Metadata {
467 match self {
468 Self::PointCloud(cloud) => &mut cloud.metadata,
469 Self::Mesh(mesh) => &mut mesh.metadata,
470 }
471 }
472}