mujoco-react 10.0.1 → 10.2.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/spark.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as _sparkjsdev_spark from '@sparkjsdev/spark';
3
- import { n as SplatEnvironmentProps, q as PairedSplatEnvironmentConfig, S as SceneConfig, t as SplatEnvironmentReadiness, o as VisualScenarioConfig } from './types-BaSMqJHT.js';
3
+ import { n as SplatEnvironmentProps, q as PairedSplatEnvironmentConfig, S as SceneConfig, t as SplatEnvironmentReadiness, o as VisualScenarioConfig } from './types-CLD5K3JD.js';
4
4
  import 'react';
5
5
  import '@react-three/fiber';
6
6
  import 'three';
package/dist/spark.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useSplatSceneConfig, useSplatEnvironment, SplatEnvironment, CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY } from './chunk-QTCAVQS6.js';
1
+ import { useSplatSceneConfig, useSplatEnvironment, SplatEnvironment, CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY } from './chunk-3BMNRSS2.js';
2
2
  import { useThree } from '@react-three/fiber';
3
3
  import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
4
4
  import * as THREE from 'three';
@@ -177,7 +177,15 @@ interface MujocoModel {
177
177
  geom_contype: Int32Array;
178
178
  geom_conaffinity: Int32Array;
179
179
  geom_friction: Float64Array;
180
+ mat_texid: Int32Array;
181
+ mat_texrepeat: Float32Array;
182
+ mat_texuniform: Uint8Array;
180
183
  mat_rgba: Float32Array;
184
+ tex_adr: Int32Array;
185
+ tex_data: Uint8Array;
186
+ tex_height: Int32Array;
187
+ tex_nchannel: Int32Array;
188
+ tex_width: Int32Array;
181
189
  mesh_vertadr: Int32Array;
182
190
  mesh_vertnum: Int32Array;
183
191
  mesh_faceadr: Int32Array;
@@ -313,6 +321,8 @@ interface MujocoModule {
313
321
  }
314
322
  interface SceneObject {
315
323
  name: string;
324
+ /** MuJoCo geom name. Defaults to `${name}_geom` for generated objects. */
325
+ geomName?: string;
316
326
  type: 'box' | 'sphere' | 'cylinder';
317
327
  size: [number, number, number];
318
328
  position: [number, number, number];
@@ -563,6 +573,35 @@ interface RayHit {
563
573
  geomId: number;
564
574
  distance: number;
565
575
  }
576
+ type ImagePointCoordinateSpace = 'normalized' | 'normalized-1000' | 'pixel' | 'ndc';
577
+ interface ImagePointProjectionOptions extends CameraFrameCaptureOptions {
578
+ /** X coordinate in the selected coordinate space. Defaults to normalized 0..1. */
579
+ x: number;
580
+ /** Y coordinate in the selected coordinate space. Defaults to normalized 0..1 with origin at top-left. */
581
+ y: number;
582
+ /**
583
+ * Coordinate convention for x/y:
584
+ * - normalized: 0..1 image coordinates, top-left origin
585
+ * - normalized-1000: 0..1000 detector coordinates, top-left origin
586
+ * - pixel: pixel coordinates, top-left origin
587
+ * - ndc: Three.js normalized device coordinates, -1..1
588
+ */
589
+ coordinateSpace?: ImagePointCoordinateSpace;
590
+ /** Image width for pixel coordinates. Falls back to `width` or renderer canvas width. */
591
+ imageWidth?: number;
592
+ /** Image height for pixel coordinates. Falls back to `height` or renderer canvas height. */
593
+ imageHeight?: number;
594
+ /** Ignore hits farther than this distance from the camera ray origin. */
595
+ maxDistance?: number;
596
+ }
597
+ interface ImagePointProjectionResult extends RayHit {
598
+ /** NDC coordinates used for raycasting. */
599
+ ndc: [number, number];
600
+ /** Image dimensions used when interpreting pixel coordinates. */
601
+ imageSize: [number, number];
602
+ /** Camera pose provenance, matching camera-frame capture results. */
603
+ source: CameraFrameCaptureSource;
604
+ }
566
605
  interface ModelOptions {
567
606
  timestep: number;
568
607
  gravity: [number, number, number];
@@ -598,16 +637,123 @@ interface PolicyObservationInput {
598
637
  interface PolicyInferenceInput extends PolicyObservationInput {
599
638
  observation: PolicyVector;
600
639
  }
640
+ type PolicyActionChunk = readonly PolicyVector[];
641
+ type PolicyInferenceOutput = PolicyVector | PolicyActionChunk;
642
+ type PolicyInferenceResult = PolicyInferenceOutput | Promise<PolicyInferenceOutput>;
601
643
  interface PolicyActionInput extends PolicyInferenceInput {
602
644
  action: PolicyVector;
603
645
  }
646
+ interface PolicyAPI {
647
+ readonly isRunning: boolean;
648
+ start: () => void;
649
+ stop: () => void;
650
+ clearQueue: () => void;
651
+ reset: () => void;
652
+ readonly inFlight: boolean;
653
+ readonly queuedActions: number;
654
+ readonly lastObservation: PolicyVector | null;
655
+ readonly lastAction: PolicyVector | null;
656
+ readonly lastError: unknown;
657
+ }
604
658
  interface PolicyConfig {
605
659
  frequency: number;
606
660
  enabled?: boolean;
661
+ /** Start async inference while this many queued actions remain. Defaults to 0. */
662
+ prefetchThreshold?: number;
663
+ /**
664
+ * How async action chunks update the queue.
665
+ * - append preserves legacy FIFO behavior.
666
+ * - replace is useful for receding-horizon policies where a fresh chunk should supersede stale queued actions.
667
+ */
668
+ queueStrategy?: 'append' | 'replace';
669
+ /**
670
+ * Clear queued actions and ignore in-flight async results when `stop()` is called.
671
+ * Defaults to false so callers can choose pause/resume behavior explicitly.
672
+ */
673
+ clearQueueOnStop?: boolean;
607
674
  onObservation: (input: PolicyObservationInput) => PolicyVector;
608
675
  /** Run policy inference. Omit to pass observations directly to `onAction` for custom inline controllers. */
609
- infer?: (input: PolicyInferenceInput) => PolicyVector;
676
+ infer?: (input: PolicyInferenceInput) => PolicyInferenceResult;
610
677
  onAction: (input: PolicyActionInput) => void;
678
+ /** Called when async inference rejects. */
679
+ onError?: (error: unknown) => void;
680
+ }
681
+ interface RemotePolicyRequestInput extends PolicyInferenceInput {
682
+ /** True for the first request after hook construction or `reset()`. */
683
+ reset: boolean;
684
+ /** Zero-based request index since construction or `reset()`. */
685
+ requestIndex: number;
686
+ /** Aborts when the request is no longer needed, e.g. after pause/reset. */
687
+ signal: AbortSignal;
688
+ }
689
+ interface RemotePolicyRequestInfo extends RemotePolicyRequestInput {
690
+ body: unknown;
691
+ requestStartedAt: number;
692
+ }
693
+ interface RemotePolicyResponseInfo extends RemotePolicyRequestInfo {
694
+ response: Response;
695
+ responseBody: unknown;
696
+ responseFinishedAt: number;
697
+ requestMs: number;
698
+ }
699
+ type RemotePolicyStatus = 'idle' | 'requesting' | 'ready' | 'error' | 'aborted';
700
+ interface RemotePolicyConfig extends Omit<PolicyConfig, 'infer'> {
701
+ endpoint: string | URL;
702
+ method?: string;
703
+ headers?: HeadersInit;
704
+ credentials?: RequestCredentials;
705
+ /** Additional external cancellation signal for remote inference requests. */
706
+ signal?: AbortSignal;
707
+ /**
708
+ * Abort the active HTTP request when `stop()` or `reset()` is called.
709
+ * Defaults to true so paused policies stop consuming server work.
710
+ */
711
+ abortOnStop?: boolean;
712
+ fetcher?: typeof fetch;
713
+ requestInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'credentials' | 'signal'>;
714
+ buildRequest?: (input: RemotePolicyRequestInput) => unknown | Promise<unknown>;
715
+ readResponse?: (response: Response) => unknown | Promise<unknown>;
716
+ parseResponse?: (responseBody: unknown, info: RemotePolicyResponseInfo) => PolicyInferenceResult;
717
+ onRequest?: (info: RemotePolicyRequestInfo) => void;
718
+ onResponse?: (info: RemotePolicyResponseInfo) => void;
719
+ }
720
+ interface RemotePolicyAPI extends PolicyAPI {
721
+ abort: (reason?: unknown) => void;
722
+ readonly remoteStatus: RemotePolicyStatus;
723
+ readonly requestCount: number;
724
+ readonly responseCount: number;
725
+ readonly lastRequestBody: unknown;
726
+ readonly lastResponseBody: unknown;
727
+ readonly lastHttpStatus: number | null;
728
+ readonly lastRequestMs: number | null;
729
+ }
730
+ interface PolicyCameraFrameStream extends CameraFrameCaptureOptions {
731
+ /** Image key used in policy payloads, e.g. `image`, `front`, or `wrist_cam`. */
732
+ key: string;
733
+ /** Additional payload keys that should receive the same data URL. */
734
+ aliases?: readonly string[];
735
+ }
736
+ interface PolicyCameraFrameCaptureOptions {
737
+ streams: readonly PolicyCameraFrameStream[];
738
+ /**
739
+ * Include `observation.images.${key}` for every captured stream.
740
+ * Defaults to true because LeRobot-style policies usually use these names.
741
+ */
742
+ includeObservationImageAliases?: boolean;
743
+ }
744
+ interface PolicyCameraFrameCaptureResult {
745
+ frames: Record<string, CameraFrameCaptureResult>;
746
+ images: Record<string, string>;
747
+ /** Human-readable source summary for UI/debug telemetry. */
748
+ sourceSummary: string;
749
+ capturedAt: number;
750
+ }
751
+ interface PolicyCameraFrameCaptureAPI {
752
+ status: FrameCaptureStatus;
753
+ error: Error | null;
754
+ isCapturing: boolean;
755
+ capture: (options?: Partial<PolicyCameraFrameCaptureOptions>) => Promise<PolicyCameraFrameCaptureResult>;
756
+ reset: () => void;
611
757
  }
612
758
  type ObservationOutput = 'float32' | 'float64';
613
759
  interface ObservationConfig {
@@ -977,6 +1123,7 @@ interface MujocoSimAPI {
977
1123
  bodyId: number;
978
1124
  geomId: number;
979
1125
  } | null;
1126
+ projectImagePointTo3D(options: ImagePointProjectionOptions): ImagePointProjectionResult | null;
980
1127
  setBodyMass(name: Bodies, mass: number): void;
981
1128
  setGeomFriction(name: Geoms, friction: [number, number, number]): void;
982
1129
  setGeomSize(name: Geoms, size: [number, number, number]): void;
@@ -1026,6 +1173,10 @@ interface CameraFrameCaptureOptions {
1026
1173
  lookAt?: CameraFrameCaptureVector3;
1027
1174
  quaternion?: CameraFrameCaptureQuaternion;
1028
1175
  up?: CameraFrameCaptureVector3;
1176
+ /** Local-space offset applied after resolving a mounted MuJoCo camera/site/body pose. */
1177
+ positionOffset?: CameraFrameCaptureVector3;
1178
+ /** Local-space rotation applied after resolving a mounted MuJoCo camera/site/body pose. Array values use Three.js order: [x, y, z, w]. */
1179
+ quaternionOffset?: CameraFrameCaptureQuaternion;
1029
1180
  width?: number;
1030
1181
  height?: number;
1031
1182
  type?: string;
@@ -1035,6 +1186,16 @@ interface CameraFrameCaptureOptions {
1035
1186
  far?: number;
1036
1187
  /** Provenance for the camera pose used by the capture. Usually set by the MuJoCo provider. */
1037
1188
  source?: CameraFrameCaptureSource;
1189
+ /** Hide rendered Three objects whose MuJoCo geom group is in this list. */
1190
+ hiddenGeomGroups?: readonly number[];
1191
+ /** When provided, only rendered Three objects whose MuJoCo geom group is in this list are visible. */
1192
+ visibleGeomGroups?: readonly number[];
1193
+ /** Hide rendered Three objects whose MuJoCo geom name is in this list. */
1194
+ hiddenGeomNames?: readonly string[];
1195
+ /** Optional clear color for this capture only. Defaults to the renderer's current clear color. */
1196
+ background?: THREE.ColorRepresentation;
1197
+ /** Optional clear alpha for this capture only. Defaults to the renderer's current clear alpha. */
1198
+ backgroundAlpha?: number;
1038
1199
  }
1039
1200
  type CameraFrameCaptureSource = {
1040
1201
  kind: 'mujoco-camera';
@@ -1219,4 +1380,4 @@ interface ArrayJointStateResult {
1219
1380
  velocity: React__default.RefObject<Float64Array>;
1220
1381
  }
1221
1382
 
1222
- export { type ArrayJointStateResult as $, type ActuatedJointInfo as A, type BodyProps as B, type ControlGroupInfo as C, type DragInteractionProps as D, type ScenarioLightingPreset as E, type SplatEnvironmentMetadataInput as F, type SplatEnvironmentMetadata as G, type SplatSceneInput as H, type IkConfig as I, type DebugProps as J, type GeomInfo as K, type ContactListenerProps as L, type MujocoContextValue as M, type ActuatorInfo as N, type ObservationConfig as O, type PhysicsStepCallback as P, type Sites as Q, type ReadyCallbackInput as R, type SceneConfig as S, type TrajectoryPlayerProps as T, type SitePositionResult as U, type VisualScenarioEffectsProps as V, type Sensors as W, type SensorHandle as X, type SensorInfo as Y, type Joints as Z, type ScalarJointStateResult as _, type MujocoCanvasProps as a, type Models as a$, type JointStateOptions as a0, type JointStateResult as a1, type Bodies as a2, type BodyStateResult as a3, type Actuators as a4, type CtrlHandle as a5, type ContactInfo as a6, type KeyboardTeleopConfig as a7, type KeyboardIkTargetConfig as a8, type PolicyConfig as a9, type CameraFrameSequenceStepInput as aA, type CameraInfo as aB, type ControlJointInfo as aC, type FrameCaptureStatus as aD, type FrameCaptureTarget as aE, type FrameCaptureTargetRef as aF, type Geoms as aG, type IKSolveFn as aH, type IkGizmoDragInput as aI, type IkSolveInput as aJ, type JointInfo as aK, type JointStateKind as aL, type KeyBinding as aM, type KeyboardIkTargetAction as aN, type KeyboardIkTargetBinding as aO, type Keyframes as aP, ModelActuators as aQ, ModelBodies as aR, ModelCameras as aS, ModelGeoms as aT, ModelJoints as aU, ModelKeyframes as aV, type ModelOptions as aW, type ModelResource as aX, ModelResources as aY, ModelSensors as aZ, ModelSites as a_, type PolicyVector as aa, type ObservationHandle as ab, type TrajectoryInput as ac, type TrajectoryStateChangeInput as ad, type PlaybackState as ae, type TrajectoryFrame as af, type FrameCaptureOptions as ag, type FrameCaptureResult as ah, type FrameCaptureBlobResult as ai, type FrameCaptureAPI as aj, type CameraFrameCaptureOptions as ak, type CameraFrameCaptureAPI as al, type CameraFrameSequenceRecorderAPI as am, type Cameras as an, type CameraFrameSequenceCamera as ao, type CameraFrameCaptureSource as ap, type CameraFrameSequenceOptions as aq, type CameraFrameSequenceResult as ar, type CameraFrameCaptureResult as as, type CameraFrameCaptureBlobResult as at, type BodyInfo as au, type CameraFrameCaptureQuaternion as av, type CameraFrameCaptureVector3 as aw, type CameraFrameSequenceCameraSummary as ax, type CameraFrameSequenceFrame as ay, type CameraFrameSequenceSampleInput as az, type MujocoSimAPI as b, type MujocoContact as b0, type MujocoContactArray as b1, type MujocoFrameCaptureOptions as b2, type ObservationLayoutItem as b3, type ObservationOutput as b4, type PhysicsConfig as b5, type PhysicsStepInput as b6, type PolicyActionInput as b7, type PolicyInferenceInput as b8, type PolicyObservationInput as b9, type RayHit as ba, type Register as bb, type RegisteredModelMap as bc, type ResetCallbackInput as bd, type ResolvedScenarioCameraConfig as be, type ResolvedScenarioMaterialConfig as bf, type ResourceSelector as bg, type ScenarioCameraConfig as bh, type ScenarioMaterialConfig as bi, type SceneMarker as bj, type SceneObject as bk, type SensorResult as bl, type SiteInfo as bm, type SplatAssetConfig as bn, type SplatScenarioConfig as bo, type StateSnapshot as bp, type TrajectoryData as bq, type TrajectoryFrameCallbackInput as br, type VisualScenarioMaterialFilterInput as bs, type XmlPatch as bt, getContact as bu, registerModelResources as bv, type StepCallbackInput as c, type SelectionCallbackInput as d, type MujocoModule as e, type MujocoModel as f, type MujocoData as g, type ControlGroupSelector as h, type ObservationResult as i, type IkContextValue as j, type IkGizmoProps as k, type SceneLightsProps as l, type ScenarioLightingProps as m, type SplatEnvironmentProps as n, type VisualScenarioConfig as o, type SplatRendererKind as p, type PairedSplatEnvironmentConfig as q, type SplatFormat as r, type SplatCollisionProxyConfig as s, type SplatEnvironmentReadiness as t, type SplatCollisionPrimitive as u, SplatEnvironmentReadinessStatus as v, type SplatSceneConfigInput as w, type SplatSceneConfigState as x, type VisualScenarioExecutionContextInput as y, type VisualScenarioExecutionContext as z };
1383
+ export { type ArrayJointStateResult as $, type ActuatedJointInfo as A, type BodyProps as B, type ControlGroupInfo as C, type DragInteractionProps as D, type ScenarioLightingPreset as E, type SplatEnvironmentMetadataInput as F, type SplatEnvironmentMetadata as G, type SplatSceneInput as H, type IkConfig as I, type DebugProps as J, type GeomInfo as K, type ContactListenerProps as L, type MujocoContextValue as M, type ActuatorInfo as N, type ObservationConfig as O, type PhysicsStepCallback as P, type Sites as Q, type ReadyCallbackInput as R, type SceneConfig as S, type TrajectoryPlayerProps as T, type SitePositionResult as U, type VisualScenarioEffectsProps as V, type Sensors as W, type SensorHandle as X, type SensorInfo as Y, type Joints as Z, type ScalarJointStateResult as _, type MujocoCanvasProps as a, ModelActuators as a$, type JointStateOptions as a0, type JointStateResult as a1, type Bodies as a2, type BodyStateResult as a3, type Geoms as a4, type Actuators as a5, type CtrlHandle as a6, type ContactInfo as a7, type KeyboardTeleopConfig as a8, type KeyboardIkTargetConfig as a9, type CameraFrameSequenceRecorderAPI as aA, type CameraFrameCaptureResult as aB, type CameraFrameCaptureBlobResult as aC, type ImagePointCoordinateSpace as aD, type ImagePointProjectionOptions as aE, type ImagePointProjectionResult as aF, type PolicyVector as aG, type BodyInfo as aH, type CameraFrameCaptureQuaternion as aI, type CameraFrameCaptureVector3 as aJ, type CameraFrameSequenceCameraSummary as aK, type CameraFrameSequenceFrame as aL, type CameraFrameSequenceSampleInput as aM, type CameraFrameSequenceStepInput as aN, type CameraInfo as aO, type ControlJointInfo as aP, type FrameCaptureTarget as aQ, type FrameCaptureTargetRef as aR, type IKSolveFn as aS, type IkGizmoDragInput as aT, type IkSolveInput as aU, type JointInfo as aV, type JointStateKind as aW, type KeyBinding as aX, type KeyboardIkTargetAction as aY, type KeyboardIkTargetBinding as aZ, type Keyframes as a_, type PolicyConfig as aa, type PolicyAPI as ab, type RemotePolicyConfig as ac, type RemotePolicyAPI as ad, type ObservationHandle as ae, type ObservationOutput as af, type TrajectoryInput as ag, type TrajectoryStateChangeInput as ah, type PlaybackState as ai, type TrajectoryFrame as aj, type FrameCaptureOptions as ak, type FrameCaptureResult as al, type FrameCaptureBlobResult as am, type FrameCaptureAPI as an, type CameraFrameCaptureOptions as ao, type CameraFrameCaptureAPI as ap, type Cameras as aq, type CameraFrameSequenceCamera as ar, type CameraFrameCaptureSource as as, type CameraFrameSequenceOptions as at, type CameraFrameSequenceResult as au, type PolicyCameraFrameStream as av, type PolicyCameraFrameCaptureOptions as aw, type PolicyCameraFrameCaptureResult as ax, type FrameCaptureStatus as ay, type PolicyCameraFrameCaptureAPI as az, type MujocoSimAPI as b, ModelBodies as b0, ModelCameras as b1, ModelGeoms as b2, ModelJoints as b3, ModelKeyframes as b4, type ModelOptions as b5, type ModelResource as b6, ModelResources as b7, ModelSensors as b8, ModelSites as b9, type SceneMarker as bA, type SceneObject as bB, type SensorResult as bC, type SiteInfo as bD, type SplatAssetConfig as bE, type SplatScenarioConfig as bF, type StateSnapshot as bG, type TrajectoryData as bH, type TrajectoryFrameCallbackInput as bI, type VisualScenarioMaterialFilterInput as bJ, type XmlPatch as bK, getContact as bL, registerModelResources as bM, type Models as ba, type MujocoContact as bb, type MujocoContactArray as bc, type MujocoFrameCaptureOptions as bd, type ObservationLayoutItem as be, type PhysicsConfig as bf, type PhysicsStepInput as bg, type PolicyActionChunk as bh, type PolicyActionInput as bi, type PolicyInferenceInput as bj, type PolicyInferenceOutput as bk, type PolicyInferenceResult as bl, type PolicyObservationInput as bm, type RayHit as bn, type Register as bo, type RegisteredModelMap as bp, type RemotePolicyRequestInfo as bq, type RemotePolicyRequestInput as br, type RemotePolicyResponseInfo as bs, type RemotePolicyStatus as bt, type ResetCallbackInput as bu, type ResolvedScenarioCameraConfig as bv, type ResolvedScenarioMaterialConfig as bw, type ResourceSelector as bx, type ScenarioCameraConfig as by, type ScenarioMaterialConfig as bz, type StepCallbackInput as c, type SelectionCallbackInput as d, type MujocoModule as e, type MujocoModel as f, type MujocoData as g, type ControlGroupSelector as h, type ObservationResult as i, type IkContextValue as j, type IkGizmoProps as k, type SceneLightsProps as l, type ScenarioLightingProps as m, type SplatEnvironmentProps as n, type VisualScenarioConfig as o, type SplatRendererKind as p, type PairedSplatEnvironmentConfig as q, type SplatFormat as r, type SplatCollisionProxyConfig as s, type SplatEnvironmentReadiness as t, type SplatCollisionPrimitive as u, SplatEnvironmentReadinessStatus as v, type SplatSceneConfigInput as w, type SplatSceneConfigState as x, type VisualScenarioExecutionContextInput as y, type VisualScenarioExecutionContext as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mujoco-react",
3
- "version": "10.0.1",
3
+ "version": "10.2.0",
4
4
  "description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -5,9 +5,10 @@
5
5
 
6
6
  import { useFrame } from '@react-three/fiber';
7
7
  import type { ThreeElements } from '@react-three/fiber';
8
- import { useEffect, useMemo, useRef } from 'react';
8
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
9
9
  import * as THREE from 'three';
10
10
  import { GeomBuilder } from '../rendering/GeomBuilder';
11
+ import { CAMERA_FRAME_CAPTURE_PRE_RENDER_USER_DATA_KEY } from '../rendering/cameraFrameCapture';
11
12
  import { MujocoModel } from '../types';
12
13
  import { getName } from '../core/SceneLoader';
13
14
  import { useMujocoContext } from '../core/MujocoSimProvider';
@@ -74,8 +75,7 @@ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
74
75
  bodyRefs.current = refs;
75
76
  }, [status, geomBuilder, mjModelRef]);
76
77
 
77
- // Sync body positions from mjData every frame
78
- useFrame(() => {
78
+ const syncBodiesToData = useCallback(() => {
79
79
  const data = mjDataRef.current;
80
80
  if (!data) return;
81
81
  const bodies = bodyRefs.current;
@@ -121,12 +121,19 @@ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
121
121
  );
122
122
  }
123
123
  }
124
- });
124
+ }, [interpolateRef, interpolationStateRef, mjDataRef]);
125
+
126
+ // Sync body positions from mjData every frame
127
+ useFrame(syncBodiesToData);
125
128
 
126
129
  return (
127
130
  <group
128
131
  {...props}
129
132
  ref={groupRef}
133
+ userData={{
134
+ ...props.userData,
135
+ [CAMERA_FRAME_CAPTURE_PRE_RENDER_USER_DATA_KEY]: syncBodiesToData,
136
+ }}
130
137
  onDoubleClick={(e) => {
131
138
  if (typeof props.onDoubleClick === 'function') props.onDoubleClick(e);
132
139
  e.stopPropagation();
@@ -31,6 +31,8 @@ import {
31
31
  ControlGroupSelector,
32
32
  ContactInfo,
33
33
  GeomInfo,
34
+ ImagePointProjectionOptions,
35
+ ImagePointProjectionResult,
34
36
  JointInfo,
35
37
  LoadFromFilesOptions,
36
38
  LocalMujocoFile,
@@ -61,6 +63,7 @@ import {
61
63
  getCameraFrameCaptureSourceTarget,
62
64
  isMountedCameraFrameCaptureSource,
63
65
  } from '../rendering/cameraFrameSource';
66
+ import { projectImagePointTo3D as projectImagePointTo3DFromScene } from '../rendering/imageProjection';
64
67
  import {
65
68
  loadScene,
66
69
  createSceneConfigFromFiles,
@@ -188,6 +191,56 @@ function omitResolvedCameraSelectors(
188
191
  return rest;
189
192
  }
190
193
 
194
+ function vector3FromCaptureValue(value: NonNullable<CameraFrameCaptureOptions['positionOffset']>) {
195
+ return value instanceof THREE.Vector3
196
+ ? value.clone()
197
+ : new THREE.Vector3(value[0], value[1], value[2]);
198
+ }
199
+
200
+ function quaternionFromCaptureValue(value: NonNullable<CameraFrameCaptureOptions['quaternionOffset']>) {
201
+ return value instanceof THREE.Quaternion
202
+ ? value.clone()
203
+ : new THREE.Quaternion(value[0], value[1], value[2], value[3]);
204
+ }
205
+
206
+ function applyMountedCameraPoseOffsets(
207
+ options: CameraFrameCaptureOptions,
208
+ position: [number, number, number],
209
+ quaternion: [number, number, number, number]
210
+ ) {
211
+ const resolvedPosition = new THREE.Vector3(position[0], position[1], position[2]);
212
+ const resolvedQuaternion = new THREE.Quaternion(
213
+ quaternion[0],
214
+ quaternion[1],
215
+ quaternion[2],
216
+ quaternion[3]
217
+ );
218
+
219
+ if (options.positionOffset) {
220
+ resolvedPosition.add(
221
+ vector3FromCaptureValue(options.positionOffset).applyQuaternion(resolvedQuaternion)
222
+ );
223
+ }
224
+
225
+ if (options.quaternionOffset) {
226
+ resolvedQuaternion.multiply(quaternionFromCaptureValue(options.quaternionOffset)).normalize();
227
+ }
228
+
229
+ return {
230
+ position: [
231
+ resolvedPosition.x,
232
+ resolvedPosition.y,
233
+ resolvedPosition.z,
234
+ ] as [number, number, number],
235
+ quaternion: [
236
+ resolvedQuaternion.x,
237
+ resolvedQuaternion.y,
238
+ resolvedQuaternion.z,
239
+ resolvedQuaternion.w,
240
+ ] as [number, number, number, number],
241
+ };
242
+ }
243
+
191
244
  function countMountedCameraSelectors(options: CameraFrameCaptureOptions) {
192
245
  return Number(Boolean(options.cameraName)) +
193
246
  Number(Boolean(options.siteName)) +
@@ -1033,10 +1086,11 @@ export function MujocoSimProvider({
1033
1086
  );
1034
1087
  }
1035
1088
 
1089
+ const pose = applyMountedCameraPoseOffsets(options, position, quaternion);
1090
+
1036
1091
  return {
1037
1092
  ...baseOptions,
1038
- position,
1039
- quaternion,
1093
+ ...pose,
1040
1094
  fov: options.fov ?? model.cam_fovy?.[cameraId],
1041
1095
  source: { kind: 'mujoco-camera', cameraName: options.cameraName },
1042
1096
  };
@@ -1048,10 +1102,15 @@ export function MujocoSimProvider({
1048
1102
  throw new Error(`MuJoCo site "${options.siteName}" was not found.`);
1049
1103
  }
1050
1104
 
1105
+ const pose = applyMountedCameraPoseOffsets(
1106
+ options,
1107
+ vector3FromArray(data.site_xpos, siteId * 3),
1108
+ quaternionFromXmat(data.site_xmat, siteId * 9)
1109
+ );
1110
+
1051
1111
  return {
1052
1112
  ...baseOptions,
1053
- position: vector3FromArray(data.site_xpos, siteId * 3),
1054
- quaternion: quaternionFromXmat(data.site_xmat, siteId * 9),
1113
+ ...pose,
1055
1114
  source: { kind: 'mujoco-site', siteName: options.siteName },
1056
1115
  };
1057
1116
  }
@@ -1067,10 +1126,15 @@ export function MujocoSimProvider({
1067
1126
  );
1068
1127
  }
1069
1128
 
1129
+ const pose = applyMountedCameraPoseOffsets(
1130
+ options,
1131
+ vector3FromArray(data.xpos, bodyId * 3),
1132
+ quaternionFromXmat(data.xmat, bodyId * 9)
1133
+ );
1134
+
1070
1135
  return {
1071
1136
  ...baseOptions,
1072
- position: vector3FromArray(data.xpos, bodyId * 3),
1073
- quaternion: quaternionFromXmat(data.xmat, bodyId * 9),
1137
+ ...pose,
1074
1138
  source: { kind: 'mujoco-body', bodyName: options.bodyName },
1075
1139
  };
1076
1140
  }
@@ -1529,6 +1593,31 @@ export function MujocoSimProvider({
1529
1593
  [camera, gl]
1530
1594
  );
1531
1595
 
1596
+ const projectImagePointTo3D = useCallback(
1597
+ (options: ImagePointProjectionOptions): ImagePointProjectionResult | null => {
1598
+ const {
1599
+ x,
1600
+ y,
1601
+ coordinateSpace,
1602
+ imageWidth,
1603
+ imageHeight,
1604
+ maxDistance,
1605
+ ...captureOptions
1606
+ } = options;
1607
+ const resolvedCaptureOptions = resolveCameraCaptureOptions(captureOptions);
1608
+ return projectImagePointTo3DFromScene(gl, scene, camera, {
1609
+ ...resolvedCaptureOptions,
1610
+ x,
1611
+ y,
1612
+ coordinateSpace,
1613
+ imageWidth,
1614
+ imageHeight,
1615
+ maxDistance,
1616
+ });
1617
+ },
1618
+ [camera, gl, resolveCameraCaptureOptions, scene]
1619
+ );
1620
+
1532
1621
  // --- Domain randomization ---
1533
1622
 
1534
1623
  const setBodyMass = useCallback((name: string, mass: number): void => {
@@ -1615,6 +1704,7 @@ export function MujocoSimProvider({
1615
1704
  captureCameraFrameBlob: captureCameraFrameBlobApi,
1616
1705
  recordCameraSequence: recordCameraSequenceApi,
1617
1706
  project2DTo3D,
1707
+ projectImagePointTo3D,
1618
1708
  setBodyMass,
1619
1709
  setGeomFriction,
1620
1710
  setGeomSize,
@@ -1635,6 +1725,7 @@ export function MujocoSimProvider({
1635
1725
  captureCameraFrameApi, captureCameraFrameBlobApi,
1636
1726
  recordCameraSequenceApi,
1637
1727
  project2DTo3D,
1728
+ projectImagePointTo3D,
1638
1729
  setBodyMass, setGeomFriction, setGeomSize,
1639
1730
  ]
1640
1731
  );
@@ -482,8 +482,14 @@ export function createContiguousControlGroup(mjModel: MujocoModel, count: number
482
482
  */
483
483
  function sceneObjectToXml(obj: SceneObject): string {
484
484
  const joint = obj.freejoint ? '<freejoint/>' : '';
485
+ const geomName = obj.geomName ?? `${obj.name}_geom`;
485
486
  const pos = obj.position.map((v) => v.toFixed(3)).join(' ');
486
- const size = obj.size.map((v) => v.toFixed(3)).join(' ');
487
+ const sizeValues = obj.type === 'sphere'
488
+ ? obj.size.slice(0, 1)
489
+ : obj.type === 'cylinder'
490
+ ? obj.size.slice(0, 2)
491
+ : obj.size;
492
+ const size = sizeValues.map((v) => v.toFixed(3)).join(' ');
487
493
  const rgba = obj.rgba.join(' ');
488
494
  const mass = obj.mass ? ` mass="${obj.mass}"` : '';
489
495
  const friction = obj.friction ? ` friction="${obj.friction}"` : '';
@@ -492,7 +498,7 @@ function sceneObjectToXml(obj: SceneObject): string {
492
498
  const condim = obj.condim ? ` condim="${obj.condim}"` : '';
493
499
  const group = obj.group !== undefined ? ` group="${obj.group}"` : '';
494
500
  // Always set contype/conaffinity=1 so objects collide regardless of model defaults
495
- 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}${group}/></body>`;
501
+ return `<body name="${obj.name}" pos="${pos}">${joint}<geom name="${geomName}" type="${obj.type}" size="${size}" rgba="${rgba}" contype="1" conaffinity="1"${mass}${friction}${solref}${solimp}${condim}${group}/></body>`;
496
502
  }
497
503
 
498
504
  /** Create virtual directory structure for a file path. */
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ *
5
+ * Bounded contact history for rollout verification and debugging.
6
+ */
7
+
8
+ import { useCallback, useRef } from 'react';
9
+ import type { RefObject } from 'react';
10
+ import { useAfterPhysicsStep } from '../core/MujocoSimProvider';
11
+ import { getName } from '../core/SceneLoader';
12
+ import { getContact, withContacts } from '../types';
13
+ import type { ContactInfo, MujocoModel } from '../types';
14
+
15
+ export interface ContactHistoryEntry extends ContactInfo {
16
+ time: number;
17
+ body1: number;
18
+ body1Name: string;
19
+ body2: number;
20
+ body2Name: string;
21
+ }
22
+
23
+ export interface ContactHistoryOptions {
24
+ maxLength?: number;
25
+ bodyNames?: readonly string[];
26
+ geomNames?: readonly string[];
27
+ includeWorldBody?: boolean;
28
+ }
29
+
30
+ export interface ContactHistoryHandle {
31
+ entries: RefObject<ContactHistoryEntry[]>;
32
+ clear: () => void;
33
+ countPair: (nameA: string, nameB: string) => number;
34
+ }
35
+
36
+ const geomNameCacheByModel = new WeakMap<MujocoModel, Map<number, string>>();
37
+ const bodyNameCacheByModel = new WeakMap<MujocoModel, Map<number, string>>();
38
+
39
+ function getCachedName(
40
+ cacheByModel: WeakMap<MujocoModel, Map<number, string>>,
41
+ model: MujocoModel,
42
+ id: number,
43
+ address: number
44
+ ) {
45
+ if (id < 0) return '';
46
+ let cache = cacheByModel.get(model);
47
+ if (!cache) {
48
+ cache = new Map();
49
+ cacheByModel.set(model, cache);
50
+ }
51
+ let name = cache.get(id);
52
+ if (name === undefined) {
53
+ name = getName(model, address);
54
+ cache.set(id, name);
55
+ }
56
+ return name;
57
+ }
58
+
59
+ function matchesFilter(
60
+ entry: ContactHistoryEntry,
61
+ bodyNames: readonly string[] | undefined,
62
+ geomNames: readonly string[] | undefined,
63
+ includeWorldBody: boolean
64
+ ) {
65
+ if (!includeWorldBody && (entry.body1 === 0 || entry.body2 === 0)) return false;
66
+ if (
67
+ bodyNames &&
68
+ !bodyNames.includes(entry.body1Name) &&
69
+ !bodyNames.includes(entry.body2Name)
70
+ ) {
71
+ return false;
72
+ }
73
+ if (
74
+ geomNames &&
75
+ !geomNames.includes(entry.geom1Name) &&
76
+ !geomNames.includes(entry.geom2Name)
77
+ ) {
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+
83
+ export function useContactHistory(options: ContactHistoryOptions = {}): ContactHistoryHandle {
84
+ const entriesRef = useRef<ContactHistoryEntry[]>([]);
85
+ const optionsRef = useRef(options);
86
+ optionsRef.current = options;
87
+
88
+ const clear = useCallback(() => {
89
+ entriesRef.current = [];
90
+ }, []);
91
+
92
+ const countPair = useCallback((nameA: string, nameB: string) => {
93
+ let count = 0;
94
+ for (const entry of entriesRef.current) {
95
+ const matchesBodies =
96
+ (entry.body1Name === nameA && entry.body2Name === nameB) ||
97
+ (entry.body1Name === nameB && entry.body2Name === nameA);
98
+ const matchesGeoms =
99
+ (entry.geom1Name === nameA && entry.geom2Name === nameB) ||
100
+ (entry.geom1Name === nameB && entry.geom2Name === nameA);
101
+ if (matchesBodies || matchesGeoms) count += 1;
102
+ }
103
+ return count;
104
+ }, []);
105
+
106
+ useAfterPhysicsStep(({ model, data }) => {
107
+ if ((data.ncon ?? 0) <= 0) return;
108
+
109
+ const {
110
+ maxLength = 2000,
111
+ bodyNames,
112
+ geomNames,
113
+ includeWorldBody = false,
114
+ } = optionsRef.current;
115
+ if (maxLength <= 0) return;
116
+
117
+ const nextEntries: ContactHistoryEntry[] = [];
118
+ withContacts(data, (contacts) => {
119
+ for (let index = 0; index < data.ncon; index += 1) {
120
+ const contact = getContact(contacts, index);
121
+ if (!contact) break;
122
+ const body1 = model.geom_bodyid[contact.geom1] ?? -1;
123
+ const body2 = model.geom_bodyid[contact.geom2] ?? -1;
124
+ const entry: ContactHistoryEntry = {
125
+ geom1: contact.geom1,
126
+ geom2: contact.geom2,
127
+ geom1Name: getCachedName(geomNameCacheByModel, model, contact.geom1, model.name_geomadr[contact.geom1]),
128
+ geom2Name: getCachedName(geomNameCacheByModel, model, contact.geom2, model.name_geomadr[contact.geom2]),
129
+ body1,
130
+ body2,
131
+ body1Name: body1 >= 0 ? getCachedName(bodyNameCacheByModel, model, body1, model.name_bodyadr[body1]) : '',
132
+ body2Name: body2 >= 0 ? getCachedName(bodyNameCacheByModel, model, body2, model.name_bodyadr[body2]) : '',
133
+ pos: [contact.pos[0], contact.pos[1], contact.pos[2]],
134
+ depth: contact.dist,
135
+ time: data.time,
136
+ };
137
+ if (matchesFilter(entry, bodyNames, geomNames, includeWorldBody)) {
138
+ nextEntries.push(entry);
139
+ }
140
+ }
141
+ });
142
+
143
+ if (nextEntries.length === 0) return;
144
+ entriesRef.current.push(...nextEntries);
145
+ if (entriesRef.current.length > maxLength) {
146
+ entriesRef.current.splice(0, entriesRef.current.length - maxLength);
147
+ }
148
+ });
149
+
150
+ return {
151
+ entries: entriesRef,
152
+ clear,
153
+ countPair,
154
+ };
155
+ }