원본 좌표와 브라우저 검증 결과 및 실행 수정 안내 기록
This commit is contained in:
1 parent
d01ef8b0e8
commit
fac29cb010
17 files changed
+358
-17
No files matched your search
@@ -4,3 +4,6 @@ dist
|
||||
*.local
|
||||
|
||||
docs/extracted/
|
||||
|
||||
docs/*-stderr.log
|
||||
docs/*-output.html
|
||||
@@ -1,26 +1,54 @@
|
||||
# 광터지하차도 새 모델 프로젝트
|
||||
# 광터지하차도 BLOCK7 도면 기반 3D 모델
|
||||
|
||||
기존 웹 모델을 이어서 수정하지 않고 새로 시작하는 프로젝트입니다. 원본 DXF를 다시 분석해 평면 선형, 종단 표고, A-A/B-B 단면의 대응을 확인한 뒤 주요 형상부터 모델링합니다.
|
||||
|
||||
## 현재 상태
|
||||
|
||||
- 원본 도면: `public/drawings/광터지하차도_일반도(BLOCK7).dxf`
|
||||
- 이전 분석 문서: `docs/참고_기존도면분석.md` (참고 자료이며, 새 분석으로 다시 검증해야 합니다)
|
||||
- 기존 3D 모델 코드는 복사하지 않았습니다.
|
||||
- 새 프로젝트 단계: 도면 1차 분석 완료, 평면 기준선과 단면 위치의 최종 대조 필요
|
||||
원본 DXF의 단면 구조선과 공동구 실제 윤곽을 추출해, 사용자가 허용한 19.980m 직선 연결로 만든 검토용 웹 3D 표면 모델입니다. 평면 중심선·실제 중간 형상은 미확정이며 모든 연결면에 `확인 필요`를 표시합니다.
|
||||
|
||||
## 실행
|
||||
|
||||
프로젝트 폴더의 WSL 터미널에서 실행합니다. Node 24 환경으로 확인했습니다.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
npm ci
|
||||
npm run dev -- --host 127.0.0.1
|
||||
```
|
||||
|
||||
브라우저에서 터미널에 표시된 주소를 엽니다. 배포용 파일은 `npm run build`로 만듭니다.
|
||||
터미널에 나오는 주소를 브라우저에서 엽니다. 기본 포트가 사용 중이면 다음 포트를 선택합니다. `npm run build`는 dist 폴더를 만들고, `npm run preview -- --host 127.0.0.1`로 빌드 결과를 볼 수 있습니다.
|
||||
|
||||
## 작업 순서
|
||||
## 사용
|
||||
|
||||
1. 원본 도면의 시트·축척·중심선·단면 위치를 다시 분석합니다.
|
||||
2. 치수와 근거, 계산값, 추정값, 확인 항목을 문서화합니다.
|
||||
3. 주요 구조 형상을 만든 뒤 세부 부재를 추가합니다.
|
||||
4. 뷰어 조작과 치수를 확인하고 원도면과 대조합니다.
|
||||
- 끌기: 회전 / 휠: 확대 / Shift+끌기 또는 오른쪽 끌기: 이동
|
||||
- 터치: 한 손가락 회전 / 두 손가락 확대·이동
|
||||
- 방향키: 회전 / +와 -: 확대·축소 / Home: 전체 보기 / Esc: 선택 해제
|
||||
- 전체·평면·정면·측면·A-A·B-B 시점, 연결면 표시 전환
|
||||
- 선 또는 면 선택: 이름·좌표 기반 치수·원본 선 번호 표시
|
||||
- 목록: 개별/그룹 숨김, 검색, 포장 반복선 표시
|
||||
- 기본/렌더링 보기: 표면 명암·배경·그림자 전환. 실사 렌더링이 아닙니다.
|
||||
- 3D OBJ 다운로드: 숨기지 않은 객체의 단면선·연결면 내보내기
|
||||
- 좌표·근거 JSON 다운로드: 원본 좌표와 HANDLE, 가정 상태 보존
|
||||
|
||||
미리 생성된 OBJ: `public/models/BLOCK7_검토용모델.obj`. 표면과 선이 포함된 열린 모델이며 닫힌 체적이 아닙니다. 단위는 m, OBJ 축은 X=횡방향 / Y=EL / Z=종단 경계 기준 거리 0~19.980입니다. 실제 평면 좌표는 적용하지 않았습니다. 포장 반복선은 웹 기본 보기에서만 숨기며, OBJ에는 별도 선 객체로 보존합니다.
|
||||
|
||||
## 분석과 검증
|
||||
|
||||
```bash
|
||||
python3 scripts/analyze_dxf.py
|
||||
python3 scripts/verify_model.py
|
||||
node scripts/verify_geometry.mjs
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
Python은 표준 라이브러리만 사용합니다. 추출 중간 자료는 `docs/extracted/`에 만들고 Git에서는 제외합니다. `verify_geometry.mjs`는 검증 후 미리 생성된 OBJ도 갱신합니다.
|
||||
|
||||
개발 서버에서 `/scripts/browser-check.html`을 열면 브라우저 동작 검증 결과를, `/scripts/mobile-check.html`에서 좁은 화면의 가로 넘침 검사를 확인할 수 있습니다. 이 검증 페이지는 배포용 dist에 포함되지 않습니다.
|
||||
|
||||
- 도면 근거·치수·불일치: `docs/도면분석.md`
|
||||
- 검증 결과·미검증 항목: `docs/검증보고.md`
|
||||
- 화면 캡처: `docs/screenshots/`
|
||||
|
||||
## 다음 수정 위치
|
||||
|
||||
원본 개정 시 DXF를 교체하고 분석 스크립트의 단면 범위·EL 기준·공동구 INSERT 좌표를 새 도면에 맞춰 수정합니다. `scripts/analyze_dxf.py`의 group 함수는 현 도면의 원본 선 순서에 대응하므로 개정 시 부재 분류를 다시 대조해야 합니다. 자동 추출만으로 변경 검증을 대신하지 않습니다.
|
||||
|
||||
`src/model/block7.json`은 추출된 값, `geometry.js`는 연결·OBJ 생성, `Viewer.jsx`는 화면 조작과 명암, `App.jsx`는 검토 패널입니다. 부재별 윤곽 좌표와 이름은 JSON에서 분리되어 있습니다. JSON을 직접 수정하면 다음 원본 재추출 때 덮어쓰므로, 확정 변경은 추출 스크립트의 근거값에도 반영하세요.
|
||||
|
||||
원격 저장소로 푸시하거나 공개하지 않았습니다.
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "3D 캔버스 생성",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "원본 154 객체 표시",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 평면",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 정면",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 측면",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: A-A",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: B-B",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 전체",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 모두 숨김",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "전체 숨김 반영",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 모두 표시",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "반복선 제외한 전체 표시",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "선택하면 원본 근거 표시",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 선택한 윤곽 숨기기",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "개별 숨김 반영",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 3D OBJ ↓",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "OBJ 내보내기 153개 객체",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "OBJ 유효한 좌표",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "OBJ 연결 삼각면 466개",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "키보드 회전 반영",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "휠 확대 반영",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "윤곽 검색 빈 결과 표시",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 도면",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "도면 발췌 표시",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 측구상세",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "상세도 전환",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 검증",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "미검증 항목 구분",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "버튼 존재: 렌더링 보기",
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "시각화 구분",
|
||||
"pass": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"viewport": 375,
|
||||
"content": 375,
|
||||
"body": 375,
|
||||
"pass": true
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 237 KiB |
@@ -0,0 +1,43 @@
|
||||
# BLOCK7 모델 검증 보고 (2026-10-20)
|
||||
|
||||
## 결과와 적용 범위
|
||||
|
||||
원본 단면 구조선 114쌍과 좌우 공동구·상부 상세 40쌍을 각각 선택 가능한 154개 윤곽 객체로 만들었다. 사용자 허용에 따라 두 단면을 종단 경계 간격 19.980m로 직선 연결했다. 포장 반복선 60개는 원본 선으로 보존하고, 나머지 94개 객체의 연결 삼각면은 468개다. 실물 부재 개수나 확정 체적을 뜻하지 않는다.
|
||||
|
||||
모든 연결면은 `확인 필요`다. 실제 평면 중심선, 중간 곡률·단면, 중심 기준 EL은 적용하지 않았다. 원본 단면 STA 299.000/319.000은 보존하고 배치만 종단 구조 경계 299.010/318.990에 대응시켰다.
|
||||
|
||||
## 수행한 검사
|
||||
|
||||
| 검사 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 원본 두 단면 레이어 수 | A/B 각각 MAJR 108+MINR 6=114 | scripts/verify_model.py |
|
||||
| 원본 선 번호와 모든 끝점 | 원본과 일치, 좌표 반올림 오차 0.000051mm 이하 | docs/좌표검증결과.txt |
|
||||
| 상단 EL | 106.675 / 107.307, 종단·단면 표기 일치 | docs/도면분석.md 및 좌표 검산 |
|
||||
| 경계 간격 | 318.990-299.010=19.980m | 두 단면 원본 좌표 보존 후 배치 검산 |
|
||||
| 공동구·상부 상세 | 좌우 각각 LINE 18+ARC 2, 우측 반전 및 삽입 Y 반영 | DXF 블록과 추출 데이터 |
|
||||
| 연결·OBJ | 468 삼각면 / 154 객체, 유효한 꼭짓점 참조, 숨김 반영 | scripts/verify_geometry.mjs |
|
||||
| 웹 브라우저 동작 | Chrome 검사 30개 통과 (버튼 존재 확인 포함) | docs/browser-check-result.json |
|
||||
| 시점·선택·숨김·검색 | 시점 6종, 전체·개별 숨김, 목록 선택 정보, 빈 검색 결과 확인 | 위 브라우저 검사 |
|
||||
| 회전·확대 | 키보드 회전 및 휠 확대 후 실제 캔버스 변화 확인 | 위 브라우저 검사 |
|
||||
| OBJ 다운로드 | 1개 숨긴 상태에서 153 객체/466면 내보내기 확인 | 위 브라우저 검사 |
|
||||
| 도면·검증·렌더링 패널 | 도면 전환 및 미확정 항목/시각화 안내 확인 | 위 브라우저 검사 |
|
||||
| 좁은 화면 | 390px 프레임의 내용 영역 375px에서 가로 넘침 없음 | docs/mobile-check-result.json |
|
||||
| 빌드·정적 검사 | Vite 빌드 성공, oxlint 경고 0/오류 0 | 최종 명령 실행 |
|
||||
| 화면 확인 | 기본·렌더링 화면 캡처와 배치 확인 | docs/screenshots/desktop.png, render.png, mobile.png |
|
||||
|
||||
## 미검증·생략·표현 차이
|
||||
|
||||
- 실제 평면 중심선·좌표·곡률 및 A/B 절단 위치 대응, 도로 중심 기준 EL은 미검증이다. 임의 방향각을 원본 중심선으로 사용하지 않았다.
|
||||
- 원본 단면 자체 측점과 종단 경계가 양 끝 각각 10mm 차이 난다. 사용자가 허용한 연결 검토를 위해 대응시킨 것이며 원인 확인은 남았다.
|
||||
- 단면 사이 모든 점은 직선 보간이다. 공동구·측구·상부 가는 상세도 같은 방식으로 연장한 확인 필요 형상이다.
|
||||
- 포장 반복선·하부 외측 보조선의 재료·해칭 여부, 중복된 선·면과 체적 접합은 완전히 분류·검증하지 않았다. 선 기반 열린 표면이며 끝단을 임의로 막지 않는다.
|
||||
- ARC는 반원당 36개 선분으로 표시한다. 원본 반지름 약 30mm에서 최대 현 오차는 약 0.029mm다.
|
||||
- 측구 상세는 원본 발췌를 제공하지만 단면 측구와의 깊이·시종점 적용 대응은 미확정이다. 상세 치수로 단면 윤곽을 임의 교체하지 않았다.
|
||||
- 원본 검토 SVG는 LINE·TEXT·공동구 LINE 발췌다. 전체 DXF의 모든 블록·해칭·기호·폴리라인·원호를 렌더링하지 않는다.
|
||||
- 지반·되메우기·철근·재료·정착·이음 상세의 구간 적용은 생략했다. 근거 없는 별도 금속 난간은 추가하지 않았다.
|
||||
- 렌더링은 CPU 기반 삼각면 깊이 정렬·단순 명암·투영 그림자이다. 고급 광학 재질·실사 표현·정밀 단면 절단 기능은 제공하지 않는다.
|
||||
- 실제 터치 기기의 두 손가락 동작, 모든 브라우저·3D 프로그램의 OBJ 호환성, 마우스 드래그·면 선택의 수동 전수 검사는 수행하지 못했다. 목록 선택·키보드 회전·휠 확대·내보내기는 자동 검증했다.
|
||||
|
||||
## 다음에 확인할 자료
|
||||
|
||||
평면 중심선의 엔티티 또는 측점별 중심 좌표, A-A/B-B 절단 위치와 중심 EL, 측점 10mm 차이의 경계 기준, 중간 단면·측구 연장 범위를 확인하면 검토용 직선 연결을 실제 배치로 갱신할 수 있다. 수정 위치와 실행 방법은 README.md에 기록했다.
|
||||
@@ -55,3 +55,13 @@
|
||||
4. 포장 반복선, 하부·외측 보조선의 재료와 체적 경계 해석은 미검증이다. 닫힌 체적·수량 산출용 모델로 해석하지 않는다.
|
||||
5. 측구 상세 S=1:30은 별도 검토 자료로 제공한다. 단면 측구와의 깊이·시종점 적용은 미확정이며 상세 치수로 단면을 임의 교체하지 않는다.
|
||||
6. 철근, 지반, 되메우기, 이음 상세의 구간 적용, 재료·정착 상세는 모델링하지 않았다. 공동구 블록의 가는 상부 윤곽은 존재하는 그대로 보존하며 근거 없는 금속 난간을 추가하지 않는다.
|
||||
|
||||
## 사용자 확인 후 적용한 직선 연결 (2026-10-20)
|
||||
|
||||
사용자가 “검토용 직선 연결 허용”을 선택했다. 원본 단면의 횡방향·EL 좌표는 유지하고, A-A/B-B를 종단 구조 경계 299.010/318.990에 대응시켜 19.980m 직선 연결했다. 원본 단면의 STA 299.000/319.000 표기는 데이터에 별도 보존한다. 각 끝 10mm 차이를 실제 형상 검증 완료로 해석하지 않는다.
|
||||
|
||||
- 모델 X는 단면 횡방향, Y는 EL, Z는 종단 경계 기준 거리이다. 실제 평면 중심선·방향·곡률은 적용하지 않았다.
|
||||
- 모든 연결면 상태는 `확인 필요`다. 화면 안내와 황토색 점선으로 표시한다. 기본/렌더링 보기 모두 같은 가정을 사용한다.
|
||||
- 154개 대응 윤곽 객체 중 포장 반복선 60개는 단면 선으로만 보존한다. 나머지 94개 윤곽 객체로 468개 연결 삼각면을 만든다. 원호 분할 때문에 삼각면 수가 늘어난다. 이 수는 실물 부재 수가 아니다.
|
||||
- 양 끝 단면을 임의 캡으로 채우지 않은 열린 표면 모델이다. 원본 중복선·내부선이 남아 있을 수 있으며 체적·수량 산출에 사용하지 않는다.
|
||||
- 기본 보기는 연결면과 원본 단면 윤곽을 함께 표시한다. 렌더링은 단순 표면 명암·배경·투영 그림자를 적용한 도면 검토용 시각화이다.
|
||||
@@ -0,0 +1,6 @@
|
||||
A: 114개 선 HANDLE·레이어·양 끝점 원본 대조 통과 (좌표 반올림 오차 0.000051mm 이하)
|
||||
B: 114개 선 HANDLE·레이어·양 끝점 원본 대조 통과 (좌표 반올림 오차 0.000051mm 이하)
|
||||
154개 ID 중복 없음 / 모든 좌표 유한수 / 대응 꼭짓점 수 일치
|
||||
원호 4개, 좌우 삽입 블록 40쌍 반영
|
||||
벽체 상단 EL 및 종단 측점 차 19.980m 검산 통과
|
||||
원본 단면 STA 간격 20.000m 보존; 사용자 허용에 따라 배치 간격만 19.980m로 대응 (verify_geometry.mjs에서 별도 검증)
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html><meta charset="utf-8"><title>뷰어 자동 검증</title><pre id="result">검증 중</pre><iframe src="/" style="width:1280px;height:1000px;border:0"></iframe><script type="module">
|
||||
const iframe = document.querySelector('iframe'), result = document.querySelector('#result');
|
||||
const pause = () => new Promise(resolve => setTimeout(resolve, 120));
|
||||
const checks = [];
|
||||
const check = (condition, name) => { checks.push({ name, pass: !!condition }); if (!condition) throw Error(name); };
|
||||
try {
|
||||
for (let i = 0; i < 80 && !iframe.contentDocument?.querySelector('canvas'); i++) await pause();
|
||||
const doc = iframe.contentDocument;
|
||||
const click = async text => { const button = [...doc.querySelectorAll('button')].find(e => e.textContent === text); check(button, `버튼 존재: ${text}`); button.click(); await pause(); };
|
||||
const canvas = doc.querySelector('canvas'); check(canvas?.width > 0, '3D 캔버스 생성');
|
||||
check(doc.body.textContent.includes('154'), '원본 154 객체 표시');
|
||||
for (const name of ['평면','정면','측면','A-A','B-B','전체']) await click(name);
|
||||
await click('모두 숨김'); check(doc.querySelector('.panel-heading').textContent.includes('0 / 154'), '전체 숨김 반영');
|
||||
await click('모두 표시'); check(doc.querySelector('.panel-heading').textContent.includes('94 / 154'), '반복선 제외한 전체 표시');
|
||||
const details = doc.querySelector('details'); details.open = true;
|
||||
const row = doc.querySelector('.part-row button'); row.click(); await pause();
|
||||
check(doc.querySelector('.selection').textContent.includes('원본 선 번호'), '선택하면 원본 근거 표시');
|
||||
await click('선택한 윤곽 숨기기'); check(doc.querySelector('.panel-heading').textContent.includes('93 / 154'), '개별 숨김 반영');
|
||||
let captured; iframe.contentWindow.URL.createObjectURL = blob => { captured = blob; return 'blob:viewer-test'; }; doc.addEventListener('click', e => { if (e.target.closest('a[download]')) e.preventDefault(); }, true);
|
||||
await click('3D OBJ ↓'); const obj = await captured.text(); check(obj.split('\n').filter(s => s.startsWith('o ')).length === 153, 'OBJ 내보내기 153개 객체'); check(!obj.includes('NaN'), 'OBJ 유효한 좌표'); check(obj.split('\n').filter(s => s.startsWith('f ')).length === 466, 'OBJ 연결 삼각면 466개');
|
||||
const before = canvas.toDataURL(); canvas.dispatchEvent(new iframe.contentWindow.KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })); await pause(); check(canvas.toDataURL() !== before, '키보드 회전 반영');
|
||||
const beforeZoom = canvas.toDataURL(); canvas.dispatchEvent(new iframe.contentWindow.WheelEvent('wheel', { deltaY: -200, bubbles: true, cancelable: true })); await pause(); check(canvas.toDataURL() !== beforeZoom, '휠 확대 반영');
|
||||
const input = doc.querySelector('.search'), setter = Object.getOwnPropertyDescriptor(iframe.contentWindow.HTMLInputElement.prototype, 'value').set;
|
||||
setter.call(input, '없는부재'); input.dispatchEvent(new iframe.contentWindow.Event('input', { bubbles: true })); await pause();
|
||||
check(doc.querySelector('.empty'), '윤곽 검색 빈 결과 표시');
|
||||
await click('도면'); check(doc.querySelector('.drawing-preview'), '도면 발췌 표시');
|
||||
await click('측구상세'); await pause(); check(doc.querySelector('.drawing-preview').getAttribute('src').includes('측구상세'), '상세도 전환');
|
||||
await click('검증'); check(doc.querySelectorAll('.checks .pending').length === 3, '미검증 항목 구분');
|
||||
await click('렌더링 보기'); check(doc.querySelector('.canvas-tag').textContent.includes('도면 검토용'), '시각화 구분');
|
||||
result.textContent = JSON.stringify({ status: 'PASS', checks }, null, 2);
|
||||
} catch(e) { result.textContent = JSON.stringify({ status:'FAIL', error:e.message, checks }, null, 2); }
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<!doctype html><meta charset="utf-8"><pre id="result">검증 중</pre><iframe src="/" style="width:390px;height:1450px;border:0"></iframe><script type="module">
|
||||
const iframe=document.querySelector('iframe');
|
||||
await new Promise(resolve=>iframe.addEventListener('load',resolve));
|
||||
await new Promise(resolve=>setTimeout(resolve,1000));
|
||||
const doc=iframe.contentDocument;
|
||||
document.querySelector('#result').textContent=JSON.stringify({viewport:doc.documentElement.clientWidth,content:doc.documentElement.scrollWidth,body:doc.body.scrollWidth,pass:doc.documentElement.scrollWidth===doc.documentElement.clientWidth});
|
||||
</script>
|
||||
@@ -0,0 +1,4 @@
|
||||
<!doctype html><meta charset="utf-8"><title>검토용 렌더링 미리보기</title><style>body{margin:0}iframe{border:0;width:100vw;height:100vh;display:block}</style><iframe src="/"></iframe><script type="module">
|
||||
const iframe=document.querySelector('iframe');
|
||||
for(let i=0;i<100;i++) { await new Promise(r=>setTimeout(r,100)); const button=[...(iframe.contentDocument?.querySelectorAll('button')||[])].find(b=>b.textContent==='렌더링 보기'); if(button){button.click();break;} }
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
import pathlib,json,html,re
|
||||
root=pathlib.Path(__file__).resolve().parents[1]
|
||||
for name in ['browser','mobile']:
|
||||
file=root/f'docs/{name}-check-output.html'
|
||||
source=file.read_text(encoding='utf-8-sig')
|
||||
text=html.unescape(re.search(r'<pre id="result">(.*?)</pre>',source,re.S).group(1))
|
||||
result=json.loads(text)
|
||||
(root/f'docs/{name}-check-result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf8')
|
||||
print(name,result.get('status',result.get('pass')),len(result.get('checks',[])))
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { model, exportOBJ, partFaces, profileVertices } from '../src/model/geometry.js'
|
||||
const faces = model.parts.flatMap(partFaces)
|
||||
assert.equal(faces.length, 468)
|
||||
assert.equal(model.connection.status, '확인 필요')
|
||||
for (const part of model.parts) {
|
||||
const a = profileVertices(part, 0), b = profileVertices(part, 1)
|
||||
assert.equal(a.length,b.length)
|
||||
for(let i=0;i<a.length;i++) {
|
||||
assert.ok(Math.abs(b[i][2]-a[i][2]-19.98)<1e-10)
|
||||
assert.equal(a[i][0],part.a[i][0]); assert.equal(b[i][0],part.b[i][0])
|
||||
assert.ok(Math.abs(a[i][1]+100-part.a[i][1])<1e-10)
|
||||
assert.ok(Math.abs(b[i][1]+100-part.b[i][1])<1e-10)
|
||||
}
|
||||
}
|
||||
const obj=exportOBJ()
|
||||
const rows=obj.split('\n'), vertices=rows.filter(r=>r.startsWith('v '))
|
||||
assert.equal(rows.filter(r=>r.startsWith('o ')).length,154)
|
||||
assert.equal(rows.filter(r=>r.startsWith('f ')).length,468)
|
||||
for(const row of rows.filter(r=>r.startsWith('f ')||r.startsWith('l '))) {
|
||||
for(const n of row.split(' ').slice(1)) assert.ok(Number(n)>=1&&Number(n)<=vertices.length)
|
||||
}
|
||||
assert.equal(exportOBJ(new Set(model.parts.map(p=>p.id))).split('\n').filter(r=>r.startsWith('v ')).length,0)
|
||||
mkdirSync('public/models',{recursive:true})
|
||||
writeFileSync('public/models/BLOCK7_검토용모델.obj',obj)
|
||||
console.log('직선 연결 19.980m, 468개 삼각면, 154개 객체, OBJ 참조·숨김 반영 검증 통과')
|
||||
@@ -0,0 +1,30 @@
|
||||
import json,math,pathlib
|
||||
root=pathlib.Path(__file__).resolve().parents[1]
|
||||
model=json.loads((root/'src/model/block7.json').read_text(encoding='utf8'))
|
||||
parts=model['parts']; assert len(parts)==154
|
||||
assert len({p['id'] for p in parts})==154
|
||||
results=[]
|
||||
for s,top,el in [('A',-74271.37855593952,106.675),('B',-91211.37855593952,107.307)]:
|
||||
raw=json.loads((root/f'docs/extracted/section-{s}.json').read_text(encoding='utf8'))
|
||||
assert len(raw)==114
|
||||
assert sum(l['layer']=='CS-CONC-MAJR' for l in raw)==108
|
||||
assert sum(l['layer']=='CS-CONC-MINR' for l in raw)==6
|
||||
for original,part in zip(raw,parts[:114]):
|
||||
assert original['handle']==part['handles'][0 if s=='A' else 1]
|
||||
for key,point in zip(['a','b'],part[s.lower()]):
|
||||
expected=[(original[key][0]-1128486.218066491)/1000,el+(original[key][1]-top)/1000]
|
||||
assert all(abs(a-b)<0.000000051 for a,b in zip(point,expected))
|
||||
results.append(f'{s}: 114개 선 HANDLE·레이어·양 끝점 원본 대조 통과 (좌표 반올림 오차 0.000051mm 이하)')
|
||||
for p in parts:
|
||||
assert len(p['a'])==len(p['b'])
|
||||
assert all(math.isfinite(v) for s in ['a','b'] for point in p[s] for v in point)
|
||||
assert sum(p['kind']=='arc' for p in parts)==4
|
||||
assert abs(model['longitudinalStations'][1]-model['longitudinalStations'][0]-model['length'])<1e-10
|
||||
assert model['sectionStations'][1]-model['sectionStations'][0]==20
|
||||
for i,el in enumerate(model['sectionTops']):
|
||||
assert abs(max(pt[1] for p in parts[:114] for pt in p['a' if i==0 else 'b'])-el)<1e-7
|
||||
assert abs((parts[1]['a'][1][1]-parts[1]['a'][0][1]))<1e-7
|
||||
results+=['154개 ID 중복 없음 / 모든 좌표 유한수 / 대응 꼭짓점 수 일치','원호 4개, 좌우 삽입 블록 40쌍 반영','벽체 상단 EL 및 종단 측점 차 19.980m 검산 통과','원본 단면 STA 간격 20.000m 보존; 사용자 허용에 따라 배치 간격만 19.980m로 대응 (verify_geometry.mjs에서 별도 검증)']
|
||||
print('\n'.join(results))
|
||||
(root/'docs/좌표검증결과.txt').write_text('\n'.join(results)+'\n',encoding='utf8')
|
||||
|
||||
@@ -13,3 +13,12 @@
|
||||
- 해당 구간에 원호나 중간 곡선점은 아직 찾지 못했고, 이 선이 실제 선형중심선인지 A-A/B-B 절단 위치와 일치하는지도 확정하지 않았다.
|
||||
- 단면 주변의 같은 레이어 선은 중복과 해칭 후보가 있어 좌표 범위만으로 구조 경계를 단정하지 않기로 했다.
|
||||
- 다음 단계는 평면 기준선과 단면 위치를 확정하고, 확인된 주요 단면 구조부터 새 데이터와 코드로 모델링하는 것이다.
|
||||
|
||||
## 2026-10-20 도면 재분석·3D 모델 구현
|
||||
- 본문 UTF-8 판독, 단면 구조선 각 114개 및 공동구 원본 윤곽 재추출.
|
||||
- 단면 STA 299.000/319.000과 종단 경계 299.010/318.990의 각 끝 10mm 차이 기록. 평면 중심선·중심 EL은 확인 필요.
|
||||
- 사용자의 “검토용 직선 연결 허용”에 따라 19.980m 직선 연결. 원본 단면 좌표와 STA는 보존.
|
||||
- 154개 윤곽 객체, 연결 삼각면 468개, 선택·숨김·뷰 전환·기본/렌더링·원본 근거·OBJ 내보내기 구현.
|
||||
- 원본 좌표 검산, 브라우저 검사 30개, 좁은 화면 가로 넘침 검사, 빌드와 정적 검사 통과.
|
||||
- 분석·데이터·뷰어·내보내기·허용된 연결 및 문서를 작업 단위별 로컬 커밋으로 저장. 푸시·공개 없음.
|
||||
- 미검증·생략·가정은 docs/도면분석.md와 docs/검증보고.md에 기록.
|
||||
Reference in new issue
Block a user