1use std::collections::HashMap;
2
3#[cfg(not(target_family = "wasm"))]
4pub use native::*;
5#[cfg(target_family = "wasm")]
6pub use wasm::*;
7
8pub(crate) fn replace_package_with_path(
9 filename: &str,
10 package_path: &HashMap<String, String>,
11) -> Option<String> {
12 let path = filename.strip_prefix("package://")?;
13 let (package_name, path) = path.split_once('/')?;
14 let package_path = package_path.get(package_name)?;
15 Some(format!(
16 "{}/{path}",
17 package_path.strip_suffix('/').unwrap_or(package_path),
18 ))
19}
20
21pub(crate) fn is_url(path: &str) -> bool {
22 path.starts_with("https://") || path.starts_with("http://")
23}
24
25#[cfg(not(target_family = "wasm"))]
26mod native {
27 use std::{
28 collections::HashMap,
29 ffi::OsStr,
30 fs, mem,
31 path::Path,
32 sync::{Arc, Mutex},
33 };
34
35 use tracing::error;
36
37 use crate::{utils::is_url, Result};
38
39 fn read_urdf(path: &str, xacro_args: &[(String, String)]) -> Result<(urdf_rs::Robot, String)> {
40 let urdf_text = if Path::new(path).extension().and_then(OsStr::to_str) == Some("xacro") {
41 urdf_rs::utils::convert_xacro_to_urdf_with_args(path, xacro_args)?
42 } else if is_url(path) {
43 ureq::get(path)
44 .call()
45 .map_err(|e| crate::Error::Other(e.to_string()))?
46 .into_string()?
47 } else {
48 fs::read_to_string(path)?
49 };
50 let robot = urdf_rs::read_from_string(&urdf_text)?;
51 Ok((robot, urdf_text))
52 }
53
54 #[derive(Debug)]
55 pub struct RobotModel {
56 pub(crate) path: String,
57 pub(crate) urdf_text: Arc<Mutex<String>>,
58 robot: urdf_rs::Robot,
59 package_path: HashMap<String, String>,
60 xacro_args: Vec<(String, String)>,
61 }
62
63 impl RobotModel {
64 pub fn new(
65 path: impl Into<String>,
66 package_path: HashMap<String, String>,
67 xacro_args: &[(String, String)],
68 ) -> Result<Self> {
69 let path = path.into();
70 let (robot, urdf_text) = read_urdf(&path, xacro_args)?;
71 Ok(Self {
72 path,
73 urdf_text: Arc::new(Mutex::new(urdf_text)),
74 robot,
75 package_path,
76 xacro_args: xacro_args.to_owned(),
77 })
78 }
79
80 pub async fn from_text(
81 path: impl Into<String>,
82 urdf_text: impl Into<String>,
83 package_path: HashMap<String, String>,
84 ) -> Result<Self> {
85 let path = path.into();
86 let urdf_text = urdf_text.into();
87 let robot = urdf_rs::read_from_string(&urdf_text)?;
88 Ok(Self {
89 path,
90 urdf_text: Arc::new(Mutex::new(urdf_text)),
91 robot,
92 package_path,
93 xacro_args: Vec::new(),
94 })
95 }
96
97 pub(crate) fn get(&mut self) -> &urdf_rs::Robot {
98 &self.robot
99 }
100
101 pub(crate) fn reload(&mut self) {
102 match read_urdf(&self.path, &self.xacro_args) {
103 Ok((robot, text)) => {
104 self.robot = robot;
105 *self.urdf_text.lock().unwrap() = text;
106 }
107 Err(e) => {
108 error!("{e}");
109 }
110 }
111 }
112
113 pub(crate) fn take_package_path_map(&mut self) -> HashMap<String, String> {
114 mem::take(&mut self.package_path)
115 }
116 }
117
118 #[cfg(feature = "assimp")]
119 pub(crate) fn fetch_tempfile(url: &str) -> Result<tempfile::NamedTempFile> {
121 use std::io::{Read, Write};
122
123 const RESPONSE_SIZE_LIMIT: usize = 10 * 1_024 * 1_024;
124
125 let mut buf: Vec<u8> = vec![];
126 ureq::get(url)
127 .call()
128 .map_err(|e| crate::Error::Other(e.to_string()))?
129 .into_reader()
130 .take((RESPONSE_SIZE_LIMIT + 1) as u64)
131 .read_to_end(&mut buf)?;
132 if buf.len() > RESPONSE_SIZE_LIMIT {
133 return Err(crate::Error::Other(format!("{url} is too big")));
134 }
135 let mut file = tempfile::NamedTempFile::new()?;
136 file.write_all(&buf)?;
137 Ok(file)
138 }
139}
140
141#[cfg(target_family = "wasm")]
142mod wasm {
143 use std::{
144 collections::HashMap,
145 mem,
146 path::Path,
147 str,
148 sync::{Arc, Mutex},
149 };
150
151 use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
152 use js_sys::Uint8Array;
153 use serde::{Deserialize, Serialize};
154 use tracing::debug;
155 use wasm_bindgen::JsCast;
156 use wasm_bindgen_futures::JsFuture;
157 use web_sys::Response;
158
159 use crate::{utils::is_url, Error, Result};
160
161 #[derive(Serialize, Deserialize)]
162 pub(crate) struct Mesh {
163 pub(crate) path: String,
164 data: MeshData,
165 }
166
167 impl Mesh {
168 pub(crate) fn decode(data: &str) -> Result<Self> {
169 let mut mesh: Self = serde_json::from_str(data).map_err(|e| e.to_string())?;
170 match &mesh.data {
171 MeshData::None => {}
172 MeshData::Base64(s) => {
173 mesh.data = MeshData::Bytes(BASE64.decode(s).map_err(|e| e.to_string())?);
174 }
175 MeshData::Bytes(_) => unreachable!(),
176 }
177 Ok(mesh)
178 }
179
180 pub(crate) fn bytes(&self) -> Option<&[u8]> {
181 match &self.data {
182 MeshData::None => None,
183 MeshData::Bytes(bytes) => Some(bytes),
184 MeshData::Base64(_) => unreachable!(),
185 }
186 }
187 }
188
189 #[derive(Serialize, Deserialize)]
190 enum MeshData {
191 Base64(String),
192 Bytes(Vec<u8>),
193 None,
194 }
195
196 pub fn window() -> Result<web_sys::Window> {
197 Ok(web_sys::window().ok_or("failed to get window")?)
198 }
199
200 async fn fetch(input_file: &str) -> Result<Response> {
201 let promise = window()?.fetch_with_str(input_file);
202
203 let response = JsFuture::from(promise)
204 .await?
205 .dyn_into::<Response>()
206 .unwrap();
207
208 Ok(response)
209 }
210
211 pub async fn read_to_string(input_file: impl AsRef<str>) -> Result<String> {
212 let promise = fetch(input_file.as_ref()).await?.text()?;
213
214 let s = JsFuture::from(promise).await?;
215
216 Ok(s.as_string()
217 .ok_or_else(|| format!("{} is not string", input_file.as_ref()))?)
218 }
219
220 pub async fn read(input_file: impl AsRef<str>) -> Result<Vec<u8>> {
221 let promise = fetch(input_file.as_ref()).await?.array_buffer()?;
222
223 let bytes = JsFuture::from(promise).await?;
224
225 Ok(Uint8Array::new(&bytes).to_vec())
226 }
227
228 pub async fn load_mesh(
229 robot: &mut urdf_rs::Robot,
230 urdf_path: impl AsRef<Path>,
231 package_path: &HashMap<String, String>,
232 ) -> Result<()> {
233 let urdf_path = urdf_path.as_ref();
234 for geometry in robot.links.iter_mut().flat_map(|link| {
235 link.visual
236 .iter_mut()
237 .map(|v| &mut v.geometry)
238 .chain(link.collision.iter_mut().map(|c| &mut c.geometry))
239 }) {
240 if let urdf_rs::Geometry::Mesh { filename, .. } = geometry {
241 let input_file = if is_url(filename) {
242 filename.clone()
243 } else if filename.starts_with("package://") {
244 crate::utils::replace_package_with_path(filename, package_path).ok_or_else(||
245 format!(
246 "ros package ({filename}) is not supported in wasm; consider using `package-path[]` URL parameter",
247 ))?
248 } else if filename.starts_with("file://") {
249 return Err(Error::from(format!(
250 "local file ({filename}) is not supported in wasm",
251 )));
252 } else {
253 urdf_path
258 .with_file_name(&filename)
259 .to_str()
260 .unwrap()
261 .to_string()
262 };
263
264 debug!("loading {input_file}");
265 let data = MeshData::Base64(BASE64.encode(read(&input_file).await?));
266
267 let new = serde_json::to_string(&Mesh {
268 path: filename.clone(),
269 data,
270 })
271 .unwrap();
272 *filename = new;
273 }
274 }
275 Ok(())
276 }
277
278 #[derive(Debug)]
279 pub struct RobotModel {
280 pub(crate) path: String,
281 pub(crate) urdf_text: Arc<Mutex<String>>,
282 robot: urdf_rs::Robot,
283 package_path: HashMap<String, String>,
284 }
285
286 impl RobotModel {
287 pub async fn new(
288 path: impl Into<String>,
289 package_path: HashMap<String, String>,
290 ) -> Result<Self> {
291 let path = path.into();
292 let urdf_text = read_to_string(&path).await?;
293 Self::from_text(path, urdf_text, package_path).await
294 }
295
296 pub async fn from_text(
297 path: impl Into<String>,
298 urdf_text: impl Into<String>,
299 package_path: HashMap<String, String>,
300 ) -> Result<Self> {
301 let path = path.into();
302 let urdf_text = urdf_text.into();
303 let mut robot = urdf_rs::read_from_string(&urdf_text)?;
304 load_mesh(&mut robot, &path, &package_path).await?;
305 Ok(Self {
306 path,
307 urdf_text: Arc::new(Mutex::new(urdf_text)),
308 robot,
309 package_path,
310 })
311 }
312
313 pub(crate) fn get(&mut self) -> &urdf_rs::Robot {
314 &self.robot
315 }
316
317 pub(crate) fn take_package_path_map(&mut self) -> HashMap<String, String> {
318 mem::take(&mut self.package_path)
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_replace_package_with_path() {
329 let mut package_path = HashMap::new();
330 package_path.insert("a".to_owned(), "path".to_owned());
331 assert_eq!(
332 replace_package_with_path("package://a/b/c", &package_path),
333 Some("path/b/c".to_owned())
334 );
335 assert_eq!(
336 replace_package_with_path("package://a", &package_path),
337 None
338 );
339 assert_eq!(
340 replace_package_with_path("package://b/b/c", &package_path),
341 None
342 );
343 assert_eq!(replace_package_with_path("a", &package_path), None);
344 }
345}