import fs from 'node:fs'; import crypto from 'node:crypto'; import assert from 'node:assert/strict'; import {login,call} from './role-permissions-live.mjs'; const dir='reports/bridge-005-new-model-20260917';fs.mkdirSync(dir,{recursive:true}); const signed=await login('root'); 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;}; const fixturePath='reports/bridge-005-authoring-20260916/pilot.json'; const fixture=JSON.parse(fs.readFileSync(fixturePath)); assert.equal(fixture.sceneId,'457');assert.equal(fixture.trainingId,'458');assert.equal(fixture.taskId,'116'); const task=await api('teaching/assignments/116');assert.equal(task.status,'DRAFT'); const scene=await api('content/projects/457'),training=await api('content/projects/458'); assert.equal(scene.project.name,'005 液压泵应急切换(编排验证)'); 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)); const map={ '1液压泵':'1_24柱塞泵3_05前泵3_08新泵', '2取力器':'1_25取力器', '2取力器开关':'1_25和1_27和2_03和2_08取力器开关', '3球阀开关':'1_26球阀开关', '5辅助控制盒':'1_28辅助控制盒内', '5辅助控制盒盖':'1_28辅助控制盒盖', '5互救开关':'1_28和2_06互救开关', }; const bytes=fs.readFileSync('../../模型/0916架桥车.glb'); const gltf=JSON.parse(bytes.subarray(20,20+bytes.readUInt32LE(12))); for(const name of Object.values(map))assert.equal(gltf.nodes.filter(n=>n.name===name).length,1,name); const filename='bridge-erector-20260916.glb',resourceUrl='/models/'+filename; const dest='../unreal_tran_web/public/models/'+filename; if(fs.existsSync(dest))assert.deepEqual(fs.readFileSync(dest),bytes);else fs.copyFileSync('../../模型/0916架桥车.glb',dest); 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; const content=replace(structuredClone(scene.currentVersion.content)); content.presetId='terrain-1';content.name='真实野外训练场'; content.environment={...content.environment,presetId:'terrain-1',useModelFloor:true}; content.objects=content.objects.filter(o=>o.type!=='scene-environment'); const terrain=content.objects.find(o=>o.sceneId==='front-pump-training-yard'); Object.assign(terrain,{assetCode:'PILOT-TERRAIN-005',sourceName:'terrain-005-20260917.glb',resourceUrl:'/models/terrain-005-20260917.glb',sourceAssetId:''}); const vehicle=content.objects.find(o=>o.sceneId==='front-pump-rescue-vehicle');assert.ok(vehicle); if(vehicle.sourceName!=='0916架桥车.glb'){ // GLB world minima: original -0.6291751861572266; new -0.00013103485107424. // Preserve the existing wheel contact height under the published scene scale. vehicle.position[1]-=0.6290441513061523*vehicle.scale[1]; } Object.assign(vehicle,{sourceName:'0916架桥车.glb',resourceUrl,assetKey:'',sourceAssetId:'',assetCode:'PILOT-VEHICLE-0916',sizeBytes:bytes.length,modelEditorRevision:2,modelEditorAssetId:'bridge-erector-0916',modelOverrides:{},partOverrides:{}}); 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}`;} const assets=scene.currentVersion.assets.filter(a=>!['PILOT-VEHICLE-0916','PILOT-TERRAIN-005'].includes(a.code)); const terrainBytes=fs.readFileSync('../unreal_tran_web/public/models/terrain-005-20260917.glb'); 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}); 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}); let saved=await api('content/projects/457',{...scene.project,content,assets,dependencies:scene.currentVersion.dependencies,version:scene.project.version,changeSummary:'005 验证工程更换 0916 架桥车,重绑七个部件,保持轮胎高度'},'PUT'); saved=await api('content/projects/457/publish',{version:saved.project.version}); fixture.sceneVersionId=saved.project.publishedVersionId;fs.writeFileSync(fixturePath,JSON.stringify(fixture,null,2)); const trainingContent=replace(structuredClone(training.currentVersion.content)); await api('content/projects/458',{...training.project,content:trainingContent,assets:training.currentVersion.assets,dependencies:training.currentVersion.dependencies,version:training.project.version,changeSummary:'更新新版架桥车源节点名称,待编排编辑器重绑新场景'},'PUT'); 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)); console.log('New scene published',fixture.sceneVersionId,'; bind and publish training through the editor next');