mujoco-react 8.2.1 → 8.4.0
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/README.md +129 -7
- package/dist/index.d.ts +93 -8
- package/dist/index.js +484 -87
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/ContactMarkers.tsx +14 -12
- package/src/components/Debug.tsx +18 -16
- package/src/components/FlexRenderer.tsx +1 -1
- package/src/components/TendonRenderer.tsx +1 -1
- package/src/core/GenericIK.ts +13 -10
- package/src/core/MujocoProvider.tsx +93 -8
- package/src/core/MujocoSimProvider.tsx +63 -18
- package/src/core/SceneLoader.ts +354 -2
- package/src/hooks/useContacts.ts +20 -18
- package/src/hooks/useIkController.ts +30 -11
- package/src/index.ts +10 -0
- package/src/rendering/GeomBuilder.ts +1 -1
- package/src/types.ts +94 -7
- package/src/wasm-url.d.ts +4 -0
package/src/core/SceneLoader.ts
CHANGED
|
@@ -3,9 +3,27 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import type {
|
|
7
|
+
ActuatedJointInfo,
|
|
8
|
+
ActuatorInfo,
|
|
9
|
+
ControlGroupInfo,
|
|
10
|
+
ControlGroupSelector,
|
|
11
|
+
ControlJointInfo,
|
|
12
|
+
JointInfo,
|
|
13
|
+
MujocoData,
|
|
14
|
+
MujocoModel,
|
|
15
|
+
MujocoModule,
|
|
16
|
+
ResourceSelector,
|
|
17
|
+
} from '../types';
|
|
7
18
|
import { SceneConfig, SceneObject, XmlPatch } from '../types';
|
|
8
19
|
|
|
20
|
+
const JOINT_TYPE_NAMES: Record<number, string> = {
|
|
21
|
+
0: 'free',
|
|
22
|
+
1: 'ball',
|
|
23
|
+
2: 'slide',
|
|
24
|
+
3: 'hinge',
|
|
25
|
+
};
|
|
26
|
+
|
|
9
27
|
/**
|
|
10
28
|
* Reads a null-terminated C string from MuJoCo's WASM memory.
|
|
11
29
|
*/
|
|
@@ -120,6 +138,330 @@ export function getActuatedScalarQposAdr(mjModel: MujocoModel, actuatorId: numbe
|
|
|
120
138
|
return mjModel.jnt_qposadr[jointId];
|
|
121
139
|
}
|
|
122
140
|
|
|
141
|
+
function getScalarJointDim(jointType: number): 0 | 1 {
|
|
142
|
+
return jointType === 2 || jointType === 3 ? 1 : 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function unlimitedRange(): [number, number] {
|
|
146
|
+
return [-Infinity, Infinity];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isScalarJoint(mjModel: MujocoModel, jointId: number): boolean {
|
|
150
|
+
return jointId >= 0 && jointId < mjModel.njnt && getScalarJointDim(mjModel.jnt_type[jointId]) === 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getActuatorJointId(mjModel: MujocoModel, actuatorId: number): number {
|
|
154
|
+
if (actuatorId < 0 || actuatorId >= mjModel.nu) return -1;
|
|
155
|
+
const trnType = mjModel.actuator_trntype?.[actuatorId];
|
|
156
|
+
if (trnType !== undefined && trnType !== 0 && trnType !== 1) return -1;
|
|
157
|
+
const jointId = mjModel.actuator_trnid[2 * actuatorId];
|
|
158
|
+
return isScalarJoint(mjModel, jointId) ? jointId : -1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getJointInfo(mjModel: MujocoModel, jointId: number): JointInfo {
|
|
162
|
+
const type = mjModel.jnt_type[jointId];
|
|
163
|
+
const range: [number, number] = [mjModel.jnt_range[2 * jointId], mjModel.jnt_range[2 * jointId + 1]];
|
|
164
|
+
return {
|
|
165
|
+
id: jointId,
|
|
166
|
+
name: getName(mjModel, mjModel.name_jntadr[jointId]),
|
|
167
|
+
type,
|
|
168
|
+
typeName: JOINT_TYPE_NAMES[type] ?? `unknown(${type})`,
|
|
169
|
+
range,
|
|
170
|
+
limited: range[0] < range[1],
|
|
171
|
+
bodyId: mjModel.jnt_bodyid[jointId],
|
|
172
|
+
qposAdr: mjModel.jnt_qposadr[jointId],
|
|
173
|
+
dofAdr: mjModel.jnt_dofadr[jointId],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getActuatorInfo(mjModel: MujocoModel, actuatorId: number): ActuatorInfo {
|
|
178
|
+
const hasRange = mjModel.actuator_ctrlrange[2 * actuatorId] < mjModel.actuator_ctrlrange[2 * actuatorId + 1];
|
|
179
|
+
return {
|
|
180
|
+
id: actuatorId,
|
|
181
|
+
name: getName(mjModel, mjModel.name_actuatoradr[actuatorId]),
|
|
182
|
+
range: hasRange
|
|
183
|
+
? [mjModel.actuator_ctrlrange[2 * actuatorId], mjModel.actuator_ctrlrange[2 * actuatorId + 1]]
|
|
184
|
+
: unlimitedRange(),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function includesResourceName(names: readonly string[], name: string): boolean {
|
|
189
|
+
return names.includes(name);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function matchesSelector<TInfo extends { name: string }, TName extends string>(
|
|
193
|
+
info: TInfo,
|
|
194
|
+
selector: ResourceSelector<TInfo, TName>
|
|
195
|
+
): boolean {
|
|
196
|
+
if (typeof selector === 'string') return info.name === selector;
|
|
197
|
+
if (selector instanceof RegExp) return selector.test(info.name);
|
|
198
|
+
if (Array.isArray(selector)) return includesResourceName(selector, info.name);
|
|
199
|
+
if (typeof selector === 'function') return selector(info);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function orderedJointIdsFromSelector(
|
|
204
|
+
mjModel: MujocoModel,
|
|
205
|
+
selector: ResourceSelector<JointInfo, string>
|
|
206
|
+
): number[] {
|
|
207
|
+
if (typeof selector === 'string') {
|
|
208
|
+
const id = findJointByName(mjModel, selector);
|
|
209
|
+
return id >= 0 && isScalarJoint(mjModel, id) ? [id] : [];
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(selector)) {
|
|
212
|
+
return selector
|
|
213
|
+
.map((name) => findJointByName(mjModel, name))
|
|
214
|
+
.filter((id) => id >= 0 && isScalarJoint(mjModel, id));
|
|
215
|
+
}
|
|
216
|
+
const ids: number[] = [];
|
|
217
|
+
for (let i = 0; i < mjModel.njnt; i++) {
|
|
218
|
+
if (!isScalarJoint(mjModel, i)) continue;
|
|
219
|
+
const info = getJointInfo(mjModel, i);
|
|
220
|
+
if (matchesSelector(info, selector)) ids.push(i);
|
|
221
|
+
}
|
|
222
|
+
return ids;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function orderedActuatorIdsFromSelector(
|
|
226
|
+
mjModel: MujocoModel,
|
|
227
|
+
selector: ResourceSelector<ActuatorInfo, string>
|
|
228
|
+
): number[] {
|
|
229
|
+
if (typeof selector === 'string') {
|
|
230
|
+
const id = findActuatorByName(mjModel, selector);
|
|
231
|
+
return id >= 0 && getActuatorJointId(mjModel, id) >= 0 ? [id] : [];
|
|
232
|
+
}
|
|
233
|
+
if (Array.isArray(selector)) {
|
|
234
|
+
return selector
|
|
235
|
+
.map((name) => findActuatorByName(mjModel, name))
|
|
236
|
+
.filter((id) => id >= 0 && getActuatorJointId(mjModel, id) >= 0);
|
|
237
|
+
}
|
|
238
|
+
const ids: number[] = [];
|
|
239
|
+
for (let i = 0; i < mjModel.nu; i++) {
|
|
240
|
+
if (getActuatorJointId(mjModel, i) < 0) continue;
|
|
241
|
+
const info = getActuatorInfo(mjModel, i);
|
|
242
|
+
if (matchesSelector(info, selector)) ids.push(i);
|
|
243
|
+
}
|
|
244
|
+
return ids;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function inferScalarJointChain(mjModel: MujocoModel, bodyId: number): number[] {
|
|
248
|
+
if (bodyId < 0 || bodyId >= mjModel.nbody) return [];
|
|
249
|
+
const chainByBody: number[][] = [];
|
|
250
|
+
let current = bodyId;
|
|
251
|
+
const seen = new Set<number>();
|
|
252
|
+
|
|
253
|
+
while (current >= 0 && current < mjModel.nbody && !seen.has(current)) {
|
|
254
|
+
seen.add(current);
|
|
255
|
+
const joints: number[] = [];
|
|
256
|
+
const jointCount = mjModel.body_jntnum[current] ?? 0;
|
|
257
|
+
const jointStart = mjModel.body_jntadr[current] ?? -1;
|
|
258
|
+
for (let i = 0; i < jointCount; i++) {
|
|
259
|
+
const jointId = jointStart + i;
|
|
260
|
+
if (isScalarJoint(mjModel, jointId)) joints.push(jointId);
|
|
261
|
+
}
|
|
262
|
+
if (joints.length) chainByBody.push(joints);
|
|
263
|
+
const parent = mjModel.body_parentid[current];
|
|
264
|
+
if (parent === current) break;
|
|
265
|
+
current = parent;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return chainByBody.reverse().flat();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function unique(values: number[]): number[] {
|
|
272
|
+
const seen = new Set<number>();
|
|
273
|
+
const result: number[] = [];
|
|
274
|
+
for (const value of values) {
|
|
275
|
+
if (seen.has(value)) continue;
|
|
276
|
+
seen.add(value);
|
|
277
|
+
result.push(value);
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function findActuatorForJoint(mjModel: MujocoModel, jointId: number, preferredActuatorIds?: number[]): number {
|
|
283
|
+
const search = preferredActuatorIds ?? Array.from({ length: mjModel.nu }, (_, i) => i);
|
|
284
|
+
for (const actuatorId of search) {
|
|
285
|
+
if (getActuatorJointId(mjModel, actuatorId) === jointId) return actuatorId;
|
|
286
|
+
}
|
|
287
|
+
return -1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildControlGroup(
|
|
291
|
+
mjModel: MujocoModel,
|
|
292
|
+
jointIds: number[],
|
|
293
|
+
preferredActuatorIds?: number[]
|
|
294
|
+
): ControlGroupInfo | null {
|
|
295
|
+
const ids = unique(jointIds).filter((id) => isScalarJoint(mjModel, id));
|
|
296
|
+
if (!ids.length) return null;
|
|
297
|
+
|
|
298
|
+
const joints: ControlJointInfo[] = [];
|
|
299
|
+
const actuators: ActuatorInfo[] = [];
|
|
300
|
+
const qposAdr: number[] = [];
|
|
301
|
+
const dofAdr: number[] = [];
|
|
302
|
+
const ctrlAdr: number[] = [];
|
|
303
|
+
|
|
304
|
+
for (const jointId of ids) {
|
|
305
|
+
const actuatorId = findActuatorForJoint(mjModel, jointId, preferredActuatorIds);
|
|
306
|
+
const joint = getJointInfo(mjModel, jointId);
|
|
307
|
+
qposAdr.push(joint.qposAdr);
|
|
308
|
+
dofAdr.push(joint.dofAdr);
|
|
309
|
+
|
|
310
|
+
if (actuatorId >= 0) {
|
|
311
|
+
const actuator = getActuatorInfo(mjModel, actuatorId);
|
|
312
|
+
actuators.push(actuator);
|
|
313
|
+
ctrlAdr.push(actuatorId);
|
|
314
|
+
joints.push({
|
|
315
|
+
...joint,
|
|
316
|
+
actuatorId,
|
|
317
|
+
actuatorName: actuator.name,
|
|
318
|
+
ctrlAdr: actuatorId,
|
|
319
|
+
ctrlRange: actuator.range,
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
joints.push({
|
|
323
|
+
...joint,
|
|
324
|
+
actuatorId: null,
|
|
325
|
+
actuatorName: null,
|
|
326
|
+
ctrlAdr: null,
|
|
327
|
+
ctrlRange: null,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
joints,
|
|
334
|
+
actuators,
|
|
335
|
+
qposAdr,
|
|
336
|
+
dofAdr,
|
|
337
|
+
ctrlAdr,
|
|
338
|
+
readQpos(data: MujocoData) {
|
|
339
|
+
return new Float64Array(qposAdr.map((adr) => data.qpos[adr] ?? 0));
|
|
340
|
+
},
|
|
341
|
+
readCtrl(data: MujocoData) {
|
|
342
|
+
return new Float64Array(joints.map((joint) => joint.ctrlAdr === null ? 0 : data.ctrl[joint.ctrlAdr] ?? 0));
|
|
343
|
+
},
|
|
344
|
+
writeQpos(data: MujocoData, values: ArrayLike<number>) {
|
|
345
|
+
for (let i = 0; i < Math.min(values.length, qposAdr.length); i++) {
|
|
346
|
+
data.qpos[qposAdr[i]] = values[i];
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
writeCtrl(data: MujocoData, values: ArrayLike<number>) {
|
|
350
|
+
for (let i = 0; i < Math.min(values.length, joints.length); i++) {
|
|
351
|
+
const adr = joints[i].ctrlAdr;
|
|
352
|
+
if (adr !== null) data.ctrl[adr] = values[i];
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function getActuatedJoints(mjModel: MujocoModel): ActuatedJointInfo[] {
|
|
359
|
+
const result: ActuatedJointInfo[] = [];
|
|
360
|
+
for (let actuatorId = 0; actuatorId < mjModel.nu; actuatorId++) {
|
|
361
|
+
const jointId = getActuatorJointId(mjModel, actuatorId);
|
|
362
|
+
if (jointId < 0) continue;
|
|
363
|
+
const actuator = getActuatorInfo(mjModel, actuatorId);
|
|
364
|
+
result.push({
|
|
365
|
+
...getJointInfo(mjModel, jointId),
|
|
366
|
+
actuatorId,
|
|
367
|
+
actuatorName: actuator.name,
|
|
368
|
+
ctrlAdr: actuatorId,
|
|
369
|
+
ctrlRange: actuator.range,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function getControlMap(mjModel: MujocoModel): ControlGroupInfo {
|
|
376
|
+
const actuatorIds = Array.from({ length: mjModel.nu }, (_, i) => i)
|
|
377
|
+
.filter((id) => getActuatorJointId(mjModel, id) >= 0);
|
|
378
|
+
const jointIds = actuatorIds.map((id) => getActuatorJointId(mjModel, id));
|
|
379
|
+
return buildControlGroup(mjModel, jointIds, actuatorIds) ?? createContiguousControlGroup(mjModel, 0);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function resolveControlGroup(
|
|
383
|
+
mjModel: MujocoModel,
|
|
384
|
+
selector: ControlGroupSelector
|
|
385
|
+
): ControlGroupInfo | null {
|
|
386
|
+
if (selector.actuators) {
|
|
387
|
+
const actuatorIds = orderedActuatorIdsFromSelector(mjModel, selector.actuators);
|
|
388
|
+
const jointIds = actuatorIds.map((id) => getActuatorJointId(mjModel, id));
|
|
389
|
+
return buildControlGroup(mjModel, jointIds, actuatorIds);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (selector.joints) {
|
|
393
|
+
return buildControlGroup(mjModel, orderedJointIdsFromSelector(mjModel, selector.joints));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (selector.siteName) {
|
|
397
|
+
const siteId = findSiteByName(mjModel, selector.siteName);
|
|
398
|
+
const bodyId = siteId >= 0 ? (mjModel.site_bodyid?.[siteId] ?? -1) : -1;
|
|
399
|
+
return buildControlGroup(mjModel, inferScalarJointChain(mjModel, bodyId));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (selector.bodyName) {
|
|
403
|
+
return buildControlGroup(mjModel, inferScalarJointChain(mjModel, findBodyByName(mjModel, selector.bodyName)));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return getControlMap(mjModel);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function createContiguousControlGroup(mjModel: MujocoModel, count: number): ControlGroupInfo {
|
|
410
|
+
const n = Math.max(0, Math.min(count, mjModel.nq, mjModel.nu));
|
|
411
|
+
const joints: ControlJointInfo[] = [];
|
|
412
|
+
const actuators: ActuatorInfo[] = [];
|
|
413
|
+
const qposAdr: number[] = [];
|
|
414
|
+
const dofAdr: number[] = [];
|
|
415
|
+
const ctrlAdr: number[] = [];
|
|
416
|
+
|
|
417
|
+
for (let i = 0; i < n; i++) {
|
|
418
|
+
qposAdr.push(i);
|
|
419
|
+
dofAdr.push(i);
|
|
420
|
+
ctrlAdr.push(i);
|
|
421
|
+
const jointId = Array.from({ length: mjModel.njnt }, (_, id) => id)
|
|
422
|
+
.find((id) => mjModel.jnt_qposadr[id] === i);
|
|
423
|
+
const actuator = getActuatorInfo(mjModel, i);
|
|
424
|
+
actuators.push(actuator);
|
|
425
|
+
joints.push({
|
|
426
|
+
...(jointId !== undefined ? getJointInfo(mjModel, jointId) : {
|
|
427
|
+
id: i,
|
|
428
|
+
name: `qpos${i}`,
|
|
429
|
+
type: 3,
|
|
430
|
+
typeName: 'hinge',
|
|
431
|
+
range: unlimitedRange(),
|
|
432
|
+
limited: false,
|
|
433
|
+
bodyId: -1,
|
|
434
|
+
qposAdr: i,
|
|
435
|
+
dofAdr: i,
|
|
436
|
+
}),
|
|
437
|
+
actuatorId: i,
|
|
438
|
+
actuatorName: actuator.name,
|
|
439
|
+
ctrlAdr: i,
|
|
440
|
+
ctrlRange: actuator.range,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
joints,
|
|
446
|
+
actuators,
|
|
447
|
+
qposAdr,
|
|
448
|
+
dofAdr,
|
|
449
|
+
ctrlAdr,
|
|
450
|
+
readQpos(data: MujocoData) {
|
|
451
|
+
return new Float64Array(qposAdr.map((adr) => data.qpos[adr] ?? 0));
|
|
452
|
+
},
|
|
453
|
+
readCtrl(data: MujocoData) {
|
|
454
|
+
return new Float64Array(ctrlAdr.map((adr) => data.ctrl[adr] ?? 0));
|
|
455
|
+
},
|
|
456
|
+
writeQpos(data: MujocoData, values: ArrayLike<number>) {
|
|
457
|
+
for (let i = 0; i < Math.min(values.length, qposAdr.length); i++) data.qpos[qposAdr[i]] = values[i];
|
|
458
|
+
},
|
|
459
|
+
writeCtrl(data: MujocoData, values: ArrayLike<number>) {
|
|
460
|
+
for (let i = 0; i < Math.min(values.length, ctrlAdr.length); i++) data.ctrl[ctrlAdr[i]] = values[i];
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
123
465
|
/**
|
|
124
466
|
* Convert a SceneObject config to MuJoCo XML.
|
|
125
467
|
*/
|
|
@@ -153,6 +495,16 @@ interface LoadResult {
|
|
|
153
495
|
mjData: MujocoData;
|
|
154
496
|
}
|
|
155
497
|
|
|
498
|
+
function loadModelFromPath(mujoco: MujocoModule, path: string): MujocoModel {
|
|
499
|
+
if (mujoco.MjModel.from_xml_path) {
|
|
500
|
+
return mujoco.MjModel.from_xml_path(path);
|
|
501
|
+
}
|
|
502
|
+
if (mujoco.MjModel.loadFromXML) {
|
|
503
|
+
return mujoco.MjModel.loadFromXML(path);
|
|
504
|
+
}
|
|
505
|
+
throw new Error('MuJoCo WASM module does not expose an XML path loader');
|
|
506
|
+
}
|
|
507
|
+
|
|
156
508
|
/**
|
|
157
509
|
* Config-driven scene loader — replaces the old RobotLoader + patchSingleRobot approach.
|
|
158
510
|
*/
|
|
@@ -260,7 +612,7 @@ export async function loadScene(
|
|
|
260
612
|
|
|
261
613
|
// 5. Load model
|
|
262
614
|
onProgress?.('Loading model...');
|
|
263
|
-
const mjModel = mujoco
|
|
615
|
+
const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
|
|
264
616
|
const mjData = new mujoco.MjData(mjModel);
|
|
265
617
|
|
|
266
618
|
// 6. Set initial pose — set both ctrl and qpos so robot starts at home.
|
package/src/hooks/useContacts.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { useCallback, useEffect, useRef } from 'react';
|
|
10
10
|
import { useMujocoContext, useAfterPhysicsStep } from '../core/MujocoSimProvider';
|
|
11
11
|
import { findBodyByName, getName } from '../core/SceneLoader';
|
|
12
|
-
import { getContact } from '../types';
|
|
12
|
+
import { getContact, withContacts } from '../types';
|
|
13
13
|
import type { Bodies, ContactInfo, MujocoModel } from '../types';
|
|
14
14
|
|
|
15
15
|
// Cache geom names per model to avoid cross-model id collisions.
|
|
@@ -77,24 +77,26 @@ export function useContacts(
|
|
|
77
77
|
const contacts: ContactInfo[] = [];
|
|
78
78
|
const filterBody = bodyIdRef.current;
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
withContacts(data, (contactArray) => {
|
|
81
|
+
for (let i = 0; i < ncon; i++) {
|
|
82
|
+
const c = getContact(contactArray, i);
|
|
83
|
+
if (!c) break;
|
|
84
|
+
// Filter by body if specified
|
|
85
|
+
if (filterBody >= 0) {
|
|
86
|
+
const b1 = model.geom_bodyid[c.geom1];
|
|
87
|
+
const b2 = model.geom_bodyid[c.geom2];
|
|
88
|
+
if (b1 !== filterBody && b2 !== filterBody) continue;
|
|
89
|
+
}
|
|
90
|
+
contacts.push({
|
|
91
|
+
geom1: c.geom1,
|
|
92
|
+
geom1Name: getGeomNameCached(model, c.geom1),
|
|
93
|
+
geom2: c.geom2,
|
|
94
|
+
geom2Name: getGeomNameCached(model, c.geom2),
|
|
95
|
+
pos: [c.pos[0], c.pos[1], c.pos[2]],
|
|
96
|
+
depth: c.dist,
|
|
97
|
+
});
|
|
88
98
|
}
|
|
89
|
-
|
|
90
|
-
geom1: c.geom1,
|
|
91
|
-
geom1Name: getGeomNameCached(model, c.geom1),
|
|
92
|
-
geom2: c.geom2,
|
|
93
|
-
geom2Name: getGeomNameCached(model, c.geom2),
|
|
94
|
-
pos: [c.pos[0], c.pos[1], c.pos[2]],
|
|
95
|
-
depth: c.dist,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
99
|
+
});
|
|
98
100
|
contactsRef.current = contacts;
|
|
99
101
|
callbackRef.current?.(contacts);
|
|
100
102
|
});
|
|
@@ -9,8 +9,8 @@ import * as THREE from 'three';
|
|
|
9
9
|
import { createControllerHook } from '../core/createController';
|
|
10
10
|
import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
11
11
|
import { GenericIK } from '../core/GenericIK';
|
|
12
|
-
import { findSiteByName } from '../core/SceneLoader';
|
|
13
|
-
import type { IkConfig, IkContextValue, IKSolveFn, MujocoData } from '../types';
|
|
12
|
+
import { createContiguousControlGroup, findSiteByName, resolveControlGroup } from '../core/SceneLoader';
|
|
13
|
+
import type { ControlGroupInfo, IkConfig, IkContextValue, IKSolveFn, MujocoData } from '../types';
|
|
14
14
|
|
|
15
15
|
// Preallocated temp for syncGizmoToSite
|
|
16
16
|
const _syncMat4 = new THREE.Matrix4();
|
|
@@ -40,6 +40,7 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
|
|
|
40
40
|
const ikCalculatingRef = useRef(false);
|
|
41
41
|
const ikTargetRef = useRef<THREE.Group>(new THREE.Group());
|
|
42
42
|
const siteIdRef = useRef(-1);
|
|
43
|
+
const controlGroupRef = useRef<ControlGroupInfo | null>(null);
|
|
43
44
|
const genericIkRef = useRef<GenericIK>(new GenericIK(mujocoRef.current));
|
|
44
45
|
const firstIkEnableRef = useRef(true);
|
|
45
46
|
const needsInitialSync = useRef(true);
|
|
@@ -54,23 +55,32 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
|
|
|
54
55
|
duration: 1000,
|
|
55
56
|
});
|
|
56
57
|
|
|
57
|
-
// Resolve site ID when model loads or config changes
|
|
58
|
+
// Resolve site ID and model-aware control group when model loads or config changes.
|
|
58
59
|
useEffect(() => {
|
|
59
60
|
if (!config) {
|
|
60
61
|
siteIdRef.current = -1;
|
|
62
|
+
controlGroupRef.current = null;
|
|
61
63
|
return;
|
|
62
64
|
}
|
|
63
65
|
const model = mjModelRef.current;
|
|
64
66
|
if (!model || status !== 'ready') {
|
|
65
67
|
siteIdRef.current = -1;
|
|
68
|
+
controlGroupRef.current = null;
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
68
71
|
siteIdRef.current = findSiteByName(model, config.siteName);
|
|
72
|
+
controlGroupRef.current = config.numJoints !== undefined
|
|
73
|
+
? createContiguousControlGroup(model, config.numJoints)
|
|
74
|
+
: resolveControlGroup(model, {
|
|
75
|
+
siteName: config.siteName,
|
|
76
|
+
joints: config.joints,
|
|
77
|
+
actuators: config.actuators,
|
|
78
|
+
});
|
|
69
79
|
const data = mjDataRef.current;
|
|
70
80
|
if (data && ikTargetRef.current) {
|
|
71
81
|
syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
|
|
72
82
|
}
|
|
73
|
-
}, [config?.siteName, status, mjModelRef, mjDataRef, config]);
|
|
83
|
+
}, [config?.siteName, config?.numJoints, config?.joints, config?.actuators, status, mjModelRef, mjDataRef, config]);
|
|
74
84
|
|
|
75
85
|
// IK solve function
|
|
76
86
|
const ikSolveFn = useCallback(
|
|
@@ -79,9 +89,10 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
|
|
|
79
89
|
if (config.ikSolveFn) return config.ikSolveFn(pos, quat, currentQ);
|
|
80
90
|
const model = mjModelRef.current;
|
|
81
91
|
const data = mjDataRef.current;
|
|
82
|
-
|
|
92
|
+
const controlGroup = controlGroupRef.current;
|
|
93
|
+
if (!model || !data || !controlGroup || siteIdRef.current === -1) return null;
|
|
83
94
|
return genericIkRef.current.solve(
|
|
84
|
-
model, data, siteIdRef.current,
|
|
95
|
+
model, data, siteIdRef.current, controlGroup.qposAdr,
|
|
85
96
|
pos, quat, currentQ,
|
|
86
97
|
{ damping: config.damping, maxIterations: config.maxIterations },
|
|
87
98
|
);
|
|
@@ -126,12 +137,20 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
|
|
|
126
137
|
if (!target) return;
|
|
127
138
|
|
|
128
139
|
ikCalculatingRef.current = true;
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
140
|
+
const controlGroup = controlGroupRef.current;
|
|
141
|
+
if (!controlGroup) return;
|
|
142
|
+
|
|
143
|
+
const currentQ = Array.from(controlGroup.readQpos(data));
|
|
144
|
+
const solution = config.ikSolveFn
|
|
145
|
+
? config.ikSolveFn(target.position, target.quaternion, currentQ, {
|
|
146
|
+
model,
|
|
147
|
+
data,
|
|
148
|
+
siteId: siteIdRef.current,
|
|
149
|
+
controlGroup,
|
|
150
|
+
})
|
|
151
|
+
: ikSolveFnRef.current(target.position, target.quaternion, currentQ);
|
|
133
152
|
if (solution) {
|
|
134
|
-
|
|
153
|
+
controlGroup.writeCtrl(data, solution);
|
|
135
154
|
}
|
|
136
155
|
});
|
|
137
156
|
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// Core
|
|
7
7
|
export { MujocoProvider, useMujocoWasm } from './core/MujocoProvider';
|
|
8
|
+
export type { MujocoLoader, MujocoLoaderOptions, MujocoProviderProps, MujocoWasmVariant } from './core/MujocoProvider';
|
|
8
9
|
export { MujocoCanvas } from './core/MujocoCanvas';
|
|
9
10
|
export { MujocoPhysics } from './core/MujocoPhysics';
|
|
10
11
|
export type { MujocoPhysicsProps } from './core/MujocoPhysics';
|
|
@@ -20,6 +21,10 @@ export {
|
|
|
20
21
|
findGeomByName,
|
|
21
22
|
findSensorByName,
|
|
22
23
|
findTendonByName,
|
|
24
|
+
getActuatedJoints,
|
|
25
|
+
getControlMap,
|
|
26
|
+
resolveControlGroup,
|
|
27
|
+
createContiguousControlGroup,
|
|
23
28
|
} from './core/SceneLoader';
|
|
24
29
|
|
|
25
30
|
// Controller factory
|
|
@@ -82,6 +87,11 @@ export type {
|
|
|
82
87
|
// Model introspection
|
|
83
88
|
BodyInfo,
|
|
84
89
|
JointInfo,
|
|
90
|
+
ActuatedJointInfo,
|
|
91
|
+
ControlJointInfo,
|
|
92
|
+
ControlGroupInfo,
|
|
93
|
+
ControlGroupSelector,
|
|
94
|
+
ResourceSelector,
|
|
85
95
|
GeomInfo,
|
|
86
96
|
SiteInfo,
|
|
87
97
|
ActuatorInfo,
|
|
@@ -61,7 +61,7 @@ export class GeomBuilder {
|
|
|
61
61
|
const MG = this.mujoco.mjtGeom; // Short alias for MuJoCo Geometry Types enum
|
|
62
62
|
let geo: THREE.BufferGeometry | null = null;
|
|
63
63
|
|
|
64
|
-
// The '.value ?? MG.XYZ' pattern handles slightly different
|
|
64
|
+
// The '.value ?? MG.XYZ' pattern handles slightly different MuJoCo WASM binding versions.
|
|
65
65
|
const getVal = (v: unknown) => (v as { value: number })?.value ?? v;
|
|
66
66
|
|
|
67
67
|
if (type === getVal(MG.mjGEOM_PLANE)) {
|