Files
rhinelab-ui/reference/viewer-motion-check.html
T

56 lines
6.3 KiB
HTML

<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>360° 导航过渡检查</title>
<style>body{font:15px system-ui;background:#e8e5e1;padding:20px}iframe{width:720px;height:405px;border:0}pre{white-space:pre-wrap}</style>
<h1>360° 导航过渡检查</h1><pre id="result">正在检查…</pre><iframe id="app" src="/?scene=detail"></iframe>
<script type="module">
import * as THREE from '/node_modules/three/build/three.module.js';
import {ViewerCameraMotion} from '/src/viewer-camera.ts';
const result=document.querySelector('#result'),app=document.querySelector('#app'),checks=[];
const check=(name,value)=>{checks.push({name,passed:!!value});if(!value)throw Error(name)};
const wait=async predicate=>{const start=performance.now();while(!predicate()){if(performance.now()-start>30000)throw Error('等待页面超时');await new Promise(r=>setTimeout(r,15))}};
const close=(a,b,epsilon=1e-5)=>Math.abs(a-b)<epsilon;
function rig(){const camera=new THREE.PerspectiveCamera(),goal=new THREE.PerspectiveCamera(),target=new THREE.Vector3();goal.position.set(7.2,3.8,12);const motion=new ViewerCameraMotion(camera);motion.snap(goal,target);return{camera,goal,target,motion}}
try {
const a=rig(),b=rig();for(const r of[a,b]){r.goal.position.multiplyScalar(.6);r.target.set(1,.5,0)}
for(let i=0;i<30;i++)a.motion.update(a.goal,a.target,1/30);for(let i=0;i<120;i++)b.motion.update(b.goal,b.target,1/120);
check('30 fps 与 120 fps 的相机过渡一致',a.camera.position.distanceTo(b.camera.position)<1e-7&&a.motion.focus.distanceTo(b.motion.focus)<1e-7);
const c=rig();const initial=c.camera.position.clone();c.goal.position.set(-6,2,-9);c.target.set(2,1,0);c.motion.update(c.goal,c.target,.1);c.goal.position.copy(initial);c.target.set(0,0,0);const before=c.camera.position.clone();c.motion.reset();
check('复位请求不会瞬间移动显示相机',c.camera.position.distanceTo(before)===0);
let minimum=Infinity;for(let i=0;i<34;i++){c.motion.update(c.goal,c.target,1/60);minimum=Math.min(minimum,c.camera.position.distanceTo(c.motion.focus))}
check('复位沿模型外侧过渡并精确回到默认视角',minimum>5&&c.camera.position.distanceTo(initial)<1e-7&&!c.motion.resetting);
c.goal.position.set(-6,3,8);c.motion.update(c.goal,c.target,.1);c.goal.position.copy(initial);c.motion.reset();c.motion.update(c.goal,c.target,.1);const interrupted=c.camera.position.clone();c.motion.interruptReset(c.goal,c.target);
check('新输入从当前画面打断复位',!c.motion.resetting&&c.goal.position.distanceTo(interrupted)<1e-7&&c.camera.position.distanceTo(interrupted)===0);
c.goal.position.set(0,1,6);c.target.set(.5,0,0);c.motion.update(c.goal,c.target,1/60,true);
check('减少动态效果时相机立即到位',c.camera.position.distanceTo(c.goal.position)<1e-7&&c.motion.focus.distanceTo(c.target)===0);
const seam=rig();seam.goal.position.setFromSpherical(new THREE.Spherical(10,1.2,Math.PI-.05));seam.motion.snap(seam.goal,seam.target);const seamStart=seam.camera.position.clone();seam.goal.position.setFromSpherical(new THREE.Spherical(10,1.2,-Math.PI+.05));seam.motion.update(seam.goal,seam.target,.05);
check('旋转越过正负 180° 时走短路径',seam.camera.position.distanceTo(seamStart)<1);
await wait(()=>app.contentWindow?.rhine&&app.contentDocument.querySelector('#stage')?.dataset.mode==='detail'&&!app.contentDocument.querySelector('#loading'));
const doc=app.contentDocument,win=app.contentWindow;
doc.querySelector('.viewer-open').click();const viewer=doc.querySelector('.model-viewer');
const stats=()=>JSON.parse(viewer.dataset.stats||'{}');await wait(()=>stats().ready);
const canvas=viewer.querySelector('canvas');
const key=key=>canvas.dispatchEvent(new win.KeyboardEvent('keydown',{key,bubbles:true,cancelable:true}));
const wheel=deltaY=>canvas.dispatchEvent(new win.WheelEvent('wheel',{deltaY,deltaMode:0,bubbles:true,cancelable:true}));
const origin=stats();key('ArrowRight');await wait(()=>stats().requestedTarget?.some(v=>Math.abs(v)>.01));
let s=stats();check('方向键平移有中间位置',new THREE.Vector3(...s.targetPosition).length()>0&&new THREE.Vector3(...s.targetPosition).distanceTo(new THREE.Vector3(...s.requestedTarget))>.001);
await wait(()=>new THREE.Vector3(...stats().targetPosition).distanceTo(new THREE.Vector3(...stats().requestedTarget))<.001);
const beforeWheel=stats().distance;wheel(-120);wheel(-120);await wait(()=>stats().requestedDistance<beforeWheel-.01);s=stats();
check('滚轮连续缩放累计目标并平滑追随',s.distance<beforeWheel&&s.distance>s.requestedDistance+.001);
await wait(()=>close(stats().distance,stats().requestedDistance,.001));
const beforeKey=stats().distance;key('+');await wait(()=>stats().requestedDistance<beforeKey-.01);s=stats();
check('加减键缩放也有中间距离',s.distance>s.requestedDistance+.001&&s.distance<beforeKey);
viewer.querySelector('[data-viewer="reset"]').click();await wait(()=>stats().resetting);
check('复位按钮进入过渡而非瞬移',!close(stats().distance,origin.distance,.001));
key('ArrowUp');await wait(()=>!stats().resetting);
check('实际方向键能中断正在复位的视角',stats().requestedTarget.some(v=>Math.abs(v)>.01));
key('Home');await wait(()=>!stats().resetting&&close(stats().distance,origin.distance,.0001));
check('Home 复位精确恢复相机和观察中心',new THREE.Vector3(...stats().cameraPosition).distanceTo(new THREE.Vector3(...origin.cameraPosition))<.0001&&new THREE.Vector3(...stats().targetPosition).length()<.0001);
for(let i=0;i<80;i++)key('ArrowRight');for(let i=0;i<80;i++)key('+');await wait(()=>close(stats().requestedDistance,5));
check('连续输入仍遵守平移与缩放边界',new THREE.Vector3(...stats().requestedTarget).length()<=5.00001&&stats().requestedDistance>=4.99999);
key('Home');await wait(()=>!stats().resetting&&close(stats().distance,origin.distance,.0001));
viewer.querySelector('[data-viewer="close"]').click();await wait(()=>viewer.hidden);
doc.querySelector('.viewer-open').click();await wait(()=>!viewer.hidden&&stats().ready);
check('关闭后重新进入没有残留镜头位移',new THREE.Vector3(...stats().targetPosition).length()<.0001&&close(stats().distance,origin.distance,.0001));
result.textContent=JSON.stringify({passed:true,checks},null,2);
}catch(error){result.textContent=JSON.stringify({passed:false,error:String(error),checks},null,2)}
</script></html>