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
146impl PointCloudBuilder {
147 #[must_use]
149 pub fn new(schema: PointSchema) -> Self {
150 Self { schema, ..Self::default() }
151 }
152
153 #[must_use]
155 pub fn xyz() -> Self {
156 Self::new(StandardSchemas::point_xyz())
157 }
158
159 #[must_use]
161 pub fn metadata(mut self, metadata: SpatialMetadata) -> Self {
162 self.metadata = metadata;
163 self
164 }
165
166 pub fn push_point<I>(&mut self, values: I) -> SpatialResult<()>
170 where
171 I: IntoIterator<Item = f32>,
172 {
173 let values: Vec<f32> = values.into_iter().collect();
174 if values.len() != self.schema.len() {
175 return Err(SpatialError::InvalidArgument(format!(
176 "expected {} field values, got {}",
177 self.schema.len(),
178 values.len()
179 )));
180 }
181
182 let fields: Vec<PointField> = self.schema.fields().to_vec();
183 for (field, value) in fields.iter().zip(values) {
184 self.push_scalar(field, value)?;
185 }
186 Ok(())
187 }
188
189 pub fn build(self) -> SpatialResult<PointCloud> {
191 let len = self
192 .schema
193 .fields()
194 .first()
195 .and_then(|field| self.buffers.get(&field.name))
196 .map(|buffer| buffer.len())
197 .unwrap_or(0);
198 PointCloud::from_builder(self, len)
199 }
200
201 fn push_scalar(&mut self, field: &PointField, value: f32) -> SpatialResult<()> {
202 let buffer = match self.buffers.get_mut(&field.name) {
203 Some(buffer) => buffer,
204 None => {
205 let buffer = PointBuffer::with_capacity(field.dtype, 0);
206 self.buffers.insert(field.name.clone(), buffer);
207 self.buffers.get_mut(&field.name).expect("buffer inserted")
208 }
209 };
210
211 match field.dtype {
212 DType::F32 | DType::F16 => buffer.as_f32_mut()?.push(value),
213 DType::F64 => match buffer {
214 PointBuffer::F64(values) => values.push(f64::from(value)),
215 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
216 },
217 DType::U8 => match buffer {
218 PointBuffer::U8(values) => values.push(value.round() as u8),
219 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
220 },
221 DType::U16 => match buffer {
222 PointBuffer::U16(values) => values.push(value.round() as u16),
223 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
224 },
225 DType::U32 => match buffer {
226 PointBuffer::U32(values) => values.push(value.round() as u32),
227 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
228 },
229 DType::I32 => match buffer {
230 PointBuffer::I32(values) => values.push(value.round() as i32),
231 _ => return Err(SpatialError::UnsupportedDType(field.dtype)),
232 },
233 }
234 Ok(())
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::PointCloudBuilder;
241 use crate::{FieldSemantic, StandardSchemas};
242
243 #[test]
244 fn build_xyz_cloud() {
245 let mut builder = PointCloudBuilder::xyz();
246 builder.push_point([0.0, 0.0, 0.0]).unwrap();
247 builder.push_point([1.0, 0.0, 0.0]).unwrap();
248 let cloud = builder.build().unwrap();
249 assert_eq!(cloud.len(), 2);
250 assert!(cloud.validate().is_ok());
251 let x = cloud.field("x").unwrap().as_f32().unwrap();
252 assert_eq!(x, &[0.0, 1.0]);
253 }
254
255 #[test]
256 fn standard_xyzi_has_intensity() {
257 let schema = StandardSchemas::point_xyzi();
258 assert!(schema.find_semantic(FieldSemantic::Intensity).is_some());
259 }
260}