mujoco-react 8.4.2 → 8.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mujoco-react",
3
- "version": "8.4.2",
3
+ "version": "8.5.0",
4
4
  "description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import type {
7
+ Bodies,
8
+ MujocoData,
9
+ MujocoModel,
10
+ ObservationConfig,
11
+ ObservationLayoutItem,
12
+ ObservationResult,
13
+ } from '../types';
14
+ import { findBodyByName, findSensorByName, findSiteByName } from './SceneLoader';
15
+
16
+ function append(
17
+ values: number[],
18
+ layout: ObservationLayoutItem[],
19
+ name: string,
20
+ chunk: ArrayLike<number>
21
+ ) {
22
+ const start = values.length;
23
+ for (let i = 0; i < chunk.length; i++) values.push(chunk[i] ?? 0);
24
+ layout.push({ name, start, size: chunk.length });
25
+ }
26
+
27
+ function appendScalar(
28
+ values: number[],
29
+ layout: ObservationLayoutItem[],
30
+ name: string,
31
+ value: number
32
+ ) {
33
+ const start = values.length;
34
+ values.push(value);
35
+ layout.push({ name, start, size: 1 });
36
+ }
37
+
38
+ function namedBodyList(names: Bodies | readonly Bodies[] | undefined): readonly Bodies[] {
39
+ if (!names) return [];
40
+ return typeof names === 'string' ? [names] : names;
41
+ }
42
+
43
+ function normalizedGravity(model: MujocoModel): [number, number, number] {
44
+ const gx = model.opt.gravity[0] ?? 0;
45
+ const gy = model.opt.gravity[1] ?? 0;
46
+ const gz = model.opt.gravity[2] ?? -9.81;
47
+ const length = Math.hypot(gx, gy, gz);
48
+ if (length === 0) return [0, 0, 0];
49
+ return [gx / length, gy / length, gz / length];
50
+ }
51
+
52
+ function rotateWorldVectorToBody(data: MujocoData, bodyId: number, world: readonly [number, number, number]): [number, number, number] {
53
+ const adr = bodyId * 4;
54
+ const w = data.xquat[adr] ?? 1;
55
+ const x = -(data.xquat[adr + 1] ?? 0);
56
+ const y = -(data.xquat[adr + 2] ?? 0);
57
+ const z = -(data.xquat[adr + 3] ?? 0);
58
+ const vx = world[0];
59
+ const vy = world[1];
60
+ const vz = world[2];
61
+
62
+ const tx = 2 * (y * vz - z * vy);
63
+ const ty = 2 * (z * vx - x * vz);
64
+ const tz = 2 * (x * vy - y * vx);
65
+
66
+ return [
67
+ vx + w * tx + y * tz - z * ty,
68
+ vy + w * ty + z * tx - x * tz,
69
+ vz + w * tz + x * ty - y * tx,
70
+ ];
71
+ }
72
+
73
+ /**
74
+ * Build a flat observation vector plus a layout map from live MuJoCo state.
75
+ *
76
+ * Missing named resources are skipped. The returned layout is the source of
77
+ * truth for the vector produced from the current model.
78
+ */
79
+ export function buildObservation(
80
+ model: MujocoModel,
81
+ data: MujocoData,
82
+ config: ObservationConfig
83
+ ): ObservationResult {
84
+ const values: number[] = [];
85
+ const layout: ObservationLayoutItem[] = [];
86
+
87
+ if (config.time) appendScalar(values, layout, 'time', data.time);
88
+ if (config.qpos) append(values, layout, 'qpos', data.qpos);
89
+ if (config.qvel) append(values, layout, 'qvel', data.qvel);
90
+ if (config.ctrl) append(values, layout, 'ctrl', data.ctrl);
91
+ if (config.act) append(values, layout, 'act', data.act);
92
+ if (config.sensordata) append(values, layout, 'sensordata', data.sensordata);
93
+
94
+ for (const name of config.sensors ?? []) {
95
+ const sensorId = findSensorByName(model, name);
96
+ if (sensorId < 0) continue;
97
+ const start = model.sensor_adr[sensorId] ?? 0;
98
+ const dim = model.sensor_dim[sensorId] ?? 0;
99
+ append(values, layout, `sensor:${name}`, data.sensordata.subarray(start, start + dim));
100
+ }
101
+
102
+ for (const name of config.sites ?? []) {
103
+ const siteId = findSiteByName(model, name);
104
+ if (siteId < 0) continue;
105
+ const start = siteId * 3;
106
+ append(values, layout, `site:${name}:xpos`, data.site_xpos.subarray(start, start + 3));
107
+ }
108
+
109
+ const gravity = normalizedGravity(model);
110
+ for (const name of namedBodyList(config.projectedGravity)) {
111
+ const bodyId = findBodyByName(model, name);
112
+ if (bodyId < 0) continue;
113
+ append(values, layout, `projectedGravity:${name}`, rotateWorldVectorToBody(data, bodyId, gravity));
114
+ }
115
+
116
+ return {
117
+ values: config.output === 'float64' ? new Float64Array(values) : new Float32Array(values),
118
+ layout,
119
+ };
120
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { useMemo, useRef } from 'react';
7
+ import { useMujocoContext } from '../core/MujocoSimProvider';
8
+ import { buildObservation } from '../core/ObservationBuilder';
9
+ import type { ObservationConfig, ObservationHandle, ObservationResult } from '../types';
10
+
11
+ const EMPTY_OBSERVATION: ObservationResult = {
12
+ values: new Float32Array(0),
13
+ layout: [],
14
+ };
15
+
16
+ /**
17
+ * Live observation reader for policy loops and telemetry.
18
+ *
19
+ * The handle is stable; call `read()` inside callbacks to sample the latest
20
+ * MuJoCo model/data state without forcing React renders.
21
+ */
22
+ export function useObservation(config: ObservationConfig): ObservationHandle {
23
+ const { mjModelRef, mjDataRef } = useMujocoContext();
24
+ const configRef = useRef(config);
25
+ configRef.current = config;
26
+
27
+ return useMemo(() => ({
28
+ read() {
29
+ const model = mjModelRef.current;
30
+ const data = mjDataRef.current;
31
+ if (!model || !data) return EMPTY_OBSERVATION;
32
+ return buildObservation(model, data, configRef.current);
33
+ },
34
+ readValues() {
35
+ return this.read().values;
36
+ },
37
+ }), [mjDataRef, mjModelRef]);
38
+ }
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export {
26
26
  resolveControlGroup,
27
27
  createContiguousControlGroup,
28
28
  } from './core/SceneLoader';
29
+ export { buildObservation } from './core/ObservationBuilder';
29
30
 
30
31
  // Controller factory
31
32
  export { createController, createControllerHook } from './core/createController';
@@ -57,6 +58,7 @@ export { useCtrl } from './hooks/useCtrl';
57
58
  export { useContacts, useContactEvents } from './hooks/useContacts';
58
59
  export { useKeyboardTeleop } from './hooks/useKeyboardTeleop';
59
60
  export { usePolicy } from './hooks/usePolicy';
61
+ export { useObservation } from './hooks/useObservation';
60
62
  export { useTrajectoryPlayer } from './hooks/useTrajectoryPlayer';
61
63
  export { useTrajectoryRecorder } from './hooks/useTrajectoryRecorder';
62
64
  export { useGamepad } from './hooks/useGamepad';
@@ -112,6 +114,12 @@ export type {
112
114
  KeyboardTeleopConfig,
113
115
  // Policy
114
116
  PolicyConfig,
117
+ // Observations
118
+ ObservationConfig,
119
+ ObservationHandle,
120
+ ObservationLayoutItem,
121
+ ObservationOutput,
122
+ ObservationResult,
115
123
  // Component props
116
124
  BodyProps,
117
125
  IkGizmoProps,
package/src/types.ts CHANGED
@@ -595,6 +595,51 @@ export interface PolicyConfig {
595
595
  onAction: (action: Float32Array | Float64Array | number[], model: MujocoModel, data: MujocoData) => void;
596
596
  }
597
597
 
598
+ // ---- Observation Builder ----
599
+
600
+ export type ObservationOutput = 'float32' | 'float64';
601
+
602
+ export interface ObservationConfig {
603
+ /** Include scalar simulation time. */
604
+ time?: boolean;
605
+ /** Include all qpos values. */
606
+ qpos?: boolean;
607
+ /** Include all qvel values. */
608
+ qvel?: boolean;
609
+ /** Include all ctrl values. */
610
+ ctrl?: boolean;
611
+ /** Include all actuator activation values. */
612
+ act?: boolean;
613
+ /** Include all raw sensordata values. */
614
+ sensordata?: boolean;
615
+ /** Include named sensor values in the configured order. */
616
+ sensors?: readonly Sensors[];
617
+ /** Include named site world positions in the configured order. */
618
+ sites?: readonly Sites[];
619
+ /** Include world gravity projected into each named body's local frame. */
620
+ projectedGravity?: Bodies | readonly Bodies[];
621
+ /** Output array type. Defaults to Float32Array. */
622
+ output?: ObservationOutput;
623
+ }
624
+
625
+ export interface ObservationLayoutItem {
626
+ name: string;
627
+ start: number;
628
+ size: number;
629
+ }
630
+
631
+ export interface ObservationResult {
632
+ values: Float32Array | Float64Array;
633
+ layout: ObservationLayoutItem[];
634
+ }
635
+
636
+ export interface ObservationHandle {
637
+ /** Read a fresh observation from the current live MuJoCo model/data refs. */
638
+ read(): ObservationResult;
639
+ /** Read just the vector values for policy inference. */
640
+ readValues(): Float32Array | Float64Array;
641
+ }
642
+
598
643
  // ---- Debug Component (spec 6.1) ----
599
644
 
600
645
  export interface DebugProps {