/** * @license * SPDX-License-Identifier: Apache-2.0 */ import { useFrame } from '@react-three/fiber'; import { useEffect, useLayoutEffect, useRef } from 'react'; import * as THREE from 'three'; import { useMujocoContext } from '../core/MujocoSimProvider'; import { findBodyByName } from '../core/SceneLoader'; import type { BodyProps, SceneObject } from '../types'; /** * Declarative physics body component. Registers a body definition in the * provider-level registry so it gets injected into the MJCF XML at load time. * * Bodies present at initial mount cause zero extra reloads (useLayoutEffect * runs before the provider's loadScene useEffect). Bodies added/removed after * the initial load trigger a debounced scene reload. */ export function Body({ name, type, size, position = [0, 0, 0], rgba = [0.5, 0.5, 0.5, 1], mass, freejoint, friction, solref, solimp, condim, group, children, }: BodyProps) { const { bodyRegistryRef, hiddenBodiesRef, requestBodyReload, mjDataRef, mjModelRef, status } = useMujocoContext(); const bodyIdRef = useRef(-1); const groupRef = useRef(null); const initialLoadRef = useRef(true); const hasChildren = children != null; // Register in body registry BEFORE the provider's loadScene useEffect fires. useLayoutEffect(() => { const definition: SceneObject = { name, type, size, position, rgba, mass, freejoint, friction, solref, solimp, condim, group, }; bodyRegistryRef.current.set(name, { definition, hasCustomChildren: hasChildren }); if (hasChildren) { hiddenBodiesRef.current.add(name); } return () => { bodyRegistryRef.current.delete(name); hiddenBodiesRef.current.delete(name); if (!initialLoadRef.current) { requestBodyReload(); } }; }, [name, type, size, position, rgba, mass, freejoint, friction, solref, solimp, condim, group, hasChildren, bodyRegistryRef, hiddenBodiesRef, requestBodyReload]); // Resolve body ID once the scene is ready useEffect(() => { if (status !== 'ready') return; const model = mjModelRef.current; if (!model) return; bodyIdRef.current = findBodyByName(model, name); initialLoadRef.current = false; }, [status, name, mjModelRef]); // Sync group transform to body pose each frame (only when children are provided) useFrame(() => { if (!hasChildren) return; const data = mjDataRef.current; const id = bodyIdRef.current; const group = groupRef.current; if (!data || id < 0 || !group) return; const i3 = id * 3; const i4 = id * 4; group.position.set(data.xpos[i3], data.xpos[i3 + 1], data.xpos[i3 + 2]); group.quaternion.set( data.xquat[i4 + 1], data.xquat[i4 + 2], data.xquat[i4 + 3], data.xquat[i4], ); }); if (!hasChildren) return null; return {children}; }