원본 좌표와 브라우저 검증 결과 및 실행 수정 안내 기록

This commit is contained in:
김지영 committed 2026-10-20 11:03:43 +09:00
1 parent d01ef8b0e8
commit fac29cb010
17 files changed
+358 -17

No files matched your search

+3
View File
@@ -4,3 +4,6 @@ dist
*.local
docs/extracted/
docs/*-stderr.log
docs/*-output.html
+45 -17
View File
@@ -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을 직접 수정하면 다음 원본 재추출 때 덮어쓰므로, 확정 변경은 추출 스크립트의 근거값에도 반영하세요.
원격 저장소로 푸시하거나 공개하지 않았습니다.
+125
View File
@@ -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
}
]
}
+6
View File
@@ -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

+43
View File
@@ -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에 기록했다.
+10
View File
@@ -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개 연결 삼각면을 만든다. 원호 분할 때문에 삼각면 수가 늘어난다. 이 수는 실물 부재 수가 아니다.
- 양 끝 단면을 임의 캡으로 채우지 않은 열린 표면 모델이다. 원본 중복선·내부선이 남아 있을 수 있으며 체적·수량 산출에 사용하지 않는다.
- 기본 보기는 연결면과 원본 단면 윤곽을 함께 표시한다. 렌더링은 단순 표면 명암·배경·투영 그림자를 적용한 도면 검토용 시각화이다.
+6
View File
@@ -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에서 별도 검증)
+34
View File
@@ -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>
+7
View File
@@ -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>
+4
View File
@@ -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>
+9
View File
@@ -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',[])))
+27
View File
@@ -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 참조·숨김 반영 검증 통과')
+30
View File
@@ -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')
+9
View File
@@ -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에 기록.