254 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|