spatialrust_interchange/
usd.rs1use spatialrust_scene::TriangleMesh;
8
9use crate::{InterchangeError, InterchangeResult};
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13pub struct UsdPrimPath(pub String);
14
15impl UsdPrimPath {
16 pub fn try_new(path: impl Into<String>) -> InterchangeResult<Self> {
18 let path = path.into();
19 if !path.starts_with('/') || path.len() < 2 {
20 return Err(InterchangeError::InvalidConfiguration(
21 "USD prim path must be absolute like /World/Mesh".into(),
22 ));
23 }
24 Ok(Self(path))
25 }
26
27 #[must_use]
29 pub fn leaf_name(&self) -> &str {
30 self.0.rsplit('/').next().unwrap_or(self.0.as_str())
31 }
32}
33
34#[derive(Clone, Debug, Default, PartialEq)]
36pub struct UsdStageDescription {
37 pub root_layer: String,
39 pub mesh_prims: Vec<UsdPrimPath>,
41}
42
43pub trait UsdStageAdapter {
45 fn declare_mesh(&mut self, path: UsdPrimPath, mesh: &TriangleMesh) -> InterchangeResult<()>;
47
48 fn description(&self) -> &UsdStageDescription;
50}
51
52#[derive(Clone, Debug, Default)]
54pub struct MemoryUsdStageAdapter {
55 description: UsdStageDescription,
56 meshes: Vec<(UsdPrimPath, TriangleMesh)>,
57}
58
59impl MemoryUsdStageAdapter {
60 #[must_use]
62 pub fn new(root_layer: impl Into<String>) -> Self {
63 Self {
64 description: UsdStageDescription {
65 root_layer: root_layer.into(),
66 mesh_prims: Vec::new(),
67 },
68 meshes: Vec::new(),
69 }
70 }
71
72 #[must_use]
74 pub fn meshes(&self) -> &[(UsdPrimPath, TriangleMesh)] {
75 &self.meshes
76 }
77
78 pub fn export_usda(&self) -> InterchangeResult<String> {
80 export_stage_usda(self)
81 }
82}
83
84impl UsdStageAdapter for MemoryUsdStageAdapter {
85 fn declare_mesh(&mut self, path: UsdPrimPath, mesh: &TriangleMesh) -> InterchangeResult<()> {
86 if mesh.is_empty() {
87 return Err(InterchangeError::InvalidConfiguration(
88 "cannot declare an empty mesh prim".into(),
89 ));
90 }
91 if mesh.positions.len() % 3 != 0 || mesh.indices.len() % 3 != 0 {
92 return Err(InterchangeError::InvalidConfiguration(
93 "mesh positions/indices must be multiples of 3".into(),
94 ));
95 }
96 self.description.mesh_prims.push(path.clone());
97 self.meshes.push((path, mesh.clone()));
98 Ok(())
99 }
100
101 fn description(&self) -> &UsdStageDescription {
102 &self.description
103 }
104}
105
106pub fn export_stage_usda(stage: &MemoryUsdStageAdapter) -> InterchangeResult<String> {
108 if stage.meshes.is_empty() {
109 return Err(InterchangeError::InvalidConfiguration(
110 "stage has no mesh prims to export".into(),
111 ));
112 }
113 let mut out =
114 String::from("#usda 1.0\n(\n defaultPrim = \"World\"\n)\n\ndef Xform \"World\"\n{\n");
115 for (path, mesh) in &stage.meshes {
116 out.push_str(&format!(" def Mesh \"{}\"\n {{\n", path.leaf_name()));
117 out.push_str(" point3f[] points = [");
118 for (i, chunk) in mesh.positions.chunks_exact(3).enumerate() {
119 if i > 0 {
120 out.push_str(", ");
121 }
122 out.push_str(&format!("({}, {}, {})", chunk[0], chunk[1], chunk[2]));
123 }
124 out.push_str("]\n");
125 let tri_count = mesh.indices.len() / 3;
126 out.push_str(" int[] faceVertexCounts = [");
127 for i in 0..tri_count {
128 if i > 0 {
129 out.push_str(", ");
130 }
131 out.push('3');
132 }
133 out.push_str("]\n");
134 out.push_str(" int[] faceVertexIndices = [");
135 for (i, idx) in mesh.indices.iter().enumerate() {
136 if i > 0 {
137 out.push_str(", ");
138 }
139 out.push_str(&idx.to_string());
140 }
141 out.push_str("]\n");
142 out.push_str(&format!(" custom string spatialrust:primPath = \"{}\"\n", path.0));
143 out.push_str(" }\n");
144 }
145 out.push_str("}\n");
146 Ok(out)
147}
148
149pub fn import_mesh_from_usda(usda: &str) -> InterchangeResult<(UsdPrimPath, TriangleMesh)> {
151 if !usda.contains("#usda") {
152 return Err(InterchangeError::InvalidConfiguration("missing USDA header".into()));
153 }
154 let path = extract_quoted_after(usda, "spatialrust:primPath = ").or_else(|_| {
155 let leaf = extract_mesh_leaf(usda)?;
156 UsdPrimPath::try_new(format!("/World/{leaf}"))
157 })?;
158 let points_blob = extract_bracket_list(usda, "point3f[] points = ")?;
159 let indices_blob = extract_bracket_list(usda, "int[] faceVertexIndices = ")?;
160 let mut positions = Vec::new();
161 for tok in points_blob.split(['(', ')', ',', ' ']).filter(|t| !t.is_empty()) {
162 let v: f32 = tok.parse().map_err(|_| {
163 InterchangeError::InvalidConfiguration(format!("bad point component `{tok}`"))
164 })?;
165 positions.push(v);
166 }
167 if positions.len() % 3 != 0 {
168 return Err(InterchangeError::InvalidConfiguration(
169 "points length must be a multiple of 3".into(),
170 ));
171 }
172 let mut indices = Vec::new();
173 for tok in indices_blob.split([',', ' ']).filter(|t| !t.is_empty()) {
174 let v: u32 = tok.parse().map_err(|_| {
175 InterchangeError::InvalidConfiguration(format!("bad face index `{tok}`"))
176 })?;
177 indices.push(v);
178 }
179 if indices.len() % 3 != 0 {
180 return Err(InterchangeError::InvalidConfiguration(
181 "faceVertexIndices length must be a multiple of 3".into(),
182 ));
183 }
184 Ok((path, TriangleMesh { positions, indices }))
185}
186
187fn extract_mesh_leaf(usda: &str) -> InterchangeResult<String> {
188 let key = "def Mesh \"";
189 let start = usda
190 .find(key)
191 .ok_or_else(|| InterchangeError::InvalidConfiguration("missing Mesh prim".into()))?
192 + key.len();
193 let end = usda[start..]
194 .find('"')
195 .ok_or_else(|| InterchangeError::InvalidConfiguration("unterminated Mesh name".into()))?
196 + start;
197 Ok(usda[start..end].to_string())
198}
199
200fn extract_quoted_after(usda: &str, marker: &str) -> InterchangeResult<UsdPrimPath> {
201 let start = usda
202 .find(marker)
203 .ok_or_else(|| InterchangeError::InvalidConfiguration(format!("missing {marker}")))?
204 + marker.len();
205 let rest = usda[start..].trim_start();
206 if !rest.starts_with('"') {
207 return Err(InterchangeError::InvalidConfiguration("expected quoted string".into()));
208 }
209 let end = rest[1..]
210 .find('"')
211 .ok_or_else(|| InterchangeError::InvalidConfiguration("unterminated string".into()))?
212 + 1;
213 UsdPrimPath::try_new(rest[1..end].to_string())
214}
215
216fn extract_bracket_list(usda: &str, marker: &str) -> InterchangeResult<String> {
217 let start = usda
218 .find(marker)
219 .ok_or_else(|| InterchangeError::InvalidConfiguration(format!("missing {marker}")))?
220 + marker.len();
221 let rest = &usda[start..];
222 let open = rest
223 .find('[')
224 .ok_or_else(|| InterchangeError::InvalidConfiguration("missing '['".into()))?;
225 let close = rest[open..]
226 .find(']')
227 .ok_or_else(|| InterchangeError::InvalidConfiguration("missing ']'".into()))?
228 + open;
229 Ok(rest[open + 1..close].to_string())
230}
231
232#[cfg(test)]
233mod tests {
234 use super::{
235 export_stage_usda, import_mesh_from_usda, MemoryUsdStageAdapter, UsdPrimPath,
236 UsdStageAdapter,
237 };
238 use spatialrust_scene::TriangleMesh;
239
240 #[test]
241 fn usda_roundtrip_preserves_mesh() {
242 let mut stage = MemoryUsdStageAdapter::new("scene.usda");
243 let mesh = TriangleMesh {
244 positions: vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
245 indices: vec![0, 1, 2],
246 };
247 stage.declare_mesh(UsdPrimPath::try_new("/World/Mesh").unwrap(), &mesh).unwrap();
248 let usda = export_stage_usda(&stage).unwrap();
249 assert!(usda.starts_with("#usda 1.0"));
250 let (path, imported) = import_mesh_from_usda(&usda).unwrap();
251 assert_eq!(path.0, "/World/Mesh");
252 assert_eq!(imported.positions, mesh.positions);
253 assert_eq!(imported.indices, mesh.indices);
254 }
255}