Import the 2026-10-09 source release, Fastify/SQLite backend, restored source assets, and analytics-enabled server build. Replace files from the previous release while retaining the existing repository history. Make builds portable across Windows and Linux, configure local startup, normalize Windows static asset paths, and fix test fixture cleanup and file URL handling. Exclude local environment settings, databases, dependencies, validation output, and TypeScript build cache from version control. Validation: npm ci, static and server builds, 106 arrival tests, and actual local HTTP checks passed. The full suite passed 467 of 470 tests; three tests still require QA fixtures omitted from the supplied release, documented in README-서버실행.md.
41 lines
7.5 KiB
JavaScript
41 lines
7.5 KiB
JavaScript
/** CPU ray/geometry regression, not GLSL or browser pixel execution. */
|
|
import assert from 'node:assert/strict';
|
|
import * as THREE from 'three';
|
|
import {loadTypescript} from './support/load-typescript.mjs';
|
|
import {photoSurface,makeDepthTexture,tracePhotoDDA,triangleOracle,snell,exitGlass} from './support/mesh-refraction-reference.mjs';
|
|
const {WindowReveal}=await loadTypescript(new URL('../src/objects/WindowReveal.ts',import.meta.url));
|
|
const {createPatternedGlassGeometry}=await loadTypescript(new URL('../src/rendering/PatternedGlassGeometry.ts',import.meta.url));
|
|
const {attachMeshRefraction}=await loadTypescript(new URL('../src/rendering/MeshRefraction.ts',import.meta.url));
|
|
const {fitWindowScale}=await loadTypescript(new URL('../src/objects/BlenderWindowFrame.ts',import.meta.url));
|
|
const aspects=[.8,1,1.6],gaps=[.04,.1296,.1576],depths=[0,.25,.4],opens=[0,.5,1],views=[[0,-.9],[0,.9],[-.9,0],[.9,0],[-.9,-.9],[.9,-.9],[-.9,.9],[.9,.9],[0,0]],edges=[[-.995,.01313],[.995,.01313],[.01313,-.995],[.01313,.995],[-.99273,-.995],[-.99273,.995],[.99273,-.995],[.99273,.995]];
|
|
const results=[],counts={cases:0,glass:0,open:0,photo:0,reveal:0,photoOnly:0,revealOnly:0,both:0,tir:0,photoFirst:0,revealFirst:0,noGlassAtTarget:0,originalPhotoMissRepaired:0};
|
|
function revealShader(uniforms,o,d){let best=null;const v=uniforms.uBRevealVertices.value;for(let wall=0;wall<4;wall++)for(const tri of [[0,1,2],[0,2,3]]){const [a,b,c]=tri.map(k=>v[wall*4+k]);const e1=b.clone().sub(a),e2=c.clone().sub(a),p=d.clone().cross(e2),det=e1.dot(p);if(Math.abs(det)<1e-9)continue;const delta=o.clone().sub(a),u=delta.dot(p)/det,q=delta.clone().cross(e1),w=d.dot(q)/det;if(u<-.00001||w<-.00001||u+w>1.00001)continue;const t=e2.dot(q)/det;if(t<.000001)continue;if(!best||t<best.t)best={t,wall};}return best;}
|
|
function check(o,d,surface,reveal,uniforms,label){const photo=tracePhotoDDA(surface,o,d).hit,oracle=triangleOracle(surface,o,d);assert.equal(!!photo,!!oracle,`${label} photo presence`);if(photo)assert.ok(Math.abs(photo.t-oracle.t)<1e-6,`${label} photo distance`);const wall=revealShader(uniforms,o,d),oracleWall=new THREE.Raycaster(o,d).intersectObjects(reveal.meshes)[0];assert.equal(!!wall,!!oracleWall,`${label} reveal presence`);if(wall)assert.ok(Math.abs(wall.t-oracleWall.distance)<1e-6,`${label} reveal distance`);const chosen=wall&&(!photo||wall.t<photo.t)?'reveal':photo?'photo':'empty';const expected=oracleWall&&(!oracle||oracleWall.distance<oracle.t)?'reveal':oracle?'photo':'empty';assert.equal(chosen,expected,`${label} ordering`);return {chosen,photoT:photo?.t??null,revealT:wall?.t??null,wall:wall?.wall??null};}
|
|
for(let i=0;i<72;i++){
|
|
const aspect=aspects[i%3],gap=gaps[Math.floor(i/3)%3],depth=depths[Math.floor(i/9)%3],open=opens[Math.floor(i/24)],curvature=i%2?1:-1,[cx,cy]=views[i%9],edge=edges[Math.floor(i/9)];
|
|
const H=812,W=375,baseH=fitWindowScale(W/H,aspect),baseW=baseH*aspect,fw=baseW*.8,fh=baseH*.8,z=.6,backZ=z-gap-depth,camera=new THREE.Vector3(cx,cy,3),centerX=-fw*open;
|
|
const pw=baseW*(3-backZ)/3,ph=baseH*(3-backZ)/3,texture=makeDepthTexture(9,9,(x,y)=>x===4&&y===4?1:0),matrix=new THREE.Matrix4().compose(new THREE.Vector3(0,0,backZ),new THREE.Quaternion(),new THREE.Vector3(pw,ph,1)),surface=photoSurface(texture,{depthScale:depth,matrix});
|
|
const source=new THREE.Mesh(surface.geometry,new THREE.ShaderMaterial({uniforms:{uTexture:{value:texture},uDepthMap:{value:texture},uDepthScale:{value:depth},uDepthBias:{value:0}}}));source.matrixAutoUpdate=false;source.matrix.copy(matrix);source.updateMatrixWorld(true);
|
|
const reveal=new WindowReveal(new THREE.MeshBasicMaterial({side:THREE.DoubleSide}));reveal.update(fw,fh,pw,ph,z,backZ);const scene=new THREE.Scene();scene.add(...reveal.meshes);scene.updateMatrixWorld(true);
|
|
const refraction=attachMeshRefraction(new THREE.MeshPhysicalMaterial(),source);refraction.syncReveal(reveal.meshes,scene);
|
|
const built=createPatternedGlassGeometry({width:fw,height:fh,grid:20,curvature}),glass=new THREE.Mesh(built.geometry,new THREE.MeshBasicMaterial());glass.position.set(centerX,0,z);glass.updateMatrixWorld(true);refraction.sync({width:fw,height:fh,grid:20,curvature},z+built.backZ,new THREE.Color(),centerX);
|
|
const target=new THREE.Vector3(pw/2*edge[0],ph/2*edge[1],backZ),incident=target.clone().sub(camera).normalize(),entry=new THREE.Raycaster(camera,incident).intersectObject(glass)[0];
|
|
let checked,status='direct',direct=check(camera,incident,surface,reveal,refraction.uniforms,`case${i} direct`);assert.notEqual(direct.chosen,'empty',`case${i} direct hole`);
|
|
if(entry){counts.glass++;const normal=entry.normal.clone().normalize(),internal=snell(incident,normal,1/1.5),local=entry.point.clone().sub(glass.position),exit=exitGlass(local,internal,{width:fw,height:fh,backZ:built.backZ});assert.ok(exit,`case${i} exit`);const outgoing=snell(internal,exit.normal.clone().negate(),1.5);if(!outgoing){counts.tir++;status='tir';}else{checked=check(exit.point.clone().add(glass.position),outgoing,surface,reveal,refraction.uniforms,`case${i} optical`);status=checked.chosen;if(checked.chosen==='empty'){const masked=new THREE.Raycaster(camera,incident).intersectObjects(reveal.meshes)[0];assert.ok(masked,`case${i} unmasked optical hole`);status='masked optical miss';}else counts[checked.chosen]++;if(checked.photoT!==null&&checked.revealT!==null){counts.both++;counts[checked.chosen==='photo'?'photoFirst':'revealFirst']++;}else if(checked.photoT!==null)counts.photoOnly++;else if(checked.revealT!==null){counts.revealOnly++;counts.originalPhotoMissRepaired++;}}}
|
|
else{counts.open++;checked=direct;counts[checked.chosen]++;}
|
|
counts.cases++;results.push({i,aspect,gap,depth,open,curvature,camera:[cx,cy],edge,status,...checked});
|
|
built.geometry.dispose();glass.material.dispose();reveal.dispose();surface.geometry.dispose();texture.dispose();
|
|
}
|
|
// Targeted ordering counterpart: same boundary ray, but flat foreground photo
|
|
// sits in front of the wall. Both must hit and the photo must win.
|
|
{
|
|
const z=.2204,pw=1.45*.8*(3-z)/3,ph=1.45*(3-z)/3;
|
|
const t=makeDepthTexture(2,2,()=>1),surface=photoSurface(t,{depthScale:.25,matrix:new THREE.Matrix4().compose(new THREE.Vector3(0,0,z),new THREE.Quaternion(),new THREE.Vector3(pw,ph,1))});
|
|
const reveal=new WindowReveal(new THREE.MeshBasicMaterial({side:THREE.DoubleSide}));reveal.update(.928,1.16,pw,ph,.6,z);const scene=new THREE.Scene();scene.add(...reveal.meshes);scene.updateMatrixWorld(true);
|
|
const source=new THREE.Mesh(surface.geometry,new THREE.ShaderMaterial({uniforms:{uTexture:{value:t}}})),r=attachMeshRefraction(new THREE.MeshPhysicalMaterial(),source);r.syncReveal(reveal.meshes,scene);
|
|
const hit=check(new THREE.Vector3(.008207625903806859,.49163956178806933,.588056),new THREE.Vector3(.03234743889587834,.452479898306991,-.8911877382599991),surface,reveal,r.uniforms,'targeted photo-first');
|
|
assert.equal(hit.chosen,'photo');assert.ok(hit.photoT!==null&&hit.revealT!==null);counts.cases++;counts.photo++;counts.both++;counts.photoFirst++;results.push({i:72,targeted:'photo in front of reveal',...hit});
|
|
}
|
|
assert.ok(counts.originalPhotoMissRepaired>0,'matrix must exercise original bug');assert.ok(counts.open>0&&counts.glass>0);assert.ok(counts.both>0,'matrix must exercise depth ordering');
|
|
const report={counts,coverage:{aspects,gaps,depths,opens,views,edges,curvature:[-1,1],stratified:true,cartesian:false},results,limits:'CPU actual geometry and independent triangle oracle only; no WebGL/browser pixel verification. No shader execution. Test sample count is73 (72 stratified + 1 targeted ordering), not exhaustive all combinations.'};console.log(JSON.stringify({counts,coverage:report.coverage},null,2));
|