1use crate::{
2 CpuDevice, DType, Device, PointBuffer, PointBufferSet, PointField, PointSchema, SpatialError,
3 SpatialMetadata, SpatialResult, StandardSchemas,
4};
5
6#[derive(Clone, Debug, PartialEq)]
8pub struct PointCloud {
9 schema: PointSchema,
10 buffers: PointBufferSet,
11 len: usize,
12 metadata: SpatialMetadata,
13 device: CpuDevice,
14}
15
16#[derive(Clone, Debug, Default)]
18pub struct PointCloudBuilder {
19 schema: PointSchema,
20 buffers: PointBufferSet,
21 metadata: SpatialMetadata,
22}
23
24impl PointCloud {
25 #[must_use]
27 pub fn with_schema(schema: PointSchema) -> Self {
28 Self {
29 schema,
30 buffers: PointBufferSet::new(),
31 len: 0,
32 metadata: SpatialMetadata::default(),
33 device: CpuDevice,
34 }
35 }
36
37 #[must_use]
39 pub fn xyz() -> Self {
40 Self::with_schema(StandardSchemas::point_xyz())
41 }
42
43 #[must_use]
45 pub fn schema(&self) -> &PointSchema {
46 &self.schema
47 }
48
49 #[must_use]
51 pub const fn len(&self) -> usize {
52 self.len
53 }
54
55 #[must_use]
57 pub const fn is_empty(&self) -> bool {
58 self.len == 0
59 }
60
61 #[must_use]
63 pub fn metadata(&self) -> &SpatialMetadata {
64 &self.metadata
65 }
66
67 #[must_use]
69 pub fn device(&self) -> &dyn Device {
70 &self.device
71 }
72
73 pub fn field(&self, name: &str) -> SpatialResult<&PointBuffer> {
75 self.buffers.get(name).ok_or_else(|| SpatialError::MissingField(name.to_owned()))
76 }
77
78 pub fn validate(&self) -> SpatialResult<()> {
80 self.schema.validate_positions()?;
81 for field in self.schema.fields() {
82 let buffer = self.field(&field.name)?;
83 if buffer.len() != self.len {
84 return Err(SpatialError::BufferLengthMismatch {
85 expected: self.len,
86 found: buffer.len(),
87 });
88 }
89 if buffer.dtype() != field.dtype && field.dtype != DType::F16 {
90 return Err(SpatialError::SchemaValidation(format!(
91 "field `{}` dtype mismatch",
92 field.name
93 )));
94 }
95 }
96 Ok(())
97 }
98
99 pub(crate) fn from_builder(builder: PointCloudBuilder, len: usize) -> SpatialResult<Self> {
100 let cloud = Self {
101 schema: builder.schema,
102 buffers: builder.buffers,
103 len,
104 metadata: builder.metadata,
105 device: CpuDevice,
106 };
107 cloud.validate()?;
108 Ok(cloud)
109 }
110
111 pub fn try_from_parts(
113 schema: PointSchema,
114 buffers: PointBufferSet,
115 metadata: SpatialMetadata,
116 ) -> SpatialResult<Self> {
117 if schema.is_empty() {
118 return Ok(Self { schema, buffers, len: 0, metadata, device: CpuDevice });
119 }
120
121 let len = schema
122 .fields()
123 .first()
124 .and_then(|field| buffers.get(&field.name))
125 .map(|buffer| buffer.len())
126 .ok_or_else(|| SpatialError::MissingField(schema.fields()[0].name.clone()))?;
127
128 for field in schema.fields() {
129 let buffer = buffers
130 .get(&field.name)
131 .ok_or_else(|| SpatialError::MissingField(field.name.clone()))?;
132 if buffer.len() != len {
133 return Err(SpatialError::BufferLengthMismatch {
134 expected: len,
135 found: buffer.len(),
136 });
137 }
138 }
139
140 let cloud = Self { schema, buffers, len, metadata, device: CpuDevice };
141 cloud.validate()?;
142 Ok(cloud)
143 }
144
145 #[must_use]
151 pub fn into_parts(self) -> (PointSchema, PointBufferSet, SpatialMetadata) {
152 (self.schema, self.buffers, self.metadata)
153 }
154}
155
156impl PointCloudBuilder {
157 #[must_use]
159 pub fn new(schema: PointSchema) -> Self {
160 Self { schema, ..Self::default() }
161 }
162
163 #[must_use]
165 pub fn xyz() -> Self {
166 Self::new(StandardSchemas::point_xyz())
167 }
168
169 #[must_use]
171 pub fn metadata(mut self, metadata: SpatialMetadata) -> Self {
172 self.metadata = metadata;
173 self
174 }
175
176 pub fn push_point<I>(&mut self, values: I) -> SpatialResult<()>
180 where
181 I: IntoIterator<Item = f32>,
182 {
183 let values: Vec<f32> = values.into_iter().collect();
184 if values.len() != self.schema.len() {
185 return Err(SpatialError::InvalidArgument(format!(
186 "expected {} field values, got {}",
187 self.schema.len(),
188 values.len()
189 )));
190 }
191
192 let fields: Vec<PointField> = self.schema.fields().to_vec();
193 for (field, value) in fields.iter().zip(values) {
194 self.push_scalar(field, value)?;
195 }
196 Ok(())
197 }
198
199 pub fn build(self) -> SpatialResult<PointCloud> {
201 let len = self
202 .schema
203 .fields()
204 .first()
205 .and_then(|field| self.buffers.get(&field.name))
206 .map(|buffer| buffer.len())
207 .unwrap_or(0);
208 PointCloud::from_builder(self, len)
209 }
210
211 fn push_scalar(&mut self, field: &PointField, value: f32) -> SpatialResult<()> {
212 let buffer = match self.buffers.get_mut(&field.name) {
213 Some(buffer) => buffer,
214 None => {
215 let buffer = PointBuffer::with_capacity(field.dtype, 0);
216 self.buffers.insert(field.name.clone(), buffer);
217 self.buffers.get_mut(&field.name).expect("buffer inserted")
218 }
219 };
220
221 match field.dtype {
222 DType::F32 | DType::F16 => buffer.as_f32_mut()?.push(value),
223 DType::F64 => match buffer {
224 PointBuffer::F64(values) => values.push(f64::from(value)),
225 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
226 },
227 DType::U8 => match buffer {
228 PointBuffer::U8(values) => values.push(value.round() as u8),
229 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
230 },
231 DType::U16 => match buffer {
232 PointBuffer::U16(values) => values.push(value.round() as u16),
233 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
234 },
235 DType::U32 => match buffer {
236 PointBuffer::U32(values) => values.push(value.round() as u32),
237 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
238 },
239 DType::I32 => match buffer {
240 PointBuffer::I32(values) => values.push(value.round() as i32),
241 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
242 },
243 }
244 Ok(())
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::{PointCloud, PointCloudBuilder};
251 use crate::{FieldSemantic, StandardSchemas};
252
253 #[test]
254 fn build_xyz_cloud() {
255 let mut builder = PointCloudBuilder::xyz();
256 builder.push_point([0.0, 0.0, 0.0]).unwrap();
257 builder.push_point([1.0, 0.0, 0.0]).unwrap();
258 let cloud = builder.build().unwrap();
259 assert_eq!(cloud.len(), 2);
260 assert!(cloud.validate().is_ok());
261 let x = cloud.field("x").unwrap().as_f32().unwrap();
262 assert_eq!(x, &[0.0, 1.0]);
263 }
264
265 #[test]
266 fn standard_xyzi_has_intensity() {
267 let schema = StandardSchemas::point_xyzi();
268 assert!(schema.find_semantic(FieldSemantic::Intensity).is_some());
269 }
270
271 #[test]
272 fn into_parts_roundtrips_without_copying_columns() {
273 let mut builder = PointCloudBuilder::xyz();
274 builder.push_point([1.0, 2.0, 3.0]).unwrap();
275 let cloud = builder.build().unwrap();
276 let (schema, buffers, metadata) = cloud.into_parts();
277 let rebuilt = PointCloud::try_from_parts(schema, buffers, metadata).unwrap();
278 assert_eq!(rebuilt.field("x").unwrap().as_f32().unwrap(), &[1.0]);
279 assert_eq!(rebuilt.len(), 1);
280 }
281}