1use serde_json::Value;
24
25use crate::error::DecodeError;
26use crate::json;
27use crate::math::Vec3;
28
29pub const ATTRIBUTE: &str = "usd::usdgeom::basiscurves";
31
32#[derive(Debug, Clone, PartialEq)]
34pub struct Polyline {
35 pub points: Vec<Vec3>,
37 pub closed: bool,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct UnsupportedCurve {
44 pub curve_type: String,
46 pub basis: Option<String>,
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub enum CurveGeometry {
53 Polylines(Vec<Polyline>),
55 Unsupported(UnsupportedCurve),
58}
59
60impl CurveGeometry {
61 pub fn from_attribute(value: &Value) -> Result<Self, DecodeError> {
75 let object = json::object(value, ATTRIBUTE)?;
76 let curve_type = json::optional_str(object, "type")?;
77 let basis = json::optional_str(object, "basis")?;
78 let wrap = json::optional_str(object, "wrap")?;
79 let points = json::points3(json::field(object, "points")?, "points")?;
80
81 if let Some(t) = curve_type.filter(|t| *t != "linear") {
82 return Ok(Self::Unsupported(UnsupportedCurve {
83 curve_type: t.to_owned(),
84 basis: basis.map(str::to_owned),
85 }));
86 }
87 let closed = match wrap {
88 None | Some("nonperiodic") | Some("pinned") => false,
89 Some("periodic") => true,
90 Some(_) => {
91 return Err(DecodeError::WrongShape {
92 at: "wrap".to_owned(),
93 expected: "\"nonperiodic\", \"periodic\", or \"pinned\"",
94 })
95 }
96 };
97
98 let counts = match object.get("curveVertexCounts") {
99 Some(v) => json::counts(v, "curveVertexCounts")?,
100 None if points.is_empty() => Vec::new(),
101 None => vec![points.len() as u64],
102 };
103 let counted = counts.iter().try_fold(0u64, |sum, &n| sum.checked_add(n));
104 if counted != Some(points.len() as u64) {
105 return Err(DecodeError::CountMismatch {
106 field: "curveVertexCounts",
107 counted: counted.unwrap_or(u64::MAX),
108 available: points.len(),
109 });
110 }
111
112 let mut polylines = Vec::with_capacity(counts.len());
113 let mut rest = points.as_slice();
114 for (curve, &n) in counts.iter().enumerate() {
115 if n < 2 {
116 return Err(DecodeError::CurveTooShort {
117 curve,
118 vertex_count: n,
119 });
120 }
121 let (head, tail) = rest.split_at(n as usize);
123 rest = tail;
124 polylines.push(Polyline {
125 points: head.to_vec(),
126 closed,
127 });
128 }
129 Ok(Self::Polylines(polylines))
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use serde_json::json;
137
138 fn polylines(v: Value) -> Vec<Polyline> {
139 match CurveGeometry::from_attribute(&v).unwrap() {
140 CurveGeometry::Polylines(p) => p,
141 other => panic!("expected polylines, got {other:?}"),
142 }
143 }
144
145 #[test]
146 fn linear_curve_yields_its_vertices() {
147 let lines = polylines(json!({"points": [[0, 0, 0], [10, 0, 0], [10, 0.5, 3]]}));
148 assert_eq!(
149 lines,
150 vec![Polyline {
151 points: vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 0.5, 3.0]],
152 closed: false
153 }]
154 );
155 }
156
157 #[test]
158 fn explicit_linear_type_is_accepted() {
159 let lines = polylines(json!({
160 "type": "linear", "basis": "bezier", "points": [[0, 0, 0], [1, 0, 0]]
161 }));
162 assert_eq!(lines.len(), 1);
163 }
164
165 #[test]
166 fn curve_vertex_counts_split_curves() {
167 let lines = polylines(json!({
168 "points": [[0, 0, 0], [1, 0, 0], [2, 0, 0], [0, 5, 0], [0, 5, 3]],
169 "curveVertexCounts": [3, 2]
170 }));
171 assert_eq!(lines.len(), 2);
172 assert_eq!(lines[0].points.len(), 3);
173 assert_eq!(lines[1].points, vec![[0.0, 5.0, 0.0], [0.0, 5.0, 3.0]]);
174 }
175
176 #[test]
177 fn no_points_means_no_curves() {
178 assert!(polylines(json!({"points": []})).is_empty());
179 }
180
181 #[test]
182 fn periodic_wrap_closes() {
183 let lines = polylines(json!({
184 "wrap": "periodic", "points": [[0, 0, 0], [1, 0, 0], [0, 1, 0]]
185 }));
186 assert!(lines[0].closed);
187 }
188
189 #[test]
190 fn non_linear_types_are_unsupported() {
191 let g = CurveGeometry::from_attribute(&json!({
192 "type": "cubic", "basis": "bspline",
193 "points": [[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]]
194 }))
195 .unwrap();
196 assert_eq!(
197 g,
198 CurveGeometry::Unsupported(UnsupportedCurve {
199 curve_type: "cubic".into(),
200 basis: Some("bspline".into())
201 })
202 );
203 }
204
205 #[test]
206 fn malformed_curves_are_errors() {
207 let err = |v: Value| CurveGeometry::from_attribute(&v).unwrap_err();
208 assert_eq!(
209 err(json!({"curveVertexCounts": [2]})),
210 DecodeError::MissingField { field: "points" }
211 );
212 assert_eq!(
213 err(json!({"points": [[0, 0, 0], [1, 0, 0]], "curveVertexCounts": [3]})),
214 DecodeError::CountMismatch {
215 field: "curveVertexCounts",
216 counted: 3,
217 available: 2
218 }
219 );
220 assert_eq!(
221 err(json!({"points": [[0, 0, 0], [1, 0, 0], [2, 0, 0]], "curveVertexCounts": [2, 1]})),
222 DecodeError::CurveTooShort {
223 curve: 1,
224 vertex_count: 1
225 }
226 );
227 assert_eq!(
228 err(json!({"points": [[0, 0, 0]]})),
229 DecodeError::CurveTooShort {
230 curve: 0,
231 vertex_count: 1
232 }
233 );
234 assert_eq!(
235 err(json!({"points": [[0, 0, 0], [1, 0]]})),
236 DecodeError::WrongShape {
237 at: "points[1]".into(),
238 expected: "array of 3 numbers"
239 }
240 );
241 assert!(matches!(
242 err(json!({"type": 3, "points": []})),
243 DecodeError::WrongShape { .. }
244 ));
245 assert!(matches!(
246 err(json!({"wrap": "spiral", "points": [[0, 0, 0], [1, 0, 0]]})),
247 DecodeError::WrongShape { .. }
248 ));
249 }
250}