From ef0c01eb67cb99acd10fa61acafa5c1ba00e5f3b Mon Sep 17 00:00:00 2001 From: Claude Haiku Date: Wed, 21 Oct 2026 10:47:03 +0900 Subject: [PATCH] =?UTF-8?q?v4:=20=EA=B2=80=EC=A6=9D=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20=EC=88=98?= =?UTF-8?q?=EB=9F=89=20=EB=B0=8F=20H=ED=98=95=EA=B0=95=20=EB=8B=A8?= =?UTF-8?q?=EB=A9=B4=20=EA=B8=B0=EC=A4=80=20=EA=B0=84=EC=84=AD=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 부재 수량을 설계 기준과 대조하고, H형강은 플랜지와 웨브로 나눠 간섭을 검사한다. 박스 근사에서 나오던 엄지말뚝-토류판 충돌은 실제 단면 기준으로 해소됨을 확인한다. Co-Authored-By: Claude Haiku 4.5 --- PROGRESS.md | 6 ++- src/build/verify-v4.mjs | 96 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/build/verify-v4.mjs diff --git a/PROGRESS.md b/PROGRESS.md index e72a219..d214aec 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -246,8 +246,12 @@ C:\Users\User\.claude\projects\d--Obsidian-Vault-999ETC-261016_HW_DwfTo3D\memory - 버팀보 X 간격: 도면 치수 "11@5.000" 및 4m 치수 기준 (13지점) - 벽체 방향 대응: A-A 단면 X ↔ 계획도 X 선형 대응, 말뚝 높은 쪽은 X 큰 쪽 +**검증 (2026-10-21)** +- `node src/build/verify-v4.mjs` : 부재 수량 8개 항목 일치, 간섭 없음 (H형강 플랜지·웨브 분리 검사) +- 이전 박스 근사 검사의 엄지말뚝↔토류판 240쌍은 실제 단면 기준으로 겹치지 않음 +- 열린 가정 목록은 위 항목 그대로 유지 (도면 확인 후 갱신) + **다음 단계** -- 검증 자동화 (수량, 간섭, 가정 목록) - 웹앱 이식 (src/web) 및 Vercel 배포 --- diff --git a/src/build/verify-v4.mjs b/src/build/verify-v4.mjs new file mode 100644 index 0000000..2b123f6 --- /dev/null +++ b/src/build/verify-v4.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * verify-v4.mjs - v4 통합모델 검증 + * 1) 부재 수량: 설계 기준과 일치하는지 + * 2) 간섭: H형강은 플랜지 2개 + 웨브로 나눠 검사 (상자 근사보다 정확) + * 실패 항목이 있으면 종료 코드 1 + */ + +import fs from 'fs'; + +const EXPECTED = { + '엄지말뚝': 122, + '토류판': 120, + '띠장': 48, + '수직 PIECE': 52, + '버팀보': 26, + '스크류잭': 26, + '채널': 12, + '중간말뚝': 13 +}; + +const d = JSON.parse(fs.readFileSync('out/gasiseol-v4-stats.json', 'utf8')); +const EPS = 1e-3; +let failed = false; + +console.log('\n[1] 부재 수량'); +const counts = {}; +d.parts.forEach(p => { counts[p.memberType] = (counts[p.memberType] || 0) + 1; }); +Object.keys({ ...EXPECTED, ...counts }).forEach(type => { + const exp = EXPECTED[type] ?? 0, got = counts[type] ?? 0; + const ok = exp === got; + if (!ok) failed = true; + console.log(` ${ok ? 'OK ' : 'NG '} ${type.padEnd(12)} 기대 ${String(exp).padStart(3)} 실제 ${String(got).padStart(3)}`); +}); + +// 로컬 좌표(u: 폭, v: 춤, a: 길이) → 월드 좌표 축 매핑 (viewer 와 동일) +// vertical: u→x, v→y, a→z / horizontal(rot y π/2): u→z, v→y, a→x / horizontalY(rot x π/2): u→x, v→z, a→y +function hBoxes(p) { + const { h, w, tf, tw, length: L } = p.dims; + const local = [ + [-w / 2, w / 2, h / 2 - tf, h / 2], + [-w / 2, w / 2, -h / 2, -h / 2 + tf], + [-tw / 2, tw / 2, -h / 2 + tf, h / 2 - tf] + ]; + return local.map(([u0, u1, v0, v1]) => { + const a0 = -L / 2, a1 = L / 2; + if (p.axis === 'horizontal') return { x: [a0, a1], y: [v0, v1], z: [u0, u1] }; + if (p.axis === 'horizontalY') return { x: [u0, u1], y: [a0, a1], z: [v0, v1] }; + return { x: [u0, u1], y: [v0, v1], z: [a0, a1] }; + }); +} + +function components(p) { + const { x, y, z } = p.position; + const shift = (b) => ({ x: [b.x[0] + x, b.x[1] + x], y: [b.y[0] + y, b.y[1] + y], z: [b.z[0] + z, b.z[1] + z] }); + if (p.shape === 'PANEL') { + const { w, t, length: L } = p.dims; + return [shift({ x: [-w / 2, w / 2], y: [-t / 2, t / 2], z: [-L / 2, L / 2] })]; + } + if (p.shape === 'JACK') { + return [shift({ x: [-0.15, 0.15], y: [-0.29975, 0.29975], z: [-0.1547, 0.1547] })]; + } + if (p.shape === 'CHANNEL') { + const { w, h, length: L } = p.dims; + return [shift({ x: [-L / 2, L / 2], y: [-w / 2, w / 2], z: [-h / 2, h / 2] })]; + } + return hBoxes(p).map(shift); +} + +function overlap(a, b) { + return ['x', 'y', 'z'].every(k => Math.min(a[k][1], b[k][1]) - Math.max(a[k][0], b[k][0]) > EPS); +} + +console.log('\n[2] 간섭 (H형강: 플랜지·웨브 분리 검사)'); +const items = d.parts.map((p, i) => ({ i, comps: components(p) })); +const conflicts = {}; +for (let a = 0; a < items.length; a++) { + for (let b = a + 1; b < items.length; b++) { + const A = items[a], B = items[b]; + const hit = A.comps.some(ca => B.comps.some(cb => overlap(ca, cb))); + if (hit) { + const key = `${d.parts[A.i].memberType} ↔ ${d.parts[B.i].memberType}`; + conflicts[key] = (conflicts[key] || 0) + 1; + } + } +} +const keys = Object.keys(conflicts); +if (keys.length === 0) { + console.log(' OK 간섭 없음'); +} else { + failed = true; + keys.forEach(k => console.log(` NG ${k}: ${conflicts[k]}쌍`)); +} + +console.log(`\n결과: ${failed ? '실패' : '통과'}`); +process.exit(failed ? 1 : 0);