Files
Mujoco_WASM/web_platform/src/app/components/ModelControlsSidebar.tsx
T
chenlin 72e27b5a6c
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
feat(web-platform): release V0.8.3 常规界面优化
2026-09-04 15:34:24 +08:00

254 lines
8.8 KiB
TypeScript

import type { ReactNode } from 'react';
import { Box, Database, MapPinned, PanelRight, SlidersHorizontal } from 'lucide-react';
import type { MapEntry, ModelEntry } from '../../project/types';
import type { ActuatorParameters, SimulationSnapshot } from '../../simulation/SimulationSession';
import type { UrdfBaseMode, UrdfLoadMode } from '../../simulation/PhysicsAdapter';
import type { ViewerSelection } from '../../viewer/MuJoCoViewer';
import type { MapSelection, PlacedMapSelection } from '../../map/types';
import type {
EditableMapDocument,
MapEditorInteractionCallbacks,
MapEditorSessionState,
} from '../../map/editor/types';
import { PhysicalMapPanel } from '../../map/PhysicalMapPanel';
import {
BodyInspector,
InspectorIdentity,
JointInspector,
ModelSummaryInspector,
} from '../../simulation/RobotInspector';
import type { EditorSelection } from '../editorSelection';
import { SidebarPanel } from './SidebarPanel';
import type { WorkspaceTool } from './WorkspaceToolsPanel';
import { RightSidebarTabs } from './RightSidebarTabs';
interface ModelControlsProps {
snapshot?: SimulationSnapshot;
selection: EditorSelection | null;
viewerSelection: ViewerSelection | null;
selectedFormat?: ModelEntry['format'];
loading: boolean;
visible?: boolean;
urdfMode: UrdfLoadMode;
baseMode: UrdfBaseMode;
showCollision: boolean;
ignoreJointLimits: boolean;
jointAdvanced: boolean;
angleUnit: 'rad' | 'deg';
mapSelection: MapSelection;
activeMapAssetId?: string;
activeMapAssetName?: string;
mapSceneDirty: boolean;
maps: MapEntry[];
showVisualMap: boolean;
showMapCollision: boolean;
editorDocument: EditableMapDocument | null;
editorDraftDocument?: EditableMapDocument;
workspaceTool: WorkspaceTool | null;
workspaceTools?: ReactNode;
onWorkspaceToolChange: (tool: WorkspaceTool | null) => void;
onSelectJoint: (jointId: number, bodyId: number) => void;
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;
onApplyMap: (value: MapSelection) => void;
onMapDraft: (value: PlacedMapSelection) => void;
onEditorPreview: (document: EditableMapDocument | null) => void;
onEditorDraftChange: (
descriptorPath: string,
document: EditableMapDocument,
dirty: boolean,
) => void;
onEditorApply: (document: EditableMapDocument) => Promise<boolean>;
onEditorExport: () => void;
onEditorConvert: () => Promise<boolean>;
onEditorBindInteraction: (callbacks: MapEditorInteractionCallbacks | null) => void;
onEditorSelect: (id: string | null) => void;
onEditorSessionStateChange?: (
descriptorPath: string,
state: MapEditorSessionState | null,
) => void;
onEditorSurfaceHeight?: (position: readonly [number, number, number]) => number | null;
onMapDisplay: (visual: boolean, collision: boolean) => void;
}
export function ModelControlsSidebar(props: ModelControlsProps) {
const snapshot = props.snapshot;
const activeView = props.workspaceTool ?? 'inspector';
const title =
activeView === 'controls' ? '控制台' : activeView === 'data' ? '数据录制' : '检查器';
const icon =
activeView === 'controls' ? (
<SlidersHorizontal />
) : activeView === 'data' ? (
<Database />
) : (
<PanelRight />
);
const tabs = (
<RightSidebarTabs
value={activeView}
onChange={(value) => props.onWorkspaceToolChange(value === 'inspector' ? null : value)}
/>
);
if (!snapshot)
return (
<SidebarPanel title={title} side="right" visible={props.visible} icon={icon}>
{tabs}
{props.workspaceTool ? (
props.workspaceTools
) : (
<div className="p-4 text-sm text-text-tertiary">导入模型后显示检查器</div>
)}
</SidebarPanel>
);
const selection = props.selection;
const selectedBody =
selection?.kind === 'body'
? snapshot.bodies.find((body) => body.id === selection.bodyId)
: undefined;
const selectedJoint =
selection?.kind === 'joint'
? snapshot.joints.find((joint) => joint.id === selection.jointId)
: undefined;
const mapSelected =
(selection?.kind === 'map' || selection?.kind === 'map-object') &&
selection.mapAssetId === props.activeMapAssetId;
const selectedMapObject =
selection?.kind === 'map-object' && mapSelected
? (props.editorDraftDocument ?? props.editorDocument)?.objects.find(
(object) => object.id === selection.objectId,
)
: undefined;
const mapPanel = (
<PhysicalMapPanel
key={`${props.activeMapAssetId ?? 'none'}:${
props.mapSelection.kind === 'project'
? props.mapSelection.descriptorPath
: props.mapSelection.kind
}`}
value={props.mapSelection}
maps={props.maps}
rootBodies={snapshot.bodies
.filter(
(body) =>
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}
editorDraftDocument={props.editorDraftDocument}
sceneDirty={props.mapSceneDirty}
onEditorPreview={props.onEditorPreview}
onEditorDraftChange={props.onEditorDraftChange}
onEditorApply={props.onEditorApply}
onEditorExport={props.onEditorExport}
onEditorConvert={props.onEditorConvert}
selectedEditorObjectId={
selection?.kind === 'map-object' && mapSelected ? selection.objectId : undefined
}
onEditorBindInteraction={props.onEditorBindInteraction}
onEditorSelect={props.onEditorSelect}
onEditorSessionStateChange={props.onEditorSessionStateChange}
onEditorSurfaceHeight={props.onEditorSurfaceHeight}
onMapDisplay={props.onMapDisplay}
onDraft={props.onMapDraft}
onApply={props.onApplyMap}
/>
);
let inspector: ReactNode;
if (selectedBody) {
inspector = (
<BodyInspector
body={selectedBody}
snapshot={snapshot}
viewerSelection={props.viewerSelection}
showCollision={props.showCollision}
onShowCollision={props.onShowCollision}
onSelectJoint={props.onSelectJoint}
/>
);
} else if (selectedJoint) {
inspector = (
<JointInspector
joint={selectedJoint}
snapshot={snapshot}
loading={props.loading}
ignoreJointLimits={props.ignoreJointLimits}
jointAdvanced={props.jointAdvanced}
angleUnit={props.angleUnit}
onResetJoints={props.onResetJoints}
onToggleJointLimits={props.onToggleJointLimits}
onToggleAdvanced={props.onToggleAdvanced}
onToggleAngleUnit={props.onToggleAngleUnit}
onJoint={props.onJoint}
onActuator={props.onActuator}
onActuatorParameters={props.onActuatorParameters}
/>
);
} else if (mapSelected) {
const mapName = selectedMapObject?.name ?? props.activeMapAssetName ?? '地图实例';
inspector = (
<>
<InspectorIdentity
icon={selectedMapObject ? <Box /> : <MapPinned />}
eyebrow={selectedMapObject ? 'Map / Object' : 'Map / Instance'}
name={mapName}
meta={
selectedMapObject
? `${selectedMapObject.type} · ${props.activeMapAssetName ?? '地图'}`
: props.mapSelection.kind === 'builtin'
? '参数化地形'
: '工程地图包实例'
}
/>
{mapPanel}
</>
);
} else {
inspector = (
<>
<ModelSummaryInspector
snapshot={snapshot}
selectedFormat={props.selectedFormat}
loading={props.loading}
urdfMode={props.urdfMode}
baseMode={props.baseMode}
showCollision={props.showCollision}
onUrdfMode={props.onUrdfMode}
onBaseMode={props.onBaseMode}
onShowCollision={props.onShowCollision}
/>
{props.mapSelection.kind === 'none' && mapPanel}
</>
);
}
return (
<SidebarPanel title={title} side="right" visible={props.visible} icon={icon}>
{tabs}
<div
hidden={props.workspaceTool !== null}
role="tabpanel"
aria-label="检查器"
className="min-h-0 flex-1 overflow-auto panel-scroll"
>
{inspector}
</div>
{props.workspaceTool && props.workspaceTools}
</SidebarPanel>
);
}