diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8b5c47..785484dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ 本项目的重要变更记录在此文件中,版本标签沿用仓库现有的 `V主版本.次版本[.修订版本]` 格式。 +## [0.7.2] - 2026-09-01 + +### 变更 + +- 前端改为“应用壳 + 业务域 + 共享组件”的目录结构,并补充架构与依赖边界文档。 +- 拆分工程侧栏、模型控制侧栏、执行器控制和通用侧栏容器,降低单文件职责和跨域耦合。 +- 将地图、控制器、强化学习、本地训练和遥测面板及测试与各自业务域共置。 +- 将遥测模块从仿真调度目录独立到 `telemetry/`,保留通过显式接口与 `SimulationSession` 集成。 + ## [0.7.1] - 2026-09-01 ### 新增 diff --git a/README.md b/README.md index f7467dee..288cd02c 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,23 @@ npm run lint:python └── LICENSE Apache License 2.0 ``` -核心前端分层: +核心前端采用“应用壳 + 业务域 + 共享组件”分层: -- `web_platform/src/app/`:工作区、工具栏、面板和对话框 -- `web_platform/src/project/`:工程导入、路径规范化和 MEMFS -- `web_platform/src/simulation/`:MuJoCo WASM 会话与仿真调度 +- `web_platform/src/app/`:工作区编排、侧栏外壳、工具栏和全局对话框 +- `web_platform/src/components/ui/`:无业务依赖的共享 UI 基础组件 +- `web_platform/src/project/`:工程导入、路径规范化、项目树和 MEMFS +- `web_platform/src/simulation/`:MuJoCo WASM 会话、仿真调度及执行器控制 UI - `web_platform/src/viewer/`:Three.js 渲染、选择和物理交互 -- `web_platform/src/controller/`:Pyodide 控制器运行时 -- `web_platform/src/rl/`:ONNX 策略运行时 +- `web_platform/src/map/`:地图加载、创作、校验及对应 UI +- `web_platform/src/controller/`:Pyodide 控制器运行时及控制面板 +- `web_platform/src/rl/`:ONNX 策略运行时、任务绑定及策略面板 +- `web_platform/src/training/`:本地训练服务客户端、类型及训练面板 +- `web_platform/src/telemetry/`:遥测数据源、记录器、导出与数据面板 +- `web_platform/src/stores/`:跨业务域的应用状态 -详细使用说明见 [`web_platform/README.md`](web_platform/README.md),训练服务说明见 [`training_server/README.md`](training_server/README.md)。 +组件和测试优先与所属业务域共置;`app/components/` 仅保留跨域编排和应用壳组件,避免业务逻辑继续集中到单一侧栏文件。 + +详细使用说明见 [`web_platform/README.md`](web_platform/README.md),前端依赖边界见 [`web_platform/ARCHITECTURE.md`](web_platform/ARCHITECTURE.md),训练服务说明见 [`training_server/README.md`](training_server/README.md)。 ## 数据与安全边界 diff --git a/package-lock.json b/package-lock.json index 0f266f91..53b1c0b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mujoco-web-platform", - "version": "0.7.1", + "version": "0.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mujoco-web-platform", - "version": "0.7.1", + "version": "0.7.2", "license": "Apache-2.0", "dependencies": { "@monaco-editor/react": "^4.7.0", diff --git a/package.json b/package.json index 91c4f3c3..758cd3a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mujoco-web-platform", - "version": "0.7.1", + "version": "0.7.2", "description": "基于 MuJoCo WebAssembly 的本地机器人仿真与控制平台", "private": true, "type": "module", diff --git a/web_platform/ARCHITECTURE.md b/web_platform/ARCHITECTURE.md new file mode 100644 index 00000000..050c99c9 --- /dev/null +++ b/web_platform/ARCHITECTURE.md @@ -0,0 +1,41 @@ +# 前端架构 + +`src/` 采用“应用壳 + 业务域 + 共享基础设施”的组织方式。 + +## 目录职责 + +```text +src/ +├── app/ 应用入口、跨域编排、工作区壳和全局反馈 +├── components/ui/ 无业务依赖的共享 UI 基础组件 +├── controller/ Python 控制器运行时、类型和面板 +├── map/ 地图加载、生成、编辑和面板 +├── project/ 工程导入、转换、文件树和工作区 +├── rl/ ONNX 策略运行时、任务绑定、类型和面板 +├── simulation/ MuJoCo 会话、物理适配器和仿真控制组件 +├── telemetry/ 数据源抽象、记录器、导出和数据面板 +├── training/ 本地训练客户端、类型和面板 +├── viewer/ Three.js 场景、渲染和交互 +├── stores/ 跨域应用状态 +└── test/ 全局测试初始化 +``` + +## 依赖规则 + +1. `components/ui` 不依赖任何业务域。 +2. 业务组件、类型和测试与所属业务域共置。 +3. `app` 负责组合业务域,不承载可复用的业务算法或运行时。 +4. `simulation` 不依赖 React 应用状态;遥测通过 `TelemetrySource` 接口读取仿真数据。 +5. 业务域之间通过显式类型和门面交互,避免从其他域读取内部状态。 +6. 新文件优先放入已有业务域;只有跨域复用且不含业务语义的组件才进入 `components/ui`。 + +## 主要运行链路 + +```text +项目文件 → project → PhysicsAdapter → SimulationSession → MuJoCo WASM + ├→ controller / rl + └→ telemetry +SimulationSession snapshot → app → viewer / 各业务面板 +``` + +测试文件使用 `*.test.ts(x)` 与被测模块共置;端到端测试统一保存在 `e2e/`。 diff --git a/web_platform/src/app/App.tsx b/web_platform/src/app/App.tsx index 92116e0e..3d756c91 100644 --- a/web_platform/src/app/App.tsx +++ b/web_platform/src/app/App.tsx @@ -43,7 +43,7 @@ import { type UrdfLoadMode, } from '../simulation/PhysicsAdapter'; import type { ActuatorParameters } from '../simulation/SimulationSession'; -import type { DataRecorderConfig } from '../simulation/DataRecorder'; +import type { DataRecorderConfig } from '../telemetry/DataRecorder'; import type { ControllerCommand, ControllerStatus } from '../controller/types'; import type { RLCommand, RLPolicyStatus } from '../rl/types'; import type { MuJoCoViewer, InteractionMode, ViewerTheme } from '../viewer/MuJoCoViewer'; @@ -54,7 +54,8 @@ import { import { useAppStore, type AppDiagnostic } from '../stores/useAppStore'; import { WorkbenchHeader } from './components/WorkbenchHeader'; import { ViewerToolDock } from './components/ViewerToolDock'; -import { ProjectSidebar, ModelControlsSidebar } from './components/SidebarPanel'; +import { ModelControlsSidebar } from './components/ModelControlsSidebar'; +import { ProjectSidebar } from './components/ProjectSidebar'; import { WorkspaceOverlays, type ImportProgress } from './components/WorkspaceOverlays'; import { EntrySelectionDialog } from './components/EntrySelectionDialog'; import { ErrorRecoveryPanel } from './components/ErrorRecoveryPanel'; diff --git a/web_platform/src/app/components/ModelControlsSidebar.tsx b/web_platform/src/app/components/ModelControlsSidebar.tsx new file mode 100644 index 00000000..8583c6ba --- /dev/null +++ b/web_platform/src/app/components/ModelControlsSidebar.tsx @@ -0,0 +1,389 @@ +import { useState } from 'react'; +import { Database, Info, Map as MapIcon, 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 } from '../../map/types'; +import type { + EditableMapDocument, + MapEditorInteractionCallbacks, + MapEditorTransformMode, +} 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 { SidebarPanel } from './SidebarPanel'; + +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} + /> + ), + }, + ]} + /> + + ); +} diff --git a/web_platform/src/app/components/ProjectSidebar.tsx b/web_platform/src/app/components/ProjectSidebar.tsx new file mode 100644 index 00000000..cf3b3b66 --- /dev/null +++ b/web_platform/src/app/components/ProjectSidebar.tsx @@ -0,0 +1,190 @@ +import { useState } from 'react'; +import { Box, FolderTree, Map as MapIcon } from 'lucide-react'; +import type { ModelEntry } from '../../project/types'; +import { + countProjectSearchResults, + ProjectTree, + type ProjectTreeFile, +} from '../../project/ProjectTree'; +import { + countModelStructureSearchResults, + ModelStructureTree, +} from '../../project/ModelStructureTree'; +import type { SimulationSnapshot } from '../../simulation/SimulationSession'; +import { Button, Tabs } from '../../components/ui'; +import { + DEFAULT_PHYSICAL_MAP_CONFIG, + type MapSelection, + type SystemTerrainPreset, +} from '../../map/types'; +import type { + EditableMapDocument, + EditableMapObjectType, + MapObjectPlacementMode, +} from '../../map/editor/types'; +import { MapAssetLibrary } from '../../map/MapAssetLibrary'; +import { ProjectBreadcrumb } from './ProjectBreadcrumb'; +import { SidebarPanel } from './SidebarPanel'; +import { TreeSearchField } from './TreeSearchField'; + +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: ( + + ), + }, + ]} + /> + + ) : ( +
导入模型后显示工程资源
+ )} +
+ ); +} diff --git a/web_platform/src/app/components/SidebarPanel.tsx b/web_platform/src/app/components/SidebarPanel.tsx index fb3bb20d..c0aa106c 100644 --- a/web_platform/src/app/components/SidebarPanel.tsx +++ b/web_platform/src/app/components/SidebarPanel.tsx @@ -1,63 +1,6 @@ -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'; +import type { ReactNode } from 'react'; +import { Settings2 } from 'lucide-react'; +import { ResizablePanel } from '../../components/ui'; export function SidebarPanel({ title, @@ -86,828 +29,3 @@ export function SidebarPanel({ ); } - -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 ( - - ); -} diff --git a/web_platform/src/app/components/PythonControllerPanel.test.tsx b/web_platform/src/controller/PythonControllerPanel.test.tsx similarity index 100% rename from web_platform/src/app/components/PythonControllerPanel.test.tsx rename to web_platform/src/controller/PythonControllerPanel.test.tsx diff --git a/web_platform/src/app/components/PythonControllerPanel.tsx b/web_platform/src/controller/PythonControllerPanel.tsx similarity index 97% rename from web_platform/src/app/components/PythonControllerPanel.tsx rename to web_platform/src/controller/PythonControllerPanel.tsx index c3c314bd..93cb5d18 100644 --- a/web_platform/src/app/components/PythonControllerPanel.tsx +++ b/web_platform/src/controller/PythonControllerPanel.tsx @@ -10,8 +10,8 @@ import { RotateCw, Trash2, } from 'lucide-react'; -import type { ControllerCommand, ControllerStatus } from '../../controller/types'; -import { Badge, Button, PropertyRow, Select } from '../../components/ui'; +import type { ControllerCommand, ControllerStatus } from './types'; +import { Badge, Button, PropertyRow, Select } from '../components/ui'; export interface PythonControllerPanelProps { paths: string[]; diff --git a/web_platform/src/app/components/MapAssetLibrary.test.tsx b/web_platform/src/map/MapAssetLibrary.test.tsx similarity index 98% rename from web_platform/src/app/components/MapAssetLibrary.test.tsx rename to web_platform/src/map/MapAssetLibrary.test.tsx index 061834e2..9cdebb60 100644 --- a/web_platform/src/app/components/MapAssetLibrary.test.tsx +++ b/web_platform/src/map/MapAssetLibrary.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { MAP_ASSET_DRAG_MIME, MAP_ASSET_PLACEMENT_MIME } from '../../map/editor/assetCatalog'; +import { MAP_ASSET_DRAG_MIME, MAP_ASSET_PLACEMENT_MIME } from './editor/assetCatalog'; import { MapAssetLibrary } from './MapAssetLibrary'; const renderLibrary = ( diff --git a/web_platform/src/app/components/MapAssetLibrary.tsx b/web_platform/src/map/MapAssetLibrary.tsx similarity index 98% rename from web_platform/src/app/components/MapAssetLibrary.tsx rename to web_platform/src/map/MapAssetLibrary.tsx index 576c7502..90abdf9f 100644 --- a/web_platform/src/app/components/MapAssetLibrary.tsx +++ b/web_platform/src/map/MapAssetLibrary.tsx @@ -1,22 +1,22 @@ import { useState } from 'react'; import { BadgeCheck, CheckCircle2, Mountain, Plus } from 'lucide-react'; -import { Button, Select } from '../../components/ui'; +import { Button, Select } from '../components/ui'; import { CERTIFIED_MAP_ASSETS, MAP_ASSET_DRAG_MIME, MAP_ASSET_PLACEMENT_MIME, -} from '../../map/editor/assetCatalog'; +} from './editor/assetCatalog'; import { MAP_OBJECT_PLACEMENT_LABELS, type EditableMapDocument, type EditableMapObjectType, type MapObjectPlacementMode, -} from '../../map/editor/types'; +} from './editor/types'; import { PHYSICAL_MAP_PRESET_LABELS, SYSTEM_TERRAIN_PRESETS, type SystemTerrainPreset, -} from '../../map/types'; +} from './types'; function AssetPreview({ type, color }: { type: EditableMapObjectType; color: string }) { const shape = diff --git a/web_platform/src/app/components/MapEditorPanel.test.tsx b/web_platform/src/map/MapEditorPanel.test.tsx similarity index 98% rename from web_platform/src/app/components/MapEditorPanel.test.tsx rename to web_platform/src/map/MapEditorPanel.test.tsx index b21bca62..00b8db82 100644 --- a/web_platform/src/app/components/MapEditorPanel.test.tsx +++ b/web_platform/src/map/MapEditorPanel.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import type { EditableMapDocument } from '../../map/editor/types'; +import type { EditableMapDocument } from './editor/types'; import { MapEditorPanel } from './MapEditorPanel'; const interactionProps = { diff --git a/web_platform/src/app/components/MapEditorPanel.tsx b/web_platform/src/map/MapEditorPanel.tsx similarity index 99% rename from web_platform/src/app/components/MapEditorPanel.tsx rename to web_platform/src/map/MapEditorPanel.tsx index 9c19fb4a..4753277c 100644 --- a/web_platform/src/app/components/MapEditorPanel.tsx +++ b/web_platform/src/map/MapEditorPanel.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; -import { Button, Select } from '../../components/ui'; -import { MapEditSession } from '../../map/editor/MapEditSession'; +import { Button, Select } from '../components/ui'; +import { MapEditSession } from './editor/MapEditSession'; import { editableObjectGroundHeight, MAP_OBJECT_PLACEMENT_LABELS, @@ -10,7 +10,7 @@ import { type MapEditorInteractionCallbacks, type MapEditorTransformMode, type MapObjectPlacementMode, -} from '../../map/editor/types'; +} from './editor/types'; const labels: Record = { box: '方盒', diff --git a/web_platform/src/app/components/PhysicalMapPanel.test.tsx b/web_platform/src/map/PhysicalMapPanel.test.tsx similarity index 98% rename from web_platform/src/app/components/PhysicalMapPanel.test.tsx rename to web_platform/src/map/PhysicalMapPanel.test.tsx index 03d46cc6..41f03866 100644 --- a/web_platform/src/app/components/PhysicalMapPanel.test.tsx +++ b/web_platform/src/map/PhysicalMapPanel.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { DEFAULT_PHYSICAL_MAP_CONFIG } from '../../map/types'; +import { DEFAULT_PHYSICAL_MAP_CONFIG } from './types'; import { PhysicalMapPanel } from './PhysicalMapPanel'; const common = { diff --git a/web_platform/src/app/components/PhysicalMapPanel.tsx b/web_platform/src/map/PhysicalMapPanel.tsx similarity index 98% rename from web_platform/src/app/components/PhysicalMapPanel.tsx rename to web_platform/src/map/PhysicalMapPanel.tsx index 92dafdd4..b4d4898a 100644 --- a/web_platform/src/app/components/PhysicalMapPanel.tsx +++ b/web_platform/src/map/PhysicalMapPanel.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Button, Select } from '../../components/ui'; -import type { MapEntry } from '../../project/types'; +import { Button, Select } from '../components/ui'; +import type { MapEntry } from '../project/types'; import { DEFAULT_PHYSICAL_MAP_CONFIG, PHYSICAL_MAP_PRESET_LABELS, @@ -8,13 +8,13 @@ import { type MapSelection, type PhysicalMapConfig, type PhysicalMapPreset, -} from '../../map/types'; -import { normalizePhysicalMapConfig } from '../../map/physicalMap'; +} from './types'; +import { normalizePhysicalMapConfig } from './physicalMap'; import type { EditableMapDocument, MapEditorInteractionCallbacks, MapEditorTransformMode, -} from '../../map/editor/types'; +} from './editor/types'; import { MapEditorPanel } from './MapEditorPanel'; function NumberField({ diff --git a/web_platform/src/app/components/RLPolicyPanel.tsx b/web_platform/src/rl/RLPolicyPanel.tsx similarity index 97% rename from web_platform/src/app/components/RLPolicyPanel.tsx rename to web_platform/src/rl/RLPolicyPanel.tsx index cb53777f..1f05a697 100644 --- a/web_platform/src/app/components/RLPolicyPanel.tsx +++ b/web_platform/src/rl/RLPolicyPanel.tsx @@ -1,7 +1,7 @@ import { useRef, type ChangeEvent } from 'react'; import { BrainCircuit, FileUp, Power, RotateCw, Trash2 } from 'lucide-react'; -import type { RLCommand, RLPolicyStatus } from '../../rl/types'; -import { Badge, Button, PropertyRow, Select } from '../../components/ui'; +import type { RLCommand, RLPolicyStatus } from './types'; +import { Badge, Button, PropertyRow, Select } from '../components/ui'; export interface RLPolicyPanelProps { paths: string[]; diff --git a/web_platform/src/app/components/ActuatorControl.test.tsx b/web_platform/src/simulation/ActuatorControl.test.tsx similarity index 96% rename from web_platform/src/app/components/ActuatorControl.test.tsx rename to web_platform/src/simulation/ActuatorControl.test.tsx index 0be3099e..b8d61b68 100644 --- a/web_platform/src/app/components/ActuatorControl.test.tsx +++ b/web_platform/src/simulation/ActuatorControl.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { ActuatorControl } from './SidebarPanel'; -import type { ActuatorInfo } from '../../simulation/SimulationSession'; +import { ActuatorControl } from './ActuatorControl'; +import type { ActuatorInfo } from './SimulationSession'; const actuator: ActuatorInfo = { id: 0, diff --git a/web_platform/src/simulation/ActuatorControl.tsx b/web_platform/src/simulation/ActuatorControl.tsx new file mode 100644 index 00000000..56147418 --- /dev/null +++ b/web_platform/src/simulation/ActuatorControl.tsx @@ -0,0 +1,307 @@ +import type { ActuatorInfo, ActuatorParameters } from './SimulationSession'; +import { Badge } from '../components/ui'; + +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 ( + + ); +} +export function Check({ + label, + checked, + onChange, +}: { + label: string; + checked: boolean; + onChange: (value: boolean) => void; +}) { + return ( + + ); +} +export 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 ( + + ); +} diff --git a/web_platform/src/simulation/PhysicsAdapter.ts b/web_platform/src/simulation/PhysicsAdapter.ts index d9c1ecbb..f95360cf 100644 --- a/web_platform/src/simulation/PhysicsAdapter.ts +++ b/web_platform/src/simulation/PhysicsAdapter.ts @@ -16,7 +16,11 @@ import { } from './SimulationSession'; import type { ControllerCommand, ControllerStatus } from '../controller/types'; import type { RLCommand, RLPolicyStatus } from '../rl/types'; -import type { DataRecorderConfig, DataRecorderStatus, TelemetryChannel } from './DataRecorder'; +import type { + DataRecorderConfig, + DataRecorderStatus, + TelemetryChannel, +} from '../telemetry/DataRecorder'; import { composePhysicalMap } from '../map/physicalMap'; import { composeProjectMap } from '../map/MapComposer'; import { resolveProjectMap } from '../map/MapLoader'; diff --git a/web_platform/src/simulation/SimulationSession.ts b/web_platform/src/simulation/SimulationSession.ts index 0dc2796f..6d653314 100644 --- a/web_platform/src/simulation/SimulationSession.ts +++ b/web_platform/src/simulation/SimulationSession.ts @@ -11,7 +11,7 @@ import { type DataRecorderStatus, type TelemetryBody, type TelemetryChannel, -} from './DataRecorder'; +} from '../telemetry/DataRecorder'; export interface ActuatorParameters { gear: number; diff --git a/web_platform/src/simulation/DataRecorder.test.ts b/web_platform/src/telemetry/DataRecorder.test.ts similarity index 100% rename from web_platform/src/simulation/DataRecorder.test.ts rename to web_platform/src/telemetry/DataRecorder.test.ts diff --git a/web_platform/src/simulation/DataRecorder.ts b/web_platform/src/telemetry/DataRecorder.ts similarity index 100% rename from web_platform/src/simulation/DataRecorder.ts rename to web_platform/src/telemetry/DataRecorder.ts diff --git a/web_platform/src/app/components/DataRecordingPanel.test.tsx b/web_platform/src/telemetry/DataRecordingPanel.test.tsx similarity index 96% rename from web_platform/src/app/components/DataRecordingPanel.test.tsx rename to web_platform/src/telemetry/DataRecordingPanel.test.tsx index b500a535..91890c84 100644 --- a/web_platform/src/app/components/DataRecordingPanel.test.tsx +++ b/web_platform/src/telemetry/DataRecordingPanel.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { DataRecordingPanel } from './DataRecordingPanel'; -import type { DataRecorderStatus } from '../../simulation/DataRecorder'; +import type { DataRecorderStatus } from './DataRecorder'; const status: DataRecorderStatus = { recording: false, diff --git a/web_platform/src/app/components/DataRecordingPanel.tsx b/web_platform/src/telemetry/DataRecordingPanel.tsx similarity index 97% rename from web_platform/src/app/components/DataRecordingPanel.tsx rename to web_platform/src/telemetry/DataRecordingPanel.tsx index 672c6c8d..fe46a9f9 100644 --- a/web_platform/src/app/components/DataRecordingPanel.tsx +++ b/web_platform/src/telemetry/DataRecordingPanel.tsx @@ -1,7 +1,7 @@ import { Circle, Download, Square, Trash2 } from 'lucide-react'; -import type { BodyInfo } from '../../simulation/SimulationSession'; -import type { DataRecorderConfig, DataRecorderStatus } from '../../simulation/DataRecorder'; -import { Badge, Button, PropertyRow, Select } from '../../components/ui'; +import type { BodyInfo } from '../simulation/SimulationSession'; +import type { DataRecorderConfig, DataRecorderStatus } from './DataRecorder'; +import { Badge, Button, PropertyRow, Select } from '../components/ui'; const SAMPLE_RATES = [10, 20, 50, 100, 200]; const radiansToDegrees = 180 / Math.PI; diff --git a/web_platform/src/app/components/LocalTrainingPanel.test.tsx b/web_platform/src/training/LocalTrainingPanel.test.tsx similarity index 100% rename from web_platform/src/app/components/LocalTrainingPanel.test.tsx rename to web_platform/src/training/LocalTrainingPanel.test.tsx diff --git a/web_platform/src/app/components/LocalTrainingPanel.tsx b/web_platform/src/training/LocalTrainingPanel.tsx similarity index 98% rename from web_platform/src/app/components/LocalTrainingPanel.tsx rename to web_platform/src/training/LocalTrainingPanel.tsx index e2732296..43766efc 100644 --- a/web_platform/src/app/components/LocalTrainingPanel.tsx +++ b/web_platform/src/training/LocalTrainingPanel.tsx @@ -1,13 +1,8 @@ import { useEffect, useState, type ReactNode } from 'react'; import { Download, Link, Play, Server, Square } from 'lucide-react'; -import { Badge, Button, ProgressBar, PropertyRow, Select } from '../../components/ui'; -import { LocalTrainingClient } from '../../training/LocalTrainingClient'; -import type { - TrainingDevice, - TrainingJob, - TrainingServerInfo, - WandbMode, -} from '../../training/types'; +import { Badge, Button, ProgressBar, PropertyRow, Select } from '../components/ui'; +import { LocalTrainingClient } from './LocalTrainingClient'; +import type { TrainingDevice, TrainingJob, TrainingServerInfo, WandbMode } from './types'; const ENDPOINT_KEY = 'mujoco-local-training-endpoint', JOB_KEY = 'mujoco-local-training-job',