chore(web-platform): release V0.6.1 工程质量优化
This commit is contained in:
@@ -1,17 +1,33 @@
|
||||
import loadMujoco, {type MainModule} from '@mujoco/mujoco';
|
||||
import type {ProjectFile,ProjectManifest} from '../project/types';
|
||||
import {prepareProjectForMujoco} from '../project/importer';
|
||||
import {enhanceConvertedMjcf,groundConvertedMjcf,type UrdfBaseMode,type UrdfEnhancementOptions} from '../project/urdfToMjcf';
|
||||
import {MemfsWorkspace} from '../project/workspace';
|
||||
import {SimulationSession,type ActuatorParameters,type FrameResult,type SimulationSnapshot} from './SimulationSession';
|
||||
import type {ControllerCommand,ControllerStatus} from '../controller/types';
|
||||
import type {RLCommand,RLPolicyStatus} from '../rl/types';
|
||||
import loadMujoco, { type MainModule } from '@mujoco/mujoco';
|
||||
import type { ProjectFile, ProjectManifest } from '../project/types';
|
||||
import { prepareProjectForMujoco } from '../project/importer';
|
||||
import {
|
||||
enhanceConvertedMjcf,
|
||||
groundConvertedMjcf,
|
||||
type UrdfBaseMode,
|
||||
type UrdfEnhancementOptions,
|
||||
} from '../project/urdfToMjcf';
|
||||
import { MemfsWorkspace } from '../project/workspace';
|
||||
import {
|
||||
SimulationSession,
|
||||
type ActuatorParameters,
|
||||
type FrameResult,
|
||||
type SimulationSnapshot,
|
||||
} from './SimulationSession';
|
||||
import type { ControllerCommand, ControllerStatus } from '../controller/types';
|
||||
import type { RLCommand, RLPolicyStatus } from '../rl/types';
|
||||
|
||||
export type UrdfLoadMode='mjcf'|'native';
|
||||
export type {UrdfBaseMode,UrdfEnhancementOptions};
|
||||
export type UrdfLoadMode = 'mjcf' | 'native';
|
||||
export type { UrdfBaseMode, UrdfEnhancementOptions };
|
||||
|
||||
export interface PhysicsAdapter {
|
||||
load(manifest:ProjectManifest,entryPath:string,urdfMode?:UrdfLoadMode,baseMode?:UrdfBaseMode,enhancements?:UrdfEnhancementOptions):Promise<SimulationSnapshot>;
|
||||
load(
|
||||
manifest: ProjectManifest,
|
||||
entryPath: string,
|
||||
urdfMode?: UrdfLoadMode,
|
||||
baseMode?: UrdfBaseMode,
|
||||
enhancements?: UrdfEnhancementOptions,
|
||||
): Promise<SimulationSnapshot>;
|
||||
advance(now: number): FrameResult;
|
||||
snapshot(): SimulationSnapshot | null;
|
||||
setPaused(paused: boolean): void;
|
||||
@@ -19,21 +35,21 @@ export interface PhysicsAdapter {
|
||||
reset(): void;
|
||||
singleStep(): void;
|
||||
setActuator(id: number, value: number): void;
|
||||
setActuatorParameters(id:number,parameters:ActuatorParameters):boolean;
|
||||
setJointPosition(id:number,value:number):boolean;
|
||||
resetJoints():void;
|
||||
setIgnoreJointLimits(ignore:boolean):void;
|
||||
setActuatorParameters(id: number, parameters: ActuatorParameters): boolean;
|
||||
setJointPosition(id: number, value: number): boolean;
|
||||
resetJoints(): void;
|
||||
setIgnoreJointLimits(ignore: boolean): void;
|
||||
setExternalForce(bodyId: number, force: [number, number, number]): void;
|
||||
clearExternalForce(): void;
|
||||
loadPythonController(source:string,path:string):Promise<ControllerStatus>;
|
||||
setControllerEnabled(enabled:boolean):void;
|
||||
sendControllerCommand(command:ControllerCommand):void;
|
||||
removeController():void;
|
||||
loadRLPolicy(model:Uint8Array,path:string):Promise<RLPolicyStatus>;
|
||||
setRLPolicyEnabled(enabled:boolean):void;
|
||||
setRLCommand(command:RLCommand):void;
|
||||
removeRLPolicy():void;
|
||||
cachedSupportFiles():ProjectFile[];
|
||||
loadPythonController(source: string, path: string): Promise<ControllerStatus>;
|
||||
setControllerEnabled(enabled: boolean): void;
|
||||
sendControllerCommand(command: ControllerCommand): void;
|
||||
removeController(): void;
|
||||
loadRLPolicy(model: Uint8Array, path: string): Promise<RLPolicyStatus>;
|
||||
setRLPolicyEnabled(enabled: boolean): void;
|
||||
setRLCommand(command: RLCommand): void;
|
||||
removeRLPolicy(): void;
|
||||
cachedSupportFiles(): ProjectFile[];
|
||||
exportMjcf(): Uint8Array;
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -53,68 +69,204 @@ export function getMujocoModule(): Promise<MainModule> {
|
||||
export class MainThreadPhysicsAdapter implements PhysicsAdapter {
|
||||
session: SimulationSession | null = null;
|
||||
workspace: MemfsWorkspace | null = null;
|
||||
private supportFiles:ProjectFile[]=[];
|
||||
private supportFiles: ProjectFile[] = [];
|
||||
|
||||
async load(manifest:ProjectManifest,entryPath:string,urdfMode:UrdfLoadMode='mjcf',baseMode:UrdfBaseMode='floating',enhancements:UrdfEnhancementOptions={addActuators:false,addSensors:false,sensorType:'camera'}):Promise<SimulationSnapshot> {
|
||||
this.releaseCurrent();const module=await getMujocoModule();const workspace=new MemfsWorkspace(module,manifest.id);const prepared=prepareProjectForMujoco(manifest,entryPath);
|
||||
this.supportFiles=prepared.manifest.files.filter(file=>!manifest.files.some(original=>original.path===file.path));
|
||||
try{
|
||||
console.info('[MuJoCo] 写入 MEMFS',prepared.manifest.files.length);workspace.mount(prepared.manifest);
|
||||
const entry=prepared.manifest.entries.find(candidate=>candidate.path===entryPath);let modelPath=workspace.path(entryPath);const warnings=[...prepared.warnings];
|
||||
if(entry?.format==='urdf'&&urdfMode==='mjcf'){
|
||||
console.info('[MuJoCo] 编译 URDF 中间模型',entryPath);const intermediate=new SimulationSession(module,modelPath);
|
||||
try{
|
||||
const minimumZ=intermediate.minimumGeometryZ();const slash=entryPath.lastIndexOf('/');const directory=slash>=0?entryPath.slice(0,slash+1):'';const convertedPath=`${directory}.__mujoco_converted_${manifest.id.replace(/[^a-zA-Z0-9_-]/g,'_')}.xml`;
|
||||
if(module.mj_saveLastXML(workspace.path(convertedPath),intermediate.model)===0)throw new Error('MuJoCo 无法导出中间 MJCF');
|
||||
const grounded=groundConvertedMjcf(new TextEncoder().encode(workspace.readText(convertedPath)),minimumZ,baseMode);
|
||||
const enhanced=enhanceConvertedMjcf(grounded,enhancements);
|
||||
workspace.writeGenerated(convertedPath,enhanced.data);modelPath=workspace.path(convertedPath);
|
||||
warnings.push(`URDF 已转换为 MJCF(${baseMode==='floating'?'浮动基座':'固定基座'}),并整体平移 ${(-minimumZ).toFixed(4)} m,使最低点接触 z=0 地面`);
|
||||
if(enhanced.actuatorCount)warnings.push(`已为 ${enhanced.actuatorCount} 个 hinge/slide 关节生成 motor 驱动器(控制输入不限幅;hinge 输出单位 N·m,slide 输出单位 N)`);
|
||||
if(enhanced.unitreeGo2wTuned)warnings.unshift('已识别 Unitree Go2-W,并补齐官方 MuJoCo 关节惯量/阻尼、力矩限幅和轮胎接触参数');
|
||||
if(enhanced.imuAdded)warnings.unshift('已在浮动基座添加6轴 IMU:imu_gyro(三轴角速度)和 imu_acc(三轴加速度)');
|
||||
if(enhanced.cameraAdded)warnings.push(`已将 640×480 摄像头固连到 ${enhancements.cameraMountBody||'自动选择的头部/末端 body'},局部位置 ${(enhancements.cameraPosition??[.1,0,.05]).join(' ')} m,朝向 ${enhancements.cameraDirection??'+X'}`);
|
||||
}finally{intermediate.dispose();}
|
||||
async load(
|
||||
manifest: ProjectManifest,
|
||||
entryPath: string,
|
||||
urdfMode: UrdfLoadMode = 'mjcf',
|
||||
baseMode: UrdfBaseMode = 'floating',
|
||||
enhancements: UrdfEnhancementOptions = {
|
||||
addActuators: false,
|
||||
addSensors: false,
|
||||
sensorType: 'camera',
|
||||
},
|
||||
): Promise<SimulationSnapshot> {
|
||||
this.releaseCurrent();
|
||||
const module = await getMujocoModule();
|
||||
const workspace = new MemfsWorkspace(module, manifest.id);
|
||||
const prepared = prepareProjectForMujoco(manifest, entryPath);
|
||||
this.supportFiles = prepared.manifest.files.filter(
|
||||
(file) => !manifest.files.some((original) => original.path === file.path),
|
||||
);
|
||||
try {
|
||||
console.info('[MuJoCo] 写入 MEMFS', prepared.manifest.files.length);
|
||||
workspace.mount(prepared.manifest);
|
||||
const entry = prepared.manifest.entries.find((candidate) => candidate.path === entryPath);
|
||||
let modelPath = workspace.path(entryPath);
|
||||
const warnings = [...prepared.warnings];
|
||||
if (entry?.format === 'urdf' && urdfMode === 'mjcf') {
|
||||
console.info('[MuJoCo] 编译 URDF 中间模型', entryPath);
|
||||
const intermediate = new SimulationSession(module, modelPath);
|
||||
try {
|
||||
const minimumZ = intermediate.minimumGeometryZ();
|
||||
const slash = entryPath.lastIndexOf('/');
|
||||
const directory = slash >= 0 ? entryPath.slice(0, slash + 1) : '';
|
||||
const convertedPath = `${directory}.__mujoco_converted_${manifest.id.replace(/[^a-zA-Z0-9_-]/g, '_')}.xml`;
|
||||
if (module.mj_saveLastXML(workspace.path(convertedPath), intermediate.model) === 0)
|
||||
throw new Error('MuJoCo 无法导出中间 MJCF');
|
||||
const grounded = groundConvertedMjcf(
|
||||
new TextEncoder().encode(workspace.readText(convertedPath)),
|
||||
minimumZ,
|
||||
baseMode,
|
||||
);
|
||||
const enhanced = enhanceConvertedMjcf(grounded, enhancements);
|
||||
workspace.writeGenerated(convertedPath, enhanced.data);
|
||||
modelPath = workspace.path(convertedPath);
|
||||
warnings.push(
|
||||
`URDF 已转换为 MJCF(${baseMode === 'floating' ? '浮动基座' : '固定基座'}),并整体平移 ${(-minimumZ).toFixed(4)} m,使最低点接触 z=0 地面`,
|
||||
);
|
||||
if (enhanced.actuatorCount)
|
||||
warnings.push(
|
||||
`已为 ${enhanced.actuatorCount} 个 hinge/slide 关节生成 motor 驱动器(控制输入不限幅;hinge 输出单位 N·m,slide 输出单位 N)`,
|
||||
);
|
||||
if (enhanced.unitreeGo2wTuned)
|
||||
warnings.unshift(
|
||||
'已识别 Unitree Go2-W,并补齐官方 MuJoCo 关节惯量/阻尼、力矩限幅和轮胎接触参数',
|
||||
);
|
||||
if (enhanced.imuAdded)
|
||||
warnings.unshift(
|
||||
'已在浮动基座添加6轴 IMU:imu_gyro(三轴角速度)和 imu_acc(三轴加速度)',
|
||||
);
|
||||
if (enhanced.cameraAdded)
|
||||
warnings.push(
|
||||
`已将 640×480 摄像头固连到 ${enhancements.cameraMountBody || '自动选择的头部/末端 body'},局部位置 ${(enhancements.cameraPosition ?? [0.1, 0, 0.05]).join(' ')} m,朝向 ${enhancements.cameraDirection ?? '+X'}`,
|
||||
);
|
||||
} finally {
|
||||
intermediate.dispose();
|
||||
}
|
||||
}
|
||||
console.info('[MuJoCo] 编译模型',modelPath);const session=new SimulationSession(module,modelPath,warnings);
|
||||
if(entry?.format==='urdf'&&urdfMode==='native'){const offset=session.alignLowestPointToGround();warnings.push(`原生 URDF 已整体平移 ${offset.toFixed(4)} m,使最低点位于 z=0`);}
|
||||
if(warnings.length)console.info('[MuJoCo] URDF 兼容处理',warnings);
|
||||
console.info('[MuJoCo] 模型编译完成');this.workspace=workspace;this.session=session;const snapshot=session.snapshot();console.info('[MuJoCo] 状态快照完成');return snapshot;
|
||||
}catch(error){workspace.dispose();throw new Error(`模型编译失败(${entryPath}):${error instanceof Error?error.message:String(error)}`,{cause:error});}
|
||||
console.info('[MuJoCo] 编译模型', modelPath);
|
||||
const session = new SimulationSession(module, modelPath, warnings);
|
||||
if (entry?.format === 'urdf' && urdfMode === 'native') {
|
||||
const offset = session.alignLowestPointToGround();
|
||||
warnings.push(`原生 URDF 已整体平移 ${offset.toFixed(4)} m,使最低点位于 z=0`);
|
||||
}
|
||||
if (warnings.length) console.info('[MuJoCo] URDF 兼容处理', warnings);
|
||||
console.info('[MuJoCo] 模型编译完成');
|
||||
this.workspace = workspace;
|
||||
this.session = session;
|
||||
const snapshot = session.snapshot();
|
||||
console.info('[MuJoCo] 状态快照完成');
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
workspace.dispose();
|
||||
throw new Error(
|
||||
`模型编译失败(${entryPath}):${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
advance(now:number):FrameResult{return this.session?.advance(now)??{steps:0,stepMs:0,overBudget:false};}
|
||||
snapshot():SimulationSnapshot|null{return this.session?.snapshot()??null;}
|
||||
setPaused(value:boolean):void{this.session?.setPaused(value);}
|
||||
setSpeed(value:number):void{this.session?.setSpeed(value);}
|
||||
reset():void{this.session?.reset();}
|
||||
singleStep():void{this.session?.singleStep();}
|
||||
setActuator(id:number,value:number):void{this.session?.setActuator(id,value);}
|
||||
setActuatorParameters(id:number,parameters:ActuatorParameters):boolean{return this.session?.setActuatorParameters(id,parameters)??false;}
|
||||
setJointPosition(id:number,value:number):boolean{return this.session?.setJointPosition(id,value)??false;}
|
||||
resetJoints():void {this.session?.resetJoints();}
|
||||
setIgnoreJointLimits(ignore:boolean):void {this.session?.setIgnoreJointLimits(ignore);}
|
||||
setExternalForce(bodyId:number,force:[number,number,number]):void{this.session?.setExternalForce(bodyId,force);}
|
||||
clearExternalForce():void{this.session?.clearExternalForce();}
|
||||
async loadPythonController(source:string,path:string):Promise<ControllerStatus>{if(!this.session)throw new Error('请先加载模型');return this.session.loadPythonController(source,path);}
|
||||
setControllerEnabled(enabled:boolean):void{this.session?.setControllerEnabled(enabled);}
|
||||
sendControllerCommand(command:ControllerCommand):void{this.session?.sendControllerCommand(command);}
|
||||
removeController():void{this.session?.removeController();}
|
||||
async loadRLPolicy(model:Uint8Array,path:string):Promise<RLPolicyStatus>{if(!this.session)throw new Error('请先加载模型');return this.session.loadRLPolicy(model,path);}
|
||||
setRLPolicyEnabled(enabled:boolean):void{this.session?.setRLPolicyEnabled(enabled);}
|
||||
setRLCommand(command:RLCommand):void{this.session?.setRLCommand(command);}
|
||||
removeRLPolicy():void{this.session?.removeRLPolicy();}
|
||||
cachedSupportFiles():ProjectFile[]{return this.supportFiles.map(file=>({...file,data:file.data.slice()}));}
|
||||
exportMjcf():Uint8Array{
|
||||
if(!this.session||!this.workspace)throw new Error('尚未加载可导出的模型');
|
||||
const relative='.__platform_export__.xml';
|
||||
if(this.session.module.mj_saveLastXML(this.workspace.path(relative),this.session.model)===0)throw new Error('MuJoCo 无法生成 MJCF');
|
||||
const source=this.workspace.readText(relative),document=new DOMParser().parseFromString(source,'application/xml'),actuatorSection=document.querySelector('mujoco > actuator');
|
||||
if(document.querySelector('parsererror'))return new TextEncoder().encode(source);
|
||||
const snapshot=this.session.snapshot();
|
||||
if(actuatorSection){for(const info of snapshot.actuators){const element=Array.from(actuatorSection.children).find(candidate=>candidate.getAttribute('name')===info.name);if(!element)continue;element.setAttribute('ctrllimited',info.ctrlLimited?'true':'false');element.setAttribute('forcelimited',info.forceLimited?'true':'false');}}
|
||||
for(const info of snapshot.actuators){if(info.kind!=='motor'||!info.jointName)continue;const joint=Array.from(document.querySelectorAll('worldbody joint[name]')).find(candidate=>candidate.getAttribute('name')===info.jointName);if(joint){joint.setAttribute('stiffness',String(info.kp));joint.setAttribute('damping',String(info.kv));}}
|
||||
const output=new TextEncoder().encode(new XMLSerializer().serializeToString(document));this.workspace.writeGenerated(relative,output);return output;
|
||||
advance(now: number): FrameResult {
|
||||
return this.session?.advance(now) ?? { steps: 0, stepMs: 0, overBudget: false };
|
||||
}
|
||||
snapshot(): SimulationSnapshot | null {
|
||||
return this.session?.snapshot() ?? null;
|
||||
}
|
||||
setPaused(value: boolean): void {
|
||||
this.session?.setPaused(value);
|
||||
}
|
||||
setSpeed(value: number): void {
|
||||
this.session?.setSpeed(value);
|
||||
}
|
||||
reset(): void {
|
||||
this.session?.reset();
|
||||
}
|
||||
singleStep(): void {
|
||||
this.session?.singleStep();
|
||||
}
|
||||
setActuator(id: number, value: number): void {
|
||||
this.session?.setActuator(id, value);
|
||||
}
|
||||
setActuatorParameters(id: number, parameters: ActuatorParameters): boolean {
|
||||
return this.session?.setActuatorParameters(id, parameters) ?? false;
|
||||
}
|
||||
setJointPosition(id: number, value: number): boolean {
|
||||
return this.session?.setJointPosition(id, value) ?? false;
|
||||
}
|
||||
resetJoints(): void {
|
||||
this.session?.resetJoints();
|
||||
}
|
||||
setIgnoreJointLimits(ignore: boolean): void {
|
||||
this.session?.setIgnoreJointLimits(ignore);
|
||||
}
|
||||
setExternalForce(bodyId: number, force: [number, number, number]): void {
|
||||
this.session?.setExternalForce(bodyId, force);
|
||||
}
|
||||
clearExternalForce(): void {
|
||||
this.session?.clearExternalForce();
|
||||
}
|
||||
async loadPythonController(source: string, path: string): Promise<ControllerStatus> {
|
||||
if (!this.session) throw new Error('请先加载模型');
|
||||
return this.session.loadPythonController(source, path);
|
||||
}
|
||||
setControllerEnabled(enabled: boolean): void {
|
||||
this.session?.setControllerEnabled(enabled);
|
||||
}
|
||||
sendControllerCommand(command: ControllerCommand): void {
|
||||
this.session?.sendControllerCommand(command);
|
||||
}
|
||||
removeController(): void {
|
||||
this.session?.removeController();
|
||||
}
|
||||
async loadRLPolicy(model: Uint8Array, path: string): Promise<RLPolicyStatus> {
|
||||
if (!this.session) throw new Error('请先加载模型');
|
||||
return this.session.loadRLPolicy(model, path);
|
||||
}
|
||||
setRLPolicyEnabled(enabled: boolean): void {
|
||||
this.session?.setRLPolicyEnabled(enabled);
|
||||
}
|
||||
setRLCommand(command: RLCommand): void {
|
||||
this.session?.setRLCommand(command);
|
||||
}
|
||||
removeRLPolicy(): void {
|
||||
this.session?.removeRLPolicy();
|
||||
}
|
||||
cachedSupportFiles(): ProjectFile[] {
|
||||
return this.supportFiles.map((file) => ({ ...file, data: file.data.slice() }));
|
||||
}
|
||||
exportMjcf(): Uint8Array {
|
||||
if (!this.session || !this.workspace) throw new Error('尚未加载可导出的模型');
|
||||
const relative = '.__platform_export__.xml';
|
||||
if (this.session.module.mj_saveLastXML(this.workspace.path(relative), this.session.model) === 0)
|
||||
throw new Error('MuJoCo 无法生成 MJCF');
|
||||
const source = this.workspace.readText(relative),
|
||||
document = new DOMParser().parseFromString(source, 'application/xml'),
|
||||
actuatorSection = document.querySelector('mujoco > actuator');
|
||||
if (document.querySelector('parsererror')) return new TextEncoder().encode(source);
|
||||
const snapshot = this.session.snapshot();
|
||||
if (actuatorSection) {
|
||||
for (const info of snapshot.actuators) {
|
||||
const element = Array.from(actuatorSection.children).find(
|
||||
(candidate) => candidate.getAttribute('name') === info.name,
|
||||
);
|
||||
if (!element) continue;
|
||||
element.setAttribute('ctrllimited', info.ctrlLimited ? 'true' : 'false');
|
||||
element.setAttribute('forcelimited', info.forceLimited ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
for (const info of snapshot.actuators) {
|
||||
if (info.kind !== 'motor' || !info.jointName) continue;
|
||||
const joint = Array.from(document.querySelectorAll('worldbody joint[name]')).find(
|
||||
(candidate) => candidate.getAttribute('name') === info.jointName,
|
||||
);
|
||||
if (joint) {
|
||||
joint.setAttribute('stiffness', String(info.kp));
|
||||
joint.setAttribute('damping', String(info.kv));
|
||||
}
|
||||
}
|
||||
const output = new TextEncoder().encode(new XMLSerializer().serializeToString(document));
|
||||
this.workspace.writeGenerated(relative, output);
|
||||
return output;
|
||||
}
|
||||
private releaseCurrent(): void {
|
||||
this.session?.dispose();
|
||||
this.session = null;
|
||||
this.workspace?.dispose();
|
||||
this.workspace = null;
|
||||
this.supportFiles = [];
|
||||
}
|
||||
dispose(): void {
|
||||
this.releaseCurrent();
|
||||
}
|
||||
private releaseCurrent():void{this.session?.dispose(); this.session=null; this.workspace?.dispose(); this.workspace=null;this.supportFiles=[];}
|
||||
dispose():void{this.releaseCurrent();}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user