mujoco-react 8.1.0 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mujoco-react",
3
- "version": "8.1.0",
3
+ "version": "8.1.1",
4
4
  "description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -137,6 +137,17 @@ function sceneObjectToXml(obj: SceneObject): string {
137
137
  return `<body name="${obj.name}" pos="${pos}">${joint}<geom type="${obj.type}" size="${size}" rgba="${rgba}" contype="1" conaffinity="1"${mass}${friction}${solref}${solimp}${condim}/></body>`;
138
138
  }
139
139
 
140
+ /** Create virtual directory structure for a file path. */
141
+ function ensureDir(mujoco: MujocoModule, fname: string) {
142
+ const dirParts = fname.split('/');
143
+ dirParts.pop();
144
+ let currentPath = '/working';
145
+ for (const part of dirParts) {
146
+ currentPath += '/' + part;
147
+ try { mujoco.FS.mkdir(currentPath); } catch { /* ignore */ }
148
+ }
149
+ }
150
+
140
151
  interface LoadResult {
141
152
  mjModel: MujocoModel;
142
153
  mjData: MujocoData;
@@ -157,15 +168,22 @@ export async function loadScene(
157
168
  const baseUrl = config.src.endsWith('/') ? config.src : config.src + '/';
158
169
 
159
170
  const downloaded = new Set<string>();
160
- const queue: string[] = [config.sceneFile];
171
+ const xmlQueue: string[] = [config.sceneFile];
172
+ const assetFiles: string[] = [];
161
173
  const parser = new DOMParser();
162
174
 
163
- // 2. Download all model files
164
- while (queue.length > 0) {
165
- const fname = queue.shift()!;
175
+ // 2a. Download XML files sequentially (to discover dependencies)
176
+ while (xmlQueue.length > 0) {
177
+ const fname = xmlQueue.shift()!;
166
178
  if (downloaded.has(fname)) continue;
167
179
  downloaded.add(fname);
168
180
 
181
+ if (!fname.endsWith('.xml')) {
182
+ // Non-XML discovered during XML scan — collect for parallel download
183
+ assetFiles.push(fname);
184
+ continue;
185
+ }
186
+
169
187
  onProgress?.(`Downloading ${fname}...`);
170
188
 
171
189
  const res = await fetch(baseUrl + fname);
@@ -174,61 +192,69 @@ export async function loadScene(
174
192
  continue;
175
193
  }
176
194
 
177
- // Create virtual directory structure
178
- const dirParts = fname.split('/');
179
- dirParts.pop();
180
- let currentPath = '/working';
181
- for (const part of dirParts) {
182
- currentPath += '/' + part;
183
- try { mujoco.FS.mkdir(currentPath); } catch { /* ignore */ }
184
- }
185
-
186
- if (fname.endsWith('.xml')) {
187
- let text = await res.text();
188
-
189
- // 3. Apply XML patches from config
190
- for (const patch of config.xmlPatches ?? []) {
191
- if (fname.endsWith(patch.target) || fname === patch.target) {
192
- if (patch.replace) {
193
- const [from, to] = patch.replace;
194
- if (text.includes(from)) {
195
- text = text.replace(from, to);
196
- } else {
197
- const preview = from.length > 80 ? `${from.slice(0, 80)}...` : from;
198
- console.warn(`XML patch replace pattern not found in ${fname}: "${preview}"`);
199
- }
195
+ let text = await res.text();
196
+
197
+ // 3. Apply XML patches from config
198
+ for (const patch of config.xmlPatches ?? []) {
199
+ if (fname.endsWith(patch.target) || fname === patch.target) {
200
+ if (patch.replace) {
201
+ const [from, to] = patch.replace;
202
+ if (text.includes(from)) {
203
+ text = text.replace(from, to);
204
+ } else {
205
+ const preview = from.length > 80 ? `${from.slice(0, 80)}...` : from;
206
+ console.warn(`XML patch replace pattern not found in ${fname}: "${preview}"`);
200
207
  }
201
- if (patch.inject && patch.injectAfter) {
202
- const idx = text.indexOf(patch.injectAfter);
203
- if (idx !== -1) {
204
- // Find the end of the opening tag (next '>') after the match
205
- const tagEnd = text.indexOf('>', idx + patch.injectAfter.length);
206
- if (tagEnd !== -1) {
207
- text = text.slice(0, tagEnd + 1) + patch.inject + text.slice(tagEnd + 1);
208
- } else {
209
- console.warn(`XML patch inject failed in ${fname}: could not find tag end after "${patch.injectAfter}"`);
210
- }
208
+ }
209
+ if (patch.inject && patch.injectAfter) {
210
+ const idx = text.indexOf(patch.injectAfter);
211
+ if (idx !== -1) {
212
+ const tagEnd = text.indexOf('>', idx + patch.injectAfter.length);
213
+ if (tagEnd !== -1) {
214
+ text = text.slice(0, tagEnd + 1) + patch.inject + text.slice(tagEnd + 1);
211
215
  } else {
212
- const preview = patch.injectAfter.length > 80
213
- ? `${patch.injectAfter.slice(0, 80)}...`
214
- : patch.injectAfter;
215
- console.warn(`XML patch inject anchor not found in ${fname}: "${preview}"`);
216
+ console.warn(`XML patch inject failed in ${fname}: could not find tag end after "${patch.injectAfter}"`);
216
217
  }
218
+ } else {
219
+ const preview = patch.injectAfter.length > 80
220
+ ? `${patch.injectAfter.slice(0, 80)}...`
221
+ : patch.injectAfter;
222
+ console.warn(`XML patch inject anchor not found in ${fname}: "${preview}"`);
217
223
  }
218
224
  }
219
225
  }
226
+ }
220
227
 
221
- // 4. Inject scene objects into the scene file
222
- if (fname === config.sceneFile && config.sceneObjects?.length) {
223
- const xml = config.sceneObjects.map((obj) => sceneObjectToXml(obj)).join('');
224
- text = text.replace('</worldbody>', xml + '</worldbody>');
225
- }
228
+ // 4. Inject scene objects into the scene file
229
+ if (fname === config.sceneFile && config.sceneObjects?.length) {
230
+ const xml = config.sceneObjects.map((obj) => sceneObjectToXml(obj)).join('');
231
+ text = text.replace('</worldbody>', xml + '</worldbody>');
232
+ }
226
233
 
227
- mujoco.FS.writeFile(`/working/${fname}`, text);
228
- scanDependencies(text, fname, parser, downloaded, queue);
229
- } else {
230
- const buffer = new Uint8Array(await res.arrayBuffer());
231
- mujoco.FS.writeFile(`/working/${fname}`, buffer);
234
+ ensureDir(mujoco, fname);
235
+ mujoco.FS.writeFile(`/working/${fname}`, text);
236
+ scanDependencies(text, fname, parser, downloaded, xmlQueue);
237
+ }
238
+
239
+ // 2b. Download all binary assets (meshes, textures) in parallel
240
+ if (assetFiles.length > 0) {
241
+ onProgress?.(`Downloading ${assetFiles.length} assets...`);
242
+
243
+ const results = await Promise.all(
244
+ assetFiles.map(async (fname) => {
245
+ const res = await fetch(baseUrl + fname);
246
+ if (!res.ok) {
247
+ console.warn(`Failed to fetch ${fname}: ${res.status} ${res.statusText}`);
248
+ return null;
249
+ }
250
+ return { fname, buffer: new Uint8Array(await res.arrayBuffer()) };
251
+ })
252
+ );
253
+
254
+ for (const result of results) {
255
+ if (!result) continue;
256
+ ensureDir(mujoco, result.fname);
257
+ mujoco.FS.writeFile(`/working/${result.fname}`, result.buffer);
232
258
  }
233
259
  }
234
260