Skip to main content

point_formats/
types.rs

1use crate::format::Format;
2use std::collections::BTreeMap;
3
4/// Three-dimensional vector or coordinate.
5#[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/// RGB color stored as 16-bit components so LAS/E57 style precision is not
66/// discarded when passing through richer formats.
67#[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/// Dynamic per-point attribute retained by adapters that need fields outside
124/// the normalized point model.
125#[derive(Debug, Clone, PartialEq)]
126pub enum AttributeValue {
127    Int(i64),
128    UInt(u64),
129    Float(f64),
130    Text(String),
131}
132
133/// Wrapper for point attributes to optimize memory footprint and allocations.
134#[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/// Normalized LiDAR/point-cloud point.
191#[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/// Axis-aligned bounds for a point cloud or mesh.
248#[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/// Metadata shared by point clouds and meshes. The crate stores CRS and scanner
295/// transforms but does not invent them for formats that do not carry them.
296#[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/// Owned point cloud. Suitable for moderate-size conversion and tests. Large
308/// production LAS/COPC/E57 adapters should implement streaming codecs using the
309/// adapter traits in [`crate::adapters`].
310#[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/// Mesh vertex. The crate keeps optional color/normal because PLY/OBJ can carry
362/// them, while STL only stores per-facet normals.
363#[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/// Triangle face using zero-based vertex indices.
382#[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/// Triangle mesh.
395#[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/// Geometry returned by readers. Formats like PLY/OBJ can be either a point
437/// cloud or mesh depending on whether face data is present.
438#[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}