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/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import loadMujoco from 'mujoco-js';
1
+ import loadMujoco from '@mujoco/mujoco';
2
+ import defaultMujocoWasmUrl from '@mujoco/mujoco/mujoco.wasm?url';
2
3
  import { createContext, forwardRef, useEffect, useContext, useState, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
3
4
  import { jsx, jsxs } from 'react/jsx-runtime';
4
5
  import { Canvas, useThree, useFrame } from '@react-three/fiber';
@@ -14,15 +15,57 @@ var MujocoContext = createContext({
14
15
  function useMujocoWasm() {
15
16
  return useContext(MujocoContext);
16
17
  }
17
- function MujocoProvider({ wasmUrl, timeout = 3e4, children, onError }) {
18
+ function canUseThreadedWasm() {
19
+ return typeof globalThis !== "undefined" && globalThis.crossOriginIsolated === true;
20
+ }
21
+ function isMujocoModule(value) {
22
+ return typeof value === "object" && value !== null && "FS" in value && "MjModel" in value && "MjData" in value && "mj_step" in value;
23
+ }
24
+ function hasWasmUrl(value) {
25
+ return typeof value === "string" && value.length > 0;
26
+ }
27
+ function resolveWasmVariant(variant, threadedLoader, mtWasmUrl) {
28
+ if (variant === "threaded") return "threaded";
29
+ if (variant === "auto" && threadedLoader && mtWasmUrl && canUseThreadedWasm()) return "threaded";
30
+ return "single";
31
+ }
32
+ function MujocoProvider({
33
+ wasmUrl,
34
+ mtWasmUrl,
35
+ threadedLoader,
36
+ wasmVariant = "single",
37
+ timeout = 3e4,
38
+ children,
39
+ onError
40
+ }) {
18
41
  const [status, setStatus] = useState("loading");
19
42
  const [error, setError] = useState(null);
20
43
  const moduleRef = useRef(null);
21
44
  const isMounted = useRef(true);
22
45
  useEffect(() => {
23
46
  isMounted.current = true;
24
- const wasmPromise = loadMujoco({
25
- ...wasmUrl ? { locateFile: (path) => path.endsWith(".wasm") ? wasmUrl : path } : {},
47
+ const variant = resolveWasmVariant(wasmVariant, threadedLoader, mtWasmUrl);
48
+ if (variant === "threaded" && !threadedLoader) {
49
+ const err = new Error('MujocoProvider wasmVariant="threaded" requires a threadedLoader from @mujoco/mujoco/mt');
50
+ setError(err.message);
51
+ setStatus("error");
52
+ onError?.(err);
53
+ return;
54
+ }
55
+ let selectedWasmUrl = wasmUrl ?? defaultMujocoWasmUrl;
56
+ if (variant === "threaded") {
57
+ if (!hasWasmUrl(mtWasmUrl)) {
58
+ const err = new Error('MujocoProvider wasmVariant="threaded" requires mtWasmUrl from @mujoco/mujoco/mt/mujoco.wasm?url');
59
+ setError(err.message);
60
+ setStatus("error");
61
+ onError?.(err);
62
+ return;
63
+ }
64
+ selectedWasmUrl = mtWasmUrl;
65
+ }
66
+ const load = variant === "threaded" && threadedLoader ? threadedLoader : loadMujoco;
67
+ const wasmPromise = load({
68
+ locateFile: (path) => path.endsWith(".wasm") ? selectedWasmUrl : path,
26
69
  printErr: (text) => {
27
70
  if (text.includes("Aborted") && isMounted.current) {
28
71
  setError("Simulation crashed. Reload page.");
@@ -35,6 +78,9 @@ function MujocoProvider({ wasmUrl, timeout = 3e4, children, onError }) {
35
78
  );
36
79
  Promise.race([wasmPromise, timeoutPromise]).then((inst) => {
37
80
  if (isMounted.current) {
81
+ if (!isMujocoModule(inst)) {
82
+ throw new Error("MuJoCo WASM module initialized with an unexpected shape");
83
+ }
38
84
  moduleRef.current = inst;
39
85
  setStatus("ready");
40
86
  }
@@ -49,7 +95,7 @@ function MujocoProvider({ wasmUrl, timeout = 3e4, children, onError }) {
49
95
  return () => {
50
96
  isMounted.current = false;
51
97
  };
52
- }, [wasmUrl, timeout]);
98
+ }, [wasmUrl, mtWasmUrl, threadedLoader, wasmVariant, timeout, onError]);
53
99
  return /* @__PURE__ */ jsx(
54
100
  MujocoContext.Provider,
55
101
  {
@@ -60,13 +106,21 @@ function MujocoProvider({ wasmUrl, timeout = 3e4, children, onError }) {
60
106
  }
61
107
 
62
108
  // src/types.ts
63
- function getContact(data, i) {
109
+ function getContact(contacts, i) {
64
110
  try {
65
- return data.contact.get(i);
111
+ return contacts.get(i);
66
112
  } catch {
67
113
  return void 0;
68
114
  }
69
115
  }
116
+ function withContacts(data, read) {
117
+ const contacts = data.contact;
118
+ try {
119
+ return read(contacts);
120
+ } finally {
121
+ contacts.delete?.();
122
+ }
123
+ }
70
124
  var CapsuleGeometry = class extends THREE11.BufferGeometry {
71
125
  parameters;
72
126
  constructor(radius = 1, length = 1, capSegments = 4, radialSegments = 8) {
@@ -311,6 +365,12 @@ var GeomBuilder = class {
311
365
  };
312
366
 
313
367
  // src/core/SceneLoader.ts
368
+ var JOINT_TYPE_NAMES = {
369
+ 0: "free",
370
+ 1: "ball",
371
+ 2: "slide",
372
+ 3: "hinge"
373
+ };
314
374
  function getName(mjModel, address) {
315
375
  let name = "";
316
376
  let idx = address;
@@ -379,6 +439,276 @@ function getActuatedScalarQposAdr(mjModel, actuatorId) {
379
439
  if (jntType !== 2 && jntType !== 3) return -1;
380
440
  return mjModel.jnt_qposadr[jointId];
381
441
  }
442
+ function getScalarJointDim(jointType) {
443
+ return jointType === 2 || jointType === 3 ? 1 : 0;
444
+ }
445
+ function unlimitedRange() {
446
+ return [-Infinity, Infinity];
447
+ }
448
+ function isScalarJoint(mjModel, jointId) {
449
+ return jointId >= 0 && jointId < mjModel.njnt && getScalarJointDim(mjModel.jnt_type[jointId]) === 1;
450
+ }
451
+ function getActuatorJointId(mjModel, actuatorId) {
452
+ if (actuatorId < 0 || actuatorId >= mjModel.nu) return -1;
453
+ const trnType = mjModel.actuator_trntype?.[actuatorId];
454
+ if (trnType !== void 0 && trnType !== 0 && trnType !== 1) return -1;
455
+ const jointId = mjModel.actuator_trnid[2 * actuatorId];
456
+ return isScalarJoint(mjModel, jointId) ? jointId : -1;
457
+ }
458
+ function getJointInfo(mjModel, jointId) {
459
+ const type = mjModel.jnt_type[jointId];
460
+ const range = [mjModel.jnt_range[2 * jointId], mjModel.jnt_range[2 * jointId + 1]];
461
+ return {
462
+ id: jointId,
463
+ name: getName(mjModel, mjModel.name_jntadr[jointId]),
464
+ type,
465
+ typeName: JOINT_TYPE_NAMES[type] ?? `unknown(${type})`,
466
+ range,
467
+ limited: range[0] < range[1],
468
+ bodyId: mjModel.jnt_bodyid[jointId],
469
+ qposAdr: mjModel.jnt_qposadr[jointId],
470
+ dofAdr: mjModel.jnt_dofadr[jointId]
471
+ };
472
+ }
473
+ function getActuatorInfo(mjModel, actuatorId) {
474
+ const hasRange = mjModel.actuator_ctrlrange[2 * actuatorId] < mjModel.actuator_ctrlrange[2 * actuatorId + 1];
475
+ return {
476
+ id: actuatorId,
477
+ name: getName(mjModel, mjModel.name_actuatoradr[actuatorId]),
478
+ range: hasRange ? [mjModel.actuator_ctrlrange[2 * actuatorId], mjModel.actuator_ctrlrange[2 * actuatorId + 1]] : unlimitedRange()
479
+ };
480
+ }
481
+ function includesResourceName(names, name) {
482
+ return names.includes(name);
483
+ }
484
+ function matchesSelector(info, selector) {
485
+ if (typeof selector === "string") return info.name === selector;
486
+ if (selector instanceof RegExp) return selector.test(info.name);
487
+ if (Array.isArray(selector)) return includesResourceName(selector, info.name);
488
+ if (typeof selector === "function") return selector(info);
489
+ return false;
490
+ }
491
+ function orderedJointIdsFromSelector(mjModel, selector) {
492
+ if (typeof selector === "string") {
493
+ const id = findJointByName(mjModel, selector);
494
+ return id >= 0 && isScalarJoint(mjModel, id) ? [id] : [];
495
+ }
496
+ if (Array.isArray(selector)) {
497
+ return selector.map((name) => findJointByName(mjModel, name)).filter((id) => id >= 0 && isScalarJoint(mjModel, id));
498
+ }
499
+ const ids = [];
500
+ for (let i = 0; i < mjModel.njnt; i++) {
501
+ if (!isScalarJoint(mjModel, i)) continue;
502
+ const info = getJointInfo(mjModel, i);
503
+ if (matchesSelector(info, selector)) ids.push(i);
504
+ }
505
+ return ids;
506
+ }
507
+ function orderedActuatorIdsFromSelector(mjModel, selector) {
508
+ if (typeof selector === "string") {
509
+ const id = findActuatorByName(mjModel, selector);
510
+ return id >= 0 && getActuatorJointId(mjModel, id) >= 0 ? [id] : [];
511
+ }
512
+ if (Array.isArray(selector)) {
513
+ return selector.map((name) => findActuatorByName(mjModel, name)).filter((id) => id >= 0 && getActuatorJointId(mjModel, id) >= 0);
514
+ }
515
+ const ids = [];
516
+ for (let i = 0; i < mjModel.nu; i++) {
517
+ if (getActuatorJointId(mjModel, i) < 0) continue;
518
+ const info = getActuatorInfo(mjModel, i);
519
+ if (matchesSelector(info, selector)) ids.push(i);
520
+ }
521
+ return ids;
522
+ }
523
+ function inferScalarJointChain(mjModel, bodyId) {
524
+ if (bodyId < 0 || bodyId >= mjModel.nbody) return [];
525
+ const chainByBody = [];
526
+ let current = bodyId;
527
+ const seen = /* @__PURE__ */ new Set();
528
+ while (current >= 0 && current < mjModel.nbody && !seen.has(current)) {
529
+ seen.add(current);
530
+ const joints = [];
531
+ const jointCount = mjModel.body_jntnum[current] ?? 0;
532
+ const jointStart = mjModel.body_jntadr[current] ?? -1;
533
+ for (let i = 0; i < jointCount; i++) {
534
+ const jointId = jointStart + i;
535
+ if (isScalarJoint(mjModel, jointId)) joints.push(jointId);
536
+ }
537
+ if (joints.length) chainByBody.push(joints);
538
+ const parent = mjModel.body_parentid[current];
539
+ if (parent === current) break;
540
+ current = parent;
541
+ }
542
+ return chainByBody.reverse().flat();
543
+ }
544
+ function unique(values) {
545
+ const seen = /* @__PURE__ */ new Set();
546
+ const result = [];
547
+ for (const value of values) {
548
+ if (seen.has(value)) continue;
549
+ seen.add(value);
550
+ result.push(value);
551
+ }
552
+ return result;
553
+ }
554
+ function findActuatorForJoint(mjModel, jointId, preferredActuatorIds) {
555
+ const search = preferredActuatorIds ?? Array.from({ length: mjModel.nu }, (_, i) => i);
556
+ for (const actuatorId of search) {
557
+ if (getActuatorJointId(mjModel, actuatorId) === jointId) return actuatorId;
558
+ }
559
+ return -1;
560
+ }
561
+ function buildControlGroup(mjModel, jointIds, preferredActuatorIds) {
562
+ const ids = unique(jointIds).filter((id) => isScalarJoint(mjModel, id));
563
+ if (!ids.length) return null;
564
+ const joints = [];
565
+ const actuators = [];
566
+ const qposAdr = [];
567
+ const dofAdr = [];
568
+ const ctrlAdr = [];
569
+ for (const jointId of ids) {
570
+ const actuatorId = findActuatorForJoint(mjModel, jointId, preferredActuatorIds);
571
+ const joint = getJointInfo(mjModel, jointId);
572
+ qposAdr.push(joint.qposAdr);
573
+ dofAdr.push(joint.dofAdr);
574
+ if (actuatorId >= 0) {
575
+ const actuator = getActuatorInfo(mjModel, actuatorId);
576
+ actuators.push(actuator);
577
+ ctrlAdr.push(actuatorId);
578
+ joints.push({
579
+ ...joint,
580
+ actuatorId,
581
+ actuatorName: actuator.name,
582
+ ctrlAdr: actuatorId,
583
+ ctrlRange: actuator.range
584
+ });
585
+ } else {
586
+ joints.push({
587
+ ...joint,
588
+ actuatorId: null,
589
+ actuatorName: null,
590
+ ctrlAdr: null,
591
+ ctrlRange: null
592
+ });
593
+ }
594
+ }
595
+ return {
596
+ joints,
597
+ actuators,
598
+ qposAdr,
599
+ dofAdr,
600
+ ctrlAdr,
601
+ readQpos(data) {
602
+ return new Float64Array(qposAdr.map((adr) => data.qpos[adr] ?? 0));
603
+ },
604
+ readCtrl(data) {
605
+ return new Float64Array(joints.map((joint) => joint.ctrlAdr === null ? 0 : data.ctrl[joint.ctrlAdr] ?? 0));
606
+ },
607
+ writeQpos(data, values) {
608
+ for (let i = 0; i < Math.min(values.length, qposAdr.length); i++) {
609
+ data.qpos[qposAdr[i]] = values[i];
610
+ }
611
+ },
612
+ writeCtrl(data, values) {
613
+ for (let i = 0; i < Math.min(values.length, joints.length); i++) {
614
+ const adr = joints[i].ctrlAdr;
615
+ if (adr !== null) data.ctrl[adr] = values[i];
616
+ }
617
+ }
618
+ };
619
+ }
620
+ function getActuatedJoints(mjModel) {
621
+ const result = [];
622
+ for (let actuatorId = 0; actuatorId < mjModel.nu; actuatorId++) {
623
+ const jointId = getActuatorJointId(mjModel, actuatorId);
624
+ if (jointId < 0) continue;
625
+ const actuator = getActuatorInfo(mjModel, actuatorId);
626
+ result.push({
627
+ ...getJointInfo(mjModel, jointId),
628
+ actuatorId,
629
+ actuatorName: actuator.name,
630
+ ctrlAdr: actuatorId,
631
+ ctrlRange: actuator.range
632
+ });
633
+ }
634
+ return result;
635
+ }
636
+ function getControlMap(mjModel) {
637
+ const actuatorIds = Array.from({ length: mjModel.nu }, (_, i) => i).filter((id) => getActuatorJointId(mjModel, id) >= 0);
638
+ const jointIds = actuatorIds.map((id) => getActuatorJointId(mjModel, id));
639
+ return buildControlGroup(mjModel, jointIds, actuatorIds) ?? createContiguousControlGroup(mjModel, 0);
640
+ }
641
+ function resolveControlGroup(mjModel, selector) {
642
+ if (selector.actuators) {
643
+ const actuatorIds = orderedActuatorIdsFromSelector(mjModel, selector.actuators);
644
+ const jointIds = actuatorIds.map((id) => getActuatorJointId(mjModel, id));
645
+ return buildControlGroup(mjModel, jointIds, actuatorIds);
646
+ }
647
+ if (selector.joints) {
648
+ return buildControlGroup(mjModel, orderedJointIdsFromSelector(mjModel, selector.joints));
649
+ }
650
+ if (selector.siteName) {
651
+ const siteId = findSiteByName(mjModel, selector.siteName);
652
+ const bodyId = siteId >= 0 ? mjModel.site_bodyid?.[siteId] ?? -1 : -1;
653
+ return buildControlGroup(mjModel, inferScalarJointChain(mjModel, bodyId));
654
+ }
655
+ if (selector.bodyName) {
656
+ return buildControlGroup(mjModel, inferScalarJointChain(mjModel, findBodyByName(mjModel, selector.bodyName)));
657
+ }
658
+ return getControlMap(mjModel);
659
+ }
660
+ function createContiguousControlGroup(mjModel, count) {
661
+ const n = Math.max(0, Math.min(count, mjModel.nq, mjModel.nu));
662
+ const joints = [];
663
+ const actuators = [];
664
+ const qposAdr = [];
665
+ const dofAdr = [];
666
+ const ctrlAdr = [];
667
+ for (let i = 0; i < n; i++) {
668
+ qposAdr.push(i);
669
+ dofAdr.push(i);
670
+ ctrlAdr.push(i);
671
+ const jointId = Array.from({ length: mjModel.njnt }, (_, id) => id).find((id) => mjModel.jnt_qposadr[id] === i);
672
+ const actuator = getActuatorInfo(mjModel, i);
673
+ actuators.push(actuator);
674
+ joints.push({
675
+ ...jointId !== void 0 ? getJointInfo(mjModel, jointId) : {
676
+ id: i,
677
+ name: `qpos${i}`,
678
+ type: 3,
679
+ typeName: "hinge",
680
+ range: unlimitedRange(),
681
+ limited: false,
682
+ bodyId: -1,
683
+ qposAdr: i,
684
+ dofAdr: i
685
+ },
686
+ actuatorId: i,
687
+ actuatorName: actuator.name,
688
+ ctrlAdr: i,
689
+ ctrlRange: actuator.range
690
+ });
691
+ }
692
+ return {
693
+ joints,
694
+ actuators,
695
+ qposAdr,
696
+ dofAdr,
697
+ ctrlAdr,
698
+ readQpos(data) {
699
+ return new Float64Array(qposAdr.map((adr) => data.qpos[adr] ?? 0));
700
+ },
701
+ readCtrl(data) {
702
+ return new Float64Array(ctrlAdr.map((adr) => data.ctrl[adr] ?? 0));
703
+ },
704
+ writeQpos(data, values) {
705
+ for (let i = 0; i < Math.min(values.length, qposAdr.length); i++) data.qpos[qposAdr[i]] = values[i];
706
+ },
707
+ writeCtrl(data, values) {
708
+ for (let i = 0; i < Math.min(values.length, ctrlAdr.length); i++) data.ctrl[ctrlAdr[i]] = values[i];
709
+ }
710
+ };
711
+ }
382
712
  function sceneObjectToXml(obj) {
383
713
  const joint = obj.freejoint ? "<freejoint/>" : "";
384
714
  const pos = obj.position.map((v) => v.toFixed(3)).join(" ");
@@ -403,6 +733,15 @@ function ensureDir(mujoco, fname) {
403
733
  }
404
734
  }
405
735
  }
736
+ function loadModelFromPath(mujoco, path) {
737
+ if (mujoco.MjModel.from_xml_path) {
738
+ return mujoco.MjModel.from_xml_path(path);
739
+ }
740
+ if (mujoco.MjModel.loadFromXML) {
741
+ return mujoco.MjModel.loadFromXML(path);
742
+ }
743
+ throw new Error("MuJoCo WASM module does not expose an XML path loader");
744
+ }
406
745
  async function loadScene(mujoco, config, onProgress) {
407
746
  try {
408
747
  mujoco.FS.unmount("/working");
@@ -486,7 +825,7 @@ async function loadScene(mujoco, config, onProgress) {
486
825
  }
487
826
  }
488
827
  onProgress?.("Loading model...");
489
- const mjModel = mujoco.MjModel.loadFromXML(`/working/${config.sceneFile}`);
828
+ const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
490
829
  const mjData = new mujoco.MjData(mjModel);
491
830
  if (config.homeJoints) {
492
831
  const homeCount = Math.min(config.homeJoints.length, mjModel.nu);
@@ -609,7 +948,7 @@ function SceneRenderer(props) {
609
948
  }
610
949
  );
611
950
  }
612
- var JOINT_TYPE_NAMES = ["free", "ball", "slide", "hinge"];
951
+ var JOINT_TYPE_NAMES2 = ["free", "ball", "slide", "hinge"];
613
952
  var GEOM_TYPE_NAMES = ["plane", "hfield", "sphere", "capsule", "ellipsoid", "cylinder", "box", "mesh"];
614
953
  var SENSOR_TYPE_NAMES = {
615
954
  0: "touch",
@@ -662,6 +1001,22 @@ var SENSOR_TYPE_NAMES = {
662
1001
  47: "plugin",
663
1002
  48: "user"
664
1003
  };
1004
+ var EMPTY_CONTROL_GROUP = {
1005
+ joints: [],
1006
+ actuators: [],
1007
+ qposAdr: [],
1008
+ dofAdr: [],
1009
+ ctrlAdr: [],
1010
+ readQpos: () => new Float64Array(0),
1011
+ readCtrl: () => new Float64Array(0),
1012
+ writeQpos: () => {
1013
+ },
1014
+ writeCtrl: () => {
1015
+ }
1016
+ };
1017
+ function isMutableApiRef(ref) {
1018
+ return typeof ref === "object" && ref !== null && "current" in ref;
1019
+ }
665
1020
  var _applyForce = new Float64Array(3);
666
1021
  var _applyTorque = new Float64Array(3);
667
1022
  var _applyPoint = new Float64Array(3);
@@ -859,7 +1214,7 @@ function MujocoSimProvider({
859
1214
  if (externalApiRef) {
860
1215
  if (typeof externalApiRef === "function") {
861
1216
  externalApiRef(api2);
862
- } else {
1217
+ } else if (isMutableApiRef(externalApiRef)) {
863
1218
  externalApiRef.current = api2;
864
1219
  }
865
1220
  }
@@ -1080,18 +1435,20 @@ function MujocoSimProvider({
1080
1435
  if (!model || !data) return [];
1081
1436
  const contacts = [];
1082
1437
  const ncon = data.ncon;
1083
- for (let i = 0; i < ncon; i++) {
1084
- const c = getContact(data, i);
1085
- if (!c) break;
1086
- contacts.push({
1087
- geom1: c.geom1,
1088
- geom1Name: getName(model, model.name_geomadr[c.geom1]),
1089
- geom2: c.geom2,
1090
- geom2Name: getName(model, model.name_geomadr[c.geom2]),
1091
- pos: [c.pos[0], c.pos[1], c.pos[2]],
1092
- depth: c.dist
1093
- });
1094
- }
1438
+ withContacts(data, (contactArray) => {
1439
+ for (let i = 0; i < ncon; i++) {
1440
+ const c = getContact(contactArray, i);
1441
+ if (!c) break;
1442
+ contacts.push({
1443
+ geom1: c.geom1,
1444
+ geom1Name: getName(model, model.name_geomadr[c.geom1]),
1445
+ geom2: c.geom2,
1446
+ geom2Name: getName(model, model.name_geomadr[c.geom2]),
1447
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
1448
+ depth: c.dist
1449
+ });
1450
+ }
1451
+ });
1095
1452
  return contacts;
1096
1453
  }, []);
1097
1454
  const getBodies = useCallback(() => {
@@ -1114,14 +1471,14 @@ function MujocoSimProvider({
1114
1471
  const result = [];
1115
1472
  for (let i = 0; i < model.njnt; i++) {
1116
1473
  const type = model.jnt_type[i];
1117
- const limited = model.jnt_limited ? model.jnt_limited[i] !== 0 : false;
1474
+ const range = [model.jnt_range[2 * i], model.jnt_range[2 * i + 1]];
1118
1475
  result.push({
1119
1476
  id: i,
1120
1477
  name: getName(model, model.name_jntadr[i]),
1121
1478
  type,
1122
- typeName: JOINT_TYPE_NAMES[type] ?? `unknown(${type})`,
1123
- range: [model.jnt_range[2 * i], model.jnt_range[2 * i + 1]],
1124
- limited,
1479
+ typeName: JOINT_TYPE_NAMES2[type] ?? `unknown(${type})`,
1480
+ range,
1481
+ limited: range[0] < range[1],
1125
1482
  bodyId: model.jnt_bodyid[i],
1126
1483
  qposAdr: model.jnt_qposadr[i],
1127
1484
  dofAdr: model.jnt_dofadr[i]
@@ -1173,6 +1530,18 @@ function MujocoSimProvider({
1173
1530
  }
1174
1531
  return result;
1175
1532
  }, []);
1533
+ const getControlMapApi = useCallback(() => {
1534
+ const model = mjModelRef.current;
1535
+ return model ? getControlMap(model) : EMPTY_CONTROL_GROUP;
1536
+ }, []);
1537
+ const getActuatedJointsApi = useCallback(() => {
1538
+ const model = mjModelRef.current;
1539
+ return model ? getActuatedJoints(model) : [];
1540
+ }, []);
1541
+ const resolveControlGroupApi = useCallback((selector) => {
1542
+ const model = mjModelRef.current;
1543
+ return model ? resolveControlGroup(model, selector) : null;
1544
+ }, []);
1176
1545
  const getSensors = useCallback(() => {
1177
1546
  const model = mjModelRef.current;
1178
1547
  if (!model) return [];
@@ -1409,6 +1778,9 @@ function MujocoSimProvider({
1409
1778
  getQvel,
1410
1779
  setCtrl,
1411
1780
  getCtrl: getCtrl2,
1781
+ getControlMap: getControlMapApi,
1782
+ getActuatedJoints: getActuatedJointsApi,
1783
+ resolveControlGroup: resolveControlGroupApi,
1412
1784
  applyForce,
1413
1785
  applyTorque: applyTorqueApi,
1414
1786
  setExternalForce,
@@ -1455,6 +1827,9 @@ function MujocoSimProvider({
1455
1827
  getQvel,
1456
1828
  setCtrl,
1457
1829
  getCtrl2,
1830
+ getControlMapApi,
1831
+ getActuatedJointsApi,
1832
+ resolveControlGroupApi,
1458
1833
  applyForce,
1459
1834
  applyTorqueApi,
1460
1835
  setExternalForce,
@@ -1648,16 +2023,16 @@ var GenericIK = class {
1648
2023
  * @param model MuJoCo model
1649
2024
  * @param data MuJoCo data (qpos will be temporarily modified, then restored)
1650
2025
  * @param siteId Index of the end-effector site to control
1651
- * @param numJoints Number of arm joints (assumes qpos[0..numJoints-1])
2026
+ * @param qposAdr qpos addresses for scalar joints in solve order
1652
2027
  * @param targetPos Target position in world frame
1653
2028
  * @param targetQuat Target orientation in world frame
1654
- * @param currentQ Current joint angles (length = numJoints)
2029
+ * @param currentQ Current joint angles matching qposAdr order
1655
2030
  * @param opts Optional solver parameters
1656
2031
  * @returns Joint angles array, or null if solver diverged
1657
2032
  */
1658
- solve(model, data, siteId, numJoints, targetPos, targetQuat, currentQ, opts) {
2033
+ solve(model, data, siteId, qposAdr, targetPos, targetQuat, currentQ, opts) {
1659
2034
  const o = { ...DEFAULTS, ...opts };
1660
- const n = numJoints;
2035
+ const n = qposAdr.length;
1661
2036
  const savedQpos = new Float64Array(data.qpos.length);
1662
2037
  savedQpos.set(data.qpos);
1663
2038
  const R_target = quatToMat3(targetQuat);
@@ -1674,8 +2049,9 @@ var GenericIK = class {
1674
2049
  const pertSiteMat = new Float64Array(9);
1675
2050
  let bestQ = null;
1676
2051
  let bestErr = Infinity;
2052
+ if (n === 0) return null;
1677
2053
  for (let iter = 0; iter < o.maxIterations; iter++) {
1678
- for (let i = 0; i < n; i++) data.qpos[i] = q[i];
2054
+ for (let i = 0; i < n; i++) data.qpos[qposAdr[i]] = q[i];
1679
2055
  this.mujoco.mj_forward(model, data);
1680
2056
  const sp = data.site_xpos;
1681
2057
  const sm = data.site_xmat;
@@ -1704,8 +2080,9 @@ var GenericIK = class {
1704
2080
  }
1705
2081
  if (errNorm < o.tolerance) break;
1706
2082
  for (let j = 0; j < n; j++) {
1707
- const saved = data.qpos[j];
1708
- data.qpos[j] = q[j] + o.epsilon;
2083
+ const adr = qposAdr[j];
2084
+ const saved = data.qpos[adr];
2085
+ data.qpos[adr] = q[j] + o.epsilon;
1709
2086
  this.mujoco.mj_forward(model, data);
1710
2087
  for (let i = 0; i < 3; i++) pertSitePos[i] = sp[off3 + i];
1711
2088
  for (let i = 0; i < 9; i++) pertSiteMat[i] = sm[off9 + i];
@@ -1716,9 +2093,9 @@ var GenericIK = class {
1716
2093
  J[3 * n + j] = dRot[0] / o.epsilon * o.rotWeight;
1717
2094
  J[4 * n + j] = dRot[1] / o.epsilon * o.rotWeight;
1718
2095
  J[5 * n + j] = dRot[2] / o.epsilon * o.rotWeight;
1719
- data.qpos[j] = saved;
2096
+ data.qpos[adr] = saved;
1720
2097
  }
1721
- for (let i = 0; i < n; i++) data.qpos[i] = q[i];
2098
+ for (let i = 0; i < n; i++) data.qpos[qposAdr[i]] = q[i];
1722
2099
  for (let r = 0; r < 6; r++) {
1723
2100
  for (let c = 0; c < 6; c++) {
1724
2101
  let sum = 0;
@@ -1890,6 +2267,7 @@ var useIkController = createControllerHook(
1890
2267
  const ikCalculatingRef = useRef(false);
1891
2268
  const ikTargetRef = useRef(new THREE11.Group());
1892
2269
  const siteIdRef = useRef(-1);
2270
+ const controlGroupRef = useRef(null);
1893
2271
  const genericIkRef = useRef(new GenericIK(mujocoRef.current));
1894
2272
  const firstIkEnableRef = useRef(true);
1895
2273
  const needsInitialSync = useRef(true);
@@ -1905,31 +2283,39 @@ var useIkController = createControllerHook(
1905
2283
  useEffect(() => {
1906
2284
  if (!config) {
1907
2285
  siteIdRef.current = -1;
2286
+ controlGroupRef.current = null;
1908
2287
  return;
1909
2288
  }
1910
2289
  const model = mjModelRef.current;
1911
2290
  if (!model || status !== "ready") {
1912
2291
  siteIdRef.current = -1;
2292
+ controlGroupRef.current = null;
1913
2293
  return;
1914
2294
  }
1915
2295
  siteIdRef.current = findSiteByName(model, config.siteName);
2296
+ controlGroupRef.current = config.numJoints !== void 0 ? createContiguousControlGroup(model, config.numJoints) : resolveControlGroup(model, {
2297
+ siteName: config.siteName,
2298
+ joints: config.joints,
2299
+ actuators: config.actuators
2300
+ });
1916
2301
  const data = mjDataRef.current;
1917
2302
  if (data && ikTargetRef.current) {
1918
2303
  syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
1919
2304
  }
1920
- }, [config?.siteName, status, mjModelRef, mjDataRef, config]);
2305
+ }, [config?.siteName, config?.numJoints, config?.joints, config?.actuators, status, mjModelRef, mjDataRef, config]);
1921
2306
  const ikSolveFn = useCallback(
1922
2307
  (pos, quat, currentQ) => {
1923
2308
  if (!config) return null;
1924
2309
  if (config.ikSolveFn) return config.ikSolveFn(pos, quat, currentQ);
1925
2310
  const model = mjModelRef.current;
1926
2311
  const data = mjDataRef.current;
1927
- if (!model || !data || siteIdRef.current === -1) return null;
2312
+ const controlGroup = controlGroupRef.current;
2313
+ if (!model || !data || !controlGroup || siteIdRef.current === -1) return null;
1928
2314
  return genericIkRef.current.solve(
1929
2315
  model,
1930
2316
  data,
1931
2317
  siteIdRef.current,
1932
- config.numJoints,
2318
+ controlGroup.qposAdr,
1933
2319
  pos,
1934
2320
  quat,
1935
2321
  currentQ,
@@ -1968,12 +2354,17 @@ var useIkController = createControllerHook(
1968
2354
  const target = ikTargetRef.current;
1969
2355
  if (!target) return;
1970
2356
  ikCalculatingRef.current = true;
1971
- const numJoints = config.numJoints;
1972
- const currentQ = [];
1973
- for (let i = 0; i < numJoints; i++) currentQ.push(data.qpos[i]);
1974
- const solution = ikSolveFnRef.current(target.position, target.quaternion, currentQ);
2357
+ const controlGroup = controlGroupRef.current;
2358
+ if (!controlGroup) return;
2359
+ const currentQ = Array.from(controlGroup.readQpos(data));
2360
+ const solution = config.ikSolveFn ? config.ikSolveFn(target.position, target.quaternion, currentQ, {
2361
+ model,
2362
+ data,
2363
+ siteId: siteIdRef.current,
2364
+ controlGroup
2365
+ }) : ikSolveFnRef.current(target.position, target.quaternion, currentQ);
1975
2366
  if (solution) {
1976
- for (let i = 0; i < numJoints; i++) data.ctrl[i] = solution[i];
2367
+ controlGroup.writeCtrl(data, solution);
1977
2368
  }
1978
2369
  });
1979
2370
  useEffect(() => {
@@ -2255,18 +2646,20 @@ function ContactMarkers({
2255
2646
  }
2256
2647
  const ncon = data.ncon;
2257
2648
  const count = Math.min(ncon, maxContacts);
2258
- for (let i = 0; i < count; i++) {
2259
- const c = getContact(data, i);
2260
- if (!c) {
2261
- mesh.count = i;
2262
- mesh.instanceMatrix.needsUpdate = true;
2263
- return;
2649
+ let resolvedCount = count;
2650
+ withContacts(data, (contactArray) => {
2651
+ for (let i = 0; i < count; i++) {
2652
+ const c = getContact(contactArray, i);
2653
+ if (!c) {
2654
+ resolvedCount = i;
2655
+ return;
2656
+ }
2657
+ _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
2658
+ _dummy.updateMatrix();
2659
+ mesh.setMatrixAt(i, _dummy.matrix);
2264
2660
  }
2265
- _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
2266
- _dummy.updateMatrix();
2267
- mesh.setMatrixAt(i, _dummy.matrix);
2268
- }
2269
- mesh.count = count;
2661
+ });
2662
+ mesh.count = resolvedCount;
2270
2663
  mesh.instanceMatrix.needsUpdate = true;
2271
2664
  });
2272
2665
  if (status !== "ready") return null;
@@ -2807,22 +3200,24 @@ function Debug({
2807
3200
  if (!data || pool.length === 0) return;
2808
3201
  const ncon = data.ncon;
2809
3202
  let arrowIdx = 0;
2810
- for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
2811
- const c = getContact(data, i);
2812
- if (!c) break;
2813
- _contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
2814
- _contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
2815
- const force = Math.abs(c.dist) * 100;
2816
- const length = Math.min(force * 0.01, 0.1);
2817
- if (length > 1e-3 && arrowIdx < pool.length) {
2818
- const arrow = pool[arrowIdx];
2819
- arrow.position.copy(_contactPos);
2820
- arrow.setDirection(_contactNormal);
2821
- arrow.setLength(length, length * 0.3, length * 0.15);
2822
- arrow.visible = true;
2823
- arrowIdx++;
3203
+ withContacts(data, (contactArray) => {
3204
+ for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
3205
+ const c = getContact(contactArray, i);
3206
+ if (!c) break;
3207
+ _contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
3208
+ _contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
3209
+ const force = Math.abs(c.dist) * 100;
3210
+ const length = Math.min(force * 0.01, 0.1);
3211
+ if (length > 1e-3 && arrowIdx < pool.length) {
3212
+ const arrow = pool[arrowIdx];
3213
+ arrow.position.copy(_contactPos);
3214
+ arrow.setDirection(_contactNormal);
3215
+ arrow.setLength(length, length * 0.3, length * 0.15);
3216
+ arrow.visible = true;
3217
+ arrowIdx++;
3218
+ }
2824
3219
  }
2825
- }
3220
+ });
2826
3221
  for (let i = arrowIdx; i < pool.length; i++) {
2827
3222
  pool[i].visible = false;
2828
3223
  }
@@ -3053,23 +3448,25 @@ function useContacts(bodyName, callback) {
3053
3448
  }
3054
3449
  const contacts = [];
3055
3450
  const filterBody = bodyIdRef.current;
3056
- for (let i = 0; i < ncon; i++) {
3057
- const c = getContact(data, i);
3058
- if (!c) break;
3059
- if (filterBody >= 0) {
3060
- const b1 = model.geom_bodyid[c.geom1];
3061
- const b2 = model.geom_bodyid[c.geom2];
3062
- if (b1 !== filterBody && b2 !== filterBody) continue;
3451
+ withContacts(data, (contactArray) => {
3452
+ for (let i = 0; i < ncon; i++) {
3453
+ const c = getContact(contactArray, i);
3454
+ if (!c) break;
3455
+ if (filterBody >= 0) {
3456
+ const b1 = model.geom_bodyid[c.geom1];
3457
+ const b2 = model.geom_bodyid[c.geom2];
3458
+ if (b1 !== filterBody && b2 !== filterBody) continue;
3459
+ }
3460
+ contacts.push({
3461
+ geom1: c.geom1,
3462
+ geom1Name: getGeomNameCached(model, c.geom1),
3463
+ geom2: c.geom2,
3464
+ geom2Name: getGeomNameCached(model, c.geom2),
3465
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
3466
+ depth: c.dist
3467
+ });
3063
3468
  }
3064
- contacts.push({
3065
- geom1: c.geom1,
3066
- geom1Name: getGeomNameCached(model, c.geom1),
3067
- geom2: c.geom2,
3068
- geom2Name: getGeomNameCached(model, c.geom2),
3069
- pos: [c.pos[0], c.pos[1], c.pos[2]],
3070
- depth: c.dist
3071
- });
3072
- }
3469
+ });
3073
3470
  contactsRef.current = contacts;
3074
3471
  callbackRef.current?.(contacts);
3075
3472
  });
@@ -4058,7 +4455,7 @@ function useCameraAnimation() {
4058
4455
  * WASM fields used: model.ntendon, model.ten_wrapadr, model.ten_wrapnum
4059
4456
  * data.wrap_xpos, data.ten_wrapadr (runtime)
4060
4457
  *
4061
- * Note: ten_rgba and ten_width are NOT available in mujoco-js 0.0.7.
4458
+ * Note: ten_rgba and ten_width may not be available in all MuJoCo WASM builds.
4062
4459
  * Tendons use a default color and width.
4063
4460
  */
4064
4461
  /**
@@ -4176,6 +4573,6 @@ function useCameraAnimation() {
4176
4573
  * useCameraAnimation — composable camera animation hook.
4177
4574
  */
4178
4575
 
4179
- export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, SceneLights, TendonRenderer, TrajectoryPlayer, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getContact, getName, loadScene, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
4576
+ export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, SceneLights, TendonRenderer, TrajectoryPlayer, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
4180
4577
  //# sourceMappingURL=index.js.map
4181
4578
  //# sourceMappingURL=index.js.map