feat: 프로젝트 공통 지반모델·교대 지반 통합·재료표/수량 진입·옥성교 랜딩 히어로 (11-06~11-09 작업분)
공용 파일(main.ts·index.html·style.css)에서 서로 얽혀 있어 한 커밋으로 묶음. - 지반: ① 형상 프로젝트 지반모델 관리창(생성·JSON 불러오기·교각별 비교·선택 적용), 프로젝트 JSON 내장 groundModel 추출, 교대 8행 통합 적용·공통지형 중복격자 제외 - 재료표·수량: 상단 ⑤ 메뉴·배근 바로가기, 결과 교각/탭 기억, 수량 워크북·정합 보정 - 랜딩: 옥성교 6교각 히어로 이미지·투영 강조점·좌→우 광선·지형 라운드/도로 연결 - 검증 스크립트(verify-project-ground/ground-abutments/okseong-user-files/quantity-*/landing-hero 등) 정본: DOC/04_실행/실행_*_2026-11-09_v0.1.00.md Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b2bebe47eb
commit
fc520ec03d
47 files changed
+2848
-149
No files matched your search
@@ -0,0 +1,64 @@
|
||||
// Run with Windows Node and an isolated Chrome on CDP 9444; Vite must be on 5173.
|
||||
const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict');
|
||||
const preview=process.argv.includes('--horizontal-preview');
|
||||
const lowerPreview=process.argv.includes('--lower-preview');
|
||||
const out = path.resolve(__dirname, lowerPreview?'../.verify/landing-lower-20261109':process.argv.includes('--all')?'../.verify/landing-hero-20261109':'../.verify/landing-horizontal-20261109');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { checks: [], errors: [] };
|
||||
let ws, send;
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
(async () => {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json();
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; });
|
||||
let seq = 0; const pending = new Map();
|
||||
ws.onmessage = ({ data }) => {
|
||||
const m = JSON.parse(data), p = pending.get(m.id);
|
||||
if (p) { clearTimeout(p.timer); pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
|
||||
if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
|
||||
};
|
||||
send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq, timer = setTimeout(() => reject(new Error('Timeout ' + method)), 30000);
|
||||
pending.set(id, { resolve, reject, timer }); ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const ev = async expression => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const until = async expression => {
|
||||
for (let i = 0; i < 120; i++) { if (await ev(expression)) return; await delay(150); }
|
||||
throw new Error('Wait ' + expression);
|
||||
};
|
||||
const check = (name, value) => { assert.ok(value, name); report.checks.push(name); console.log('PASS', name); };
|
||||
const click = expression => ev(`(()=>{const e=${expression};if(!e||e.disabled)throw new Error('Missing target');e.click();})()`);
|
||||
const shot = async name => {
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64'));
|
||||
};
|
||||
await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await send('Page.navigate', { url: process.env.PIERZAINER_URL || 'http://127.0.0.1:5173/' });
|
||||
await until(`typeof window.openSampleProject==='function'`);
|
||||
const projectPath=process.env.OKSEONG_PROJECT||path.resolve(__dirname,'../.verify/ground-abutments-20261109/saved.pzr.json');
|
||||
const projectText=fs.readFileSync(projectPath,'utf8');
|
||||
const hash=require('node:crypto').createHash('sha256').update(projectText).digest('hex');
|
||||
await ev(`window.__heroProject=JSON.parse(${JSON.stringify(projectText)})`);
|
||||
const variants=lowerPreview ? [['lower-33',.65,0,30,true,true],['lower-27',.5,0,30,true,true],['lower-19',.35,0,30,true,true],['lower-27-cutaway',.5,0,30,true,true,8]] : preview ? [['horizontal',.85,0,30,true],['horizontal-smooth',.85,0,30,true,true]] : process.argv.includes('--all') ? [['low',.35,0],['balanced',.55,.12],['high',.8,-.12],['trim30',.55,.12,30],['trim20',.55,.12,20],['refined',.85,.12,30,true]] : [['horizontal-smooth',.85,0,30,true,true]];
|
||||
for(const [name,elevation,along,margin,refined,smooth,cutaway] of variants){
|
||||
const result=await ev(`(async()=>{const {renderLandingHero}=await import('/scripts/render-landing-hero.ts');return renderLandingHero(window.__heroProject,${elevation},${along},${margin},${refined},${smooth},${cutaway});})()`);
|
||||
for(const type of ['png','webp'])fs.writeFileSync(path.join(out,name+'.'+type),Buffer.from(result[type].split(',')[1],'base64'));
|
||||
fs.writeFileSync(path.join(out,name+'.json'),JSON.stringify({...result.metadata,sourceSha256:hash},null,2));
|
||||
check(name+' includes six piers and two abutments',result.metadata.piers===6&&result.metadata.abutments===2&&result.metadata.points.length===8);
|
||||
check(name+' all projected markers stay inside image',result.metadata.points.every(p=>p.x>0&&p.x<result.metadata.width&&p.y>0&&p.y<result.metadata.height));
|
||||
check(name+' light samples actual terrain densely',result.metadata.terrainFlow.filter(Boolean).length>100&&result.metadata.terrainFlow.every(p=>!p||Math.abs(p.world[1]-p.el-.12)<.0001));
|
||||
if(refined)check('both road extensions contain continuous terrain-supported sections',result.metadata.roads.length===2&&result.metadata.roads.every(r=>r.missingSections===0&&r.validSections===81&&r.triangles>0));
|
||||
if(smooth){
|
||||
check('outline trims real pixels without filling missing terrain',result.metadata.outline.addedOutsideSourcePixels===0&&result.metadata.outline.retainedTerrainRatio>.8&&result.metadata.outline.retainedTerrainRatio<.99);
|
||||
check('smooth outline preserves all eight structure tops',await ev(`(()=>{const c=document.createElement('canvas');c.width=${result.metadata.width*2};c.height=${result.metadata.height*2};return new Promise(resolve=>{const i=new Image();i.onload=()=>{const ctx=c.getContext('2d');ctx.drawImage(i,0,0);resolve(${JSON.stringify(result.metadata.points)}.every(p=>{let n=0;for(let x=-8;x<=8;x++)for(let y=0;y<30;y++)n+=ctx.getImageData(Math.round(p.x*2+x),Math.round(p.y*2+y),1,1).data[3]>0;return n>15;}));};i.src=${JSON.stringify(result.png)};});})()`));
|
||||
}
|
||||
}
|
||||
check('no runtime exceptions',report.errors.length===0);report.result='PASS';
|
||||
})().catch(e=>{report.result='FAIL';report.failure=e.stack;process.exitCode=1;}).finally(()=>{
|
||||
fs.writeFileSync(path.join(out,'report.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report));ws?.close();
|
||||
});
|
||||
Reference in new issue
Block a user