1use crate::{SpatialError, SpatialResult};
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub enum DType {
7 F32,
9 F64,
11 U8,
13 U16,
15 U32,
17 I32,
19 F16,
21}
22
23impl DType {
24 #[must_use]
26 pub const fn size_bytes(self) -> usize {
27 match self {
28 Self::F32 => 4,
29 Self::F64 => 8,
30 Self::U8 => 1,
31 Self::U16 => 2,
32 Self::U32 | Self::I32 => 4,
33 Self::F16 => 2,
34 }
35 }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub enum FieldSemantic {
42 PositionX,
44 PositionY,
46 PositionZ,
48 NormalX,
50 NormalY,
52 NormalZ,
54 Intensity,
56 ColorR,
58 ColorG,
60 ColorB,
62 Curvature,
64 Ring,
66 TimeOffset,
68 Label,
70 Embedding,
72 Unknown,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct PointField {
80 pub name: String,
82 pub semantic: FieldSemantic,
84 pub dtype: DType,
86 pub components: usize,
88}
89
90impl PointField {
91 #[must_use]
93 pub fn scalar(name: impl Into<String>, semantic: FieldSemantic, dtype: DType) -> Self {
94 Self { name: name.into(), semantic, dtype, components: 1 }
95 }
96
97 #[must_use]
99 pub fn byte_size(&self) -> usize {
100 self.dtype.size_bytes() * self.components
101 }
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Eq)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107pub struct PointSchema {
108 fields: Vec<PointField>,
109}
110
111impl PointSchema {
112 #[must_use]
114 pub fn new() -> Self {
115 Self::default()
116 }
117
118 pub fn with_field(mut self, field: PointField) -> Self {
120 self.fields.push(field);
121 self
122 }
123
124 #[must_use]
126 pub fn fields(&self) -> &[PointField] {
127 &self.fields
128 }
129
130 #[must_use]
132 pub fn len(&self) -> usize {
133 self.fields.len()
134 }
135
136 #[must_use]
138 pub fn is_empty(&self) -> bool {
139 self.fields.is_empty()
140 }
141
142 #[must_use]
144 pub fn find_semantic(&self, semantic: FieldSemantic) -> Option<&PointField> {
145 self.fields.iter().find(|field| field.semantic == semantic)
146 }
147
148 pub fn validate_positions(&self) -> SpatialResult<()> {
150 for semantic in
151 [FieldSemantic::PositionX, FieldSemantic::PositionY, FieldSemantic::PositionZ]
152 {
153 let count = self.fields.iter().filter(|field| field.semantic == semantic).count();
154 if count != 1 {
155 return Err(SpatialError::SchemaValidation(format!(
156 "expected exactly one {semantic:?} field, found {count}"
157 )));
158 }
159 }
160 Ok(())
161 }
162}
163
164pub struct StandardSchemas;
166
167impl StandardSchemas {
168 #[must_use]
170 pub fn point_xyz() -> PointSchema {
171 PointSchema::new()
172 .with_field(PointField::scalar("x", FieldSemantic::PositionX, DType::F32))
173 .with_field(PointField::scalar("y", FieldSemantic::PositionY, DType::F32))
174 .with_field(PointField::scalar("z", FieldSemantic::PositionZ, DType::F32))
175 }
176
177 #[must_use]
179 pub fn point_xyzi() -> PointSchema {
180 Self::point_xyz().with_field(PointField::scalar(
181 "intensity",
182 FieldSemantic::Intensity,
183 DType::F32,
184 ))
185 }
186
187 #[must_use]
189 pub fn point_xyzrgb() -> PointSchema {
190 Self::point_xyz()
191 .with_field(PointField::scalar("r", FieldSemantic::ColorR, DType::U8))
192 .with_field(PointField::scalar("g", FieldSemantic::ColorG, DType::U8))
193 .with_field(PointField::scalar("b", FieldSemantic::ColorB, DType::U8))
194 }
195
196 #[must_use]
198 pub fn point_xyzirgb() -> PointSchema {
199 Self::point_xyzi()
200 .with_field(PointField::scalar("r", FieldSemantic::ColorR, DType::U8))
201 .with_field(PointField::scalar("g", FieldSemantic::ColorG, DType::U8))
202 .with_field(PointField::scalar("b", FieldSemantic::ColorB, DType::U8))
203 }
204
205 #[must_use]
207 pub fn point_xyzinormal() -> PointSchema {
208 Self::point_xyzi()
209 .with_field(PointField::scalar("normal_x", FieldSemantic::NormalX, DType::F32))
210 .with_field(PointField::scalar("normal_y", FieldSemantic::NormalY, DType::F32))
211 .with_field(PointField::scalar("normal_z", FieldSemantic::NormalZ, DType::F32))
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::{FieldSemantic, StandardSchemas};
218
219 #[test]
220 fn standard_xyz_schema_validates() {
221 let schema = StandardSchemas::point_xyz();
222 schema.validate_positions().unwrap();
223 assert_eq!(schema.len(), 3);
224 assert!(schema.find_semantic(FieldSemantic::PositionX).is_some());
225 }
226
227 #[test]
228 fn standard_xyzirgb_has_intensity_and_color() {
229 let schema = StandardSchemas::point_xyzirgb();
230 assert!(schema.find_semantic(FieldSemantic::Intensity).is_some());
231 assert!(schema.find_semantic(FieldSemantic::ColorR).is_some());
232 assert_eq!(schema.len(), 7);
233 }
234}