spec §3·§4.4(2026-11-06 개정)와 3주형형상배근.md §2.2(2주형 DWG 실측)를 따른다. - 기둥 사이 하면: Level 끝에서 헌치 수평 1500 동안 500 올라가 평탄, 다음 기둥 앞에서 내려옴 (이전 Level + 300, 헌치 300 × 300 대체) - 다주형 Level 폭 기본값 = D + 200 (입력값이 있으면 입력 우선). T형은 2100 유지 - 오목 평탄부가 없으면 RB-M01 오류 - 3주형 테스트는 오목형이 성립하는 20 m 코핑으로 바꿈 (P1 길이 12 m는 RB-M01) - 2주형 DWG 치수(L 24200, D 3000) 하면 꺾임점 테스트 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
5.2 KiB
JavaScript
78 lines
5.2 KiB
JavaScript
// Run with Windows Node against the task's isolated Chrome CDP on port 9444.
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const out = path.resolve(__dirname, '../.verify/multicol-coping-20261105');
|
|
fs.mkdirSync(out, { recursive: true });
|
|
(async () => {
|
|
const targets = await (await fetch('http://127.0.0.1:9444/json')).json();
|
|
const page = targets.find(t => t.type === 'page' && t.url.startsWith('http://127.0.0.1:5173'));
|
|
if (!page) throw new Error('Task browser page missing');
|
|
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
|
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
|
let seq = 0;
|
|
const pending = new Map();
|
|
ws.onmessage = ({ data }) => {
|
|
const msg = JSON.parse(data);
|
|
const p = pending.get(msg.id);
|
|
if (p) { pending.delete(msg.id); clearTimeout(p.timer); msg.error ? p.reject(new Error(JSON.stringify(msg.error))) : p.resolve(msg.result); }
|
|
};
|
|
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
|
const id = ++seq;
|
|
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`Timeout ${method}`)); }, 20000);
|
|
pending.set(id, { resolve, reject, timer });
|
|
ws.send(JSON.stringify({ id, method, params }));
|
|
});
|
|
try {
|
|
await send('Emulation.setDeviceMetricsOverride', { width: 1200, height: 750, deviceScaleFactor: 1, mobile: false });
|
|
const results = [];
|
|
for (const type of ['2주형', '3주형']) {
|
|
const res = await send('Runtime.evaluate', { awaitPromise: true, returnByValue: true, expression: `
|
|
(async () => {
|
|
const THREE = await import('/node_modules/.vite/deps/three.js');
|
|
const XLSX = await import('/node_modules/xlsx/xlsx.mjs');
|
|
const { parseWorkbook } = await import('/src/core/parseWorkbook.ts');
|
|
const { buildPier } = await import('/src/core/geometry.ts');
|
|
const { buildPierScene } = await import('/src/view/scene.ts');
|
|
const buf = await (await fetch('/samples/' + encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer();
|
|
const base = parseWorkbook(XLSX.read(buf)).piers.find(p => p.name === 'P1');
|
|
if (!base) throw new Error('P1 sample missing');
|
|
// 3주형 needs a 20 m coping for the 오목형 spans (P1 length leaves no flat part, RB-M01).
|
|
const m = buildPier({ ...base, ...(${type === '3주형'} ? { copingLengthMm: 20000 } : {}), type: ${JSON.stringify(type)}, columnCount: ${type === '2주형' ? 2 : 3} });
|
|
const built = buildPierScene(m);
|
|
const scene = new THREE.Scene(); scene.background = new THREE.Color(0xe9eef4);
|
|
const group = new THREE.Group();
|
|
group.add(built.parts.coping, built.parts.columns); scene.add(group);
|
|
const center = new THREE.Vector3();
|
|
const bounds = new THREE.Box3().setFromObject(built.parts.coping);
|
|
bounds.getCenter(center);
|
|
// Align the camera along the cross-axis to make both interior haunches visible.
|
|
const u = new THREE.Vector3(m.u.e, 0, -m.u.n);
|
|
const cross = new THREE.Vector3(m.nrm.e, 0, -m.nrm.n);
|
|
const camera = new THREE.PerspectiveCamera(36, 1200/750, 0.1, 1000);
|
|
const target = center.clone().add(new THREE.Vector3(0, -0.9, 0));
|
|
camera.position.copy(target).addScaledVector(cross, m.copingL * 1.6).addScaledVector(u, -m.copingL * 0.12).add(new THREE.Vector3(0,-2.5,0));
|
|
camera.lookAt(target);
|
|
scene.add(new THREE.HemisphereLight(0xffffff, 0x687784, 2.4));
|
|
const light = new THREE.DirectionalLight(0xffffff, 2); light.position.copy(camera.position).add(new THREE.Vector3(0,10,0)); scene.add(light);
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
|
renderer.setSize(1200,750); renderer.localClippingEnabled = true;
|
|
document.body.replaceChildren(renderer.domElement); document.body.style.margin = '0';
|
|
renderer.render(scene,camera);
|
|
const profile = m.copingParts[0].profile;
|
|
const raised = profile.filter(([,z]) => Math.abs(z-m.levelBottom-0.5)<1e-8).length;
|
|
if (raised !== (m.columns.length-1)*2) throw new Error('Raised span count mismatch');
|
|
if (Math.abs(Math.min(...profile.map(p=>p[1]))-m.levelBottom)>1e-8) throw new Error('Underside below column Level');
|
|
if (!m.columns.every(c=>c.top===m.levelBottom)) throw new Error('Column connection changed');
|
|
return { type: ${JSON.stringify(type)}, raisedVertices: raised, levelBottom:m.levelBottom, interiorBottom:m.levelBottom+0.5, columns:m.columns.length, profile, screenshot:renderer.domElement.toDataURL('image/png') };
|
|
})()
|
|
` });
|
|
if (res.exceptionDetails) throw new Error(res.exceptionDetails.exception?.description || res.exceptionDetails.text);
|
|
const { screenshot, ...result } = res.result.value;
|
|
fs.writeFileSync(path.join(out, `${type}.png`), Buffer.from(screenshot.split(',')[1], 'base64'));
|
|
results.push(result);
|
|
}
|
|
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(results, null, 2));
|
|
console.log(JSON.stringify(results.map(({ profile, ...r }) => r)));
|
|
} finally { ws.close(); }
|
|
})().catch(e => { console.error(e); process.exitCode = 1; });
|