import { useState, type ReactNode } from 'react'; import { Box, Database, FolderTree, Info, Map as MapIcon, Settings2, SlidersHorizontal, } from 'lucide-react'; import type { MapEntry, ModelEntry } from '../../project/types'; import { countProjectSearchResults, ProjectTree, type ProjectTreeFile, } from '../../project/ProjectTree'; import { countModelStructureSearchResults, ModelStructureTree, } from '../../project/ModelStructureTree'; import type { ActuatorInfo, ActuatorParameters, SimulationSnapshot, } from '../../simulation/SimulationSession'; import type { UrdfBaseMode, UrdfLoadMode } from '../../simulation/PhysicsAdapter'; import type { DataRecorderConfig } from '../../simulation/DataRecorder'; import type { ViewerSelection } from '../../viewer/MuJoCoViewer'; import type { ControllerCommand, ControllerStatus } from '../../controller/types'; import type { RLCommand, RLPolicyStatus } from '../../rl/types'; import { Badge, Button, CollapsibleSection, CopyButton, PropertyRow, ResizablePanel, Select, Tabs, } from '../../components/ui'; import { TreeSearchField } from './TreeSearchField'; import { ProjectBreadcrumb } from './ProjectBreadcrumb'; import { PythonControllerPanel } from './PythonControllerPanel'; import { RLPolicyPanel } from './RLPolicyPanel'; import { LocalTrainingPanel } from './LocalTrainingPanel'; import { DataRecordingPanel } from './DataRecordingPanel'; import { PhysicalMapPanel } from './PhysicalMapPanel'; import { MapAssetLibrary } from './MapAssetLibrary'; import { DEFAULT_PHYSICAL_MAP_CONFIG, type MapSelection, type SystemTerrainPreset, } from '../../map/types'; import type { EditableMapDocument, EditableMapObjectType, MapEditorInteractionCallbacks, MapEditorTransformMode, MapObjectPlacementMode, } from '../../map/editor/types'; export function SidebarPanel({ title, side, children, visible = true, }: { title: string; side: 'left' | 'right'; children: ReactNode; visible?: boolean; }) { return ( ); } export function ProjectSidebar({ projectName, files, entries, selectedEntry, snapshot, loading, visible = true, nativeUrdf, mapSelection, editorDocument, activeTab, onActiveTabChange, onRemove, onSelectEntry, onJointHover, onAddMapAsset, onSelectTerrain, onSelectMapObject, }: { projectName?: string; files: ProjectTreeFile[]; entries: ModelEntry[]; selectedEntry?: string; snapshot?: SimulationSnapshot; loading: boolean; visible?: boolean; nativeUrdf: boolean; mapSelection: MapSelection; editorDocument: EditableMapDocument | null; activeTab?: 'project' | 'structure' | 'assets'; onActiveTabChange?: (value: 'project' | 'structure' | 'assets') => void; onRemove: () => void; onSelectEntry: (path: string) => void; onJointHover: (jointId: number | null) => void; onAddMapAsset: ( type: EditableMapObjectType, placementMode: MapObjectPlacementMode, ) => void | Promise; onSelectTerrain: (preset: SystemTerrainPreset) => void; onSelectMapObject: (id: string) => void; }) { const [internalTab, setInternalTab] = useState<'project' | 'structure' | 'assets'>('project'), [fileQuery, setFileQuery] = useState(''), [structureQuery, setStructureQuery] = useState(''); const tab = activeTab ?? internalTab, fileMatches = countProjectSearchResults(files, fileQuery), structureMatches = snapshot ? countModelStructureSearchResults(snapshot.bodies, snapshot.joints, structureQuery) : 0; return ( {projectName ? ( <>
{projectName}
{files.length} 个文件
{ setInternalTab(value); onActiveTabChange?.(value); }} items={[ { value: 'project', label: '工程', icon: , content: ( <>
), }, { value: 'structure', label: '模型结构', icon: , disabled: !snapshot, content: snapshot ? ( <>
) : (

加载模型后显示结构

), }, { value: 'assets', label: '资产', icon: , disabled: !snapshot, content: ( ), }, ]} /> ) : (
导入模型后显示工程资源
)}
); } interface ModelControlsProps { snapshot?: SimulationSnapshot; selection: ViewerSelection | null; selectedFormat?: ModelEntry['format']; loading: boolean; visible?: boolean; urdfMode: UrdfLoadMode; baseMode: UrdfBaseMode; showCollision: boolean; ignoreJointLimits: boolean; jointAdvanced: boolean; angleUnit: 'rad' | 'deg'; forceScale: number; controllerPaths: string[]; selectedControllerPath?: string; controllerStatus?: ControllerStatus; policyPaths: string[]; selectedPolicyPath?: string; policyStatus?: RLPolicyStatus; mapSelection: MapSelection; maps: MapEntry[]; showVisualMap: boolean; showMapCollision: boolean; editorDocument: EditableMapDocument | null; onUrdfMode: (value: UrdfLoadMode) => void; onBaseMode: (value: UrdfBaseMode) => void; onShowCollision: (value: boolean) => void; onResetJoints: () => void; onToggleJointLimits: () => void; onToggleAdvanced: () => void; onToggleAngleUnit: () => void; onActuator: (id: number, value: number) => void; onActuatorParameters: (id: number, parameters: ActuatorParameters) => void; onJoint: (id: number, value: number) => void; onForceScale: (value: number) => void; onSelectControllerPath: (path: string) => void; onLoadControllerPath: (path: string) => void; onImportController: (file: File) => void; onToggleController: (enabled: boolean) => void; onControllerCommand: (command: ControllerCommand) => void; onRemoveController: () => void; onSelectPolicyPath: (path: string) => void; onLoadPolicyPath: (path: string) => void; onImportPolicy: (file: File) => void; onTogglePolicy: (enabled: boolean) => void; onPolicyCommand: (command: RLCommand) => void; onRemovePolicy: () => void; onApplyMap: (value: MapSelection) => void; onEditorPreview: (document: EditableMapDocument | null) => void; onEditorApply: (document: EditableMapDocument) => Promise; onEditorExport: () => void; onEditorConvert: () => Promise; onEditorBindInteraction: (callbacks: MapEditorInteractionCallbacks | null) => void; onEditorSelect: (id: string | null) => void; onEditorTransformMode: (mode: MapEditorTransformMode) => void; onEditorSnapping: (translation: number | null, rotationDegrees: number | null) => void; onMapDisplay: (visual: boolean, collision: boolean) => void; onMapTabOpen?: () => void; onDataRecorderConfigure: (patch: Partial) => void; onDataRecordingStart: () => void; onDataRecordingStop: () => void; onDataRecordingClear: () => void; onDataRecordingExport: (format: 'csv' | 'json') => void; } export function ModelControlsSidebar(props: ModelControlsProps) { const [tab, setTab] = useState<'properties' | 'controls' | 'data' | 'map'>('properties'), s = props.snapshot; if (!s) return (
导入模型后显示属性
); const properties = ( <> {s.model.nbody} Body}>
{props.selectedFormat === 'urdf' && (

MJCF 模式保留 visual mesh、添加物理地面,并将模型最低点对齐到 z=0。

)} {props.selection ? (
} /> } /> value.toFixed(3)).join(', ')} action={} />
) : (

在视口中单击物体

)}
); const controls = ( <> {s.rlPolicy.enabled ? '推理' : '停止'} : undefined} > {s.controller.enabled ? '运行' : '停止'} : undefined} > {s.actuators.length}} > {s.actuators.length ? ( s.actuators.map((actuator) => ( props.onActuator(actuator.id, value)} onParameters={(parameters) => props.onActuatorParameters(actuator.id, parameters)} /> )) ) : (

模型没有驱动器

)}
{s.joints.length}}>
{s.joints.map((joint) => { const scale = joint.type === 3 && props.angleUnit === 'deg' ? 180 / Math.PI : 1, unit = joint.type === 3 ? props.angleUnit === 'deg' ? '°' : ' rad' : joint.type === 2 ? ' m' : ''; return ( props.onJoint(joint.id, value / scale)} /> ); })}

选择“外力施加”,在动态物体上按住拖动,松开即清零。

); return ( { setTab(value); if (value === 'map') props.onMapTabOpen?.(); }} items={[ { value: 'properties', label: '属性', icon: , content: properties, }, { value: 'controls', label: '控制', icon: , content: controls, }, { value: 'data', label: '数据', icon: , content: ( ), }, { value: 'map', label: '地图', icon: , content: ( body.id !== 0 && body.parentId === 0 && !body.name.startsWith('__platform_map_'), ) .map((body) => body.name)} loading={props.loading} nativeUrdf={props.selectedFormat === 'urdf' && props.urdfMode === 'native'} showVisualMap={props.showVisualMap} showMapCollision={props.showMapCollision} editorDocument={props.editorDocument} onEditorPreview={props.onEditorPreview} onEditorApply={props.onEditorApply} onEditorExport={props.onEditorExport} onEditorConvert={props.onEditorConvert} onEditorBindInteraction={props.onEditorBindInteraction} onEditorSelect={props.onEditorSelect} onEditorTransformMode={props.onEditorTransformMode} onEditorSnapping={props.onEditorSnapping} onMapDisplay={props.onMapDisplay} onApply={props.onApplyMap} /> ), }, ]} /> ); } export function ActuatorControl({ actuator, onControl, onParameters, }: { actuator: ActuatorInfo; onControl: (value: number) => void; onParameters: (parameters: ActuatorParameters) => void; }) { const isMotor = actuator.kind === 'motor', isPosition = actuator.kind === 'position', editable = isMotor || isPosition, baseTargetScale = isPosition && actuator.jointType === 3 ? 180 / Math.PI : 1, targetScale = isPosition && Math.abs(actuator.gear) > 1e-9 ? baseTargetScale / actuator.gear : baseTargetScale; const clampForce = (value: number) => actuator.forceLimited ? Math.min(actuator.forceMax, Math.max(actuator.forceMin, value)) : value, physicalScale = actuator.gear * actuator.gain; const forceA = clampForce(actuator.min * actuator.gain) * actuator.gear, forceB = clampForce(actuator.max * actuator.gain) * actuator.gear; const targetA = actuator.min * targetScale, targetB = actuator.max * targetScale, outputMin = isMotor ? Math.min(forceA, forceB) : Math.min(targetA, targetB), outputMax = isMotor ? Math.max(forceA, forceB) : Math.max(targetA, targetB); const outputValue = isMotor ? clampForce(actuator.value * actuator.gain) * actuator.gear : actuator.value * targetScale, outputDisabled = (isMotor && Math.abs(physicalScale) <= 1e-9) || (isPosition && Math.abs(actuator.gear) <= 1e-9); const outputLabel = isMotor ? actuator.jointType === 3 ? '输出力矩' : '输出力' : isPosition ? actuator.jointType === 3 ? '目标角度' : '目标位置' : '控制输入'; const forceUnit = actuator.jointType === 3 ? 'N·m' : actuator.jointType === 2 ? 'N' : '', jointForceA = actuator.forceMin * actuator.gear, jointForceB = actuator.forceMax * actuator.gear, jointForceMin = Math.min(jointForceA, jointForceB), jointForceMax = Math.max(jointForceA, jointForceB); const update = (patch: Partial) => onParameters({ ...actuator, ...patch }), controlLabel = isPosition ? (actuator.jointType === 3 ? '角度' : '位置') : '控制', gearSquared = actuator.gear * actuator.gear; return (
{actuator.name}
{actuator.jointName ? `关节:${actuator.jointName}` : '未关联标量关节'}
{actuator.unit || 'u'}
{actuator.controlCount === 1 ? ( { if (outputDisabled) return; onControl(isMotor ? value / physicalScale : value / targetScale); }} /> ) : (

该驱动器包含 {actuator.controlCount} 个控制分量,请在 MJCF 源码或专用控制器中设置。

)} {actuator.controlCount === 1 && !actuator.ctrlLimited && (
onControl(value / targetScale)} />
)} {editable ? (
常用参数
{isPosition ? ( <> update({ kp: kp / gearSquared })} /> update({ kv: kv / gearSquared })} /> ) : ( <> update({ kp })} /> update({ kv })} /> )}
update({ forceLimited })} />
update( actuator.gear >= 0 ? { forceMin: value / actuator.gear } : { forceMax: value / actuator.gear }, ) } /> update( actuator.gear >= 0 ? { forceMax: value / actuator.gear } : { forceMin: value / actuator.gear }, ) } />

{isPosition ? 'position 伺服使用 kp 跟踪目标位置,kv 提供速度阻尼。' : 'motor 保持力/力矩控制且控制输入不限幅。MJCF 的 motor 没有 kp/kv 属性;这里的 kp、kv 会分别保存为对应 joint 的 stiffness、damping。'}{' '} 参数修改会立即作用于当前模型,并可随 MJCF 导出。

) : (

该驱动器不是可直接编辑的 motor/position 类型,控制值按模型原始单位显示;请在 MJCF 源码中修改专用参数。

)}
); } function ParameterInput({ label, value, onCommit, disabled = false, }: { label: string; value: number; onCommit: (value: number) => void; disabled?: boolean; }) { return ( ); } function ParameterToggle({ label, checked, onChange, }: { label: string; checked: boolean; onChange: (value: boolean) => void; }) { return ( ); } function Check({ label, checked, onChange, }: { label: string; checked: boolean; onChange: (value: boolean) => void; }) { return ( ); } function ControlSlider({ label, value, min, max, onChange, disabled = false, unit = '', advanced = false, limited = false, limitsIgnored = false, limitMin = 0, limitMax = 0, }: { label: string; value: number; min: number; max: number; onChange: (value: number) => void; disabled?: boolean; unit?: string; advanced?: boolean; limited?: boolean; limitsIgnored?: boolean; limitMin?: number; limitMax?: number; }) { const sane = Number.isFinite(value) ? value : 0, format = (number: number) => `${number.toFixed(3)}${unit}`; return ( ); }