161 lines
8.5 KiB
HTML
161 lines
8.5 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<meta charset="utf-8">
|
|
<title>循环阵列运行检查</title>
|
|
<style>body{margin:0;background:#e8e5e1;color:#252620;font:13px monospace}#scene{width:1100px;height:620px}pre{white-space:pre-wrap;padding:18px}</style>
|
|
<div id="scene"></div><pre id="result">运行实际场景检查…</pre>
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { ArchiveScene } from '/src/scene.ts';
|
|
import { columnFiles, fileLocation } from '/src/data.ts';
|
|
import { wrap, fileAtCell, cellKey, COLUMN_SPACING, ROW_SPACING } from '/src/archive-loop.ts';
|
|
import { smooth, cinematicField } from '/src/motion.ts';
|
|
const scene = new ArchiveScene(document.querySelector('#scene'));
|
|
await scene.load();
|
|
const draw=scene.composer.render.bind(scene.composer);
|
|
scene.composer.render=()=>{};
|
|
let time=0,index=0;
|
|
const step=(frames=1)=>{for(let f=0;f<frames;f++)scene.update(time+=1/60);};
|
|
const errors=[];
|
|
const expect=(value,message)=>{if(!value)errors.push(message);};
|
|
const memory=Array.from({length:5},(_,lane)=>columnFiles(lane)[0]);
|
|
scene.select(index);
|
|
scene.setMode('archive');
|
|
step(300);
|
|
const initial=scene.getStats();
|
|
const moves=[];
|
|
function move(axis,direction){
|
|
const before={...scene.selectedCell};
|
|
const lane=fileLocation(index).lane;
|
|
if(axis==='row'){
|
|
const files=columnFiles(lane);
|
|
index=files[wrap(files.indexOf(index)+direction,files.length)];
|
|
}else index=memory[wrap(lane+direction,5)];
|
|
memory[fileLocation(index).lane]=index;
|
|
scene.select(index,{axis,direction});
|
|
expect(scene.selectedCell[axis]-before[axis]===direction,'Selection reversed at '+JSON.stringify(before));
|
|
expect(fileAtCell(scene.selectedCell)===index,'Wrong file in recycled cell');
|
|
step(24);
|
|
const stats=scene.getStats();
|
|
moves.push({axis,direction,file:index+1,cell:stats.selectedCell,rail:stats.rail,track:stats.columnCamera});
|
|
expect(stats.archiveCount>0&&stats.archiveCount<=stats.archiveCapacity,'Invalid dynamic draw count');
|
|
expect(new Set(scene.drawnCells.map(cellKey)).size===stats.archiveCount,'Duplicate physical cell');
|
|
}
|
|
// Cross each seam repeatedly in both directions, including negative cells.
|
|
for(let i=0;i<18;i++)move('row',1);
|
|
for(let i=0;i<22;i++)move('row',-1);
|
|
for(let i=0;i<8;i++)move('lane',1);
|
|
for(let i=0;i<12;i++)move('lane',-1);
|
|
step(300);
|
|
const afterLoops=scene.getStats();
|
|
expect(afterLoops.returningFiles===0,'Returning clones did not settle');
|
|
expect(Math.abs(afterLoops.modelPosition[0])<0.001,'Array track did not center the selected column');
|
|
expect(Math.abs(afterLoops.modelPosition[2]+2.17)<0.001,'Depth track did not center the selected row');
|
|
|
|
// Confirm that compacted GPU indices still select their physical cells.
|
|
scene.scene.updateMatrixWorld(true);
|
|
let pickSamples=0;
|
|
for(const x of [-.6,0,.6])for(const y of [-.5,0,.5]) {
|
|
scene.raycaster.setFromCamera(new THREE.Vector2(x,y),scene.camera);
|
|
const hit=scene.raycaster.intersectObjects([scene.instances[0],scene.model],true)[0];
|
|
if(hit?.instanceId===undefined)continue;
|
|
const matrix=new THREE.Matrix4();scene.instances[0].getMatrixAt(hit.instanceId,matrix);
|
|
const point=new THREE.Vector3().setFromMatrixPosition(matrix);
|
|
const expected={lane:Math.round((point.x+scene.columnCamera.value)/COLUMN_SPACING+2),row:Math.round((point.z-scene.rail.value)/ROW_SPACING+15.5)};
|
|
const rect=scene.renderer.domElement.getBoundingClientRect();
|
|
const picked=scene.pickCell(rect.left+(x+1)*rect.width/2,rect.top+(1-y)*rect.height/2);
|
|
expect(picked&&cellKey(picked)===cellKey(expected),'Packed instance picked wrong archive');pickSamples++;
|
|
}
|
|
expect(pickSamples>0,'No packed-array picking samples');
|
|
|
|
// A large logical origin must be reduced without changing physical positions,
|
|
// spring velocities, current document or idle-motion phase.
|
|
const physical=()=>({
|
|
x:(scene.selectedCell.lane-2)*COLUMN_SPACING-scene.columnCamera.value,
|
|
z:(scene.selectedCell.row-15.5)*ROW_SPACING+scene.rail.value,
|
|
phaseRow:scene.selectedCell.row+scene.coordinateOrigin.row,
|
|
phaseLane:scene.selectedCell.lane+scene.coordinateOrigin.lane,
|
|
rowVelocity:scene.rail.velocity,laneVelocity:scene.columnCamera.velocity,
|
|
});
|
|
const beforeRebase=physical();
|
|
const offset={lane:2500,row:-3200};
|
|
scene.selectedCell.lane+=offset.lane;scene.selectedCell.row+=offset.row;
|
|
scene.coordinateOrigin.lane-=offset.lane;scene.coordinateOrigin.row-=offset.row;
|
|
scene.laneFocus.value+=offset.lane;scene.shoulder.value+=offset.row;
|
|
scene.columnCamera.value+=offset.lane*COLUMN_SPACING;
|
|
scene.rail.value-=offset.row*ROW_SPACING;
|
|
for(const pulse of scene.pulses){pulse.lane+=offset.lane;pulse.row+=offset.row;}
|
|
scene.rebaseCoordinates();
|
|
const afterRebase=physical();
|
|
expect(Object.keys(beforeRebase).every(k=>Math.abs(beforeRebase[k]-afterRebase[k])<1e-9),'Logical rebasing changed the visible state');
|
|
expect(fileAtCell(scene.selectedCell)===index,'Rebasing changed selected document');
|
|
|
|
// Extraction stays vertical at a wrapped position, and return still aligns
|
|
// before lowering. Re-selecting its cell restores the same returning copy.
|
|
scene.setMode('detail');
|
|
step(300);
|
|
const detail=scene.getStats();
|
|
expect(detail.canInspect&&detail.extraction===4.05,'Wrapped file cannot enter inspection');
|
|
scene.rotation=scene.targetRotation=0.8;step(1);
|
|
const height=scene.model.position.y;
|
|
scene.setMode('archive');step(20);
|
|
const aligning=scene.getStats();
|
|
expect(aligning.returnPhase==='aligning','Wrapped card lowered before alignment');
|
|
expect(Math.abs(scene.model.position.y-height)<1e-8,'Wrapped card did not hold its height');
|
|
const oldCell={...scene.selectedCell};
|
|
const oldIndex=index;
|
|
move('row',1);
|
|
const returning=scene.outgoing.find(o=>cellKey(o.cell)===cellKey(oldCell));
|
|
expect(Boolean(returning),'Wrapped outgoing card lost its physical cell');
|
|
const returningLift=returning?.lift.value;
|
|
scene.select(oldIndex,{cell:oldCell});
|
|
expect(Math.abs(scene.lift.value-returningLift)<1e-9,'Re-selection lost returning height');
|
|
step(300);
|
|
const finalStats=scene.getStats();
|
|
// Replaying the reference animation after looping restores the original grid.
|
|
scene.setMode('hidden');scene.select(0);
|
|
const matrix=new THREE.Matrix4(),position=new THREE.Vector3();
|
|
let referenceError=0,referenceVisible=0;
|
|
for(const shot of [22.60,24.16,26.56,34]){
|
|
const zoom=0.55*smooth((shot-27.3)/1.65)+0.45*smooth((shot-29)/5);
|
|
scene.update(time+=1/60,{reveal:1,lift:0,zoom,time:shot});
|
|
const entryZ=-23*(1-Math.min(1,Math.max(0,(shot-21.92)/0.75)))**2;
|
|
let visible=0;
|
|
for(let i=0;i<scene.instances[0].count;i++){
|
|
scene.instances[0].getMatrixAt(i,matrix);
|
|
const scale=new THREE.Vector3().setFromMatrixScale(matrix).length();
|
|
if(scale>0.001)visible++;
|
|
if(i>=160)expect(scale===0,'Extra interactive column entered reference animation');
|
|
if(i<160&&i!==76){
|
|
position.setFromMatrixPosition(matrix);
|
|
const {lane,row}=scene.drawnCells[i];
|
|
expect(lane>=0&&lane<5&&row>=0&&row<32,'Extra cell entered reference animation');
|
|
const expected=new THREE.Vector3((lane-2)*5.2,-4.6+cinematicField(row,lane,shot,12,2),(row-15.5)*0.62+entryZ);
|
|
referenceError=Math.max(referenceError,position.distanceTo(expected));
|
|
}
|
|
}
|
|
referenceVisible=visible;
|
|
expect(visible===159,'Reference animation instance count changed');
|
|
}
|
|
expect(referenceError<1e-5,'Reference animation grid changed');
|
|
const opening=[];
|
|
const container=document.querySelector('#scene');
|
|
container.dataset.layout='opening';
|
|
for(const [width,height] of [[1920,1080],[3840,1080],[1080,1920]]) {
|
|
container.style.width=width+'px';container.style.height=height+'px';scene.resize();
|
|
for(const shot of [22.2,24,25.8]) {
|
|
scene.update(time+=1/60,{reveal:1,lift:0,zoom:0,time:shot});
|
|
opening.push({width,height,shot,count:scene.drawnCells.length});
|
|
expect(scene.drawnCells.some(c=>c.row<0||c.row>=32||c.lane<0||c.lane>=5),'Normal opening retained finite grid');
|
|
expect(scene.drawnCells.length>0,'Normal opening has no cards');
|
|
}
|
|
}
|
|
delete container.dataset.layout;container.style.width='1100px';container.style.height='620px';scene.resize();
|
|
scene.update(time+=1/60,{reveal:1,lift:0,zoom:1,time:34});
|
|
expect(scene.drawnCells.length===159,'Explicit reference did not restore fixed grid after responsive opening');
|
|
draw();
|
|
document.querySelector('#result').textContent=JSON.stringify({passed:errors.length===0,errors,opening,moves:moves.length,pickSamples,initialPool:initial.archiveCount,reference:{visible:referenceVisible,error:referenceError},afterLoops,rebasing:{before:beforeRebase,after:afterRebase},detail:{canInspect:detail.canInspect,lift:detail.extraction},aligning:{phase:aligning.returnPhase,height:aligning.modelPosition[1]},lastMoves:moves.slice(-5),finalStats},null,2);
|
|
if(errors.length)throw new Error(errors.join('\n'));
|
|
</script>
|
|
</html>
|