feat(web-platform): release V0.8.3 常规界面优化
web-platform-release / Build and publish release (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
web-platform-release / Build and publish release (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
This commit is contained in:
@@ -1,38 +1,31 @@
|
||||
import { useState } from 'react';
|
||||
import { Database, Info, Map as MapIcon, SlidersHorizontal } from 'lucide-react';
|
||||
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 { DataRecorderConfig } from '../../telemetry/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,
|
||||
Select,
|
||||
Tabs,
|
||||
} from '../../components/ui';
|
||||
import type { MapSelection, PlacedMapSelection } from '../../map/types';
|
||||
import type {
|
||||
EditableMapDocument,
|
||||
MapEditorInteractionCallbacks,
|
||||
MapEditorTransformMode,
|
||||
MapEditorSessionState,
|
||||
} from '../../map/editor/types';
|
||||
import { DataRecordingPanel } from '../../telemetry/DataRecordingPanel';
|
||||
import { ActuatorControl, Check, ControlSlider } from '../../simulation/ActuatorControl';
|
||||
import { LocalTrainingPanel } from '../../training/LocalTrainingPanel';
|
||||
import { PhysicalMapPanel } from '../../map/PhysicalMapPanel';
|
||||
import { PythonControllerPanel } from '../../controller/PythonControllerPanel';
|
||||
import { RLPolicyPanel } from '../../rl/RLPolicyPanel';
|
||||
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: ViewerSelection | null;
|
||||
selection: EditorSelection | null;
|
||||
viewerSelection: ViewerSelection | null;
|
||||
selectedFormat?: ModelEntry['format'];
|
||||
loading: boolean;
|
||||
visible?: boolean;
|
||||
@@ -42,23 +35,19 @@ interface ModelControlsProps {
|
||||
ignoreJointLimits: boolean;
|
||||
jointAdvanced: boolean;
|
||||
angleUnit: 'rad' | 'deg';
|
||||
forceScale: number;
|
||||
controllerPaths: string[];
|
||||
selectedControllerPath?: string;
|
||||
controllerStatus?: ControllerStatus;
|
||||
policyPaths: string[];
|
||||
selectedPolicyPath?: string;
|
||||
policyStatus?: RLPolicyStatus;
|
||||
mapSelection: MapSelection;
|
||||
activeMapAssetId?: string;
|
||||
activeMapAssetName?: string;
|
||||
mapSceneDirty: boolean;
|
||||
maps: MapEntry[];
|
||||
showVisualMap: boolean;
|
||||
showMapCollision: boolean;
|
||||
editorDocument: EditableMapDocument | null;
|
||||
editorDraftDocument?: EditableMapDocument;
|
||||
activeTab?: 'properties' | 'controls' | 'data' | 'map';
|
||||
onActiveTabChange?: (value: 'properties' | 'controls' | 'data' | 'map') => void;
|
||||
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;
|
||||
@@ -69,19 +58,6 @@ interface ModelControlsProps {
|
||||
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;
|
||||
onMapDraft: (value: PlacedMapSelection) => void;
|
||||
onEditorPreview: (document: EditableMapDocument | null) => void;
|
||||
@@ -95,320 +71,183 @@ interface ModelControlsProps {
|
||||
onEditorConvert: () => Promise<boolean>;
|
||||
onEditorBindInteraction: (callbacks: MapEditorInteractionCallbacks | null) => void;
|
||||
onEditorSelect: (id: string | null) => void;
|
||||
onEditorTransformMode: (mode: MapEditorTransformMode) => void;
|
||||
onEditorSnapping: (translation: number | null, rotationDegrees: number | null) => void;
|
||||
onEditorSessionStateChange?: (
|
||||
descriptorPath: string,
|
||||
state: MapEditorSessionState | null,
|
||||
) => void;
|
||||
onEditorSurfaceHeight?: (position: readonly [number, number, number]) => number | null;
|
||||
onMapDisplay: (visual: boolean, collision: boolean) => void;
|
||||
onMapTabOpen?: () => void;
|
||||
onDataRecorderConfigure: (patch: Partial<DataRecorderConfig>) => void;
|
||||
onDataRecordingStart: () => void;
|
||||
onDataRecordingStop: () => void;
|
||||
onDataRecordingClear: () => void;
|
||||
onDataRecordingExport: (format: 'csv' | 'json') => void;
|
||||
}
|
||||
|
||||
export function ModelControlsSidebar(props: ModelControlsProps) {
|
||||
const [internalTab, setInternalTab] = useState<'properties' | 'controls' | 'data' | 'map'>(
|
||||
'properties',
|
||||
),
|
||||
tab = props.activeTab ?? internalTab,
|
||||
s = props.snapshot;
|
||||
if (!s)
|
||||
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="属性与参数" side="right" visible={props.visible}>
|
||||
<div className="p-4 text-sm text-text-tertiary">导入模型后显示属性</div>
|
||||
<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 properties = (
|
||||
<>
|
||||
<CollapsibleSection title="模型信息" defaultOpen badge={<Badge>{s.model.nbody} Body</Badge>}>
|
||||
<div>
|
||||
<PropertyRow label="Body" value={s.model.nbody} />
|
||||
<PropertyRow label="Joint" value={s.model.njnt} />
|
||||
<PropertyRow label="Geom" value={s.model.ngeom} />
|
||||
<PropertyRow label="Actuator" value={s.model.nactuator} />
|
||||
<PropertyRow label="qpos / qvel" value={`${s.model.nq} / ${s.model.nv}`} />
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
{props.selectedFormat === 'urdf' && (
|
||||
<CollapsibleSection title="URDF 处理方式" defaultOpen={false}>
|
||||
<Select
|
||||
aria-label="URDF 处理方式"
|
||||
className="w-full"
|
||||
value={props.urdfMode}
|
||||
disabled={props.loading}
|
||||
onChange={(event) => props.onUrdfMode(event.target.value as UrdfLoadMode)}
|
||||
>
|
||||
<option value="mjcf">转换为 MJCF(推荐)</option>
|
||||
<option value="native">MuJoCo 原生 URDF</option>
|
||||
</Select>
|
||||
<label className="mt-3 block text-xs text-text-secondary">
|
||||
<span className="mb-1 block">基座类型</span>
|
||||
<Select
|
||||
aria-label="URDF 基座类型"
|
||||
className="w-full"
|
||||
value={props.baseMode}
|
||||
disabled={props.loading || props.urdfMode === 'native'}
|
||||
onChange={(event) => props.onBaseMode(event.target.value as UrdfBaseMode)}
|
||||
>
|
||||
<option value="floating">浮动基座(Free Joint)</option>
|
||||
<option value="fixed">固定基座(连接世界)</option>
|
||||
</Select>
|
||||
</label>
|
||||
<p className="mt-2 text-xs text-text-tertiary">
|
||||
MJCF 模式保留 visual mesh、添加物理地面,并将模型最低点对齐到 z=0。
|
||||
</p>
|
||||
<Check
|
||||
label="显示碰撞几何"
|
||||
checked={props.showCollision}
|
||||
onChange={props.onShowCollision}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
<CollapsibleSection title="当前选择" defaultOpen>
|
||||
{props.selection ? (
|
||||
<div className="text-xs">
|
||||
<PropertyRow
|
||||
label="Body"
|
||||
value={props.selection.bodyName}
|
||||
action={<CopyButton value={props.selection.bodyName} label="复制 Body 名称" />}
|
||||
/>
|
||||
<PropertyRow
|
||||
label="标识"
|
||||
value={`${props.selection.bodyId} / ${props.selection.geomId} / ${props.selection.geomType}`}
|
||||
action={
|
||||
<CopyButton
|
||||
value={`body ${props.selection.bodyId}, geom ${props.selection.geomId}, type ${props.selection.geomType}`}
|
||||
label="复制标识"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PropertyRow
|
||||
label="位置"
|
||||
value={props.selection.position.map((value) => value.toFixed(3)).join(', ')}
|
||||
action={<CopyButton value={props.selection.position.join(', ')} label="复制位置" />}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="flex items-center gap-2 text-xs text-text-tertiary">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
在视口中单击物体
|
||||
</p>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
</>
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
const controls = (
|
||||
<>
|
||||
<CollapsibleSection
|
||||
title="ONNX 强化学习策略"
|
||||
defaultOpen
|
||||
badge={s.rlPolicy ? <Badge>{s.rlPolicy.enabled ? '推理' : '停止'}</Badge> : undefined}
|
||||
>
|
||||
<RLPolicyPanel
|
||||
paths={props.policyPaths}
|
||||
selectedPath={props.selectedPolicyPath}
|
||||
status={props.policyStatus ?? s.rlPolicy}
|
||||
loading={props.loading}
|
||||
onSelectPath={props.onSelectPolicyPath}
|
||||
onLoadPath={props.onLoadPolicyPath}
|
||||
onImport={props.onImportPolicy}
|
||||
onToggle={props.onTogglePolicy}
|
||||
onCommand={props.onPolicyCommand}
|
||||
onRemove={props.onRemovePolicy}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="本地强化学习训练" defaultOpen={false}>
|
||||
<LocalTrainingPanel onPolicyReady={props.onImportPolicy} />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
title="Python 控制器"
|
||||
defaultOpen
|
||||
badge={s.controller ? <Badge>{s.controller.enabled ? '运行' : '停止'}</Badge> : undefined}
|
||||
>
|
||||
<PythonControllerPanel
|
||||
paths={props.controllerPaths}
|
||||
selectedPath={props.selectedControllerPath}
|
||||
status={props.controllerStatus ?? s.controller}
|
||||
loading={props.loading}
|
||||
onSelectPath={props.onSelectControllerPath}
|
||||
onLoadPath={props.onLoadControllerPath}
|
||||
onImport={props.onImportController}
|
||||
onToggle={props.onToggleController}
|
||||
onCommand={props.onControllerCommand}
|
||||
onRemove={props.onRemoveController}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
title="Actuator"
|
||||
defaultOpen={false}
|
||||
badge={<Badge>{s.actuators.length}</Badge>}
|
||||
>
|
||||
{s.actuators.length ? (
|
||||
s.actuators.map((actuator) => (
|
||||
<ActuatorControl
|
||||
key={actuator.id}
|
||||
actuator={actuator}
|
||||
onControl={(value) => props.onActuator(actuator.id, value)}
|
||||
onParameters={(parameters) => props.onActuatorParameters(actuator.id, parameters)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-xs text-text-tertiary">模型没有驱动器</p>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="关节" defaultOpen badge={<Badge>{s.joints.length}</Badge>}>
|
||||
<div className="mb-4 grid grid-cols-2 gap-2">
|
||||
<Button onClick={props.onResetJoints}>重置关节</Button>
|
||||
<Button
|
||||
variant={props.ignoreJointLimits ? 'primary' : 'secondary'}
|
||||
aria-pressed={props.ignoreJointLimits}
|
||||
onClick={props.onToggleJointLimits}
|
||||
>
|
||||
忽略关节限位
|
||||
</Button>
|
||||
<Button
|
||||
variant={props.jointAdvanced ? 'primary' : 'secondary'}
|
||||
aria-pressed={props.jointAdvanced}
|
||||
onClick={props.onToggleAdvanced}
|
||||
>
|
||||
高级
|
||||
</Button>
|
||||
<Button
|
||||
variant={props.angleUnit === 'deg' ? 'primary' : 'secondary'}
|
||||
aria-pressed={props.angleUnit === 'deg'}
|
||||
onClick={props.onToggleAngleUnit}
|
||||
>
|
||||
{props.angleUnit === 'rad' ? 'rad 弧度制' : '° 角度制'}
|
||||
</Button>
|
||||
</div>
|
||||
{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 (
|
||||
<ControlSlider
|
||||
key={joint.id}
|
||||
label={`${joint.name}${joint.editable ? '' : '(只读)'}`}
|
||||
value={joint.value * scale}
|
||||
min={joint.min * scale}
|
||||
max={joint.max * scale}
|
||||
unit={unit}
|
||||
advanced={props.jointAdvanced}
|
||||
limited={joint.limited}
|
||||
limitsIgnored={joint.limitsIgnored}
|
||||
limitMin={joint.limitMin * scale}
|
||||
limitMax={joint.limitMax * scale}
|
||||
disabled={!joint.editable}
|
||||
onChange={(value) => props.onJoint(joint.id, value / scale)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="外力强度" defaultOpen={false}>
|
||||
<ControlSlider
|
||||
label={`${props.forceScale.toFixed(0)} N/屏幕单位`}
|
||||
value={props.forceScale}
|
||||
min={5}
|
||||
max={200}
|
||||
onChange={props.onForceScale}
|
||||
/>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
选择“外力施加”,在动态物体上按住拖动,松开即清零。
|
||||
</p>
|
||||
</CollapsibleSection>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<SidebarPanel title="属性与参数" side="right" visible={props.visible}>
|
||||
<Tabs
|
||||
label="模型控制侧栏"
|
||||
value={tab}
|
||||
onValueChange={(value) => {
|
||||
setInternalTab(value);
|
||||
props.onActiveTabChange?.(value);
|
||||
if (value === 'map') props.onMapTabOpen?.();
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
value: 'properties',
|
||||
label: '属性',
|
||||
icon: <Info className="h-3.5 w-3.5" />,
|
||||
content: properties,
|
||||
},
|
||||
{
|
||||
value: 'controls',
|
||||
label: '控制',
|
||||
icon: <SlidersHorizontal className="h-3.5 w-3.5" />,
|
||||
content: controls,
|
||||
},
|
||||
{
|
||||
value: 'data',
|
||||
label: '数据',
|
||||
icon: <Database className="h-3.5 w-3.5" />,
|
||||
content: (
|
||||
<DataRecordingPanel
|
||||
status={s.telemetry}
|
||||
bodies={s.bodies}
|
||||
onConfigure={props.onDataRecorderConfigure}
|
||||
onStart={props.onDataRecordingStart}
|
||||
onStop={props.onDataRecordingStop}
|
||||
onClear={props.onDataRecordingClear}
|
||||
onExport={props.onDataRecordingExport}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'map',
|
||||
label: '地图',
|
||||
icon: <MapIcon className="h-3.5 w-3.5" />,
|
||||
content: (
|
||||
<PhysicalMapPanel
|
||||
key={`${props.activeMapAssetId ?? 'none'}:${
|
||||
props.mapSelection.kind === 'project'
|
||||
? props.mapSelection.descriptorPath
|
||||
: props.mapSelection.kind
|
||||
}`}
|
||||
value={props.mapSelection}
|
||||
maps={props.maps}
|
||||
rootBodies={s.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}
|
||||
onEditorBindInteraction={props.onEditorBindInteraction}
|
||||
onEditorSelect={props.onEditorSelect}
|
||||
onEditorTransformMode={props.onEditorTransformMode}
|
||||
onEditorSnapping={props.onEditorSnapping}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user