| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566 |
- import assert from 'node:assert/strict'
- import {selectDetailRegions,createRegionLoadingController} from '../../src/modules/cockpit/services/regionLoading.js'
- import {removeSceneTileset} from '../../src/modules/cockpit/services/tilesetCleanup.js'
- function event(){const f=new Set();return{addEventListener(fn){f.add(fn);return()=>f.delete(fn)},raise(...args){for(const fn of f)fn(...args)}}}
- function model(){return {tileLoad:event(),tileUnload:event(),tileVisible:event(),tileFailed:event(),loadProgress:event(),tilesLoaded:false}}
- const regions=Array.from({length:113},(_,id)=>({id,contentCount:30+(id%4)*20,retryAt:0,distance:10+id,active:false,url:'region'+id}))
- const wanted=selectDetailRegions(regions);assert.equal(wanted.length,2);assert.ok(wanted.reduce((s,i)=>s+regions[i].contentCount,0)<=180)
- assert.deepEqual(selectDetailRegions(regions,{pipeline:true}),[])
- assert.deepEqual(selectDetailRegions(regions.map(r=>({...r,distance:1000}))),[])
- const memory=selectDetailRegions(regions.map(r=>({...r,decodedGeometryBytes:60*1024*1024})));assert.equal(memory.length,1)
- const retained=selectDetailRegions(regions.map(r=>({...r,active:r.id===20})));assert.equal(retained[0],20)
- assert.deepEqual(selectDetailRegions([{id:0,active:true,distance:500,contentCount:20,retryAt:0},{id:1,active:false,distance:10,contentCount:20,retryAt:0}],{maxRegions:1}),[1],'new near region must outrank old distant active region')
- let started=[],added=[],removed=[],ticks=0
- const models=new Map(),controller=createRegionLoadingController({regions:[{id:0,contentCount:2,expectedContentUrls:['a','b'],retryAt:0,url:'zero'},{id:1,contentCount:2,retryAt:0,url:'one'}],
- load:async r=>{started.push(r.id);const m=model();models.set(r.id,m);return m},add:m=>added.push(m),remove:m=>removed.push(m),distance:()=>10,now:()=>ticks,
- setTimer:()=>1,clearTimer:()=>{},requestRender:()=>{}})
- controller.refresh();await Promise.resolve();await Promise.resolve();assert.deepEqual(started,[0],'only one initial stream')
- const a=models.get(0);assert.equal(a.show,false);assert.equal(a.preloadWhenHidden,true)
- a.tileLoad.raise({content:{url:'unrelated'}})
- a.tileLoad.raise({content:{url:'a'}});a.tilesLoaded=true;controller.onRender();assert.equal(controller.detailReady(0),false,'one missing content preserves coarse');assert.equal(a.show,false);assert.deepEqual(started,[0])
- a.tileLoad.raise({content:{url:'b'}});controller.onRender();await Promise.resolve();await Promise.resolve()
- assert.equal(a.show,true);assert.equal(controller.detailReady(0),false,'must observe rendered detail after hidden preload')
- a.tileVisible.raise({content:{trianglesLength:1}});controller.onRender()
- assert.equal(controller.detailReady(0),true);controller.refresh();assert.equal(controller.detailReady(0),true,'timers must not unmask between rendered frames');assert.deepEqual(started,[0,1],'second admitted only after actual complete first region')
- a.tileUnload.raise({content:{url:'b'}});assert.equal(controller.detailReady(0),false,'eviction must restore coarse')
- a.tileLoad.raise({content:{url:'b'}});a.tileVisible.raise({content:{trianglesLength:1}});controller.onRender();assert.equal(controller.detailReady(0),true)
- controller.setPipeline(true);assert.equal(controller.snapshot().length,0);assert.equal(controller.detailReady(0),false);assert.equal(removed.length,2,'pipeline releases all fine regions')
- controller.setPipeline(false);await Promise.resolve();await Promise.resolve();assert.ok(controller.snapshot().length<=2)
- const fresh=models.get(0);fresh.tileFailed.raise({message:'test failure'});controller.refresh();assert.equal(controller.detailReady(0),false)
- controller.detach();
- let gate=false,front=0,frustumStarts=[],frustumRemoved=[]
- const frustum=createRegionLoadingController({regions:[{id:0,contentCount:1,retryAt:0},{id:1,contentCount:1,retryAt:0}],load:async r=>{frustumStarts.push(r.id);return model()},add:()=>{},remove:m=>frustumRemoved.push(m),canStart:()=>gate,distance:r=>r.id===front?10:Infinity,setTimer:()=>1,clearTimer:()=>{}})
- frustum.refresh();await Promise.resolve();assert.deepEqual(frustumStarts,[],'coarse must finish before fine starts')
- gate=true;frustum.refresh();await Promise.resolve();await Promise.resolve();assert.deepEqual(frustumStarts,[0])
- front=1;frustum.refresh();await Promise.resolve();await Promise.resolve();assert.deepEqual(frustumStarts,[0,1]);assert.equal(frustumRemoved.length,1,'offscreen region must release serial slot')
- frustum.detach();
- // Late fromUrl completion after view switch must be destroyed, never added.
- let resolveLoad,lateAdded=0,lateRemoved=0
- const late=createRegionLoadingController({regions:[{id:0,contentCount:1,retryAt:0}],load:()=>new Promise(r=>resolveLoad=r),add:()=>lateAdded++,remove:()=>lateRemoved++,distance:()=>1,setTimer:()=>1,clearTimer:()=>{}})
- late.refresh();late.setPipeline(true);resolveLoad(model());await Promise.resolve();await Promise.resolve();assert.equal(lateAdded,0);assert.equal(lateRemoved,1);late.detach()
- let ranked=[]
- const ranking=createRegionLoadingController({regions:[{id:0,contentCount:1,retryAt:0},{id:1,contentCount:1,retryAt:0}],load:async r=>{ranked.push(r.id);return model()},add:()=>{},remove:()=>{},distance:r=>r.id?1:100,setTimer:()=>1,clearTimer:()=>{}})
- ranking.refresh();await Promise.resolve();await Promise.resolve();assert.deepEqual(ranked,[1],'load must follow nearest priority, not manifest order');ranking.detach()
- let previewStarts=[],previewModels=new Map()
- const previews=createRegionLoadingController({regions:[{id:0,retryAt:0,contentCount:3,expectedContentUrls:['parent','fineA','fineB'],expectedPreviewContentUrls:['parent']},{id:1,retryAt:0,contentCount:1}],
- load:async r=>{previewStarts.push(r.id);const m=model();previewModels.set(r.id,m);return m},add:()=>{},remove:()=>{},distance:()=>1,setTimer:()=>1,clearTimer:()=>{}})
- previews.refresh();await Promise.resolve();await Promise.resolve()
- const parent=previewModels.get(0);parent.tileLoad.raise({content:{url:'parent'}});parent.tilesLoaded=true;previews.onRender();await Promise.resolve();await Promise.resolve()
- assert.equal(parent.show,true);assert.deepEqual(previewStarts,[0,1],'complete visible preview releases serial slot without unused LOD leaves')
- parent.tileVisible.raise({content:{trianglesLength:1}});previews.onRender();assert.equal(previews.detailReady(0),true)
- assert.equal(previews.snapshot().find(r=>r.id===0).fineVisibleLastRender,false,'preview-only readiness is not fine rendering')
- parent.tileLoad.raise({content:{url:'fineA'}});previews.refresh();assert.equal(previews.snapshot().find(r=>r.id===0).fineLoaded,1)
- assert.equal(previews.snapshot().find(r=>r.id===0).fineVisibleLastRender,false,'loaded fine is not yet visible')
- parent.tileVisible.raise({content:{url:'fineA',trianglesLength:1}});previews.onRender();assert.equal(previews.snapshot().find(r=>r.id===0).fineVisibleLastRender,true)
- previews.onRender();assert.equal(previews.snapshot().find(r=>r.id===0).fineVisibleLastRender,false,'next rendered frame without fine clears visibility')
- parent.tileVisible.raise({content:{url:'parent',trianglesLength:1}});previews.onRender()
- parent.loadProgress.raise(1,1);assert.equal(previews.detailReady(0),true,'loaded preview preserves coarse coverage while fine waits')
- parent.tileUnload.raise({content:{url:'parent'}});assert.equal(previews.detailReady(0),false,'missing preview restores global coarse')
- previews.detach()
- let destroys=0
- const unadded={isDestroyed:()=>destroys>0,destroy:()=>{destroys++}}
- const deadScene={isDestroyed:()=>true,get primitives(){throw Error('destroyed scene collection accessed')}}
- removeSceneTileset(deadScene,unadded);removeSceneTileset(deadScene,unadded);assert.equal(destroys,1)
- let collectionRemoved=0
- removeSceneTileset({isDestroyed:()=>false,primitives:{remove(){collectionRemoved++;return true}}},{isDestroyed:()=>false,destroy(){throw Error('double destruction')}});assert.equal(collectionRemoved,1)
- console.log(JSON.stringify({passed:true,scope:'2-region/180-content admission, serial content readiness, unload/failure coarse restoration, pipeline release, late async completion'}))
|