You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

56 regels
5.5 KiB

  1. import fs from 'node:fs';
  2. import crypto from 'node:crypto';
  3. import assert from 'node:assert/strict';
  4. import {login,call} from './role-permissions-live.mjs';
  5. const dir='reports/bridge-005-new-model-20260917';fs.mkdirSync(dir,{recursive:true});
  6. const signed=await login('root');
  7. const api=async(endpoint,body,method)=>{const r=await call('tran/v1/'+endpoint,signed.accessToken,body,method);assert.equal(r.status,200,endpoint+': '+r.message);return r.data;};
  8. const fixturePath='reports/bridge-005-authoring-20260916/pilot.json';
  9. const fixture=JSON.parse(fs.readFileSync(fixturePath));
  10. assert.equal(fixture.sceneId,'457');assert.equal(fixture.trainingId,'458');assert.equal(fixture.taskId,'116');
  11. const task=await api('teaching/assignments/116');assert.equal(task.status,'DRAFT');
  12. const scene=await api('content/projects/457'),training=await api('content/projects/458');
  13. assert.equal(scene.project.name,'005 液压泵应急切换(编排验证)');
  14. for(const [name,data]of [['before-457',scene],['before-458',training],['before-116',task]])if(!fs.existsSync(dir+'/'+name+'.json'))fs.writeFileSync(dir+'/'+name+'.json',JSON.stringify(data,null,2));
  15. const map={
  16. '1液压泵':'1_24柱塞泵3_05前泵3_08新泵',
  17. '2取力器':'1_25取力器',
  18. '2取力器开关':'1_25和1_27和2_03和2_08取力器开关',
  19. '3球阀开关':'1_26球阀开关',
  20. '5辅助控制盒':'1_28辅助控制盒内',
  21. '5辅助控制盒盖':'1_28辅助控制盒盖',
  22. '5互救开关':'1_28和2_06互救开关',
  23. };
  24. const bytes=fs.readFileSync('../../模型/0916架桥车.glb');
  25. const gltf=JSON.parse(bytes.subarray(20,20+bytes.readUInt32LE(12)));
  26. for(const name of Object.values(map))assert.equal(gltf.nodes.filter(n=>n.name===name).length,1,name);
  27. const filename='bridge-erector-20260916.glb',resourceUrl='/models/'+filename;
  28. const dest='../unreal_tran_web/public/models/'+filename;
  29. if(fs.existsSync(dest))assert.deepEqual(fs.readFileSync(dest),bytes);else fs.copyFileSync('../../模型/0916架桥车.glb',dest);
  30. const replace=value=>typeof value==='string'?(map[value]||value):Array.isArray(value)?value.map(replace):value&&typeof value==='object'?Object.fromEntries(Object.entries(value).map(([k,v])=>[k,replace(v)])):value;
  31. const content=replace(structuredClone(scene.currentVersion.content));
  32. content.presetId='terrain-1';content.name='真实野外训练场';
  33. content.environment={...content.environment,presetId:'terrain-1',useModelFloor:true};
  34. content.objects=content.objects.filter(o=>o.type!=='scene-environment');
  35. const terrain=content.objects.find(o=>o.sceneId==='front-pump-training-yard');
  36. Object.assign(terrain,{assetCode:'PILOT-TERRAIN-005',sourceName:'terrain-005-20260917.glb',resourceUrl:'/models/terrain-005-20260917.glb',sourceAssetId:''});
  37. const vehicle=content.objects.find(o=>o.sceneId==='front-pump-rescue-vehicle');assert.ok(vehicle);
  38. if(vehicle.sourceName!=='0916架桥车.glb'){
  39. // GLB world minima: original -0.6291751861572266; new -0.00013103485107424.
  40. // Preserve the existing wheel contact height under the published scene scale.
  41. vehicle.position[1]-=0.6290441513061523*vehicle.scale[1];
  42. }
  43. Object.assign(vehicle,{sourceName:'0916架桥车.glb',resourceUrl,assetKey:'',sourceAssetId:'',assetCode:'PILOT-VEHICLE-0916',sizeBytes:bytes.length,modelEditorRevision:2,modelEditorAssetId:'bridge-erector-0916',modelOverrides:{},partOverrides:{}});
  44. for(const p of vehicle.modelPartTargets){const nodeIndex=gltf.nodes.findIndex(n=>n.name===p.sourceNodeName);const parentIndex=gltf.nodes.findIndex(n=>n.children?.includes(nodeIndex));const rootIndex=gltf.scenes[0].nodes.indexOf(parentIndex);p.nodePath=`0/${rootIndex}/0`;p.editorId=`bridge-0916-node-${nodeIndex}`;}
  45. const assets=scene.currentVersion.assets.filter(a=>!['PILOT-VEHICLE-0916','PILOT-TERRAIN-005'].includes(a.code));
  46. const terrainBytes=fs.readFileSync('../unreal_tran_web/public/models/terrain-005-20260917.glb');
  47. assets.push({code:'PILOT-TERRAIN-005',name:terrain.sourceName,type:'MODEL_FILE',storageUri:terrain.resourceUrl,mimeType:'model/gltf-binary',sizeBytes:terrainBytes.length,sha256:crypto.createHash('sha256').update(terrainBytes).digest('hex'),metadata:{builtin:true},status:'READY',sortOrder:4});
  48. assets.push({code:'PILOT-VEHICLE-0916',name:'0916架桥车.glb',type:'MODEL_FILE',storageUri:resourceUrl,mimeType:'model/gltf-binary',sizeBytes:bytes.length,sha256:crypto.createHash('sha256').update(bytes).digest('hex'),metadata:{builtin:true,modelRevision:'20260916'},status:'READY',sortOrder:3});
  49. let saved=await api('content/projects/457',{...scene.project,content,assets,dependencies:scene.currentVersion.dependencies,version:scene.project.version,changeSummary:'005 验证工程更换 0916 架桥车,重绑七个部件,保持轮胎高度'},'PUT');
  50. saved=await api('content/projects/457/publish',{version:saved.project.version});
  51. fixture.sceneVersionId=saved.project.publishedVersionId;fs.writeFileSync(fixturePath,JSON.stringify(fixture,null,2));
  52. const trainingContent=replace(structuredClone(training.currentVersion.content));
  53. await api('content/projects/458',{...training.project,content:trainingContent,assets:training.currentVersion.assets,dependencies:training.currentVersion.dependencies,version:training.project.version,changeSummary:'更新新版架桥车源节点名称,待编排编辑器重绑新场景'},'PUT');
  54. fs.writeFileSync(dir+'/mapping.json',JSON.stringify({source:'0916架桥车.glb',sha256:assets.at(-1).sha256,resourceUrl,sceneVersionId:fixture.sceneVersionId,map,targets:vehicle.modelPartTargets,position:vehicle.position},null,2));
  55. console.log('New scene published',fixture.sceneVersionId,'; bind and publish training through the editor next');