Replace project with current server release and Windows setup
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.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { test } from 'node:test';
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript';
|
||||
async function loadTS(relative) {
|
||||
const file = new URL(relative, import.meta.url);
|
||||
let source = await readFile(file, 'utf8');
|
||||
source = source.replace(/from ['"]three['"]/g, `from '${pathToFileURL(fileURLToPath(new URL('../node_modules/three/build/three.module.js', import.meta.url)))}'`);
|
||||
const imports = [...source.matchAll(/from ['"](\.[^'"]+)['"]/g)];
|
||||
for (const match of imports) {
|
||||
const child = new URL(match[1] + '.ts', file);
|
||||
const url = await compile(child);
|
||||
source = source.replace(match[0], `from '${url}'`);
|
||||
}
|
||||
return import(encode(source));
|
||||
}
|
||||
function encode(source) { return `data:text/javascript;base64,${Buffer.from(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ES2020, target: ts.ScriptTarget.ES2020 } }).outputText).toString('base64')}`; }
|
||||
async function compile(file) {
|
||||
let source = await readFile(file, 'utf8');
|
||||
source = source.replace(/from ['"]three['"]/g, `from '${new URL('../node_modules/three/build/three.module.js', import.meta.url)}'`);
|
||||
for (const match of [...source.matchAll(/from ['"](\.[^'"]+)['"]/g)]) source = source.replace(match[0], `from '${await compile(new URL(match[1] + '.ts', file))}'`);
|
||||
return encode(source);
|
||||
}
|
||||
const { comparisonLayout, paneAt } = await loadTS('../src/rendering/ComparisonLayout.ts');
|
||||
const { createPatternedGlassGeometry, sampleGobangSurface } = await loadTS('../src/rendering/PatternedGlassGeometry.ts');
|
||||
const { ComparisonMetrics } = await loadTS('../src/rendering/ComparisonMetrics.ts');
|
||||
|
||||
test('desktop and mobile comparison preserve a common unchanged projection aspect', () => {
|
||||
for (const [width,height,aspect] of [[1440,900,1.6],[390,844,390/844],[844,390,390/844],[320,320,1]]) {
|
||||
const {panes} = comparisonLayout(width,height,aspect);
|
||||
const [a,b] = panes;
|
||||
assert.equal(a.width,b.width); assert.equal(a.height,b.height);
|
||||
assert.ok(Math.abs(a.width/a.height-aspect)<1e-10);
|
||||
for (const p of panes) { assert.ok(p.x>=0 && p.y>=0); assert.ok(p.x+p.width<=width+1e-8); assert.ok(p.y+p.height<=height+1e-8); }
|
||||
assert.ok(a.x+a.width<=b.x || a.y+a.height<=b.y);
|
||||
assert.equal(paneAt(panes,b.x+b.width/2,b.y+b.height/2),b);
|
||||
}
|
||||
});
|
||||
|
||||
test('actual glass has finite front normals, closed edges and the same A back plane', () => {
|
||||
for(const curvature of [-1,1]) {
|
||||
const result=createPatternedGlassGeometry({width:1,height:1.32,grid:20,curvature});
|
||||
const geometry=result.geometry, pos=geometry.attributes.position, normal=geometry.attributes.normal;
|
||||
assert.ok(geometry.index.count/3<100000,'bounded tessellation, not Blender 275k mesh');
|
||||
assert.ok(geometry.index.count/3>1000,'real patterned geometry');
|
||||
assert.ok(pos.array.every(Number.isFinite)); assert.ok(normal.array.every(Number.isFinite));
|
||||
assert.ok(Math.abs(result.backZ-(-.008-.085/20))<1e-10);
|
||||
assert.ok(result.minThickness>0);
|
||||
assert.ok(result.maxThickness>result.minThickness);
|
||||
const samples=[];
|
||||
for(let i=0;i<pos.count;i++) if(normal.getZ(i)>.5) samples.push(pos.getZ(i));
|
||||
assert.ok(Math.max(...samples)-Math.min(...samples)>.002);
|
||||
// Weld attribute seams by position, then require two incident triangles on every edge.
|
||||
const key=i=>[pos.getX(i),pos.getY(i),pos.getZ(i)].map(v=>v.toFixed(6)).join(',');
|
||||
const edges=new Map();
|
||||
for(let i=0;i<geometry.index.count;i+=3) for(const [a,b] of [[0,1],[1,2],[2,0]]) { const edge=[key(geometry.index.array[i+a]),key(geometry.index.array[i+b])].sort().join('|'); edges.set(edge,(edges.get(edge)||0)+1); }
|
||||
assert.ok([...edges.values()].every(n=>n===2),'watertight triangulated volume');
|
||||
geometry.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('geometry uses the existing gobang profile and periodic analytic normals', () => {
|
||||
const c={width:1,height:1.32,grid:20,curvature:1};
|
||||
const p=sampleGobangSurface(.125,.2,c), q=sampleGobangSurface(.175,.2,c);
|
||||
assert.ok(Math.abs(p.height-q.height)<1e-10);
|
||||
const epsilon=1e-6;
|
||||
const derivative=(sampleGobangSurface(.126+epsilon,.203,c).height-sampleGobangSurface(.126-epsilon,.203,c).height)/(2*epsilon*c.width);
|
||||
const center=sampleGobangSurface(.126,.203,c);
|
||||
assert.ok(Math.abs(derivative-center.dx)<1e-5);
|
||||
const inv=sampleGobangSurface(.126,.203,{...c,curvature:-1});
|
||||
assert.equal(inv.height,-center.height); assert.equal(inv.dx,-center.dx);
|
||||
});
|
||||
|
||||
test('metrics keep CPU submission time separate from cadence and never invent GPU memory', () => {
|
||||
const metrics=new ComparisonMetrics();
|
||||
metrics.record('a',2.5,{calls:4,triangles:100}); metrics.record('b',5,{calls:6,triangles:200});
|
||||
metrics.frame(100); metrics.frame(120);
|
||||
const snap=metrics.snapshot();
|
||||
assert.equal(snap.a.cpuMs,2.5); assert.equal(snap.b.cpuMs,5);
|
||||
assert.equal(snap.a.calls,4); assert.equal(snap.b.triangles,200);
|
||||
assert.equal(snap.frameMs,20);
|
||||
assert.equal('gpuMs' in snap,false); assert.equal('gpuMemory' in snap,false);
|
||||
metrics.reset(); assert.equal(metrics.snapshot().frameMs,null);
|
||||
});
|
||||
|
||||
test('B uses explicitly extended Three transmission with shared photo/depth, never A refraction shader', async () => {
|
||||
const b=await readFile(new URL('../src/rendering/MeshGlassRenderer.ts',import.meta.url),'utf8');
|
||||
assert.match(b,/new THREE\.MeshPhysicalMaterial/); assert.match(b,/transmission:\s*1/);
|
||||
assert.doesNotMatch(b,/mosaic\.frag|new THREE\.ShaderMaterial|AiDepthProvider|Worker\(/);
|
||||
assert.match(b,/attachMeshRefraction\(this\.material,depthSource\)/);
|
||||
assert.match(b,/depthSource\.geometry/); assert.match(b,/depthSource\.material/);
|
||||
assert.match(b,/dispose\(\)/);
|
||||
const main=await readFile(new URL('../src/main.ts',import.meta.url),'utf8');
|
||||
assert.match(main,/import\('\.\/rendering\/PhysicalGlassComparison'\)/,'lazy opt-in compositor');
|
||||
assert.equal((main.match(/new AiDepthProvider/g)||[]).length,1);
|
||||
assert.match(main,/comparison\?\.dispose/);
|
||||
assert.match(main,/renderLost \|\| aiDepthBusy \|\| document\.hidden/);
|
||||
});
|
||||
|
||||
test('tessellation stays bounded on extreme aspect ratios and approximates the height profile', () => {
|
||||
for(const height of [.02,.5,1.32,4,30]) {
|
||||
const shape={width:1,height,grid:24,curvature:1};
|
||||
const result=createPatternedGlassGeometry(shape),g=result.geometry;
|
||||
assert.ok(g.index.count/3<86000,`hard triangle budget at ratio ${height}`);
|
||||
assert.ok(g.attributes.position.count<50000,`hard vertex budget at ratio ${height}`);
|
||||
if(height===1.32) {
|
||||
let maxError=0;
|
||||
const p=g.attributes.position,n=g.attributes.normal;
|
||||
for(let i=0;i<g.index.count;i+=3) {
|
||||
const [a,b,c]=[g.index.array[i],g.index.array[i+1],g.index.array[i+2]];
|
||||
if(n.getZ(a)<.5||n.getZ(b)<.5||n.getZ(c)<.5)continue;
|
||||
const x=(p.getX(a)+p.getX(b)+p.getX(c))/3,y=(p.getY(a)+p.getY(b)+p.getY(c))/3,z=(p.getZ(a)+p.getZ(b)+p.getZ(c))/3;
|
||||
const actual=sampleGobangSurface(x+.5,y/height+.5,shape).height;
|
||||
maxError=Math.max(maxError,Math.abs(z-actual));
|
||||
}
|
||||
assert.ok(maxError/(.085/24)<.04,`sampled triangle-centroid height error ${maxError/(.085/24)} of amplitude`);
|
||||
}
|
||||
g.dispose();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user