chore: import 3d-drawing history from local Code/3d-drawing (subfolder, history preserved)

This commit is contained in:
SeanJo committed 2026-10-27 13:30:09 +09:00
commit 7b754e810b
51 files changed
+20300

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
data/*.tmp
__pycache__/
*.pyc
model/parts/
model/tunnel_solids.step
+56
View File
@@ -0,0 +1,56 @@
# 대산2터널 본선 3D 솔리드 모델 (Code/3d-drawing)
대상: 본선 4+745 ~ 5+480 (당진방향), 도면 C0060303-001 기준. 판정 근거는 `DOC/05_검증/검증_본선3D솔리드모델_2026-10-22_v0.1.00.md`.
## 실행 (뷰어)
1. 이 폴더(`Code/3d-drawing`)에서 HTTP 서버를 엽니다. 예: `python3 -m http.server 8000`
2. 브라우저에서 `http://localhost:8000/viewer/` 를 엽니다.
- `file://`로 열면 GLB를 불러올 수 없습니다.
- three.js 0.160.0 (jsDelivr CDN) 사용. 인터넷 연결 필요.
## 산출물 (model/)
- `tunnel_solids.step` : B-rep 솔리드 320개 (build123d 0.13, 91.7 MB, git 제외)
- `tunnel_solids.glb` : 뷰어용 지오메트리 326개
- `tunnel_solids_meta.json` : 구성요소 목록, 지보패턴 구간 40개, 가정·미확정 항목
구성요소: 라이닝 · 굴착 여유대 · 노면 포장 280 mm · 기층·인버트(가정) · 배수 측구 L/R · 공동구 H/I(가정) · 시설한계 50 mm 참조선(솔리드 아님, 6개).
구성요소는 지보패턴 구간 경계(40구간)에서 분할되어 있습니다.
## 데이터 (data/)
- `pattern_stations_dangjin.csv` : 당진방향 지보패턴 측점표 (42행, 개착부 포함)
- `verify_solids.json` : 모델 생성 검증 요약 (솔리드 수, 구간, 부피 합)
- `verify_geom_hd2_results.json` : 도면↔모델 단면 편차 측정 결과 (4+745, 5+100, 5+470)
- `verify_lining_judgement.md` : 라이닝 도면 판독 판정 (이전 Task)
## 스크립트 (scripts/)
- `build_solids.py` : B-rep 솔리드 생성 (모드: parts_a, parts_b, mesh, step, final)
- `verify_solids_step3.py` : 결정론 점검 M1~M6, S1~S2, G1 (9/9 PASS)
- `verify_geom/` : 도면↔모델 단면 편차 측정 스크립트 (독립 에이전트 실행)
- `verify_lining.py`, `verify_viewer_headless.py` : 라이닝 판독·뷰어 헤드리스 점검 (이전 Task)
- 그 밖의 `extract_*.py`, `inventory_dxf.py`, `dump_std_table.py` 등은 DXF·엑셀 추출 스크립트
## 검증 결과 요약 (2026-10-22)
- 결정론 점검 9/9 PASS (생성 세션 자기검증).
- 도면 기준 편차 (±50 mm 허용):
- 라이닝 내측·상부·내측 헌치: 기준 이내 (17.5~21.5 mm)
- 라이닝 외측 하단 헌치 호: **미모델링, 최대 492 mm** — 사용자 결정으로 한계 수용
- 기층 하면: **최대 105 mm** — 사용자 결정으로 한계 수용
- 배수 측구 본체: 기준 이내 (14.5 mm). 상부 덮개 (도면 폭 450 mm, 78 mm)는 **설계 범위에서 제외** (사용자 결정)
- 굴착·노면 포장: 기준 이내 (14~22 mm)
- 편경사: 4+745·5+100은 도면 부호와 일치. 5+470 +2%는 도면 −2%와 상이 — **현 가정 유지** (사용자 결정, 미확정).
## 한계 (반드시 읽을 것)
- 공동구 H/I 박스는 **가정**입니다. 03-001에 박스 외형이 없고, 04-001~003 공동구·배수 계획도는 아직 대조하지 않았습니다.
- 기층·인버트 형상·두께는 **가정**입니다 (위 편차 한계 수용).
- 라이닝 구조도 상세(04-004~009)의 선분 기하는 대조하지 않았습니다. 판정은 03-001 기준입니다.
- 굴착 지보 등급(NP-4/5/6)은 모델에 반영하지 않았습니다. 굴착 여유대는 대칭 가정입니다.
- 라이닝·굴착 솔리드 사이에 약 0.0173 m² 겹침이 있습니다 (단순화).
- 측점 간 축선은 선형 보간, 단면은 10 m 간격 Ruled loft 근사입니다 (곡선부 근사).
- 뷰어: 1366×768 해상도에서 좌측 패널이 약 60 px 넘칩니다. 버튼 높이 36 px (WCAG AA 24 px 충족, AAA 44 px 미충족). Pretendard 폰트는 내장하지 않았습니다.
- 미확정 패턴(E-1, E-2, RP-2, WL-3)과 NP-5BE-1-WL-4 해석은 당진 표 밖이거나 확인 필요입니다 (`OPEN_미해결목록.md` 참조).
@@ -0,0 +1,10 @@
station_m,centre_x,centre_y,side_gap_m
4700,151067.261,483726.735,8.554
4800,151119.817,483641.679,11.191
4900,151169.128,483554.701,13.981
5000,151215.122,483465.925,16.844
5100,151258.471,483375.815,18.211
5200,151301.3,483285.451,17.956
5300,151344.993,483195.51,15.809
5400,151390.045,483106.254,12.322
5500,151437.284,483018.131,9.389
1 station_m centre_x centre_y side_gap_m
2 4700 151067.261 483726.735 8.554
3 4800 151119.817 483641.679 11.191
4 4900 151169.128 483554.701 13.981
5 5000 151215.122 483465.925 16.844
6 5100 151258.471 483375.815 18.211
7 5200 151301.3 483285.451 17.956
8 5300 151344.993 483195.51 15.809
9 5400 151390.045 483106.254 12.322
10 5500 151437.284 483018.131 9.389
@@ -0,0 +1,10 @@
station_m,centre_x,centre_y,side_gap_m
4700,151067.261,483726.735,8.554
4800,151119.817,483641.679,11.191
4900,151169.128,483554.701,13.981
5000,151215.122,483465.925,16.844
5100,151258.471,483375.815,18.211
5200,151301.3,483285.451,17.956
5300,151344.993,483195.51,15.809
5400,151390.045,483106.254,12.322
5500,151437.284,483018.131,9.389
1 station_m centre_x centre_y side_gap_m
2 4700 151067.261 483726.735 8.554
3 4800 151119.817 483641.679 11.191
4 4900 151169.128 483554.701 13.981
5 5000 151215.122 483465.925 16.844
6 5100 151258.471 483375.815 18.211
7 5200 151301.3 483285.451 17.956
8 5300 151344.993 483195.51 15.809
9 5400 151390.045 483106.254 12.322
10 5500 151437.284 483018.131 9.389
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+256
View File
@@ -0,0 +1,256 @@
{
"source": "C0060304-004..009 (콘크리트라이닝구조도)",
"sheets": [
{
"sheet": 4,
"file": "C0060304-004-콘크리트라이닝구조도(1)(2차로,자연환기300m이상).dxf",
"pattern_details": [
{
"group": "A~C-2",
"kind": "입출구부 50m",
"raw": "주철근 조립도(A~C-2패턴) 입출구부 50m"
}
],
"note5": "5. 본 콘크리트 라이닝 구조도는 지보패턴 입출구부 A~C-2 구간의 구조적 취약구간에 적용하며, 시공시 지질이상대 출현 등으로 설계조건과 상이할 경우 구조해석 검토를 통해 감독원과 협의하여 철근을 보강할 수 있다.",
"concrete": [
"사용재료 : 콘크리트 fck=27MPa"
],
"rebar_grade": [
"철 근 fy=400MPa"
],
"aggregate": [
"조골재의 최대치수 Ø25"
],
"rebar_sizes_label_count": {
"H13": 12,
"H16": 15
},
"radius_dims": [
"R1=6.892",
"R2'=3,910",
"R2=3,712"
],
"lap_dims": [
"600mm"
],
"el_texts": [
"EL. 0.000",
"EL.-0.367"
],
"entrance_exit_50m": true
},
{
"sheet": 5,
"file": "C0060304-005-콘크리트라이닝구조도(2)(2차로,자연환기300m이상).dxf",
"pattern_details": [
{
"group": "E-1~2",
"kind": "온도미고려",
"raw": "주철근 조립도(E-1~2 패턴) 온도미고려"
},
{
"group": "D-1~2",
"kind": "입출구부 50m",
"raw": "(D-1~2 패턴) 입출구부 50m"
}
],
"note5": "5. 본 콘크리트 라이닝 구조도는 지보패턴 입출구부 D-1,2, 온도미고려 E-1,2 구간의 구조적 취약구간에 적용하며, 시공시 지질이상대 출현 등으로 설계조건과 상이할 경우 구조해석 검토를 통해 감독원과 협의하여 철근을 보강할 수 있다.",
"concrete": [
"사용재료 : 콘크리트 fck=27MPa"
],
"rebar_grade": [
"철 근 fy=400MPa"
],
"aggregate": [
"조골재의 최대치수 Ø25"
],
"rebar_sizes_label_count": {
"H13": 6,
"H16": 21
},
"radius_dims": [
"R1=6.892",
"R2'=3,910",
"R2=3,712"
],
"lap_dims": [
"600mm"
],
"el_texts": [
"EL. 0.000",
"EL.-0.367"
],
"entrance_exit_50m": true
},
{
"sheet": 6,
"file": "C0060304-006-콘크리트라이닝구조도(3)(2차로,자연환기300m이상).dxf",
"pattern_details": [
{
"group": "WL-1~2,RP-1~2",
"kind": "온도미고려",
"raw": "주철근 조립도(WL-1~2,RP-1~2패턴) 온도미고려"
},
{
"group": "E-1~2",
"kind": "입출구부 50m",
"raw": "(E-1~2 패턴) 입출구부 50m"
}
],
"note5": "5. 본 콘크리트 라이닝 구조도는 지보패턴 입출구부 E-1~2 온도미고려 WL-1,2 RP-1,2 구간의 구조적 취약구간에 적용하며, 시공시 지질이상대 출현 등으로 설계조건과 상이할 경우 구조해석 검토를 통해 감독원과 협의하여 철근을 보강할 수 있다.",
"concrete": [
"사용재료 : 콘크리트 fck=27MPa"
],
"rebar_grade": [
"철 근 fy=400MPa"
],
"aggregate": [
"조골재의 최대치수 Ø25"
],
"rebar_sizes_label_count": {
"H13": 6,
"H16": 15,
"H19": 7
},
"radius_dims": [
"R1=6.892",
"R2'=3,910",
"R2=3,712"
],
"lap_dims": [
"600mm"
],
"el_texts": [
"EL. 0.000",
"EL.-0.367"
],
"entrance_exit_50m": true
},
{
"sheet": 7,
"file": "C0060304-007-콘크리트라이닝구조도(4)(2차로,자연환기300m이상).dxf",
"pattern_details": [
{
"group": "WL-1~2",
"kind": "입출구부 50m",
"raw": "주철근 조립도(WL-1~2패턴) 입출구부 50m"
},
{
"group": "WL-3~4",
"kind": "온도미고려",
"raw": "(WL-3~4패턴) 온도미고려"
}
],
"note5": "5. 본 콘크리트 라이닝 구조도는 지보패턴 입출구부 WL-1,2 온도미고려 WL-3,4 구간의 구조적 취약구간에 적용하며, 시공시 지질이상대 출현 등으로 설계조건과 상이할 경우 구조해석 검토를 통해 감독원과 협의하여 철근을 보강할 수 있다.",
"concrete": [
"사용재료 : 콘크리트 fck=27MPa"
],
"rebar_grade": [
"철 근 fy=400MPa"
],
"aggregate": [
"조골재의 최대치수 Ø25"
],
"rebar_sizes_label_count": {
"H13": 6,
"H16": 8,
"H19": 7,
"H22": 7
},
"radius_dims": [
"R1=6.892",
"R2'=3,910",
"R2=3,712"
],
"lap_dims": [
"600mm"
],
"el_texts": [
"EL. 0.000",
"EL.-0.367"
],
"entrance_exit_50m": true
},
{
"sheet": 8,
"file": "C0060304-008-콘크리트라이닝구조도(5)(2차로,자연환기300m이상).dxf",
"pattern_details": [
{
"group": "WL-3~4",
"kind": "입출구부 50m",
"raw": "주철근 조립도(WL-3~4패턴) 입출구부 50m"
}
],
"note5": "5. 본 콘크리트 라이닝 구조도는 지보패턴 입출구부 WL-3,4 구간의 구조적 취약구간에 적용하며, 시공시 지질이상대 출현 등으로 설계조건과 상이할 경우 구조해석 검토를 통해 감독원과 협의하여 철근을 보강할 수 있다.",
"concrete": [
"사용재료 : 콘크리트 fck=27MPa"
],
"rebar_grade": [
"철 근 fy=400MPa"
],
"aggregate": [
"조골재의 최대치수 Ø25"
],
"rebar_sizes_label_count": {
"H13": 6,
"H16": 8,
"H22": 7,
"H25": 7
},
"radius_dims": [
"R1=6.892",
"R2'=3,910",
"R2=3,712"
],
"lap_dims": [
"600mm"
],
"el_texts": [
"EL. 0.000",
"EL.-0.367"
],
"entrance_exit_50m": true
},
{
"sheet": 9,
"file": "C0060304-009-콘크리트라이닝구조도(6)(2차로,자연환기300m이상).dxf",
"pattern_details": [
{
"group": "P-2~3",
"kind": "입출구부 50m",
"raw": "주철근 조립도(P-2~3 패턴) 입출구부 50m"
}
],
"note5": "5. 본 콘크리트 라이닝 구조도는 지보패턴 입출구부 P-2~3 구간의 구조적 취약구간에 적용하며, 시공시 지질이상대 출현 등으로 설계조건과 상이할 경우 구조해석 검토를 통해 감독원과 협의하여 철근을 보강할 수 있다.",
"concrete": [
"사용재료 : 콘크리트 fck=27MPa"
],
"rebar_grade": [
"철 근 fy=400MPa"
],
"aggregate": [
"조골재의 최대치수 Ø25"
],
"rebar_sizes_label_count": {
"H13": 6,
"H16": 8,
"H22": 4,
"H25": 4,
"H29": 8
},
"radius_dims": [
"R1=6.892",
"R2'=3,910",
"R2=3,712"
],
"lap_dims": [
"600mm"
],
"el_texts": [
"EL. 0.000",
"EL.-0.367"
],
"entrance_exit_50m": true
}
]
}
@@ -0,0 +1,43 @@
direction,source_row,sta_from,sta_to,length_m,lining_pattern,excavation,excavation_inherited,method,remark
대산,13,4740.85,4758.85,18,P-3,NP-6,False,"상·하
반단면",
대산,14,4758.85,4768.85,10,WL-4,NP-5,False,,
대산,15,4768.85,4792.85,24,WL-2,NP-5,True,,
대산,16,4792.85,4839.85,47,D-2,NP-4,False,,
대산,17,4839.85,4848.85,9,D-1,NP-4,True,,
대산,18,4848.85,4857.85,9,C-2,NP-4,True,전단면,
대산,19,4857.85,4862.85,5,C-1,C-1,False,,
대산,20,4862.85,4868.85,6,B-2,B-2,False,,
대산,21,4868.85,4874.85,6,B-1,B-1,False,,
대산,22,4874.85,4908.85,34,A,A,False,,
대산,23,4908.85,4916.67,7.819999999999709,B-1,B-1,False,,
대산,24,4916.67,4936.67,20,RP-1,RP-1,False,,"대인용
연결통로 접속부"
대산,25,4936.67,4946.85,10.180000000000291,C-1,C-1,False,,
대산,26,4946.85,4965.85,19,C-2,C-2,False,,
대산,27,4965.85,4979.85,14,C-1,C-1,False,,
대산,28,4979.85,4987.950000000001,8.100000000000364,B-2,B-2,False,,
대산,29,4987.950000000001,4995.950000000001,8,B-1,B-1,False,,
대산,30,4995.950000000001,5107.2300000000005,111.27999999999975,A,A,False,,
대산,31,5107.2300000000005,5127.2300000000005,20,RP-1,RP-1,False,,"차량용
연결통로 접속부"
대산,32,5127.2300000000005,5139.450000000001,12.220000000000255,B-1,B-1,False,,
대산,33,5139.450000000001,5163.450000000001,24,B-2,B-2,False,,
대산,34,5163.450000000001,5205.450000000001,42,C-1,C-1,False,,
대산,35,5205.450000000001,5235.450000000001,30,B-2,B-2,False,,
대산,36,5235.450000000001,5279.35,43.899999999999636,B-1,B-1,False,,
대산,37,5279.35,5296.67,17.31999999999971,A,A,False,,
대산,38,5296.67,5316.67,20,RP-1,RP-1,False,,"대인용
연결통로 접속부"
대산,39,5316.67,5355.35,38.68000000000029,A,A,False,,
대산,40,5355.35,5362.35,7,B-1,B-1,False,,
대산,41,5362.35,5376.35,14,B-2,B-2,False,,
대산,42,5376.35,5382.35,6,WL-2,WL-2,False,상·하 반단면,
대산,43,5382.35,5388.35,6,C-1,C-1,False,전단면,
대산,44,5388.35,5398.35,10,C-2,C-2,False,,
대산,45,5398.35,5445.35,47,D-1,NP-4,False,"상·하
반단면",
대산,46,5445.35,5458.35,13,WL-2,NP-5,False,,
대산,47,5458.35,5464.35,6,P-2,NP-6,False,,
대산,48,5464.35,5491.29,26.9399999999996,P-3,NP-6,True,,
대산,49,5491.29,5496.29,5,개착부,NP-6,True,,개착부
1 direction source_row sta_from sta_to length_m lining_pattern excavation excavation_inherited method remark
2 대산 13 4740.85 4758.85 18 P-3 NP-6 False 상·하 반단면
3 대산 14 4758.85 4768.85 10 WL-4 NP-5 False
4 대산 15 4768.85 4792.85 24 WL-2 NP-5 True
5 대산 16 4792.85 4839.85 47 D-2 NP-4 False
6 대산 17 4839.85 4848.85 9 D-1 NP-4 True
7 대산 18 4848.85 4857.85 9 C-2 NP-4 True 전단면
8 대산 19 4857.85 4862.85 5 C-1 C-1 False
9 대산 20 4862.85 4868.85 6 B-2 B-2 False
10 대산 21 4868.85 4874.85 6 B-1 B-1 False
11 대산 22 4874.85 4908.85 34 A A False
12 대산 23 4908.85 4916.67 7.819999999999709 B-1 B-1 False
13 대산 24 4916.67 4936.67 20 RP-1 RP-1 False 대인용 연결통로 접속부
14 대산 25 4936.67 4946.85 10.180000000000291 C-1 C-1 False
15 대산 26 4946.85 4965.85 19 C-2 C-2 False
16 대산 27 4965.85 4979.85 14 C-1 C-1 False
17 대산 28 4979.85 4987.950000000001 8.100000000000364 B-2 B-2 False
18 대산 29 4987.950000000001 4995.950000000001 8 B-1 B-1 False
19 대산 30 4995.950000000001 5107.2300000000005 111.27999999999975 A A False
20 대산 31 5107.2300000000005 5127.2300000000005 20 RP-1 RP-1 False 차량용 연결통로 접속부
21 대산 32 5127.2300000000005 5139.450000000001 12.220000000000255 B-1 B-1 False
22 대산 33 5139.450000000001 5163.450000000001 24 B-2 B-2 False
23 대산 34 5163.450000000001 5205.450000000001 42 C-1 C-1 False
24 대산 35 5205.450000000001 5235.450000000001 30 B-2 B-2 False
25 대산 36 5235.450000000001 5279.35 43.899999999999636 B-1 B-1 False
26 대산 37 5279.35 5296.67 17.31999999999971 A A False
27 대산 38 5296.67 5316.67 20 RP-1 RP-1 False 대인용 연결통로 접속부
28 대산 39 5316.67 5355.35 38.68000000000029 A A False
29 대산 40 5355.35 5362.35 7 B-1 B-1 False
30 대산 41 5362.35 5376.35 14 B-2 B-2 False
31 대산 42 5376.35 5382.35 6 WL-2 WL-2 False 상·하 반단면
32 대산 43 5382.35 5388.35 6 C-1 C-1 False 전단면
33 대산 44 5388.35 5398.35 10 C-2 C-2 False
34 대산 45 5398.35 5445.35 47 D-1 NP-4 False 상·하 반단면
35 대산 46 5445.35 5458.35 13 WL-2 NP-5 False
36 대산 47 5458.35 5464.35 6 P-2 NP-6 False
37 대산 48 5464.35 5491.29 26.9399999999996 P-3 NP-6 True
38 대산 49 5491.29 5496.29 5 개착부 NP-6 True 개착부
@@ -0,0 +1,48 @@
direction,source_row,sta_from,sta_to,length_m,lining_pattern,excavation,excavation_inherited,method,remark
당진,55,4740.0,4745.0,5,개착부,,True,,개착부
당진,56,4745.0,4763.0,18,P-3,NP-6,False,"상·하
반단면",
당진,57,4763.0,4785.0,22,WL-2,NP-5,False,,
당진,58,4785.0,4796.0,11,WL-1,NP-5,True,,
당진,59,4796.0,4839.0,43,D-2,NP-4,False,,
당진,60,4839.0,4848.0,9,D-1,NP-4,True,,
당진,61,4848.0,4857.0,9,C-2,NP-4,True,전단면,
당진,62,4857.0,4862.0,5,C-1,NP-4,True,,
당진,63,4862.0,4867.0,5,B-2,B-2,False,,
당진,64,4867.0,4873.0,6,B-1,B-1,False,,
당진,65,4873.0,4907.0,34,A,A,False,,
당진,66,4907.0,4915.0,8,B-1,B-1,False,,
당진,67,4915.0,4920.0,5,B-2,B-2,False,,
당진,68,4920.0,4940.0,20,RP-1,B-2,True,,"대인용
연결통로 접속부"
당진,69,4940.0,4988.0,48,C-1,C-1,False,,
당진,70,4988.0,4998.0,10,B-2,B-2,False,,
당진,71,4998.0,5006.0,8,B-1,B-1,False,,
당진,72,5006.0,5019.0,13,B-2,B-2,False,,
당진,73,5019.0,5025.0,6,B-1,B-1,False,,
당진,74,5025.0,5110.0,85,A,A,False,,
당진,75,5110.0,5130.0,20,RP-1,A,True,,"차량용
연결통로 접속부"
당진,76,5130.0,5140.0,10,B-1,B-1,False,,
당진,77,5140.0,5157.0,17,B-2,B-2,False,,
당진,78,5157.0,5204.0,47,C-1,C-1,False,,
당진,79,5204.0,5211.0,7,B-2,B-2,False,,
당진,80,5211.0,5221.0,10,B-1,B-1,False,,
당진,81,5221.0,5259.0,38,A,A,False,,
당진,82,5259.0,5279.0,20,B-1,B-1,False,,
당진,83,5279.0,5300.0,21,A,A,False,,
당진,84,5300.0,5320.0,20,RP-1,A,True,,"대인용
연결통로 접속부"
당진,85,5320.0,5357.0,37,A,A,False,,
당진,86,5357.0,5363.0,6,B-1,B-1,False,,
당진,87,5363.0,5377.0,14,B-2,B-2,False,,
당진,88,5377.0,5390.0,13,C-1,C-1,False,,
당진,89,5390.0,5399.0,9,C-2,C-2,False,,
당진,90,5399.0,5406.0,7,D-1,NP-4,False,"상·하
반단면",
당진,91,5406.0,5427.0,21,D-2,NP-4,True,,
당진,92,5427.0,5446.0,19,D-1,NP-4,True,,
당진,93,5446.0,5462.0,16,WL-2,NP-5,False,,
당진,94,5462.0,5468.0,6,P-2,NP-6,False,,
당진,95,5468.0,5495.0,27,P-3,NP-6,True,,
당진,96,5495.0,5500.0,5,개착부,NP-6,True,,개착부
1 direction source_row sta_from sta_to length_m lining_pattern excavation excavation_inherited method remark
2 당진 55 4740.0 4745.0 5 개착부 True 개착부
3 당진 56 4745.0 4763.0 18 P-3 NP-6 False 상·하 반단면
4 당진 57 4763.0 4785.0 22 WL-2 NP-5 False
5 당진 58 4785.0 4796.0 11 WL-1 NP-5 True
6 당진 59 4796.0 4839.0 43 D-2 NP-4 False
7 당진 60 4839.0 4848.0 9 D-1 NP-4 True
8 당진 61 4848.0 4857.0 9 C-2 NP-4 True 전단면
9 당진 62 4857.0 4862.0 5 C-1 NP-4 True
10 당진 63 4862.0 4867.0 5 B-2 B-2 False
11 당진 64 4867.0 4873.0 6 B-1 B-1 False
12 당진 65 4873.0 4907.0 34 A A False
13 당진 66 4907.0 4915.0 8 B-1 B-1 False
14 당진 67 4915.0 4920.0 5 B-2 B-2 False
15 당진 68 4920.0 4940.0 20 RP-1 B-2 True 대인용 연결통로 접속부
16 당진 69 4940.0 4988.0 48 C-1 C-1 False
17 당진 70 4988.0 4998.0 10 B-2 B-2 False
18 당진 71 4998.0 5006.0 8 B-1 B-1 False
19 당진 72 5006.0 5019.0 13 B-2 B-2 False
20 당진 73 5019.0 5025.0 6 B-1 B-1 False
21 당진 74 5025.0 5110.0 85 A A False
22 당진 75 5110.0 5130.0 20 RP-1 A True 차량용 연결통로 접속부
23 당진 76 5130.0 5140.0 10 B-1 B-1 False
24 당진 77 5140.0 5157.0 17 B-2 B-2 False
25 당진 78 5157.0 5204.0 47 C-1 C-1 False
26 당진 79 5204.0 5211.0 7 B-2 B-2 False
27 당진 80 5211.0 5221.0 10 B-1 B-1 False
28 당진 81 5221.0 5259.0 38 A A False
29 당진 82 5259.0 5279.0 20 B-1 B-1 False
30 당진 83 5279.0 5300.0 21 A A False
31 당진 84 5300.0 5320.0 20 RP-1 A True 대인용 연결통로 접속부
32 당진 85 5320.0 5357.0 37 A A False
33 당진 86 5357.0 5363.0 6 B-1 B-1 False
34 당진 87 5363.0 5377.0 14 B-2 B-2 False
35 당진 88 5377.0 5390.0 13 C-1 C-1 False
36 당진 89 5390.0 5399.0 9 C-2 C-2 False
37 당진 90 5399.0 5406.0 7 D-1 NP-4 False 상·하 반단면
38 당진 91 5406.0 5427.0 21 D-2 NP-4 True
39 당진 92 5427.0 5446.0 19 D-1 NP-4 True
40 당진 93 5446.0 5462.0 16 WL-2 NP-5 False
41 당진 94 5462.0 5468.0 6 P-2 NP-6 False
42 당진 95 5468.0 5495.0 27 P-3 NP-6 True
43 당진 96 5495.0 5500.0 5 개착부 NP-6 True 개착부
@@ -0,0 +1,369 @@
seg_from,seg_to,pattern,status
4745,4747,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4747,4749,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4749,4751,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4751,4753,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4753,4755,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4755,4757,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4757,4759,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4759,4761,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4761,4763,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4763,4765,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4765,4767,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4767,4769,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4769,4771,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4771,4773,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4773,4775,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4775,4777,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4777,4779,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4779,4781,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4781,4783,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4783,4785,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4785,4787,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4787,4789,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4789,4791,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4791,4793,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4793,4795,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4795,4797,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4797,4799,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4799,4801,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4801,4803,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4803,4805,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4805,4807,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4807,4809,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4809,4811,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4811,4813,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4813,4815,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4815,4817,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4817,4819,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4819,4821,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4821,4823,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4823,4825,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4825,4827,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4827,4829,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4829,4831,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4831,4833,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4833,4835,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4835,4837,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4837,4839,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4839,4841,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4841,4843,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4843,4845,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4845,4847,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4847,4849,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4849,4851,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4851,4853,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4853,4855,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4855,4857,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4857,4859,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4859,4861,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4861,4863,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4863,4865,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4865,4867,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4867,4869,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4869,4871,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4871,4873,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4873,4875,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4875,4877,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4877,4879,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4879,4881,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4881,4883,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4883,4885,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4885,4887,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4887,4889,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4889,4891,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4891,4893,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4893,4895,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4895,4897,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4897,4899,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4899,4901,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4901,4903,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4903,4905,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4905,4907,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4907,4909,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4909,4911,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4911,4913,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4913,4915,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4915,4917,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4917,4919,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4919,4921,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4921,4923,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4923,4925,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4925,4927,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4927,4929,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4929,4931,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4931,4933,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4933,4935,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4935,4937,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4937,4939,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4939,4941,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4941,4943,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4943,4945,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4945,4947,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4947,4949,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4949,4951,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4951,4953,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4953,4955,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4955,4957,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4957,4959,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4959,4961,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4961,4963,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4963,4965,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4965,4967,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4967,4969,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4969,4971,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4971,4973,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4973,4975,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4975,4977,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4977,4979,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4979,4981,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4981,4983,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4983,4985,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4985,4987,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4987,4989,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4989,4991,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4991,4993,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
4993,4995,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
4995,4997,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
4997,4999,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
4999,5001,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5001,5003,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5003,5005,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5005,5007,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5007,5009,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5009,5011,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5011,5013,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5013,5015,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5015,5017,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5017,5019,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5019,5021,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5021,5023,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5023,5025,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5025,5027,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5027,5029,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5029,5031,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5031,5033,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5033,5035,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5035,5037,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5037,5039,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5039,5041,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5041,5043,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5043,5045,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5045,5047,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5047,5049,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5049,5051,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5051,5053,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5053,5055,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5055,5057,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5057,5059,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5059,5061,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5061,5063,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5063,5065,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5065,5067,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5067,5069,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5069,5071,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5071,5073,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5073,5075,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5075,5077,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5077,5079,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5079,5081,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5081,5083,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5083,5085,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5085,5087,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5087,5089,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5089,5091,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5091,5093,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5093,5095,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5095,5097,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5097,5099,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5099,5101,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5101,5103,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5103,5105,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5105,5107,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5107,5109,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5109,5111,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5111,5113,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5113,5115,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5115,5117,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5117,5119,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5119,5121,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5121,5123,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5123,5125,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5125,5127,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5127,5129,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5129,5131,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5131,5133,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5133,5135,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5135,5137,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5137,5139,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5139,5141,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5141,5143,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5143,5145,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5145,5147,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5147,5149,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5149,5151,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5151,5153,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5153,5155,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5155,5157,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5157,5159,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5159,5161,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5161,5163,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5163,5165,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5165,5167,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5167,5169,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5169,5171,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5171,5173,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5173,5175,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5175,5177,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5177,5179,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5179,5181,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5181,5183,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5183,5185,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5185,5187,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5187,5189,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5189,5191,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5191,5193,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5193,5195,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5195,5197,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5197,5199,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5199,5201,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5201,5203,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5203,5205,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5205,5207,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5207,5209,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5209,5211,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5211,5213,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5213,5215,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5215,5217,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5217,5219,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5219,5221,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5221,5223,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5223,5225,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5225,5227,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5227,5229,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5229,5231,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5231,5233,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5233,5235,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5235,5237,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5237,5239,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5239,5241,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5241,5243,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5243,5245,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5245,5247,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5247,5249,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5249,5251,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5251,5253,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5253,5255,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5255,5257,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5257,5259,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5259,5261,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5261,5263,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5263,5265,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5265,5267,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5267,5269,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5269,5271,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5271,5273,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5273,5275,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5275,5277,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5277,5279,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5279,5281,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5281,5283,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5283,5285,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5285,5287,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5287,5289,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5289,5291,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5291,5293,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5293,5295,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5295,5297,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5297,5299,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5299,5301,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5301,5303,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5303,5305,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5305,5307,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5307,5309,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5309,5311,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5311,5313,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5313,5315,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5315,5317,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5317,5319,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5319,5321,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5321,5323,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5323,5325,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5325,5327,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5327,5329,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5329,5331,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5331,5333,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5333,5335,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5335,5337,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5337,5339,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5339,5341,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5341,5343,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5343,5345,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5345,5347,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5347,5349,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5349,5351,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5351,5353,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5353,5355,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5355,5357,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5357,5359,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5359,5361,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5361,5363,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5363,5365,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5365,5367,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5367,5369,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5369,5371,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5371,5373,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5373,5375,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5375,5377,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5377,5379,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5379,5381,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5381,5383,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5383,5385,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5385,5387,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5387,5389,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5389,5391,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5391,5393,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5393,5395,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5395,5397,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5397,5399,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5399,5401,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5401,5403,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5403,5405,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5405,5407,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5407,5409,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5409,5411,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5411,5413,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5413,5415,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5415,5417,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5417,5419,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5419,5421,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5421,5423,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5423,5425,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5425,5427,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5427,5429,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5429,5431,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5431,5433,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5433,5435,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5435,5437,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5437,5439,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5439,5441,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5441,5443,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5443,5445,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5445,5447,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5447,5449,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5449,5451,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5451,5453,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5453,5455,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5455,5457,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5457,5459,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5459,5461,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5461,5463,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5463,5465,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5465,5467,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5467,5469,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5469,5471,NP-5AE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5471,5473,NP-5BE-1-WL-4,"임의 배정 (placeholder, 실제 구간 아님)"
5473,5475,NP-6A,"임의 배정 (placeholder, 실제 구간 아님)"
5475,5477,NP-6B,"임의 배정 (placeholder, 실제 구간 아님)"
5477,5479,NP-4AD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
5479,5480,NP-4BD-1-D-2,"임의 배정 (placeholder, 실제 구간 아님)"
1 seg_from seg_to pattern status
2 4745 4747 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
3 4747 4749 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
4 4749 4751 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
5 4751 4753 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
6 4753 4755 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
7 4755 4757 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
8 4757 4759 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
9 4759 4761 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
10 4761 4763 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
11 4763 4765 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
12 4765 4767 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
13 4767 4769 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
14 4769 4771 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
15 4771 4773 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
16 4773 4775 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
17 4775 4777 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
18 4777 4779 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
19 4779 4781 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
20 4781 4783 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
21 4783 4785 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
22 4785 4787 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
23 4787 4789 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
24 4789 4791 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
25 4791 4793 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
26 4793 4795 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
27 4795 4797 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
28 4797 4799 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
29 4799 4801 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
30 4801 4803 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
31 4803 4805 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
32 4805 4807 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
33 4807 4809 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
34 4809 4811 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
35 4811 4813 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
36 4813 4815 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
37 4815 4817 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
38 4817 4819 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
39 4819 4821 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
40 4821 4823 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
41 4823 4825 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
42 4825 4827 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
43 4827 4829 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
44 4829 4831 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
45 4831 4833 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
46 4833 4835 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
47 4835 4837 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
48 4837 4839 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
49 4839 4841 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
50 4841 4843 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
51 4843 4845 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
52 4845 4847 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
53 4847 4849 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
54 4849 4851 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
55 4851 4853 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
56 4853 4855 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
57 4855 4857 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
58 4857 4859 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
59 4859 4861 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
60 4861 4863 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
61 4863 4865 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
62 4865 4867 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
63 4867 4869 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
64 4869 4871 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
65 4871 4873 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
66 4873 4875 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
67 4875 4877 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
68 4877 4879 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
69 4879 4881 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
70 4881 4883 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
71 4883 4885 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
72 4885 4887 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
73 4887 4889 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
74 4889 4891 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
75 4891 4893 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
76 4893 4895 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
77 4895 4897 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
78 4897 4899 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
79 4899 4901 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
80 4901 4903 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
81 4903 4905 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
82 4905 4907 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
83 4907 4909 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
84 4909 4911 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
85 4911 4913 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
86 4913 4915 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
87 4915 4917 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
88 4917 4919 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
89 4919 4921 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
90 4921 4923 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
91 4923 4925 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
92 4925 4927 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
93 4927 4929 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
94 4929 4931 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
95 4931 4933 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
96 4933 4935 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
97 4935 4937 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
98 4937 4939 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
99 4939 4941 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
100 4941 4943 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
101 4943 4945 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
102 4945 4947 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
103 4947 4949 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
104 4949 4951 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
105 4951 4953 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
106 4953 4955 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
107 4955 4957 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
108 4957 4959 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
109 4959 4961 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
110 4961 4963 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
111 4963 4965 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
112 4965 4967 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
113 4967 4969 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
114 4969 4971 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
115 4971 4973 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
116 4973 4975 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
117 4975 4977 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
118 4977 4979 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
119 4979 4981 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
120 4981 4983 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
121 4983 4985 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
122 4985 4987 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
123 4987 4989 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
124 4989 4991 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
125 4991 4993 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
126 4993 4995 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
127 4995 4997 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
128 4997 4999 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
129 4999 5001 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
130 5001 5003 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
131 5003 5005 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
132 5005 5007 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
133 5007 5009 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
134 5009 5011 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
135 5011 5013 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
136 5013 5015 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
137 5015 5017 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
138 5017 5019 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
139 5019 5021 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
140 5021 5023 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
141 5023 5025 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
142 5025 5027 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
143 5027 5029 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
144 5029 5031 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
145 5031 5033 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
146 5033 5035 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
147 5035 5037 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
148 5037 5039 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
149 5039 5041 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
150 5041 5043 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
151 5043 5045 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
152 5045 5047 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
153 5047 5049 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
154 5049 5051 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
155 5051 5053 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
156 5053 5055 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
157 5055 5057 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
158 5057 5059 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
159 5059 5061 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
160 5061 5063 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
161 5063 5065 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
162 5065 5067 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
163 5067 5069 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
164 5069 5071 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
165 5071 5073 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
166 5073 5075 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
167 5075 5077 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
168 5077 5079 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
169 5079 5081 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
170 5081 5083 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
171 5083 5085 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
172 5085 5087 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
173 5087 5089 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
174 5089 5091 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
175 5091 5093 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
176 5093 5095 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
177 5095 5097 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
178 5097 5099 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
179 5099 5101 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
180 5101 5103 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
181 5103 5105 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
182 5105 5107 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
183 5107 5109 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
184 5109 5111 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
185 5111 5113 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
186 5113 5115 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
187 5115 5117 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
188 5117 5119 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
189 5119 5121 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
190 5121 5123 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
191 5123 5125 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
192 5125 5127 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
193 5127 5129 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
194 5129 5131 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
195 5131 5133 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
196 5133 5135 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
197 5135 5137 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
198 5137 5139 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
199 5139 5141 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
200 5141 5143 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
201 5143 5145 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
202 5145 5147 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
203 5147 5149 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
204 5149 5151 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
205 5151 5153 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
206 5153 5155 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
207 5155 5157 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
208 5157 5159 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
209 5159 5161 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
210 5161 5163 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
211 5163 5165 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
212 5165 5167 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
213 5167 5169 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
214 5169 5171 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
215 5171 5173 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
216 5173 5175 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
217 5175 5177 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
218 5177 5179 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
219 5179 5181 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
220 5181 5183 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
221 5183 5185 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
222 5185 5187 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
223 5187 5189 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
224 5189 5191 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
225 5191 5193 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
226 5193 5195 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
227 5195 5197 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
228 5197 5199 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
229 5199 5201 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
230 5201 5203 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
231 5203 5205 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
232 5205 5207 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
233 5207 5209 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
234 5209 5211 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
235 5211 5213 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
236 5213 5215 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
237 5215 5217 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
238 5217 5219 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
239 5219 5221 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
240 5221 5223 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
241 5223 5225 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
242 5225 5227 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
243 5227 5229 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
244 5229 5231 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
245 5231 5233 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
246 5233 5235 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
247 5235 5237 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
248 5237 5239 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
249 5239 5241 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
250 5241 5243 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
251 5243 5245 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
252 5245 5247 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
253 5247 5249 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
254 5249 5251 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
255 5251 5253 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
256 5253 5255 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
257 5255 5257 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
258 5257 5259 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
259 5259 5261 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
260 5261 5263 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
261 5263 5265 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
262 5265 5267 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
263 5267 5269 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
264 5269 5271 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
265 5271 5273 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
266 5273 5275 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
267 5275 5277 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
268 5277 5279 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
269 5279 5281 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
270 5281 5283 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
271 5283 5285 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
272 5285 5287 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
273 5287 5289 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
274 5289 5291 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
275 5291 5293 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
276 5293 5295 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
277 5295 5297 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
278 5297 5299 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
279 5299 5301 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
280 5301 5303 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
281 5303 5305 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
282 5305 5307 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
283 5307 5309 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
284 5309 5311 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
285 5311 5313 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
286 5313 5315 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
287 5315 5317 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
288 5317 5319 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
289 5319 5321 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
290 5321 5323 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
291 5323 5325 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
292 5325 5327 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
293 5327 5329 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
294 5329 5331 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
295 5331 5333 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
296 5333 5335 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
297 5335 5337 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
298 5337 5339 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
299 5339 5341 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
300 5341 5343 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
301 5343 5345 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
302 5345 5347 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
303 5347 5349 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
304 5349 5351 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
305 5351 5353 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
306 5353 5355 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
307 5355 5357 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
308 5357 5359 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
309 5359 5361 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
310 5361 5363 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
311 5363 5365 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
312 5365 5367 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
313 5367 5369 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
314 5369 5371 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
315 5371 5373 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
316 5373 5375 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
317 5375 5377 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
318 5377 5379 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
319 5379 5381 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
320 5381 5383 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
321 5383 5385 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
322 5385 5387 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
323 5387 5389 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
324 5389 5391 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
325 5391 5393 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
326 5393 5395 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
327 5395 5397 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
328 5397 5399 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
329 5399 5401 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
330 5401 5403 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
331 5403 5405 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
332 5405 5407 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
333 5407 5409 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
334 5409 5411 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
335 5411 5413 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
336 5413 5415 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
337 5415 5417 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
338 5417 5419 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
339 5419 5421 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
340 5421 5423 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
341 5423 5425 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
342 5425 5427 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
343 5427 5429 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
344 5429 5431 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
345 5431 5433 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
346 5433 5435 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
347 5435 5437 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
348 5437 5439 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
349 5439 5441 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
350 5441 5443 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
351 5443 5445 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
352 5445 5447 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
353 5447 5449 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
354 5449 5451 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
355 5451 5453 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
356 5453 5455 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
357 5455 5457 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
358 5457 5459 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
359 5459 5461 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
360 5461 5463 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
361 5463 5465 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
362 5465 5467 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
363 5467 5469 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
364 5469 5471 NP-5AE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
365 5471 5473 NP-5BE-1-WL-4 임의 배정 (placeholder, 실제 구간 아님)
366 5473 5475 NP-6A 임의 배정 (placeholder, 실제 구간 아님)
367 5475 5477 NP-6B 임의 배정 (placeholder, 실제 구간 아님)
368 5477 5479 NP-4AD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
369 5479 5480 NP-4BD-1-D-2 임의 배정 (placeholder, 실제 구간 아님)
+77
View File
@@ -0,0 +1,77 @@
station_anchor,est_station,across_m,value,x,y
4740,4738.3,-32.81,65.969,150794.75,483555.28
4740,4738.3,-16.41,50.495,150809.32,483562.8
4760,4758.4,-31.48,70.818,150805.68,483536.98
4760,4758.4,-16.41,50.993,150819.07,483543.89
4780,4778.4,-32.81,72.725,150814.25,483517.47
4780,4778.4,-15.97,51.492,150829.22,483525.19
4800,4798.5,-31.48,82.17,150825.19,483499.17
4800,4798.5,-15.53,51.99,150839.37,483506.48
4820,4818.5,-32.81,92.372,150833.76,483479.65
4820,4818.5,-16.86,52.488,150847.94,483486.97
4840,4838.6,-33.7,100.775,150842.73,483460.34
4840,4838.6,-16.86,52.986,150857.69,483468.06
4860,4858.6,-33.26,109.7,150852.87,483441.63
4860,4858.6,-16.86,53.485,150867.45,483449.15
4880,4878.6,-32.81,119.9,150863.02,483422.93
4880,4878.6,-16.86,53.983,150877.2,483430.25
4900,4898.7,-33.26,130.6,150872.38,483403.82
4900,4898.7,-15.97,54.481,150887.74,483411.74
4920,4918.7,-31.93,141.1,150883.32,483385.52
4920,4918.7,-16.41,54.98,150897.1,483392.63
4940,4938.8,-34.59,152.277,150890.71,483365.4
4940,4938.8,-16.86,55.478,150906.46,483373.52
4960,4958.8,-34.59,155.393,150900.46,483346.49
4960,4958.8,-16.86,55.976,150916.22,483354.62
4980,4978.9,-34.14,149.607,150910.61,483327.79
4980,4978.9,-16.86,56.474,150925.97,483335.71
5000,4998.9,-33.7,144.2,150920.75,483309.08
5000,4998.9,-16.86,56.973,150935.72,483316.8
5020,5019.0,-34.59,145.422,150929.72,483289.77
5020,5019.0,-15.97,57.471,150946.26,483298.3
5040,5039.0,-34.59,142.999,150939.47,483270.86
5040,5039.0,-16.86,57.969,150955.23,483278.99
5060,5059.0,-34.14,142.606,150949.62,483252.16
5060,5059.0,-16.86,58.467,150964.98,483260.08
5080,5079.1,-34.59,142.244,150958.98,483233.05
5080,5079.1,-16.86,58.966,150974.74,483241.18
5100,5099.1,-34.59,145.737,150968.73,483214.14
5100,5099.1,-16.86,59.464,150984.49,483222.27
5120,5119.2,-32.81,148.1,150980.06,483196.05
5120,5119.2,-16.86,59.962,150994.24,483203.36
5140,5139.2,-33.26,146.041,150989.42,483176.94
5140,5139.2,-15.97,60.46,151004.79,483184.86
5160,5159.3,-32.81,146.118,150999.57,483158.23
5160,5159.3,-16.41,60.959,151014.15,483165.75
5180,5179.3,-34.59,147.947,151007.75,483138.51
5180,5179.3,-15.97,61.457,151024.29,483147.05
5200,5199.4,-33.7,152.8,151018.29,483120.01
5200,5199.4,-15.97,61.955,151034.05,483128.14
5220,5219.4,-33.7,157.214,151028.04,483101.1
5220,5219.4,-16.86,62.453,151043.01,483108.83
5240,5239.5,-33.7,155.7,151037.8,483082.2
5240,5239.5,-16.86,62.952,151052.77,483089.92
5260,5259.5,-34.59,146.424,151046.76,483062.88
5260,5259.5,-16.41,63.45,151062.91,483071.22
5280,5279.5,-33.7,138.6,151057.3,483044.38
5280,5279.5,-16.86,63.948,151072.27,483052.11
5300,5299.6,-33.7,140.039,151067.06,483025.48
5300,5299.6,-16.86,64.446,151082.03,483033.2
5320,5319.6,-34.14,130.368,151076.42,483006.37
5320,5319.6,-16.86,64.933,151091.78,483014.29
5340,5339.7,-33.26,117.65,151086.96,482987.87
5340,5339.7,-16.86,65.378,151101.53,482995.38
5360,5359.7,-33.26,107.3,151096.71,482968.96
5360,5359.7,-15.97,65.781,151112.07,482976.88
5380,5379.8,-34.14,103.593,151105.68,482949.65
5380,5379.8,-15.97,66.142,151121.83,482957.98
5400,5399.8,-33.7,109.032,151115.83,482930.94
5400,5399.8,-16.41,66.46,151131.19,482938.87
5420,5419.9,-32.81,108.108,151126.37,482912.44
5420,5419.9,-16.86,66.737,151140.55,482919.76
5440,5439.9,-31.93,98.183,151136.91,482893.94
5440,5439.9,-16.41,66.97,151150.7,482901.05
5460,5460.0,-31.93,88.124,151146.66,482875.03
5460,5460.0,-15.97,67.162,151160.84,482882.35
5480,5480.0,-30.6,81.014,151157.6,482856.74
5480,5480.0,-15.08,67.311,151171.38,482863.85
1 station_anchor est_station across_m value x y
2 4740 4738.3 -32.81 65.969 150794.75 483555.28
3 4740 4738.3 -16.41 50.495 150809.32 483562.8
4 4760 4758.4 -31.48 70.818 150805.68 483536.98
5 4760 4758.4 -16.41 50.993 150819.07 483543.89
6 4780 4778.4 -32.81 72.725 150814.25 483517.47
7 4780 4778.4 -15.97 51.492 150829.22 483525.19
8 4800 4798.5 -31.48 82.17 150825.19 483499.17
9 4800 4798.5 -15.53 51.99 150839.37 483506.48
10 4820 4818.5 -32.81 92.372 150833.76 483479.65
11 4820 4818.5 -16.86 52.488 150847.94 483486.97
12 4840 4838.6 -33.7 100.775 150842.73 483460.34
13 4840 4838.6 -16.86 52.986 150857.69 483468.06
14 4860 4858.6 -33.26 109.7 150852.87 483441.63
15 4860 4858.6 -16.86 53.485 150867.45 483449.15
16 4880 4878.6 -32.81 119.9 150863.02 483422.93
17 4880 4878.6 -16.86 53.983 150877.2 483430.25
18 4900 4898.7 -33.26 130.6 150872.38 483403.82
19 4900 4898.7 -15.97 54.481 150887.74 483411.74
20 4920 4918.7 -31.93 141.1 150883.32 483385.52
21 4920 4918.7 -16.41 54.98 150897.1 483392.63
22 4940 4938.8 -34.59 152.277 150890.71 483365.4
23 4940 4938.8 -16.86 55.478 150906.46 483373.52
24 4960 4958.8 -34.59 155.393 150900.46 483346.49
25 4960 4958.8 -16.86 55.976 150916.22 483354.62
26 4980 4978.9 -34.14 149.607 150910.61 483327.79
27 4980 4978.9 -16.86 56.474 150925.97 483335.71
28 5000 4998.9 -33.7 144.2 150920.75 483309.08
29 5000 4998.9 -16.86 56.973 150935.72 483316.8
30 5020 5019.0 -34.59 145.422 150929.72 483289.77
31 5020 5019.0 -15.97 57.471 150946.26 483298.3
32 5040 5039.0 -34.59 142.999 150939.47 483270.86
33 5040 5039.0 -16.86 57.969 150955.23 483278.99
34 5060 5059.0 -34.14 142.606 150949.62 483252.16
35 5060 5059.0 -16.86 58.467 150964.98 483260.08
36 5080 5079.1 -34.59 142.244 150958.98 483233.05
37 5080 5079.1 -16.86 58.966 150974.74 483241.18
38 5100 5099.1 -34.59 145.737 150968.73 483214.14
39 5100 5099.1 -16.86 59.464 150984.49 483222.27
40 5120 5119.2 -32.81 148.1 150980.06 483196.05
41 5120 5119.2 -16.86 59.962 150994.24 483203.36
42 5140 5139.2 -33.26 146.041 150989.42 483176.94
43 5140 5139.2 -15.97 60.46 151004.79 483184.86
44 5160 5159.3 -32.81 146.118 150999.57 483158.23
45 5160 5159.3 -16.41 60.959 151014.15 483165.75
46 5180 5179.3 -34.59 147.947 151007.75 483138.51
47 5180 5179.3 -15.97 61.457 151024.29 483147.05
48 5200 5199.4 -33.7 152.8 151018.29 483120.01
49 5200 5199.4 -15.97 61.955 151034.05 483128.14
50 5220 5219.4 -33.7 157.214 151028.04 483101.1
51 5220 5219.4 -16.86 62.453 151043.01 483108.83
52 5240 5239.5 -33.7 155.7 151037.8 483082.2
53 5240 5239.5 -16.86 62.952 151052.77 483089.92
54 5260 5259.5 -34.59 146.424 151046.76 483062.88
55 5260 5259.5 -16.41 63.45 151062.91 483071.22
56 5280 5279.5 -33.7 138.6 151057.3 483044.38
57 5280 5279.5 -16.86 63.948 151072.27 483052.11
58 5300 5299.6 -33.7 140.039 151067.06 483025.48
59 5300 5299.6 -16.86 64.446 151082.03 483033.2
60 5320 5319.6 -34.14 130.368 151076.42 483006.37
61 5320 5319.6 -16.86 64.933 151091.78 483014.29
62 5340 5339.7 -33.26 117.65 151086.96 482987.87
63 5340 5339.7 -16.86 65.378 151101.53 482995.38
64 5360 5359.7 -33.26 107.3 151096.71 482968.96
65 5360 5359.7 -15.97 65.781 151112.07 482976.88
66 5380 5379.8 -34.14 103.593 151105.68 482949.65
67 5380 5379.8 -15.97 66.142 151121.83 482957.98
68 5400 5399.8 -33.7 109.032 151115.83 482930.94
69 5400 5399.8 -16.41 66.46 151131.19 482938.87
70 5420 5419.9 -32.81 108.108 151126.37 482912.44
71 5420 5419.9 -16.86 66.737 151140.55 482919.76
72 5440 5439.9 -31.93 98.183 151136.91 482893.94
73 5440 5439.9 -16.41 66.97 151150.7 482901.05
74 5460 5460.0 -31.93 88.124 151146.66 482875.03
75 5460 5460.0 -15.97 67.162 151160.84 482882.35
76 5480 5480.0 -30.6 81.014 151157.6 482856.74
77 5480 5480.0 -15.08 67.311 151171.38 482863.85
@@ -0,0 +1,36 @@
27400,표준 단면도
24350,표준단면 적용구간
23550,"대 산 1 , 2 터 널"
23000,구 분,편 경 사,적 용 구 간 ( 개 착 구 간 제 외 )
22550,대산방향,-2%%%,STA.3+070.00 ~ 3+555.00
22200,대산 1터널,-2%%% ~ +3%%%,STA.3+070.00 ~ 3+145.00
22000,당진방향
21850,+3%%%,STA.3+145.00 ~ 3+565.00
21500,+2%%%,STA.4+740.85 ~ 5+075.00
21150,대산방향,+2%%% ~ -2%%%,STA.5+075.00 ~ 5+135.00
20800,-2%%%,STA.5+135.00 ~ 5+476.31
20600,대산 2터널
20450,-2%%%,STA.4+745.00 ~ 5+105.00
20100,-2%%% ~ +2%%%,STA.5+105.00 ~ 5+165.00
19900,당진방향
19750,+2%%%,STA.5+165.00 ~ 5+480.00
18650,"상세 ""A""","상세 ""B"""
11500,치 수 표
10850,치 수 표
10400,S
10300,a,b,c,d,e,f,R1,R2,R3,R2',R3',φ1,φ2,φ1',φ2'
10200,(%)
9850,NOTE
9750,+3,2.504,2.354,2.166,1.978,1.500,1.776,"6,982","3,712","4,454","3,910","4,699",59.2860°,67.9676°,52.1470°,60.6263°
9300,+2,2.504,2.354,2.166,1.978,1.524,1.759,"6,982","3,712","4,454","3,910","4,699",58.5355°,67.2997°,52.8374°,61.2268°
8800,0,2.504,2.354,2.166,1.978,1.570,1.722,"6,982","3,712","4,454","3,910","4,699",57.0441°,65.9756°,54.2241°,62.4352°
8300,-2,2.504,2.354,2.166,1.978,1.614,1.683,"6,982","3,712","4,454","3,910","4,699",55.5645°,64.6664°,55.6193°,63.6543°
7800,-3,2.504,2.354,2.166,1.978,1.635,1.662,"6,982","3,712","4,454","3,910","4,699",54.8288°,64.0169°,56.3204°,64.2681°
6700,S
6550,A,B,C,D,E,F,G,H,I,J,K,라이닝 두께
6450,(%)
6050,+3,6.615,1.735,1.451,1.608,1.293,0.000,-0.430,0.509,0.785,-0.638,-0.362,300
5550,+2,6.615,1.735,1.451,1.608,1.293,0.000,-0.430,0.555,0.739,-0.592,-0.408,300
5050,0,6.615,1.735,1.451,1.608,1.293,0.000,-0.430,0.647,0.647,-0.500,-0.500,300
4600,-2,6.615,1.735,1.451,1.608,1.293,0.000,-0.430,0.739,0.555,-0.408,-0.592,300
4100,-3,6.615,1.735,1.451,1.608,1.293,0.000,-0.430,0.785,0.509,-0.362,-0.638,300
1 27400 표준 단면도
2 24350 표준단면 적용구간
3 23550 대 산 1 , 2 터 널
4 23000 구 분 편 경 사 적 용 구 간 ( 개 착 구 간 제 외 )
5 22550 대산방향 -2%%% STA.3+070.00 ~ 3+555.00
6 22200 대산 1터널 -2%%% ~ +3%%% STA.3+070.00 ~ 3+145.00
7 22000 당진방향
8 21850 +3%%% STA.3+145.00 ~ 3+565.00
9 21500 +2%%% STA.4+740.85 ~ 5+075.00
10 21150 대산방향 +2%%% ~ -2%%% STA.5+075.00 ~ 5+135.00
11 20800 -2%%% STA.5+135.00 ~ 5+476.31
12 20600 대산 2터널
13 20450 -2%%% STA.4+745.00 ~ 5+105.00
14 20100 -2%%% ~ +2%%% STA.5+105.00 ~ 5+165.00
15 19900 당진방향
16 19750 +2%%% STA.5+165.00 ~ 5+480.00
17 18650 상세 "A" 상세 "B"
18 11500 치 수 표
19 10850 치 수 표
20 10400 S
21 10300 a b c d e f R1 R2 R3 R2' R3' φ1 φ2 φ1' φ2'
22 10200 (%)
23 9850 NOTE
24 9750 +3 2.504 2.354 2.166 1.978 1.500 1.776 6,982 3,712 4,454 3,910 4,699 59.2860° 67.9676° 52.1470° 60.6263°
25 9300 +2 2.504 2.354 2.166 1.978 1.524 1.759 6,982 3,712 4,454 3,910 4,699 58.5355° 67.2997° 52.8374° 61.2268°
26 8800 0 2.504 2.354 2.166 1.978 1.570 1.722 6,982 3,712 4,454 3,910 4,699 57.0441° 65.9756° 54.2241° 62.4352°
27 8300 -2 2.504 2.354 2.166 1.978 1.614 1.683 6,982 3,712 4,454 3,910 4,699 55.5645° 64.6664° 55.6193° 63.6543°
28 7800 -3 2.504 2.354 2.166 1.978 1.635 1.662 6,982 3,712 4,454 3,910 4,699 54.8288° 64.0169° 56.3204° 64.2681°
29 6700 S
30 6550 A B C D E F G H I J K 라이닝 두께
31 6450 (%)
32 6050 +3 6.615 1.735 1.451 1.608 1.293 0.000 -0.430 0.509 0.785 -0.638 -0.362 300
33 5550 +2 6.615 1.735 1.451 1.608 1.293 0.000 -0.430 0.555 0.739 -0.592 -0.408 300
34 5050 0 6.615 1.735 1.451 1.608 1.293 0.000 -0.430 0.647 0.647 -0.500 -0.500 300
35 4600 -2 6.615 1.735 1.451 1.608 1.293 0.000 -0.430 0.739 0.555 -0.408 -0.592 300
36 4100 -3 6.615 1.735 1.451 1.608 1.293 0.000 -0.430 0.785 0.509 -0.362 -0.638 300
@@ -0,0 +1,656 @@
{
"1|4745.0005": {
"LIN_arcs": {
"ref_to_model_max": 99.05839911556747,
"ref_to_model_mean": 12.265421914456372,
"model_to_ref_max": 2699.228422004956,
"model_to_ref_mean": 139.42217237180967,
"n_ref": 66562,
"n_model": 2539,
"clipped_below_model_extent": {
"L": 427,
"R": 438,
"yrange_L": [
15264,
15659
],
"yrange_R": [
15080,
15488
]
}
},
"LIN_footlines": {
"ref_to_model_max": 199.81630323321525,
"ref_to_model_mean": 31.495823939407906,
"model_to_ref_max": 8330.781218615124,
"model_to_ref_mean": 3658.6932294230833,
"n_ref": 2395,
"n_model": 2539
},
"LIN_perarc_ref2model_max_mm": {
"LIN_crown_outer": 21.5,
"LIN_crown_inner": 17.5,
"LIN_haunchL_outer": 492.3,
"LIN_haunchL_inner": 12.9,
"LIN_haunchR_outer": 461.1,
"LIN_haunchR_inner": 15.5
},
"EXC_arcs": {
"ref_to_model_max": 49.97333468210581,
"ref_to_model_mean": 13.22944506104266,
"model_to_ref_max": 139.10210757850402,
"model_to_ref_mean": 69.63490871239516,
"n_ref": 35303,
"n_model": 2271,
"clipped_below_model_extent": {
"L": 375,
"R": 566,
"yrange_L": [
15264,
15612
],
"yrange_R": [
15080,
15612
]
}
},
"EXC_crown_ref2model_max_mm": 21.8,
"PAV": {
"ref_to_model_max": 13.6729631110277,
"ref_to_model_mean": 10.97265592073033,
"model_to_ref_max": 9.766634709250443,
"model_to_ref_mean": 9.376172149516487,
"n_ref": 18962,
"n_model": 952
},
"BASE_drawn_span": {
"ref_to_model_max": 106.89691611711953,
"ref_to_model_mean": 46.68072152026932,
"model_to_ref_max": 270.29560834777436,
"model_to_ref_mean": 47.30323588291019,
"n_ref": 18402,
"n_model": 934
},
"BASE_model_extent_x": [
7715,
20584
],
"BASE_model_ymin_ymax": [
15067,
15760
],
"DRN": {
"ref_to_model_max": 14.457886611257248,
"ref_to_model_mean": 7.048062010678219,
"model_to_ref_max": 148.08739517488308,
"model_to_ref_mean": 9.751422269720287,
"n_ref": 5820,
"n_model": 373
},
"DRN_perref_ref2model_max_mm": {
"DRN_wallL1": 10.4,
"DRN_wallL2": 11.2,
"DRN_botL": 14.4,
"DRN_pipeL": 14.5,
"DRN_wallR1": 10.2,
"DRN_wallR2": 11.1,
"DRN_botR": 14.4,
"DRN_pipeR": 14.5
},
"DRN_model_xrange": [
9099,
19149
],
"DRN_model_yrange": [
14670,
15690
]
},
"1|5100.0": {
"LIN_arcs": {
"ref_to_model_max": 98.98374727460279,
"ref_to_model_mean": 12.286715807049747,
"model_to_ref_max": 2699.208225118645,
"model_to_ref_mean": 139.4446947488841,
"n_ref": 66562,
"n_model": 2539,
"clipped_below_model_extent": {
"L": 427,
"R": 438,
"yrange_L": [
15264,
15659
],
"yrange_R": [
15080,
15488
]
}
},
"LIN_footlines": {
"ref_to_model_max": 199.75630963156448,
"ref_to_model_mean": 31.508170955828962,
"model_to_ref_max": 8330.792297541451,
"model_to_ref_mean": 3658.67650320109,
"n_ref": 2395,
"n_model": 2539
},
"LIN_perarc_ref2model_max_mm": {
"LIN_crown_outer": 21.5,
"LIN_crown_inner": 17.4,
"LIN_haunchL_outer": 492.3,
"LIN_haunchL_inner": 12.9,
"LIN_haunchR_outer": 461.1,
"LIN_haunchR_inner": 15.5
},
"EXC_arcs": {
"ref_to_model_max": 49.90231927152876,
"ref_to_model_mean": 13.25015537105612,
"model_to_ref_max": 139.12313900485057,
"model_to_ref_mean": 69.6577719572834,
"n_ref": 35303,
"n_model": 2271,
"clipped_below_model_extent": {
"L": 375,
"R": 566,
"yrange_L": [
15264,
15612
],
"yrange_R": [
15080,
15612
]
}
},
"EXC_crown_ref2model_max_mm": 21.8,
"PAV": {
"ref_to_model_max": 14.418186911034798,
"ref_to_model_mean": 10.996432167000822,
"model_to_ref_max": 10.776825902966216,
"model_to_ref_mean": 9.399395181664575,
"n_ref": 18962,
"n_model": 952
},
"BASE_drawn_span": {
"ref_to_model_max": 106.95235216721005,
"ref_to_model_mean": 46.704840227446,
"model_to_ref_max": 270.3072089440827,
"model_to_ref_mean": 47.3306581098095,
"n_ref": 18402,
"n_model": 934
},
"BASE_model_extent_x": [
7715,
20583
],
"BASE_model_ymin_ymax": [
15067,
15759
],
"DRN": {
"ref_to_model_max": 14.490884181941974,
"ref_to_model_mean": 7.0658785237396495,
"model_to_ref_max": 148.0563566649249,
"model_to_ref_mean": 9.77795072083027,
"n_ref": 5820,
"n_model": 373
},
"DRN_perref_ref2model_max_mm": {
"DRN_wallL1": 10.4,
"DRN_wallL2": 11.3,
"DRN_botL": 14.4,
"DRN_pipeL": 14.5,
"DRN_wallR1": 10.2,
"DRN_wallR2": 11.2,
"DRN_botR": 14.4,
"DRN_pipeR": 14.4
},
"DRN_model_xrange": [
9099,
19149
],
"DRN_model_yrange": [
14670,
15690
]
},
"1|5469.9995": {
"LIN_arcs": {
"ref_to_model_max": 99.05839911694011,
"ref_to_model_mean": 12.265421913408609,
"model_to_ref_max": 2699.2284220029933,
"model_to_ref_mean": 139.55032916231897,
"n_ref": 66562,
"n_model": 2540,
"clipped_below_model_extent": {
"L": 427,
"R": 438,
"yrange_L": [
15264,
15659
],
"yrange_R": [
15080,
15488
]
}
},
"LIN_footlines": {
"ref_to_model_max": 199.8163032523252,
"ref_to_model_mean": 31.495823938300035,
"model_to_ref_max": 8330.781218614657,
"model_to_ref_mean": 3657.3796663221337,
"n_ref": 2395,
"n_model": 2540
},
"LIN_perarc_ref2model_max_mm": {
"LIN_crown_outer": 21.5,
"LIN_crown_inner": 17.5,
"LIN_haunchL_outer": 492.3,
"LIN_haunchL_inner": 12.9,
"LIN_haunchR_outer": 461.1,
"LIN_haunchR_inner": 15.5
},
"EXC_arcs": {
"ref_to_model_max": 49.973334683068344,
"ref_to_model_mean": 13.229445060405267,
"model_to_ref_max": 139.10210757277747,
"model_to_ref_mean": 69.63490871114456,
"n_ref": 35303,
"n_model": 2271,
"clipped_below_model_extent": {
"L": 375,
"R": 566,
"yrange_L": [
15264,
15612
],
"yrange_R": [
15080,
15612
]
}
},
"EXC_crown_ref2model_max_mm": 21.8,
"PAV": {
"ref_to_model_max": 193.59943538166334,
"ref_to_model_mean": 85.90038197968867,
"model_to_ref_max": 193.58867016575806,
"model_to_ref_mean": 85.49583497432751,
"n_ref": 18962,
"n_model": 952
},
"BASE_drawn_span": {
"ref_to_model_max": 194.0339970575608,
"ref_to_model_mean": 76.70070967162879,
"model_to_ref_max": 270.2956083490425,
"model_to_ref_mean": 71.6861767350549,
"n_ref": 18402,
"n_model": 925
},
"BASE_model_extent_x": [
7715,
20584
],
"BASE_model_ymin_ymax": [
15067,
15760
],
"DRN": {
"ref_to_model_max": 14.45788661735982,
"ref_to_model_mean": 7.048062004823568,
"model_to_ref_max": 148.08739519731785,
"model_to_ref_mean": 9.751422260123833,
"n_ref": 5820,
"n_model": 373
},
"DRN_perref_ref2model_max_mm": {
"DRN_wallL1": 10.4,
"DRN_wallL2": 11.2,
"DRN_botL": 14.4,
"DRN_pipeL": 14.5,
"DRN_wallR1": 10.2,
"DRN_wallR2": 11.1,
"DRN_botR": 14.4,
"DRN_pipeR": 14.5
},
"DRN_model_xrange": [
9099,
19149
],
"DRN_model_yrange": [
14670,
15690
]
},
"-1|4745.0005": {
"LIN_arcs": {
"ref_to_model_max": 165.93842403641,
"ref_to_model_mean": 19.342371436798466,
"model_to_ref_max": 2694.4903611935015,
"model_to_ref_mean": 148.5127579371637,
"n_ref": 66566,
"n_model": 2539,
"clipped_below_model_extent": {
"L": 245,
"R": 616,
"yrange_L": [
15264,
15488
],
"yrange_R": [
15080,
15658
]
}
},
"LIN_footlines": {
"ref_to_model_max": 278.1041972578194,
"ref_to_model_mean": 86.14276227224678,
"model_to_ref_max": 8325.718000790263,
"model_to_ref_mean": 3664.9850644439653,
"n_ref": 2395,
"n_model": 2539
},
"LIN_perarc_ref2model_max_mm": {
"LIN_crown_outer": 21.5,
"LIN_crown_inner": 17.5,
"LIN_haunchL_outer": 278.9,
"LIN_haunchL_inner": 79.7,
"LIN_haunchR_outer": 676.9,
"LIN_haunchR_inner": 165.9
},
"EXC_arcs": {
"ref_to_model_max": 110.0366585359107,
"ref_to_model_mean": 20.147468133684814,
"model_to_ref_max": 230.36912980232339,
"model_to_ref_mean": 77.01959791893609,
"n_ref": 35303,
"n_model": 2271,
"clipped_below_model_extent": {
"L": 375,
"R": 566,
"yrange_L": [
15264,
15612
],
"yrange_R": [
15080,
15612
]
}
},
"EXC_crown_ref2model_max_mm": 21.8,
"PAV": {
"ref_to_model_max": 193.58697559581455,
"ref_to_model_mean": 85.90449213019785,
"model_to_ref_max": 193.50070137422574,
"model_to_ref_mean": 85.49177355782385,
"n_ref": 18962,
"n_model": 952
},
"BASE_drawn_span": {
"ref_to_model_max": 186.2456896549011,
"ref_to_model_mean": 65.25754410234194,
"model_to_ref_max": 241.51176893122732,
"model_to_ref_mean": 79.35668872937082,
"n_ref": 18402,
"n_model": 934
},
"BASE_model_extent_x": [
7669,
20538
],
"BASE_model_ymin_ymax": [
15067,
15760
],
"DRN": {
"ref_to_model_max": 191.09289892056788,
"ref_to_model_mean": 43.78263107043471,
"model_to_ref_max": 230.1686793175862,
"model_to_ref_mean": 53.43022849360874,
"n_ref": 5820,
"n_model": 373
},
"DRN_perref_ref2model_max_mm": {
"DRN_wallL1": 185.1,
"DRN_wallL2": 191.1,
"DRN_botL": 142.3,
"DRN_pipeL": 149.8,
"DRN_wallR1": 174.4,
"DRN_wallR2": 174.4,
"DRN_botR": 174.7,
"DRN_pipeR": 124.6
},
"DRN_model_xrange": [
9103,
19153
],
"DRN_model_yrange": [
14670,
15690
]
},
"-1|5100.0": {
"LIN_arcs": {
"ref_to_model_max": 165.86208125776304,
"ref_to_model_mean": 19.353584271599697,
"model_to_ref_max": 2694.466152098678,
"model_to_ref_mean": 148.51959481437646,
"n_ref": 66566,
"n_model": 2539,
"clipped_below_model_extent": {
"L": 245,
"R": 616,
"yrange_L": [
15264,
15488
],
"yrange_R": [
15080,
15658
]
}
},
"LIN_footlines": {
"ref_to_model_max": 278.0451437760574,
"ref_to_model_mean": 86.11198124109171,
"model_to_ref_max": 8325.7291267321,
"model_to_ref_mean": 3664.9615785773476,
"n_ref": 2395,
"n_model": 2539
},
"LIN_perarc_ref2model_max_mm": {
"LIN_crown_outer": 21.5,
"LIN_crown_inner": 17.4,
"LIN_haunchL_outer": 278.9,
"LIN_haunchL_inner": 79.6,
"LIN_haunchR_outer": 676.9,
"LIN_haunchR_inner": 165.9
},
"EXC_arcs": {
"ref_to_model_max": 109.97882644641383,
"ref_to_model_mean": 20.162782046328307,
"model_to_ref_max": 230.32659770807805,
"model_to_ref_mean": 77.03356500761437,
"n_ref": 35303,
"n_model": 2271,
"clipped_below_model_extent": {
"L": 375,
"R": 566,
"yrange_L": [
15264,
15612
],
"yrange_R": [
15080,
15612
]
}
},
"EXC_crown_ref2model_max_mm": 21.8,
"PAV": {
"ref_to_model_max": 192.60836907668372,
"ref_to_model_mean": 85.5974876462991,
"model_to_ref_max": 192.52054570763823,
"model_to_ref_mean": 85.17944998313874,
"n_ref": 18962,
"n_model": 952
},
"BASE_drawn_span": {
"ref_to_model_max": 185.37579093848115,
"ref_to_model_mean": 65.10503094806941,
"model_to_ref_max": 241.49813213935548,
"model_to_ref_mean": 79.21916715369449,
"n_ref": 18402,
"n_model": 934
},
"BASE_model_extent_x": [
7669,
20538
],
"BASE_model_ymin_ymax": [
15067,
15759
],
"DRN": {
"ref_to_model_max": 191.07984589599806,
"ref_to_model_mean": 43.782144626512356,
"model_to_ref_max": 230.1425275457013,
"model_to_ref_mean": 53.43922451031213,
"n_ref": 5820,
"n_model": 373
},
"DRN_perref_ref2model_max_mm": {
"DRN_wallL1": 185.1,
"DRN_wallL2": 191.1,
"DRN_botL": 142.3,
"DRN_pipeL": 149.8,
"DRN_wallR1": 174.4,
"DRN_wallR2": 174.4,
"DRN_botR": 174.7,
"DRN_pipeR": 124.6
},
"DRN_model_xrange": [
9103,
19153
],
"DRN_model_yrange": [
14670,
15690
]
},
"-1|5469.9995": {
"LIN_arcs": {
"ref_to_model_max": 165.93842404388747,
"ref_to_model_mean": 19.342371439075382,
"model_to_ref_max": 2694.4903611915724,
"model_to_ref_mean": 148.67066912972504,
"n_ref": 66566,
"n_model": 2540,
"clipped_below_model_extent": {
"L": 245,
"R": 616,
"yrange_L": [
15264,
15488
],
"yrange_R": [
15080,
15658
]
}
},
"LIN_footlines": {
"ref_to_model_max": 278.10419727094626,
"ref_to_model_mean": 86.14276227398764,
"model_to_ref_max": 8325.718000790119,
"model_to_ref_mean": 3663.701800289308,
"n_ref": 2395,
"n_model": 2540
},
"LIN_perarc_ref2model_max_mm": {
"LIN_crown_outer": 21.5,
"LIN_crown_inner": 17.5,
"LIN_haunchL_outer": 278.9,
"LIN_haunchL_inner": 79.7,
"LIN_haunchR_outer": 676.9,
"LIN_haunchR_inner": 165.9
},
"EXC_arcs": {
"ref_to_model_max": 110.03665854392911,
"ref_to_model_mean": 20.147468133951584,
"model_to_ref_max": 230.36912981558459,
"model_to_ref_mean": 77.01959791929141,
"n_ref": 35303,
"n_model": 2271,
"clipped_below_model_extent": {
"L": 375,
"R": 566,
"yrange_L": [
15264,
15612
],
"yrange_R": [
15080,
15612
]
}
},
"EXC_crown_ref2model_max_mm": 21.8,
"PAV": {
"ref_to_model_max": 13.753593831242364,
"ref_to_model_mean": 10.921448830959804,
"model_to_ref_max": 9.778010359538655,
"model_to_ref_mean": 9.31009466082442,
"n_ref": 18962,
"n_model": 952
},
"BASE_drawn_span": {
"ref_to_model_max": 140.76940169714493,
"ref_to_model_mean": 48.22264276831631,
"model_to_ref_max": 241.5117689307402,
"model_to_ref_mean": 55.15185176197466,
"n_ref": 18402,
"n_model": 925
},
"BASE_model_extent_x": [
7669,
20538
],
"BASE_model_ymin_ymax": [
15067,
15760
],
"DRN": {
"ref_to_model_max": 191.09289891938243,
"ref_to_model_mean": 43.782631065576105,
"model_to_ref_max": 230.16867930371887,
"model_to_ref_mean": 53.430228486469154,
"n_ref": 5820,
"n_model": 373
},
"DRN_perref_ref2model_max_mm": {
"DRN_wallL1": 185.1,
"DRN_wallL2": 191.1,
"DRN_botL": 142.3,
"DRN_pipeL": 149.8,
"DRN_wallR1": 174.4,
"DRN_wallR2": 174.4,
"DRN_botR": 174.7,
"DRN_pipeR": 124.6
},
"DRN_model_xrange": [
9103,
19153
],
"DRN_model_yrange": [
14670,
15690
]
}
}
@@ -0,0 +1,49 @@
# Step 3 판정표 — 콘크리트 라이닝 판독 (Task 6, 대산2본선3D변환 v0.1.00)
⚠️ DEGRADED: 자기검증 (생성 세션과 판정 세션이 동일. 독립 검증 에이전트 미실행 — V14 FAIL(자기검증), V12 미실시)
| # | 검증 항목 | 필수 | 판정 | 방법·모집단 | 비고 |
|---|---|---|---|---|---|
| V1 | 업무 목표 충족 | ★ | PASS(범위 내) | 목표: 라이닝 도면을 읽어 측점 없이 반영 가능한 항목을 모델·뷰어에 연결. 6장 판독, 메타·뷰어 연결 확인 | 측점별 구간 반영은 도면에 측점 없음 → 미실시·한계로 이관 |
| V2 | 형태·포맷 일치 | ★ | PASS | DOC .md(변경 이력 표 포함), 메타 JSON, 뷰어 HTML 형식 확인 | — |
| V3 | 근거 누락 없음 | | PASS | 공통 사양은 6장 전수에서 확인 (C1) | 시트별 근거는 표에 도면 번호로 기재 |
| V4 | 수치·데이터 정합성 | ★ | PASS(전수) | verify_lining.py C1·C3·C4·C5: 6/6 시트 공통값, 패턴군 9 = 보고서 9행, R1 6.982 = 03-001 원본 텍스트, 두께 0.300 m | 최초 실행 3건 FAIL: 검증 스크립트 버그 2건 수정, SD400 누락 1건 보고서 수정 후 19/19 |
| V5 | 시각 자료 반영 완전성 | | N/A | 보고서에 도표 없음 | 뷰어 화면은 V11에서 확인 |
| V6 | 논리적 비약·모순 없음 | | PASS | 두께 = 외면 R − 내면 R: 6.982 → 0.300 m(일치), 6.892 → 0.390 m(불일치) | R1 충돌은 사용자 결정(6.982 유지)으로 해소, 결정 문구 기록 |
| V7 | 사용자 제약 준수 | | PASS | 800 m 범위, 골격 LOD, 패턴 placeholder 표기, 측점 미확정 공백 표시 | — |
| V8 | 저장 규칙 준수 | ★ | PASS | DOC/03_설계/ 하위, 파일명 `[유형]_[주제]_[YYYY-MM-DD]_v[버전].md` 형식 | — |
| V9 | OUTPUT 실재성 | ★ | PASS | ls 존재, 파일명 버전 v0.1.00 = 헤더 버전, 핵심 항목(fck, SD400, 입출구부, 결정 문구) 본문 존재 (C6·C6b) | 파일명만 확인하지 않고 내용 대조 |
| V10 | 수용 기준 충족 | ★ | PASS(사후 기준) | 기준: 6장 전수 판독, 공통 사양 6/6 일치, 패턴군 전수 추출, 충돌 기록 | 착수 시 기준 미기재 → 본 판에서 사후 설정 (R8 위반 한계로 기록) |
| V11 | 산출물 가동 가능성 | ★ | PASS | verify_viewer_headless.py: 헤드리스 Chromium, 페이지 오류 0, canvas 존재, 메타 표시, 라이닝 클릭 → 측점 5+202·패턴·라이닝 사양 표시 (25 클릭 중 15번째 적중) | favicon 404 1건은 무해 |
| V12 | 원본 무결성 (지문 불변) | | FAIL(미실시) | 지문 기준값을 착수 시 기록하지 않음. 대조 불가 | 재개: 다음 Task 전 RAG 원본 sha256 기록, 이후 대조. 쓰기 코드 없음은 C8로 확인 |
| V13 | 검증 스크립트 보관 | | PASS | verify_lining.py, verify_viewer_headless.py 모두 Code/3d-drawing/scripts 보관 | 커밋 2bdda98 |
| V14 | 검증 독립성 | ★ | FAIL(자기검증) | 생성과 판정 동일 세션, 독립 에이전트 미실행 | 상단 DEGRADED 배너 기재 |
| V15 | 자율성 등급 준수 | ★ | PASS | 대조표 아래 | — |
| V16 | 오케스트레이션 규약 | ★ | N/A | 위임 없음 (ORCH-TRIGGER 비해당) | — |
| V17 | CAG 정합 | ★ | N/A | 팩 미적재. RAG DXF는 도면이므로 직접 열기 허용 범위 | — |
**V15 HITL 고정 대조표**
| 항목 | 해당 여부 | 근거 |
|---|---|---|
| (1) R5 업무 규정·도메인 규칙 해석 | 해당 | 패턴군 대응 해석, R1 채택 → 사용자 결정 확보 |
| (2) F13 원본 데이터 구조 변경 | 아니오 | RAG 원본은 읽기만 함 (C8) |
| (3) R7 재작업 상한 도달 | 아니오 | DOC 재생성 2회(결정 반영, SD400 보정). 상한(하루 2회) 이내 |
| (4) v1.0.00 이상 대외 발행 최종 확정 | 아니오 | v0.1.00 |
| (5) F5 MINOR·MAJOR 승급 | 아니오 | PATCH 단위 |
게이트 기록: 판정표 위치 = 본 파일. 검증 출력 = data/verify_lining_output.txt.
## 재검증 (수정 후, 2026-10-21)
사용자 "수정 요청"에 따라 2 m placeholder를 엑셀 `참고_지보패턴현황`의 당진방향 측점표(4+740~5+500, 42행)로 교체했다. 적용 결과 판정:
| # | 검증 항목 | 판정 | 방법·모집단 | 비고 |
|---|---|---|---|---|
| V4 | 측점표 정합 | PASS(독립, 전수) | 독립 에이전트가 원본 xlsx에서 재추출: 42행 연속, 길이 42/42, 모델 pattern 42/42 일치, 735/735 측점 커버 | C5 literal FAIL: 개착부는 모델 범위(4+745~5+480) 밖, 범위 내 12종 전부 일치 |
| V4b | 기하 색상 디코딩 | PASS(독립, 전수 735 밴드·736 링) | GLB COLOR_0 디코딩 후 측점별 패턴 대조 | 패턴 경계 39곳은 두 색의 중간색(보간)으로 정확 색 매칭 불가. 한계로 기록 |
| V14 | 검증 독립성 | PARTIAL | 측점표·색상 대조는 독립 에이전트 수행. 라이닝 도면 판독 DOC는 자기검증 | 라이닝 DOC 부분은 여전히 FAIL(자기검증) |
| V1 | 업무 목표 | PASS(범위 내) | 측점 매핑 적용 완료. 도면 E-1, E-2, RP-2, WL-3은 두 표에 없어 [미확정] | 사용자 결정 2026-10-21 |
| V7 | 사용자 제약 | PASS | 대산 WL-4는 참고만 (사용자 결정) | — |
재검증 자동 검사: verify_lining.py 결과는 data/verify_lining_output.txt 참조. V12(원본 지문)는 여전히 미실시.
+20
View File
@@ -0,0 +1,20 @@
[PASS] C1 fck 27 in all sheets: 6/6 sheets (표본 아님, 전수)
[PASS] C1 SD400 in all sheets: 6/6 sheets (표본 아님, 전수)
[PASS] C1 Ø25 in all sheets: 6/6 sheets (표본 아님, 전수)
[PASS] C1 R1 6.892 in all sheets: 6/6 sheets (표본 아님, 전수)
[PASS] C1 R2 3,712 in all sheets: 6/6 sheets (표본 아님, 전수)
[PASS] C1 R2' 3,910 in all sheets: 6/6 sheets (표본 아님, 전수)
[PASS] C1 EL -0.367 in all sheets: 6/6 sheets (표본 아님, 전수)
[PASS] C1 EL 0.000 in all sheets: 6/6 sheets (표본 아님, 전수)
[PASS] C2 온도미고려 detail sheets: 3 sheets: ['005', '006', '007'] (전수)
[PASS] C3 pattern-detail count = report rows: raw 9 vs report 9 (전수)
[PASS] C4 R1 6.982 in standard section (03-001 raw): 03-001 raw texts scanned: 322
[PASS] C4b standard table DOC states R1 6,982: 설계_표준단면치수표 본문 대조
[PASS] C4c report records decision: R1 결정 문구 존재
[PASS] C5 thickness arithmetic: 7.282-6.982=0.3 m (matches 300 mm), 7.282-6.892=0.39 m (does not match)
[PASS] C6 file name pattern and version: name v0.1.00 vs header v0.1.00
[PASS] C6b OUTPUT key items present: fck, SD400, 입출구부, 결정 문구
[PASS] C7 meta lining_reference 9 groups: 9 groups, status=C0060304-004~009 | fck 27 MPa | SD400 | ...
[PASS] C7b meta open records decision: open[] 결정 문구
[PASS] C8 no script writes into RAG: scanned 4 scripts
SUMMARY: 19 PASS / 0 FAIL of 19
+28
View File
@@ -0,0 +1,28 @@
{
"inner_radius_max_dev_m": 8.881784197001252e-16,
"lining_thickness_crown_m": 0.3,
"crown_above_road_m (expect 6.615 at S=0 station 5000)": 6.615,
"road_z_4745_m": 50.62,
"road_z_5480_m": 67.311,
"plan_chord_length_total_m (4+700..5+500)": 799.905,
"station_range_modelled": "4745 .. 5480 (ring step 1.0 m, 736 rings)",
"pattern_zones (지보패턴표 당진방향)": 42,
"pattern_coverage_all_stations": true,
"pattern_counts": {
"A": 5,
"B-1": 8,
"B-2": 7,
"C-1": 4,
"C-2": 2,
"D-1": 3,
"D-2": 2,
"P-2": 1,
"P-3": 2,
"RP-1": 3,
"WL-1": 1,
"WL-2": 2,
"개착부": 2
},
"glb_bytes": 2201764,
"glb_reload_geometries": 5
}
+45
View File
@@ -0,0 +1,45 @@
{
"solid_count": 320,
"runs": 40,
"pattern_coverage": "4745..5480 contiguous",
"lining_runs": 40,
"excavation_runs": 40,
"volume_sum_m3": 12406.138,
"step_bytes": 91713125,
"glb_bytes": 1566612,
"glb_geometries": 326,
"section_overlap_m2_by_station": {
"4745": {
"lining_vs_excavation": 0.01732111,
"base_vs_pavement": 0.0,
"lining_vs_base": 0.0,
"pavement_vs_lining": 0.0,
"base_vs_drainL": 0.0,
"base_vs_drainR": 0.0
},
"5105": {
"lining_vs_excavation": 0.01732111,
"base_vs_pavement": 0.0,
"lining_vs_base": 0.0,
"pavement_vs_lining": 0.0,
"base_vs_drainL": 0.0,
"base_vs_drainR": 0.0
},
"5165": {
"lining_vs_excavation": 0.01732111,
"base_vs_pavement": 0.0,
"lining_vs_base": 0.0,
"pavement_vs_lining": 0.0,
"base_vs_drainL": 0.0,
"base_vs_drainR": 0.0
},
"5480": {
"lining_vs_excavation": 0.01732111,
"base_vs_pavement": 0.0,
"lining_vs_base": 0.0,
"pavement_vs_lining": 0.0,
"base_vs_drainL": 0.0,
"base_vs_drainR": 0.0
}
}
}
Binary file not shown.
+326
View File
@@ -0,0 +1,326 @@
{
"model": "tunnel_skeleton.glb",
"scope": "4745~5480 (4+745~5+480)",
"units": "m",
"origin_plan_xy": [
151090.911,
483688.46
],
"sources": {
"centreline": "C0060302-001 CA-ALGN-STSM (side-tick midpoint)",
"road_grade_B": "C0060302-002 CR-TABL-TEX2 (계획고=노면)",
"GL_A": "C0060302-002 CR-TABL-TEX2 (지반고)",
"section": "C0060303-001 CS-TABL-TEX1/2, CS-CONC-MAJR, CS-EXCV"
},
"approximations": [
"walls vertical down to road; invert not modelled",
"lining band thickness at walls is radial-consistent but not as-built",
"centreline and grade linearly interpolated between nodes",
"superelevation sign convention assumed (z += S/100 * x, x = left of tangent)"
],
"layers": [
"lining_outer_R7282",
"lining_inner_R6982",
"excavation_R7402",
"road_surface_계획고",
"ground_GL_지반고"
],
"ring_step_m": 1.0,
"ring_points": 43,
"first_station": 4745,
"pattern_status": "지보패턴: 엑셀 참고_지보패턴현황 당진방향 표(4+740~5+500, 42행, 빈틈 없음) 적용. 출처 data/pattern_stations_dangjin.csv (사용자 결정 2026-10-21)",
"patterns": [
{
"from": 4740,
"to": 4745,
"pattern": "개착부"
},
{
"from": 4745,
"to": 4763,
"pattern": "P-3"
},
{
"from": 4763,
"to": 4785,
"pattern": "WL-2"
},
{
"from": 4785,
"to": 4796,
"pattern": "WL-1"
},
{
"from": 4796,
"to": 4839,
"pattern": "D-2"
},
{
"from": 4839,
"to": 4848,
"pattern": "D-1"
},
{
"from": 4848,
"to": 4857,
"pattern": "C-2"
},
{
"from": 4857,
"to": 4862,
"pattern": "C-1"
},
{
"from": 4862,
"to": 4867,
"pattern": "B-2"
},
{
"from": 4867,
"to": 4873,
"pattern": "B-1"
},
{
"from": 4873,
"to": 4907,
"pattern": "A"
},
{
"from": 4907,
"to": 4915,
"pattern": "B-1"
},
{
"from": 4915,
"to": 4920,
"pattern": "B-2"
},
{
"from": 4920,
"to": 4940,
"pattern": "RP-1"
},
{
"from": 4940,
"to": 4988,
"pattern": "C-1"
},
{
"from": 4988,
"to": 4998,
"pattern": "B-2"
},
{
"from": 4998,
"to": 5006,
"pattern": "B-1"
},
{
"from": 5006,
"to": 5019,
"pattern": "B-2"
},
{
"from": 5019,
"to": 5025,
"pattern": "B-1"
},
{
"from": 5025,
"to": 5110,
"pattern": "A"
},
{
"from": 5110,
"to": 5130,
"pattern": "RP-1"
},
{
"from": 5130,
"to": 5140,
"pattern": "B-1"
},
{
"from": 5140,
"to": 5157,
"pattern": "B-2"
},
{
"from": 5157,
"to": 5204,
"pattern": "C-1"
},
{
"from": 5204,
"to": 5211,
"pattern": "B-2"
},
{
"from": 5211,
"to": 5221,
"pattern": "B-1"
},
{
"from": 5221,
"to": 5259,
"pattern": "A"
},
{
"from": 5259,
"to": 5279,
"pattern": "B-1"
},
{
"from": 5279,
"to": 5300,
"pattern": "A"
},
{
"from": 5300,
"to": 5320,
"pattern": "RP-1"
},
{
"from": 5320,
"to": 5357,
"pattern": "A"
},
{
"from": 5357,
"to": 5363,
"pattern": "B-1"
},
{
"from": 5363,
"to": 5377,
"pattern": "B-2"
},
{
"from": 5377,
"to": 5390,
"pattern": "C-1"
},
{
"from": 5390,
"to": 5399,
"pattern": "C-2"
},
{
"from": 5399,
"to": 5406,
"pattern": "D-1"
},
{
"from": 5406,
"to": 5427,
"pattern": "D-2"
},
{
"from": 5427,
"to": 5446,
"pattern": "D-1"
},
{
"from": 5446,
"to": 5462,
"pattern": "WL-2"
},
{
"from": 5462,
"to": 5468,
"pattern": "P-2"
},
{
"from": 5468,
"to": 5495,
"pattern": "P-3"
},
{
"from": 5495,
"to": 5500,
"pattern": "개착부"
}
],
"open": [
"4+700~4+745 and 5+480~5+500 excluded ([미확정])",
"지보패턴 측점표 적용(당진). 도면 E-1~2, RP-2, WL-3~4 는 당진 표에 없어 [미확정]. 대산 표 WL-4(4+758.85~4+768.85)는 참고만 [확인 필요]",
"lining reference recorded (meta.lining_reference, 04-004~009); sheet-to-station mapping [미확정]",
"[확정 2026-10-21] R1 6,982 mm 유지 (사용자 결정). 라이닝 도면 6장의 R1 6.892 m 표기는 확인 대상 오기로 기록",
"pattern-group to excavation-pattern mapping: user explanation conflicts with filename (WL-4) - re-confirm [확인 필요]"
],
"lining_reference": {
"source": "C0060304-004..009 (콘크리트라이닝구조도)",
"concrete": "fck 27 MPa",
"rebar": "SD400 (fy 400 MPa)",
"aggregate_mm": 25,
"summary": "C0060304-004~009 | fck 27 MPa | SD400 | 입출구부 50 m 취약구간 철근 상세 (측점 대응 미확정)",
"sheets": [
{
"sheet": "C0060304-004-콘",
"groups": [
[
"A~C-2",
"입출구부 50m"
]
]
},
{
"sheet": "C0060304-005-콘",
"groups": [
[
"E-1~2",
"온도미고려"
],
[
"D-1~2",
"입출구부 50m"
]
]
},
{
"sheet": "C0060304-006-콘",
"groups": [
[
"WL-1~2,RP-1~2",
"온도미고려"
],
[
"E-1~2",
"입출구부 50m"
]
]
},
{
"sheet": "C0060304-007-콘",
"groups": [
[
"WL-1~2",
"입출구부 50m"
],
[
"WL-3~4",
"온도미고려"
]
]
},
{
"sheet": "C0060304-008-콘",
"groups": [
[
"WL-3~4",
"입출구부 50m"
]
]
},
{
"sheet": "C0060304-009-콘",
"groups": [
[
"P-2~3",
"입출구부 50m"
]
]
}
]
}
}
Binary file not shown.
File diff suppressed because it is too large. Load diff
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Task 5 (user decision 2026-10-21): no pattern-station data exists. Create a
PLACEHOLDER pattern table at 2 m segments over the modelled range 4+745..5+480,
cycling the six pattern names. Every row is marked '임의 배정 (placeholder)'.
The table is an editable CSV: replace the 'pattern' column later and rebuild.
Output: data/pattern_zones_placeholder.csv (created only if absent; never overwritten)."""
import csv
import os
import sys
ROOT = sys.argv[1]
OUT = f"{ROOT}/data/pattern_zones_placeholder.csv"
PATTERNS = ["NP-4AD-1-D-2", "NP-4BD-1-D-2", "NP-5AE-1-WL-4", "NP-5BE-1-WL-4", "NP-6A", "NP-6B"]
S0, S1, SEG = 4745, 5480, 2
if not os.path.exists(OUT):
with open(OUT, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["seg_from", "seg_to", "pattern", "status"])
for k, s in enumerate(range(S0, S1, SEG)):
w.writerow([s, min(s + SEG, S1), PATTERNS[k % len(PATTERNS)], "임의 배정 (placeholder, 실제 구간 아님)"])
print("pattern table:", OUT)
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""Task 7: axis + standard-section sweep skeleton of the main line (본선), 4+745..5+480.
Inputs (all confirmed by the user, 2026-10-21):
- centreline nodes data/alignment_plan_001_centre.csv (midpoint of side ticks)
- profile data/profile_gl_002_draft.csv (A = 지반고 GL, B = 계획고 = 노면)
- section table DOC/03_설계/설계_표준단면치수표_2026-10-21_v0.1.00.md
R1 6,982 mm (lining inner), lining 300 mm -> outer R 7,282, excavation R 7,402
crown A = 6.615 m above road (F = 0.000 = road reference)
- cross slope: 4+745~5+105 -2 %, 5+105~5+165 linear -2 -> +2 %, 5+165~5+480 +2 %
Model (skeleton, not as-built):
- Section = arch (concentric centre c = 6.615 - 6.982) + vertical walls down to the road.
Walls and invert are APPROXIMATIONS and are flagged in the metadata. No invert.
- Superelevation: z += (S/100) * x across the section (sign convention ASSUMED, flagged).
- Coordinates: plan XY in m (station spacing 100 m matches plan units), origin at 4+745.
Outputs: model/tunnel_skeleton.glb, model/tunnel_skeleton_meta.json, data/verify_skeleton.json
"""
import csv
import json
import math
import sys
import numpy as np
import trimesh
ROOT = sys.argv[1] # Code/3d-drawing
C_CSV = f"{ROOT}/data/alignment_plan_001_centre.csv"
P_CSV = f"{ROOT}/data/profile_gl_002_draft.csv"
R1, R_OUT, R_EX = 6.982, 7.282, 7.402 # m (mm / 1000)
CROWN_A = 6.615 # m above road
CENTRE_C = CROWN_A - R1 # arch centre height above road (m)
S0, S1 = 4745, 5480 # modelled range (fully determined)
DS = 1.0 # station step for rings (m); pattern segments are 2 m
ANG = np.radians(np.linspace(40.0, 140.0, 41)) # arch arc, right springline -> left
def read_centre():
rows = [r for r in csv.DictReader(open(C_CSV, encoding="utf-8"))]
return np.array([[int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"])] for r in rows])
def read_profile():
gl, dg = {}, {}
for r in csv.DictReader(open(P_CSV, encoding="utf-8")):
sta = int(r["station_anchor"])
(gl if float(r["across_m"]) < -25 else dg)[sta] = float(r["value"])
return gl, dg
def slope_pct(s):
if s < 5105: return -2.0
if s < 5165: return -2.0 + 4.0 * (s - 5105) / 60.0
return 2.0
cn = read_centre()
gl_d, dg_d = read_profile()
gl_s = np.array(sorted(gl_d)); gl_v = np.array([gl_d[k] for k in gl_s])
dg_s = np.array(sorted(dg_d)); dg_v = np.array([dg_d[k] for k in dg_s])
cn_s = cn[:, 0]
def axis_xy(s):
x = np.interp(s, cn_s, cn[:, 1]); y = np.interp(s, cn_s, cn[:, 2])
i = min(max(np.searchsorted(cn_s, s) - 1, 0), len(cn_s) - 2)
dx, dy = cn[i + 1, 1] - cn[i, 1], cn[i + 1, 2] - cn[i, 2]
L = math.hypot(dx, dy)
return x, y, dx / L, dy / L
ORIGIN = np.array(axis_xy(S0)[:2])
stations = np.arange(S0, S1 + 0.001, DS)
def section(R, s, slope):
"""ring (x lateral m, z above road m) for radius R, including superelevation."""
xs = np.concatenate([[R * math.cos(ANG[0])], R * np.cos(ANG), [R * math.cos(ANG[-1])]])
zs = np.concatenate([[0.0], CENTRE_C + R * np.sin(ANG), [0.0]])
zs = zs + slope * xs
return xs, zs
def ring_world(R, s):
x, y, tx, ty = axis_xy(s)
nx, ny = -ty, tx # left normal (horizontal)
zr = np.interp(s, dg_s, dg_v) # road (계획고 = 노면)
xs, zs = section(R, s, slope_pct(s) / 100.0)
P = np.column_stack([x - ORIGIN[0] + xs * nx, y - ORIGIN[1] + xs * ny, zr + zs])
return P
PAL = {"A": (42, 157, 143, 255), "B-1": (231, 111, 81, 255), "B-2": (244, 162, 97, 255),
"C-1": (233, 196, 106, 255), "C-2": (138, 177, 125, 255), "D-1": (38, 70, 83, 255),
"D-2": (100, 130, 160, 255), "P-2": (190, 110, 140, 255), "P-3": (150, 90, 60, 255),
"RP-1": (80, 170, 200, 255), "WL-1": (200, 200, 120, 255), "WL-2": (120, 90, 170, 255),
"개착부": (170, 170, 170, 255)}
ZONES = [(int(float(r["sta_from"])), int(float(r["sta_to"])), r["lining_pattern"], "엑셀 지보패턴표(당진방향)")
for r in csv.DictReader(open(f"{ROOT}/data/pattern_stations_dangjin.csv", encoding="utf-8"))]
def pattern_of(sta):
for a, b, pat, _ in ZONES:
if a <= sta < b:
return pat
return ZONES[-1][2]
def make_band_pattern(R, name):
rings = [ring_world(R, s) for s in stations]
n = len(rings[0]); m = len(rings)
V = np.vstack(rings)
F, C = [], []
for i in range(m - 1):
col = PAL[pattern_of(stations[i])]
for j in range(n - 1):
a = i * n + j; b = a + 1; c = a + n; d = c + 1
F += [[a, b, c], [b, d, c]]
C += [col, col]
mesh = trimesh.Trimesh(vertices=V, faces=np.array(F), process=False)
mesh.visual.face_colors = np.array(C, dtype=np.uint8)
mesh.metadata["name"] = name
mesh.metadata["ring_points"] = n
return mesh
def make_ribbon(z_fn, half_w, color, name):
V, F = [], []
for k, s in enumerate(stations):
x, y, tx, ty = axis_xy(s)
nx, ny = -ty, tx
zr = z_fn(s)
V.append([x - ORIGIN[0] - half_w * nx, y - ORIGIN[1] - half_w * ny, zr])
V.append([x - ORIGIN[0] + half_w * nx, y - ORIGIN[1] + half_w * ny, zr])
V = np.array(V)
for k in range(len(stations) - 1):
a = 2 * k; b = a + 1; c = a + 2; d = a + 3
F += [[a, c, b], [b, c, d]]
mesh = trimesh.Trimesh(vertices=V, faces=np.array(F), process=False)
mesh.visual.face_colors = np.tile(np.array(color, dtype=np.uint8), (len(F), 1))
mesh.metadata["name"] = name
return mesh
def make_band(R, color, name):
"""uniform-colour band (excavation outline)."""
rings = [ring_world(R, s) for s in stations]
n = len(rings[0]); m = len(rings)
V = np.vstack(rings)
F = []
for i in range(m - 1):
for j in range(n - 1):
a = i * n + j; b = a + 1; c = a + n; d = c + 1
F += [[a, b, c], [b, d, c]]
mesh = trimesh.Trimesh(vertices=V, faces=np.array(F), process=False)
mesh.visual.face_colors = np.tile(np.array(color, dtype=np.uint8), (len(F), 1))
mesh.metadata["name"] = name
return mesh
parts = [
make_band_pattern(R_OUT, "lining_outer_R7282"),
make_band_pattern(R1, "lining_inner_R6982"),
make_band(R_EX, (150, 120, 90, 110), "excavation_R7402"),
make_ribbon(lambda s: float(np.interp(s, dg_s, dg_v)), 5.0, (70, 70, 75, 255), "road_surface_계획고"),
make_ribbon(lambda s: float(np.interp(s, gl_s, gl_v)), 20.0, (110, 150, 90, 200), "ground_GL_지반고"),
]
scene = trimesh.Scene()
for p in parts:
scene.add_geometry(p, geom_name=p.metadata["name"])
glb = f"{ROOT}/model/tunnel_skeleton.glb"
scene.export(glb)
# ---- verification (assertions recorded, not forced) ----
checks = {}
# radius check in section plane: distance from arch centre (lateral=0, z=centre_c)
xs, zs = section(R1, S0, 0.0)
rad = np.hypot(xs[1:-1], zs[1:-1] - CENTRE_C)
checks["inner_radius_max_dev_m"] = float(np.max(np.abs(rad - R1)))
checks["lining_thickness_crown_m"] = round(R_OUT - R1, 3)
crown_z = ring_world(R1, 5000)[:, 2].max() - np.interp(5000, dg_s, dg_v)
checks["crown_above_road_m (expect 6.615 at S=0 station 5000)"] = round(float(crown_z), 4)
checks["road_z_4745_m"] = round(float(np.interp(S0, dg_s, dg_v)), 3)
checks["road_z_5480_m"] = round(float(np.interp(S1, dg_s, dg_v)), 3)
L_chord = float(np.sum([math.dist(cn[i, 1:], cn[i + 1, 1:]) for i in range(len(cn) - 1)]))
checks["plan_chord_length_total_m (4+700..5+500)"] = round(L_chord, 3)
checks["station_range_modelled"] = f"{S0} .. {S1} (ring step {DS} m, {len(stations)} rings)"
checks["pattern_zones (지보패턴표 당진방향)"] = len(ZONES)
checks["pattern_coverage_all_stations"] = bool(all(pattern_of(x) in PAL for x in stations))
checks["pattern_counts"] = {k: sum(1 for z in ZONES if z[2] == k) for k in PAL}
checks["glb_bytes"] = int(__import__("os").path.getsize(glb))
reload = trimesh.load(glb)
checks["glb_reload_geometries"] = len(reload.geometry)
json.dump(checks, open(f"{ROOT}/data/verify_skeleton.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
meta = {
"model": "tunnel_skeleton.glb", "scope": f"{S0}~{S1} (4+745~5+480)",
"units": "m", "origin_plan_xy": [round(ORIGIN[0], 3), round(ORIGIN[1], 3)],
"sources": {"centreline": "C0060302-001 CA-ALGN-STSM (side-tick midpoint)",
"road_grade_B": "C0060302-002 CR-TABL-TEX2 (계획고=노면)",
"GL_A": "C0060302-002 CR-TABL-TEX2 (지반고)",
"section": "C0060303-001 CS-TABL-TEX1/2, CS-CONC-MAJR, CS-EXCV"},
"approximations": ["walls vertical down to road; invert not modelled",
"lining band thickness at walls is radial-consistent but not as-built",
"centreline and grade linearly interpolated between nodes",
"superelevation sign convention assumed (z += S/100 * x, x = left of tangent)"],
"layers": [p.metadata["name"] for p in parts],
"ring_step_m": DS, "ring_points": int(parts[0].metadata["ring_points"]), "first_station": S0,
"pattern_status": "지보패턴: 엑셀 참고_지보패턴현황 당진방향 표(4+740~5+500, 42행, 빈틈 없음) 적용. 출처 data/pattern_stations_dangjin.csv (사용자 결정 2026-10-21)",
"patterns": [{"from": a, "to": b, "pattern": pat} for a, b, pat, _ in ZONES],
"open": ["4+700~4+745 and 5+480~5+500 excluded ([미확정])", "지보패턴 측점표 적용(당진). 도면 E-1~2, RP-2, WL-3~4 는 당진 표에 없어 [미확정]. 대산 표 WL-4(4+758.85~4+768.85)는 참고만 [확인 필요]",
"lining reference recorded (meta.lining_reference, 04-004~009); sheet-to-station mapping [미확정]",
"[확정 2026-10-21] R1 6,982 mm 유지 (사용자 결정). 라이닝 도면 6장의 R1 6.892 m 표기는 확인 대상 오기로 기록", "pattern-group to excavation-pattern mapping: user explanation conflicts with filename (WL-4) - re-confirm [확인 필요]"]
}
_ls = json.load(open(f"{ROOT}/data/lining_spec.json", encoding="utf-8"))
meta["lining_reference"] = {
"source": _ls["source"], "concrete": "fck 27 MPa", "rebar": "SD400 (fy 400 MPa)", "aggregate_mm": 25,
"summary": "C0060304-004~009 | fck 27 MPa | SD400 | 입출구부 50 m 취약구간 철근 상세 (측점 대응 미확정)",
"sheets": [{"sheet": r["file"][:14], "groups": [[g["group"], g["kind"]] for g in r["pattern_details"]]} for r in _ls["sheets"]]
}
json.dump(meta, open(f"{ROOT}/model/tunnel_skeleton_meta.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(json.dumps(checks, ensure_ascii=False, indent=1))
+329
View File
@@ -0,0 +1,329 @@
#!/usr/bin/env python3
"""Task 8: B-rep solids of the main line (본선) 4+745..5+480 -> STEP (Rhino/Autodesk) + GLB (web viewer).
Section geometry: standard section C0060303-001 (drawing 03-001), revision rev2 approved 2026-10-21.
- lining (라이닝): arch R6982 (inner) / R7282 (outer) about centre C; haunch arcs R3712/R3910 (inner),
R4454/R4699 (outer) run below road level as drawn; inner walls and inner chord (road level) as drawn.
Lining is symmetric (no superelevation). H/I boxes removed from lining (2D).
- excavation band (굴착 여유대): R7402 / R4574 / R4819 minus lining outer, above road level. Symmetric.
- road surface / 계획고 (노면, 포장 280 mm): cross slope S per station (superelevation). Only this part tilts.
- base / invert (기층·인버트, ASSUMED: not in drawing): region between lining outer and inner chord minus
pavement and side drains. Its top follows the tilted pavement bottom.
- side drains (측구): box 300 x 836 mm minus pipe r150 (drawing circles).
- H/I boxes (공동구, ASSUMED by user decision 2026-10-21).
- 시설한계 50 mm: REFERENCE polylines only (not solids), as decided by the user.
Coordinates: local section x = lateral along the LEFT normal of the alignment (sign convention flagged),
z = height above road centre. Superelevation: z += (S/100)*x (sign flagged).
Outputs: model/tunnel_solids.step, model/tunnel_solids.glb, model/tunnel_solids_meta.json, data/verify_solids.json
Usage: python3 scripts/build_solids.py "$PWD"
"""
import csv, json, math, os, sys, tempfile
import numpy as np
from shapely.geometry import Polygon, box
from build123d import Vector, Wire, Solid, Compound, Color, export_step, export_stl, import_step
import trimesh
ROOT = sys.argv[1]
MODE = sys.argv[2] if len(sys.argv) > 2 else 'all' # parts_a | parts_b | assemble (4 GB RAM: run in stages)
PARTS = f"{ROOT}/model/parts"
RANGE = (int(sys.argv[3]), int(sys.argv[4])) if len(sys.argv) > 4 else (0, 10**6) # run index range for parts_a
os.makedirs(PARTS, exist_ok=True)
C_CSV = f"{ROOT}/data/alignment_plan_001_centre.csv"
P_CSV = f"{ROOT}/data/profile_gl_002_draft.csv"
PAT_CSV = f"{ROOT}/data/pattern_stations_dangjin.csv"
S0, S1 = 4745, 5480
STEP_M = 10.0 # section spacing (m): ruled loft between sections (STEP size vs curvature)
CHUNK_M = 100
PIPE_SEG = 24
CLEAR_OFFSET = 0.05
CLEAR_LINES = 6
CLEAR_RIB_W = 0.02
# ---------- drawing geometry (mm) -> local m ----------
CX, CY, Y0 = 14126.29, 15305.02, 15672.0
def P(x, y): return ((x - CX) / 1000.0, (y - Y0) / 1000.0)
def arc(cx, cy, r, a0, a1, n=120):
return [(cx + r * math.cos(math.radians(a0 + (a1 - a0) * i / n)),
cy + r * math.sin(math.radians(a0 + (a1 - a0) * i / n))) for i in range(n + 1)]
def loc(pts): return [P(x, y) for x, y in pts]
WALL_L_INNER = [(8771, 15751), (8771, 15826), (8651, 16001), (8621, 16411), (8471, 16411),
(8171, 16411), (8045, 16411), (8034, 16446)]
WALL_R_INNER = [(19481, 15567), (19481, 15642), (19601, 15817), (19631, 16227), (19781, 16227),
(20181, 16227), (20251, 16239)]
INNER_MM = (WALL_L_INNER + arc(11621.3, 17406.9, 3712, 195.6, 140) + arc(CX, CY, 6982, 140, 40)
+ arc(16479.6, 17279.7, 3910, 40, -15.6) + WALL_R_INNER[::-1])
OUTER_MM = (arc(11959.9, 17122.8, 4454, 204.7, 140) + arc(CX, CY, 7282, 140, 40)
+ arc(16105.0, 16965.3, 4699, 40, -23.7))
EX_MM = (arc(11959.9, 17122.8, 4574, 204.0, 140) + arc(CX, CY, 7402, 140, 40)
+ arc(16105.0, 16965.3, 4819, 40, -23.0))
inner_poly = Polygon(loc(INNER_MM)).buffer(0)
outer_poly = Polygon(loc(OUTER_MM)).buffer(0)
ex_poly = Polygon(loc(EX_MM)).buffer(0)
def _fill_slivers(g, tol=1e-3):
if g.geom_type != 'Polygon': return g
return Polygon(g.exterior, [h for h in g.interiors if Polygon(h).area > tol])
ring = _fill_slivers(outer_poly.difference(inner_poly)) # 37 mm wall/haunch join leaves 1e-5 m2 slivers
CHORD_A, CHORD_B = P(8771, 15751), P(19481, 15567)
_k = (CHORD_B[1] - CHORD_A[1]) / (CHORD_B[0] - CHORD_A[0])
def chord_z(x): return CHORD_A[1] + _k * (x - CHORD_A[0])
below_chord = Polygon([(-9, chord_z(-9)), (9, chord_z(9)), (9, -5), (-9, -5)])
above_chord = Polygon([(-9, chord_z(-9)), (9, chord_z(9)), (9, 9), (-9, 9)])
XL = P(9526, 15764)[0]; XR = P(18726, 15580)[0]
T_FIX = -0.02
PAV_T = 0.28
def pav_corners(t):
return [(XL, t * XL), (XR, t * XR), (XR, t * XR - PAV_T), (XL, t * XL - PAV_T)]
pav_fix = Polygon(pav_corners(T_FIX))
def rect_local(x0_mm, x1_mm, y0_mm, y1_mm):
(a, b), (c, d) = P(x0_mm, y0_mm), P(x1_mm, y1_mm)
return box(min(a, c), min(b, d), max(a, c), max(b, d))
DRAIN_L = rect_local(9101, 9401, 14864, 15700)
DRAIN_R = rect_local(18851, 19151, 14680, 15520)
PIPE_L = (P(9251.3, 15064)[0], P(9251.3, 15064)[1], 0.150)
PIPE_R = (P(19001.3, 14880)[0], P(19001.3, 14880)[1], 0.150)
HBOX = rect_local(8171, 8471, 16051, 16411)
IBOX = rect_local(19781, 20181, 15867, 16227)
lin_full = _fill_slivers(ring.intersection(above_chord))
assert lin_full.geom_type == 'Polygon' and len(lin_full.interiors) == 0, f"lining topology: {lin_full.geom_type}"
LINING_POLY = lin_full.simplify(0.01, preserve_topology=True) # 10 mm: removes 30 um edges (loft invalid) and cuts ~700 -> ~60 section vertices (STEP size)
EXCAV_POLY = ex_poly.difference(outer_poly).intersection(box(-9, 0.0, 9, 9)).simplify(0.01, preserve_topology=True)
assert EXCAV_POLY.geom_type == 'Polygon' and len(EXCAV_POLY.interiors) == 0, f"excavation topology: {EXCAV_POLY.geom_type}"
base_fix = ring.intersection(below_chord).difference(pav_fix).difference(DRAIN_L).difference(DRAIN_R)
assert base_fix.geom_type == 'Polygon' and len(base_fix.interiors) == 0, f"base topology: {base_fix.geom_type}"
BASE_PTS_FIX = np.array(base_fix.exterior.coords)[:-1]
def _find(pt):
d = np.hypot(BASE_PTS_FIX[:, 0] - pt[0], BASE_PTS_FIX[:, 1] - pt[1])
i = int(np.argmin(d)); assert d[i] < 1e-6, f"pavement corner not on base boundary: {d[i]}"
return i
IDX_BL = _find((XL, T_FIX * XL - PAV_T)); IDX_BR = _find((XR, T_FIX * XR - PAV_T))
def base_pts(t):
pts = BASE_PTS_FIX.copy()
pts[IDX_BL, 1] = t * XL - PAV_T
pts[IDX_BR, 1] = t * XR - PAV_T
return pts
LIN_PTS = np.array(LINING_POLY.exterior.coords)[:-1]
EXC_PTS = np.array(EXCAV_POLY.exterior.coords)[:-1]
def PIPE_PTS(cx, cz, r):
return np.array([(cx + r * math.cos(2 * math.pi * k / PIPE_SEG), cz + r * math.sin(2 * math.pi * k / PIPE_SEG)) for k in range(PIPE_SEG)])
DRAIN_L_PTS = np.array(DRAIN_L.exterior.coords)[:-1]
DRAIN_R_PTS = np.array(DRAIN_R.exterior.coords)[:-1]
HBOX_PTS = np.array(HBOX.exterior.coords)[:-1]
IBOX_PTS = np.array(IBOX.exterior.coords)[:-1]
# ---------- alignment, profile ----------
cn = np.array([[int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"])]
for r in csv.DictReader(open(C_CSV, encoding="utf-8"))])
dg_d = {}
for r in csv.DictReader(open(P_CSV, encoding="utf-8")):
if float(r["across_m"]) >= -25:
dg_d[int(r["station_anchor"])] = float(r["value"])
dg_s = np.array(sorted(dg_d)); dg_v = np.array([dg_d[k] for k in dg_s])
cn_s = cn[:, 0]
def axis(s):
x = np.interp(s, cn_s, cn[:, 1]); y = np.interp(s, cn_s, cn[:, 2])
i = min(max(np.searchsorted(cn_s, s) - 1, 0), len(cn_s) - 2)
dx, dy = cn[i + 1, 1] - cn[i, 1], cn[i + 1, 2] - cn[i, 2]
L = math.hypot(dx, dy)
return x, y, dx / L, dy / L
ORX, ORY = axis(S0)[0], axis(S0)[1]
def slope_pct(s):
if s < 5105: return -2.0
if s < 5165: return -2.0 + 4.0 * (s - 5105) / 60.0
return 2.0
def world(s, x, z):
ax, ay, tx, ty = axis(s)
nx, ny = -ty, tx
zr = float(np.interp(s, dg_s, dg_v))
return (ax - ORX + x * nx, ay - ORY + x * ny, zr + z)
# ---------- B-rep helpers ----------
def loft(stations, pts2_fn):
wires = []
for s in stations:
pts3 = [world(s, float(x), float(z)) for x, z in pts2_fn(s)]
wires.append(Wire.make_polygon([Vector(*p) for p in pts3], close=True))
sol = Solid.make_loft(wires, ruled=True)
if not sol.is_valid: # end caps of lofts with near-degenerate edges; ShapeFix repairs them
sol = sol.fix()
return sol
def stations_of(a, b, step=STEP_M):
n = max(1, int(round((b - a) / step)))
return [float(v) for v in np.linspace(a, b, n + 1)]
def chunks():
out = []; a = S0
while a < S1:
b = min(a + CHUNK_M, S1); out.append((a, b)); a = b
return out
PAL = {"A": (42, 157, 143), "B-1": (231, 111, 81), "B-2": (244, 162, 97), "C-1": (233, 196, 106),
"C-2": (138, 177, 125), "D-1": (38, 70, 83), "D-2": (100, 130, 160), "P-2": (190, 110, 140),
"P-3": (150, 90, 60), "RP-1": (80, 170, 200), "WL-1": (200, 200, 120), "WL-2": (120, 90, 170),
"개착부": (170, 170, 170)}
COL = {"base": (141, 110, 99), "pav": (96, 125, 139), "drain": (30, 136, 229), "hi": (253, 216, 53),
"clear": (255, 111, 0)}
runs = []
for r in csv.DictReader(open(PAT_CSV, encoding="utf-8")):
a, b = int(float(r["sta_from"])), int(float(r["sta_to"])); pat = r["lining_pattern"]
a2, b2 = max(a, S0), min(b, S1)
if b2 > a2: runs.append((a2, b2, pat))
runs.sort()
assert runs[0][0] == S0 and runs[-1][1] == S1, f"pattern coverage {runs[0][0]}..{runs[-1][1]}"
for (a1, b1, _), (a2, _, _) in zip(runs, runs[1:]):
assert b1 == a2, f"pattern gap/overlap at {b1} vs {a2}"
solids, meta_c, clear_wires = [], [], []
def add(sol, cid, label, group, color, pattern=None, a=None, b=None):
if not sol.is_valid:
sol = sol.fix() # booleans on lofts can leave cap faces invalid
assert sol.is_valid, f"invalid solid {cid}"
sol.label = cid; sol.color = Color(*[c / 255 for c in color])
solids.append(sol)
meta_c.append({"id": cid, "label": label, "group": group, "pattern": pattern,
"from_sta": a, "to_sta": b, "volume_m3": round(sol.volume, 4), "color": list(color)})
for i, (a, b, pat) in enumerate(runs):
if MODE != 'parts_a' or not (RANGE[0] <= i < RANGE[1]): continue
print('lining run', i + 1, pat, a, b, flush=True)
lin_run = loft(stations_of(a, b), lambda s: LIN_PTS)
lin_run = lin_run - loft(stations_of(a, b), lambda s: HBOX_PTS) - loft(stations_of(a, b), lambda s: IBOX_PTS)
add(lin_run, f"LIN_{i+1:02d}_{pat}", f"라이닝 {pat} {a/1000:.3f}~{b/1000:.3f}",
"라이닝", PAL[pat], pat, a, b)
for i, (a, b, pat) in enumerate(runs):
if MODE != 'parts_a' or not (RANGE[0] <= i < RANGE[1]): continue
add(loft(stations_of(a, b), lambda s: EXC_PTS), f"EXC_{i+1:02d}_{pat}", f"굴착 여유대 {pat} {a/1000:.3f}~{b/1000:.3f}",
"굴착면", (150, 120, 90), pat, a, b)
for i, (a, b, pat) in enumerate(runs): # 기타 구성요소도 지보패턴 구간(run) 경계에서 분할
if MODE != 'parts_b' or not (RANGE[0] <= i < RANGE[1]): continue
sts = stations_of(a, b)
rid = f"{i+1:02d}_{pat}"
rng = f"{a/1000:.3f}~{b/1000:.3f}"
add(loft(sts, lambda s: np.array(pav_corners(slope_pct(s) / 100.0))), f"PAV_{rid}", f"노면 계획고 포장 {pat} {rng}",
"노면(계획고)", COL["pav"], pat, a, b)
add(loft(sts, lambda s: base_pts(slope_pct(s) / 100.0)), f"BASE_{rid}", f"기층·인버트(가정) {pat} {rng}",
"기층·인버트(가정)", COL["base"], pat, a, b)
for side, dpts, pipe in (("L", DRAIN_L_PTS, PIPE_L), ("R", DRAIN_R_PTS, PIPE_R)):
bx = loft(sts, lambda s, d=dpts: d)
pp = loft(sts, lambda s, p=pipe: PIPE_PTS(p[0], p[1], p[2]))
add(bx - pp, f"DRN{side}_{rid}", f"측구 {side} {pat} {rng}", "배수(측구)", COL["drain"], pat, a, b)
add(loft(sts, lambda s: HBOX_PTS), f"HBOX_{rid}", f"공동구 H(가정) {pat} {rng}", "공동구(가정)", COL["hi"], pat, a, b)
add(loft(sts, lambda s: IBOX_PTS), f"IBOX_{rid}", f"공동구 I(가정) {pat} {rng}", "공동구(가정)", COL["hi"], pat, a, b)
# 시설한계 50 mm reference lines (wires, not solids)
clr = inner_poly.buffer(CLEAR_OFFSET).exterior
clr_pts = [(x, z) for x, z in list(clr.coords)[:-1] if z >= chord_z(x) - 1e-9 and abs(x) <= 5.4]
idxs = np.round(np.linspace(0, len(clr_pts) - 1, CLEAR_LINES)).astype(int)
clear_lines = []
sts_all = stations_of(S0, S1)
for k, ii in enumerate(idxs):
x0, z0 = clr_pts[ii]
pts3 = [world(s, x0, z0) for s in sts_all]
clear_lines.append(pts3)
if MODE != 'parts_b' or RANGE[0] != 0:
continue
w = Wire.make_polygon([Vector(*p) for p in pts3], close=False)
w.label = f"REF_CLEAR_{k+1}"; w.color = Color(1.0, 0.44, 0.0)
clear_wires.append(w)
meta_c.append({"id": f"REF_CLEAR_{k+1}", "label": f"시설한계 50 mm 참조선 {k+1}", "group": "시설한계(참조선)",
"pattern": None, "from_sta": S0, "to_sta": S1, "volume_m3": 0, "color": list(COL["clear"]), "reference": True})
if MODE in ('parts_a', 'parts_b'):
tag = f'lin_exc_{RANGE[0]:02d}' if MODE == 'parts_a' else f'oth_{RANGE[0]:02d}'
export_step(Compound(children=solids + clear_wires, label=tag), f"{PARTS}/{tag}.step")
json.dump(meta_c, open(f"{PARTS}/{tag}.json", 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print('parts written', tag, len(solids), 'solids', len(clear_wires), 'ref wires')
sys.exit(0)
# ---- post-processing modes (each call < 180 s, 4 GB RAM) ----
import glob
TAGS = sorted(os.path.basename(x)[:-5] for x in glob.glob(f"{PARTS}/*.json"))
from build123d import Solid as _Solid
if MODE == 'mesh': # python build_solids.py ROOT mesh <tag>
tag = sys.argv[3]
imp = import_step(f"{PARTS}/{tag}.step")
items = [x for x in (imp.children or list(imp.solids())) if isinstance(x, _Solid)]
pj = [m for m in json.load(open(f"{PARTS}/{tag}.json", encoding='utf-8')) if not m.get('reference')]
assert len(items) == len(pj), f"{tag}: {len(items)} solids vs {len(pj)} meta"
sc = trimesh.Scene(); tmp = tempfile.mkdtemp(prefix='stl_')
for k, (sol, mm) in enumerate(zip(items, pj)):
stl = os.path.join(tmp, f"s{k}.stl")
export_stl(sol, stl, tolerance=0.002, angular_tolerance=0.2)
m = trimesh.load(stl, force='mesh')
m.visual.face_colors = np.tile(np.array(list(mm['color']) + [255], dtype=np.uint8), (len(m.faces), 1))
sc.add_geometry(m, geom_name=mm['id'])
sc.export(f"{PARTS}/{tag}.glb")
print('mesh', tag, len(sc.geometry), 'geometries'); sys.exit(0)
if MODE == 'step': # python build_solids.py ROOT step
allitems = []
for tag in TAGS:
imp = import_step(f"{PARTS}/{tag}.step")
allitems += list(imp.children or list(imp.solids()))
os.makedirs(f"{ROOT}/model", exist_ok=True)
export_step(Compound(children=allitems, label="본선 4+745~5+480 표준단면 모델"), f"{ROOT}/model/tunnel_solids.step")
print('step merged', len(allitems), 'items', os.path.getsize(f"{ROOT}/model/tunnel_solids.step"), 'bytes'); sys.exit(0)
if MODE == 'final': # python build_solids.py ROOT final
meta_c = []
for tag in TAGS:
meta_c += json.load(open(f"{PARTS}/{tag}.json", encoding='utf-8'))
scene = trimesh.Scene()
for tag in TAGS:
g = trimesh.load(f"{PARTS}/{tag}.glb")
for name, geom in g.geometry.items():
scene.add_geometry(geom, geom_name=name)
glb_faces = {}
for k, pts3 in enumerate(clear_lines): # 시설한계 reference ribbons (visual only; exact wires in STEP)
P3 = np.array(pts3); V = []; F = []
for i in range(len(P3)):
d = P3[min(i + 1, len(P3) - 1)] - P3[max(i - 1, 0)]
d = d / (np.linalg.norm(d) + 1e-12)
w = np.cross(d, np.array([0, 0, 1.0])); w = w / (np.linalg.norm(w) + 1e-12) * CLEAR_RIB_W
V += [P3[i] - w, P3[i] + w]
for i in range(len(P3) - 1):
a0, b0, c0, d0 = 2 * i, 2 * i + 1, 2 * i + 2, 2 * i + 3
F += [[a0, c0, b0], [b0, c0, d0]]
m = trimesh.Trimesh(vertices=np.array(V), faces=np.array(F), process=False)
m.visual.face_colors = np.tile(np.array([255, 111, 0, 255], dtype=np.uint8), (len(m.faces), 1))
scene.add_geometry(m, geom_name=f"REF_CLEAR_{k+1}")
glb_path = f"{ROOT}/model/tunnel_solids.glb"
scene.export(glb_path)
step_path = f"{ROOT}/model/tunnel_solids.step"
solid_meta = [x for x in meta_c if not x.get('reference')]
meta = {
"model": ["tunnel_solids.step (STEP B-rep, Rhino/Autodesk)", "tunnel_solids.glb (web viewer, same B-rep tessellated)"],
"scope": f"{S0}~{S1} (4+745~5+480)", "units": "m",
"origin_plan_xy": [round(ORX, 3), round(ORY, 3)],
"sections": {"lining": "rev2: 라이닝 R6982/R7282, 하단 호 도면 끝점까지", "excavation": "굴착 여유대 R7402/R4574/R4819 (노면 위)",
"road": "계획고 노면 포장 280 mm, 측점별 편경사 S (노면만)", "base": "기층·인버트: 가정 (도면 미표현)",
"drains": "측구 300x836 mm, 관 r150", "hi": "H/I 박스: 공동구 가정(사용자 결정)", "clearance": "시설한계 50 mm 참조선 (솔리드 아님)"},
"assumptions": ["기층·인버트 형상·두께는 가정 (도면 미표현, 사용자 결정 '가정하여 생성')",
"H/I 박스 = 공동구 가정 (사용자 결정)",
"편경사 부호 z = (S/100)·x, x = 접선 좌측 법선 (부호 확인 필요)",
"라이닝·굴착 여유대는 대칭 (편경사 미적용)",
"측점 간 축선은 선형 보간, 단면은 10 m 간격 Ruled loft (곡선부 근사)",
"라이닝 H/I 박스 제외 부분은 3D 불리언 차감 (공동구 가정)"],
"pattern_runs": [{"id": f"LIN_{i+1:02d}_{p}", "pattern": p, "from": a, "to": b} for i, (a, b, p) in enumerate(runs)],
"components": meta_c,
"open": ["E-1, E-2, RP-2, WL-3 패턴 배치 [미확정] (당진 표 밖)", "NP-5BE-1-WL-4 해석 [확인 필요]",
"04 공동구·배수 도면 상세 미반영 (측구·공동구 치수는 03-001 단면 기준)", "편경사 부호(좌·우) 확인 필요"]
}
json.dump(meta, open(f"{ROOT}/model/tunnel_solids_meta.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
ver = {"solid_count": len(solid_meta), "runs": len(runs), "pattern_coverage": f"{runs[0][0]}..{runs[-1][1]} contiguous",
"lining_runs": sum(1 for x in solid_meta if x['group'] == '라이닝'),
"excavation_runs": sum(1 for x in solid_meta if x['group'] == '굴착면'),
"volume_sum_m3": round(sum(x['volume_m3'] for x in solid_meta), 3),
"step_bytes": os.path.getsize(step_path), "glb_bytes": os.path.getsize(glb_path),
"glb_geometries": len(trimesh.load(glb_path).geometry)}
def _overlap(sample):
base = Polygon(base_pts(slope_pct(sample) / 100.0)); pav = Polygon(pav_corners(slope_pct(sample) / 100.0))
return {"lining_vs_excavation": round(LINING_POLY.intersection(EXCAV_POLY).area, 9),
"base_vs_pavement": round(base.intersection(pav).area, 9),
"lining_vs_base": round(LINING_POLY.intersection(base).area, 9),
"pavement_vs_lining": round(pav.intersection(LINING_POLY).area, 9),
"base_vs_drainL": round(base.intersection(DRAIN_L).area, 9),
"base_vs_drainR": round(base.intersection(DRAIN_R).area, 9)}
ver["section_overlap_m2_by_station"] = {str(st): _overlap(st) for st in [4745, 5105, 5165, 5480]}
json.dump(ver, open(f"{ROOT}/data/verify_solids.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(json.dumps(ver, ensure_ascii=False, indent=1)); sys.exit(0)
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Compare two plan sheets layer-by-layer and list elevation-like numeric
texts (decimal values in 0..1000) per layer, to locate ground levels (지반고).
Read-only on the source DXF."""
import collections
import re
import sys
import ezdxf
num = re.compile(r"^-?\d{1,4}\.\d{1,3}$")
def scan(path):
msp = ezdxf.readfile(path).modelspace()
layers = collections.Counter(e.dxf.layer for e in msp)
el = collections.defaultdict(list)
for e in msp.query("TEXT"):
s = e.dxf.text.strip()
if num.match(s) and 0 <= float(s) <= 1000:
el[e.dxf.layer].append((round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), s))
return layers, el
la, ea = scan(sys.argv[1])
lb, eb = scan(sys.argv[2])
print("layers only in 001:", sorted(set(la) - set(lb)))
print("layers only in 002:", sorted(set(lb) - set(la)))
for lay in sorted(set(la) | set(lb)):
if la[lay] != lb[lay]:
print(f" count diff {lay}: 001={la[lay]} 002={lb[lay]}")
print("== elevation-like numeric texts (layer: n, sample)")
for name, el in (("001", ea), ("002", eb)):
for lay, items in sorted(el.items(), key=lambda kv: -len(kv[1])):
print(f" {name} {lay}: n={len(items)} sample={sorted(items)[:6]}")
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Task 4: dump the variable table of the standard section (C0060303-001) as a
coordinate grid. Rows = cross slope (S, 편경사) per user answer 2026-10-21; the
first row = variable names. Every TEXT on the table layers is kept (no regex
filter), so that the S column and the header row can be checked."""
import csv
import sys
from collections import defaultdict
import ezdxf
SRC, OUT = sys.argv[1], sys.argv[2]
msp = ezdxf.readfile(SRC).modelspace()
cells = [(round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), e.dxf.text.strip())
for e in msp.query("TEXT") if e.dxf.layer in ("CS-TABL-TEX1", "CS-TABL-TEX2")]
rows = defaultdict(list)
for x, y, s in cells:
rows[round(y / 50) * 50].append((x, s)) # bucket by ~50 mm
with open(OUT, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
for y in sorted(rows, reverse=True):
line = [s for x, s in sorted(rows[y])]
w.writerow([y] + line)
print(f"Y{y:>7}: " + " | ".join(f"{x:.0f}:{s}" for x, s in sorted(rows[y])))
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Task 3 exploration: layer usage and long polylines / text clusters in a
plan-profile DXF, to locate the tunnel centreline (본선 축선) candidates."""
import collections
import json
import sys
import ezdxf
path = sys.argv[1]
doc = ezdxf.readfile(path)
msp = doc.modelspace()
by_layer = collections.Counter(e.dxf.layer for e in msp)
print("layers used:", len(by_layer))
for name, n in by_layer.most_common(25):
print(f" {n:6d} {name}")
# long polylines = candidate alignment / outline strings
polys = []
for e in msp:
if e.dxftype() == "LWPOLYLINE":
pts = list(e.get_points("xy"))
if len(pts) >= 20:
xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
polys.append({"layer": e.dxf.layer, "n": len(pts),
"x": [round(min(xs), 1), round(max(xs), 1)],
"y": [round(min(ys), 1), round(max(ys), 1)]})
polys.sort(key=lambda d: -d["n"])
print("long LWPOLYLINE (>=20 vertices):", len(polys))
for p in polys[:12]:
print(" ", p)
# text that looks like station (STA) or level (EL) labels
import re
pat = re.compile(r"(STA|No\.?|EL|E\.L|\d+\+\d+|\+\d{2,3}\.\d{3}|\d{2,3}\.\d{2,3})", re.I)
hits = []
for e in msp:
if e.dxftype() in ("TEXT", "MTEXT"):
s = e.dxf.text if e.dxftype() == "TEXT" else e.plain_text()
if s and pat.search(s):
loc = e.dxf.insert if e.dxftype() == "TEXT" else e.dxf.insert
hits.append((round(loc.x, 1), round(loc.y, 1), s.strip()[:40]))
print("station/level-like texts:", len(hits))
for h in sorted(hits)[:25]:
print(" ", h)
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Explore the concrete-lining structure drawings (04-004 .. 04-009).
Read-only on the source RAG folder. For each DXF prints layers with entity
counts, INSERT block names, dimension measurements and every text string with
its position, and writes a JSON dump for later use.
Usage: explore_lining.py <RAG_04_DIR> <OUT_JSON> [max_texts_per_file]
"""
import collections
import glob
import json
import os
import sys
import ezdxf
from ezdxf import recover
RAG_04 = sys.argv[1]
OUT_JSON = sys.argv[2]
MAX_TEXT = int(sys.argv[3]) if len(sys.argv) > 3 else 400
def load(path):
try:
return ezdxf.readfile(path), None
except Exception as exc:
try:
doc, _ = recover.readfile(path)
return doc, f"recovered after strict error: {type(exc).__name__}"
except Exception as exc2:
return None, f"{type(exc).__name__} | recover: {type(exc2).__name__}: {exc2}"
def explore(path):
rec = {"file": os.path.basename(path)}
doc, note = load(path)
if doc is None:
rec["error"] = note
return rec
rec["note"] = note
msp = doc.modelspace()
layers = collections.Counter()
counts = collections.Counter()
for e in msp:
counts[e.dxftype()] += 1
layers[e.dxf.layer] += 1
rec["layers"] = dict(layers.most_common())
rec["entity_counts"] = dict(counts.most_common())
rec["inserts"] = dict(collections.Counter(e.dxf.name for e in msp if e.dxftype() == "INSERT").most_common(30))
dims, texts = [], []
for e in msp:
t = e.dxftype()
if t == "DIMENSION":
try:
val = round(float(e.get_measurement()), 3)
except Exception:
val = None
dims.append({"layer": e.dxf.layer, "value": val, "text": e.dxf.get("text", "")})
elif t in ("TEXT", "MTEXT"):
s = e.dxf.text if t == "TEXT" else e.plain_text()
if s and s.strip():
pos = e.dxf.insert
texts.append({"layer": e.dxf.layer, "x": round(pos.x, 2), "y": round(pos.y, 2),
"text": s.strip().replace("\n", " | ")[:200]})
rec["dimensions"] = dims[:200]
rec["texts_total"] = len(texts)
rec["texts"] = texts[:MAX_TEXT]
return rec
def main():
files = sorted(glob.glob(os.path.join(RAG_04, "C0060304-00[4-9]*.dxf")))
out = [explore(f) for f in files]
with open(OUT_JSON, "w", encoding="utf-8") as fh:
json.dump(out, fh, ensure_ascii=False, indent=1)
for r in out:
print("==", r["file"])
if "error" in r:
print(" ERROR", r["error"])
continue
print(" note:", r["note"])
print(" entities:", r["entity_counts"])
print(" layers:", dict(list(r["layers"].items())[:25]))
print(" inserts:", r["inserts"])
print(" dims:", len(r["dimensions"]), "texts:", r["texts_total"])
if __name__ == "__main__":
main()
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""Task 4 exploration: layers, arcs (radius + centre) and dimension-like texts
of the standard section drawing (C0060303-001). Read-only."""
import collections
import re
import sys
import ezdxf
from ezdxf import bbox
msp = ezdxf.readfile(sys.argv[1]).modelspace()
print("layers:", collections.Counter(e.dxf.layer for e in msp).most_common(12))
ext = bbox.extents(msp)
print("extents:", [round(v, 1) for v in ext.extmin], [round(v, 1) for v in ext.extmax])
print("== ARCs (layer, centre, radius, start/end deg) sorted by radius")
arcs = sorted(msp.query("ARC"), key=lambda e: -e.dxf.radius)
for e in arcs[:14]:
c = e.dxf.center
print(f" {e.dxf.layer:14s} c=({c.x:9.2f},{c.y:9.2f}) R={e.dxf.radius:9.2f} {e.dxf.start_angle:7.1f}->{e.dxf.end_angle:7.1f}")
print("== R-labels and numeric dims")
pat = re.compile(r"^(R\d'?|\d{2,5}(\.\d+)?|\d+\.\d+%?|-?\d+\.\d+)$")
for e in msp.query("TEXT"):
s = e.dxf.text.strip()
if pat.match(s) or s.startswith("R"):
print(f" {e.dxf.layer:14s} ({e.dxf.insert.x:9.2f},{e.dxf.insert.y:9.2f}) {s}")
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Task 3 (HITL draft): main-line centreline station table from a plan sheet.
Method (layers CA-ALGN-TEXT labels and CA-ALGN-STSM station ticks):
1. Station labels: 'N+mmm' is explicit; bare numbers take the km of the next
explicit label when they precede it (700 before 5+000 -> 4+700), otherwise
the km of the previous explicit label.
2. Each station has two labels (one per side of the road). Each label is
paired with its nearest tick; the centreline point is the midpoint of the
two tick points.
3. Checks reported (not forced): side offset per station, and consecutive
centreline distance vs station difference (residual).
DRAFT for HITL review. No value is adjusted to fit."""
import csv
import math
import re
import sys
from collections import defaultdict
import ezdxf
SRC, OUT = sys.argv[1], sys.argv[2]
msp = ezdxf.readfile(SRC).modelspace()
explicit_re = re.compile(r"^(\d+)\+(\d{3})$")
labels = [(e.dxf.insert.x, e.dxf.insert.y, e.dxf.text.strip())
for e in msp.query("TEXT") if e.dxf.layer == "CA-ALGN-TEXT"]
ticks = []
for e in msp.query("LINE"):
if e.dxf.layer == "CA-ALGN-STSM":
a, b = e.dxf.start, e.dxf.end
ticks.append(((a.x + b.x) / 2, (a.y + b.y) / 2))
labels = [l for l in labels if explicit_re.match(l[2]) or l[2].isdigit()]
labels.sort(key=lambda t: -t[1]) # along the sheet, NNE -> SSW
explicit_idx = [i for i, l in enumerate(labels) if explicit_re.match(l[2])]
station_of = {}
for i, (x, y, s) in enumerate(labels):
if explicit_re.match(s):
m = explicit_re.match(s)
station_of[i] = int(m.group(1)) * 1000 + int(m.group(2))
continue
v = int(s)
nxt = next((j for j in explicit_idx if j > i), None)
prv = next((j for j in reversed(explicit_idx) if j < i), None)
if prv is not None:
km = int(explicit_re.match(labels[prv][2]).group(1))
else:
km = int(explicit_re.match(labels[nxt][2]).group(1)) - 1
station_of[i] = km * 1000 + v
groups = defaultdict(list)
for i, (x, y, s) in enumerate(labels):
groups[station_of[i]].append((x, y))
def nearest_tick(pt):
return min(ticks, key=lambda t: math.hypot(t[0] - pt[0], t[1] - pt[1]))
centre = []
for sta in sorted(groups):
pts = groups[sta]
if len(pts) != 2:
continue
t1, t2 = nearest_tick(pts[0]), nearest_tick(pts[1])
side_gap = math.dist(t1, t2)
mid = ((t1[0] + t2[0]) / 2, (t1[1] + t2[1]) / 2)
centre.append((sta, mid[0], mid[1], side_gap))
with open(OUT, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["station_m", "centre_x", "centre_y", "side_gap_m"])
for sta, x, y, g in centre:
w.writerow([sta, round(x, 3), round(y, 3), round(g, 3)])
print(f"stations={len(centre)} (labels={len(labels)}, ticks={len(ticks)})")
prev = None
for sta, x, y, g in centre:
step = "" if prev is None else f" d={math.dist(prev[1:3], (x, y)):8.3f} dSTA={sta - prev[0]:5d} res={math.dist(prev[1:3], (x, y)) - (sta - prev[0]):+.3f}"
print(f" STA {sta:>5} ({x:12.3f}, {y:12.3f}) side_gap={g:6.2f}{step}")
prev = (sta, x, y)
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Extract structured lining-reinforcement data from the concrete-lining
structure drawings 04-004..009 (read-only on RAG).
Per sheet: pattern groups and their detail kind (입출구부 50 m / 온도미고려),
note-5 scope sentence, concrete and rebar grade, rebar sizes present,
radius and lap dimensions as printed, EL datum texts.
Output: data/lining_spec.json (+ printed summary).
Usage: extract_lining_spec.py <RAG_04_DIR> <OUT_JSON>
"""
import glob
import json
import os
import re
import sys
import ezdxf
from ezdxf import recover
RAG_04, OUT = sys.argv[1], sys.argv[2]
BAR_RE = re.compile(r"^0?H(\d{2})$")
PAT_RE = re.compile(r"\(([^()]*?)\s*패턴\)")
GROUP_RE = re.compile(r"주철근 조립도\(([^()]*?)\s*패턴\)")
def load(path):
try:
return ezdxf.readfile(path)
except Exception:
doc, _ = recover.readfile(path)
return doc
def texts_of(doc):
out = []
for e in doc.modelspace():
if e.dxftype() == "TEXT":
s = e.dxf.text
elif e.dxftype() == "MTEXT":
s = e.plain_text()
else:
continue
if s and s.strip():
out.append((e.dxf.layer, round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), s.strip()))
return out
def sheet_no(name):
return int(re.search(r"C0060304-(\d{3})", name).group(1))
def main():
files = sorted(glob.glob(os.path.join(RAG_04, "C0060304-00[4-9]*.dxf")))
sheets = []
for f in files:
doc = load(f)
tx = texts_of(doc)
rec = {"sheet": sheet_no(f), "file": os.path.basename(f)}
# pattern groups: detail kind from the text itself
groups = []
for layer, x, y, s in tx:
if layer != "CS-TABL-TEX1":
continue
if "패턴" in s:
kind = "입출구부 50m" if "입출구부" in s else ("온도미고려" if "온도미고려" in s else "기타")
m = PAT_RE.search(s)
groups.append({"group": m.group(1) if m else None, "kind": kind, "raw": s})
rec["pattern_details"] = groups
# scope note 5 (full line, joined with the continuation line)
note5 = [s for _, _, _, s in tx if s.startswith("5. 본 콘크리트 라이닝 구조도")]
cont = [s for _, _, _, s in tx if s.startswith("설계조건과 상이할 경우")]
rec["note5"] = (note5[0] + " " + (cont[0] if cont else "")).strip() if note5 else None
rec["concrete"] = sorted({s for _, _, _, s in tx if "fck" in s})
rec["rebar_grade"] = sorted({s for _, _, _, s in tx if "fy=" in s})
rec["aggregate"] = sorted({s for _, _, _, s in tx if "최대치수" in s})
bars = {}
for layer, x, y, s in tx:
m = BAR_RE.match(s)
if m:
bars[f"H{m.group(1)}"] = bars.get(f"H{m.group(1)}", 0) + 1
rec["rebar_sizes_label_count"] = dict(sorted(bars.items()))
rec["radius_dims"] = sorted({s for layer, _, _, s in tx if layer == "CS-DIMT" and re.match(r"R\d?'?=", s)})
rec["lap_dims"] = sorted({s for layer, _, _, s in tx if layer == "CS-DIMT" and "mm" in s and "이음장" not in s and "600" in s})
rec["el_texts"] = sorted({s for _, _, _, s in tx if s.startswith("EL")})
rec["entrance_exit_50m"] = any("입출구부 50m" in s for _, _, _, s in tx)
sheets.append(rec)
with open(OUT, "w", encoding="utf-8") as fh:
json.dump({"source": "C0060304-004..009 (콘크리트라이닝구조도)", "sheets": sheets},
fh, ensure_ascii=False, indent=1)
for r in sheets:
print("==", r["sheet"], "| groups:", [(g["group"], g["kind"]) for g in r["pattern_details"]])
print(" note5:", (r["note5"] or "")[:140])
print(" concrete:", r["concrete"], "| rebar:", r["rebar_grade"], "| agg:", r["aggregate"])
print(" bars:", r["rebar_sizes_label_count"], "| R:", r["radius_dims"], "| EL:", r["el_texts"])
if __name__ == "__main__":
main()
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Task 5: support-pattern (지보패턴) station ranges from drawings 03-003..008.
Collects every TEXT that names a station (STA. / N+mmm / N+mmm.mm) and every
pattern name (NP-...) with its layer and position, without interpreting ranges."""
import glob
import os
import re
import sys
import ezdxf
RAG = sys.argv[1]
pat_re = re.compile(r"NP-\d[A-Z0-9-]*|WL-\d")
sta_re = re.compile(r"(STA\.?\s*)?\d{1,2}\+\d{2,3}(\.\d{2})?")
files = sorted(glob.glob(os.path.join(RAG, "03_본선터널굴착", "C0060303-00[3-8]*.dxf")))
for f in files:
msp = ezdxf.readfile(f).modelspace()
stas, pats = [], set()
for e in msp.query("TEXT MTEXT"):
s = e.dxf.text if e.dxftype() == "TEXT" else e.plain_text()
for m in sta_re.finditer(s):
stas.append((round(e.dxf.insert.x), round(e.dxf.insert.y), m.group(0).strip()))
for m in pat_re.finditer(s):
pats.add(m.group(0))
print(os.path.basename(f)[:28], "| patterns:", sorted(pats))
for x, y, t in sorted(stas, key=lambda r: (r[0], r[1]))[:12]:
print(f" ({x},{y}) {t}")
print(f" station-like texts: {len(stas)}")
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Extract the 당진방향 station pattern table from 참고_지보패턴현황 (read-only).
Rows 55..96 of the sheet: C=start STA, G=end STA, J=length, M=lining pattern
(지보패턴), O=실시설계 excavation class (blank = continues the row above),
Q=굴착공법, T=비고. Also extracts the 대산방향 table (rows 13..49) for reference.
Usage: extract_pattern_table.py <xlsx> <out_dir>"""
import csv, os, sys
import openpyxl
SRC, OUT = sys.argv[1], sys.argv[2]
wb = openpyxl.load_workbook(SRC, read_only=True, data_only=True)
ws = wb["참고_지보패턴현황"]
rows = {r: row for r, row in enumerate(ws.iter_rows(min_row=1, max_row=114, max_col=33, values_only=True), start=1)}
def table(r0, r1, tag):
out, last_o = [], None
for r in range(r0, r1 + 1):
row = rows[r]
try:
a, b = float(row[2]), float(row[6])
except (TypeError, ValueError):
continue
m = (row[12] or "").strip() if isinstance(row[12], str) else row[12]
o_raw = row[14]
o = o_raw if o_raw not in (None, "") else last_o
last_o = o
remark = (row[19] or "") if isinstance(row[19], str) else ""
out.append({"direction": tag, "source_row": r, "sta_from": a, "sta_to": b,
"length_m": row[9], "lining_pattern": m or ("개착부" if "개착" in remark else ""),
"excavation": o or "", "excavation_inherited": o_raw in (None, ""),
"method": (row[16] or "") if isinstance(row[16], str) else "",
"remark": remark})
return out
south = table(55, 96, "당진")
north = table(13, 49, "대산")
os.makedirs(OUT, exist_ok=True)
cols = list(south[0].keys())
for name, data in [("pattern_stations_dangjin.csv", south), ("pattern_stations_daesan_ref.csv", north)]:
with open(os.path.join(OUT, name), "w", encoding="utf-8", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=cols)
w.writeheader()
w.writerows(data)
print("dangjin rows", len(south), "range", south[0]["sta_from"], "->", south[-1]["sta_to"])
print("daesan rows", len(north), "range", north[0]["sta_from"], "->", north[-1]["sta_to"])
print("blank lining pattern rows (dangjin):", [(d["source_row"], d["remark"]) for d in south if not d["lining_pattern"]])
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Task 3 (user-confirmed reading): ground-level and design-grade values per
20 m station from sheet 02-002 (당진방향).
Confirmed by the user (2026-10-21): decimal texts in CR-TABL-TEX2 are 지반고 (GL),
sheet 002 values used; the ruler 740.00..980.00 is chainage 4+740..4+980, and
0.00 is 5+000 (ruler continues in 20 m steps).
Method: ruler texts give anchors (chainage = 4000+v for 740..980; 5000+v after 0.00).
Each decimal text is projected onto the ruler axis; its station is the nearest anchor.
Two decimal series appear at each station (|across| ~ 17-21 and ~ 32-39 m);
the series names are NOT assigned here (HITL). Checks are reported, not forced."""
import csv
import math
import re
import sys
import ezdxf
SRC, OUT = sys.argv[1], sys.argv[2]
msp = ezdxf.readfile(SRC).modelspace()
texts = [(e.dxf.insert.x, e.dxf.insert.y, e.dxf.text.strip())
for e in msp.query("TEXT") if e.dxf.layer == "CR-TABL-TEX2"]
ruler = {}
for x, y, s in texts:
if re.fullmatch(r"\d{1,3}\.00", s):
ruler[int(s.split(".")[0])] = (x, y)
anchors = {}
for v in range(740, 1000, 20):
if v in ruler:
anchors[4000 + v] = ruler[v]
for v in range(0, 1000, 20):
if v in ruler:
anchors[5000 + v] = ruler[v]
else:
break
stas = sorted(anchors)
print("anchors:", len(stas), "range", stas[0], "..", stas[-1])
gaps = [(stas[i + 1] - stas[i], round(math.dist(anchors[stas[i]], anchors[stas[i + 1]]), 2))
for i in range(len(stas) - 1)]
print("consecutive (dSTA, chord m):", gaps)
A0, A1 = anchors[stas[0]], anchors[stas[-1]]
L = math.dist(A0, A1)
u = ((A1[0] - A0[0]) / L, (A1[1] - A0[1]) / L)
v_ = (-u[1], u[0])
rows = []
for x, y, s in texts:
if not re.fullmatch(r"\d{1,3}\.\d{3}", s):
continue
dx, dy = x - A0[0], y - A0[1]
along = dx * u[0] + dy * u[1]
across = dx * v_[0] + dy * v_[1]
if not (-5.0 <= along <= L + 5.0 and abs(across) < 60):
continue # outside the ruler band (table noise)
sta_est = stas[0] + along * (stas[-1] - stas[0]) / L
near = min(stas, key=lambda k: abs(k - sta_est))
rows.append((near, round(sta_est, 1), round(across, 2), float(s), round(x, 2), round(y, 2)))
with open(OUT, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["station_anchor", "est_station", "across_m", "value", "x", "y"])
w.writerows(sorted(rows))
per = {}
for near, est, acr, val, _, _ in rows:
per.setdefault(near, []).append((acr, val))
for k in sorted(per):
print(f"STA {k}: " + " ".join(f"[{a:+.1f}m {val:.3f}]" for a, val in sorted(per[k])))
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Inventory the main-line (본선) DXF drawings: header, layers, entity counts,
extents, 3D flag and text samples. Read-only on the source RAG folder."""
import collections
import glob
import json
import os
import sys
import ezdxf
from ezdxf import bbox, recover
RAG_DIR = sys.argv[1]
OUT_JSON = sys.argv[2]
# 본선 scope (approved 2026-10-21): 02 평면·종단, 03 본선굴착, 04 본선기타
SUBDIRS = ["02_평면 및 종단면도", "03_본선터널굴착", "04_본선터널기타"]
def inventory(path):
rec = {"file": os.path.basename(path), "ok": False}
rec["recovered"] = False
try:
doc = ezdxf.readfile(path)
except Exception as exc: # strict load failed: retry in recover mode and record it
strict_error = f"{type(exc).__name__}: {exc}"
try:
doc, _auditor = recover.readfile(path)
rec["recovered"] = True
rec["strict_error"] = strict_error
except Exception as exc2:
rec["error"] = f"{strict_error} | recover: {type(exc2).__name__}: {exc2}"
return rec
hdr = doc.header
rec["dxfversion"] = doc.dxfversion
rec["insunits"] = hdr.get("$INSUNITS")
rec["codepage"] = hdr.get("$DWGCODEPAGE")
rec["layers"] = len(doc.layers)
msp = doc.modelspace()
counts = collections.Counter(e.dxftype() for e in msp)
rec["entity_counts"] = dict(counts.most_common())
rec["blocks"] = len([b for b in doc.blocks if not b.name.startswith("*")])
z_vals = []
texts = []
for e in msp:
t = e.dxftype()
if t == "LINE":
z_vals += [e.dxf.start.z, e.dxf.end.z]
elif t in ("TEXT", "MTEXT"):
s = e.dxf.text if t == "TEXT" else e.plain_text()
if s and len(texts) < 40:
texts.append(s.strip()[:80])
elif t in ("POINT", "INSERT", "CIRCLE", "ARC"):
z_vals.append(e.dxf.location.z if t == "POINT" else e.dxf.insert.z if t == "INSERT" else e.dxf.center.z)
rec["z_min"] = round(min(z_vals), 3) if z_vals else None
rec["z_max"] = round(max(z_vals), 3) if z_vals else None
rec["has_3d_geometry"] = bool(z_vals) and (max(z_vals) - min(z_vals)) > 1e-6
try:
ext = bbox.extents(msp)
rec["extents_min"] = [round(v, 2) for v in ext.extmin]
rec["extents_max"] = [round(v, 2) for v in ext.extmax]
except Exception as exc:
rec["extents_error"] = str(exc)
rec["text_sample"] = texts
rec["ok"] = True
return rec
def main():
paths = []
for sub in SUBDIRS:
paths += sorted(glob.glob(os.path.join(RAG_DIR, sub, "*.dxf")))
results = [inventory(p) for p in paths]
os.makedirs(os.path.dirname(OUT_JSON), exist_ok=True)
with open(OUT_JSON, "w", encoding="utf-8") as fh:
json.dump(results, fh, ensure_ascii=False, indent=2)
ok = sum(r["ok"] for r in results)
print(f"files={len(results)} ok={ok} failed={len(results) - ok}")
for r in results:
status = "OK " if r["ok"] else "ERR"
detail = r.get("error") or f"ents={sum(r['entity_counts'].values())} 3D={r['has_3d_geometry']} units={r['insunits']} cp={r['codepage']} recovered={r['recovered']}"
print(f"{status} {r['file'][:60]:60s} {detail}")
if __name__ == "__main__":
main()
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Render the Task 3 draft centreline report (DOC/03_설계) from the centreline
CSVs written by extract_alignment_plan.py. Numbers are read, not typed."""
import csv
import math
import sys
CSV_001, CSV_002, OUT_MD = sys.argv[1], sys.argv[2], sys.argv[3]
def load(path):
with open(path, encoding="utf-8") as fh:
return [(int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"]), float(r["side_gap_m"]))
for r in csv.DictReader(fh)]
a, b = load(CSV_001), load(CSV_002)
same = a == b
lines = ["# 본선 중심선 측점 좌표 (초안)\n",
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
"| v0.1.00 | 2026-10-21 | 최초 작성. 평면도 02-001 기준 측점 4+700~5+500 중심선 초안 (HITL 검토 대상) |\n",
"## 1. 추출 방법",
"- 소스: 레이어 `CA-ALGN-TEXT`(측점 라벨), `CA-ALGN-STSM`(측점 눈금) [출처: C0060302-001]",
"- 측점 라벨은 측점당 2개(좌우 측). 각 라벨의 최근접 눈금 2점의 **중점**을 중심선 점으로 채택 (정의 HITL 확인 대상)",
"- 좌표는 도면 좌표계 그대로 (단위: 도면 표기 따름, 사용자 승인 2026-10-21)\n",
"## 2. 측점별 중심선 좌표 (02-001)\n",
"| 측점(m) | 표기 | X | Y | 좌우 간격(m) | 인접 거리(m) | 측점차(m) | 잔차(m) |",
"|---|---|---|---|---|---|---|---|"]
prev = None
for sta, x, y, g in a:
label = f"{sta // 1000}+{sta % 1000:03d}"
if prev is None:
lines.append(f"| {sta} | {label} | {x:,.3f} | {y:,.3f} | {g:.2f} | — | — | — |")
else:
d = math.dist(prev[1:3], (x, y))
lines.append(f"| {sta} | {label} | {x:,.3f} | {y:,.3f} | {g:.2f} | {d:.3f} | {sta - prev[0]} | {d - (sta - prev[0]):+.3f} |")
prev = (sta, x, y)
resid = [math.dist(a[i][1:3], a[i + 1][1:3]) - (a[i + 1][0] - a[i][0]) for i in range(len(a) - 1)]
lines += ["",
f"- 검증: 인접 측점 거리와 측점차의 잔차 최대 {max(abs(r) for r in resid):.3f} m (측점 9개, 8구간). 잔차는 보정하지 않고 그대로 기재",
"- 범위: 4+700 ~ 5+500 (800m). **이 시트 밖의 연장은 확보된 도면이 없음**\n",
"## 3. 02-002 시트 대조",
f"- 02-002 추출 결과 {'02-001과 측점 좌표가 동일' if same else '02-001과 차이 있음'} ({'중복 시트 가능성' if same else '차이 확인 필요'}). 원본 파일 md5는 상이함 [출처: C0060302-002]",
"- 02-002가 종단(프로파일) 시트인지 중복 시트인지는 사용자 확인 필요\n",
"## 4. 미완 항목 (OPEN 등재 대상)",
"- 종단 EL 추출 미실시 (측점별 계획고·지반고). `CR-TABL-TEX2`의 740.00~980.00 라벨은 20m 간격으로 보이나 측점 체계와 대응 미확인 [추론]",
"- 허용오차 ±50mm 판정은 EL 추출 후 실시",
"- 측점 4+700 이전·5+500 이후 구간 평면 도면 미확보\n"]
open(OUT_MD, "w", encoding="utf-8").write("\n".join(lines) + "\n")
print("written", OUT_MD, "same_as_002=", same, "max_resid=", round(max(abs(r) for r in resid), 3))
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Generate the main-line drawing inventory report (DOC/02_분석) from the
inventory JSON so that every number in the report comes from the extraction."""
import collections
import json
import sys
SRC_JSON, OUT_MD = sys.argv[1], sys.argv[2]
rows = json.load(open(SRC_JSON, encoding="utf-8"))
ok = [r for r in rows if r["ok"]]
err = [r for r in rows if not r["ok"]]
units = collections.Counter(str(r.get("insunits")) for r in ok)
n3d = sum(1 for r in ok if r["has_3d_geometry"])
total_ents = sum(sum(r["entity_counts"].values()) for r in ok)
def top_types(r, k=3):
return ", ".join(f"{t} {n}" for t, n in list(r["entity_counts"].items())[:k])
lines = []
lines.append("# 대산2터널 본선 도면 인벤토리\n")
lines.append("| 버전 | 일자 | 변경 내용 |\n|---|---|---|")
lines.append("| v0.1.00 | 2026-10-21 | 최초 작성. 본선 21건(02 평면·종단 4, 03 본선굴착 8, 04 본선기타 9) DXF 인벤토리 |\n")
lines.append("## 1. 개요\n")
lines.append("- 입력: `RAG/03_터널명(대산2터널)/{02,03,04}` 하위 DXF 21건 (원본 DWG 변환본, 읽기 전용 사용)")
lines.append(f"- 추출 결과: 성공 {len(ok)}건 / 실패 {len(err)}건 / 복구 모드 적용 {sum(1 for r in ok if r['recovered'])}건")
lines.append(f"- 총 엔티티 {total_ents:,}개 (성공 파일 기준)")
lines.append(f"- 3D 형상(Z 편차 있음) 보유 파일: **{n3d}건** — 전 도면이 2D이므로 3D 형상은 도면 해석으로 재구성해야 함 [출처: 본 인벤토리 `has_3d_geometry`]")
lines.append(f"- `$INSUNITS` 분포: " + ", ".join(f"{k}={v}건" for k, v in units.items()) + " (0=미지정, 4=mm) — 단위는 도면 표기를 따름 (사용자 승인 2026-10-21)\n")
lines.append("## 2. 파일별 추출 결과\n")
lines.append("| # | 파일 | 상태 | 엔티티 | 주요 엔티티(상위 3) | 단위 | 코드페이지 |")
lines.append("|---|---|---|---|---|---|---|")
for i, r in enumerate(rows, 1):
if r["ok"]:
n = sum(r["entity_counts"].values())
lines.append(f"| {i} | {r['file']} | OK | {n:,} | {top_types(r)} | {r['insunits']} | {r['codepage']} |")
else:
lines.append(f"| {i} | {r['file']} | **ERR** | — | {r.get('error','')} | — | — |")
lines.append("")
lines.append("## 3. 도면 주석 관찰 (정성, 판정 어휘: 있음 / 일부 있음 / 없음 / 확인 못함)\n")
lines.append("| 항목 | 판정 | 근거 |\n|---|---|---|")
lines.append("| 표준단면 편경사 기준 | 있음 | 「편경사 -2.0%를 기준으로 작성」 [출처: C0060303-001, 주석 1] |")
lines.append("| 표준단면 방향 표기 범위 | 일부 있음 | 「당진방향 단면만 표기, 대산방향 단면은 생략」 [출처: C0060303-001, 주석 2] — 대산방향 형상은 이 도면으로 확정 불가 |")
lines.append("| 숏크리트·라이닝 두께 수치 | 없음 | 「표준지보패턴도 참조」 [출처: C0060303-001, 주석 5] — 두께 값은 다른 도면(표준지보패턴) 필요 |")
lines.append("| 굴착 순서 표기 | 있음 | N, N+1, N-1 단계 표기 및 1회 굴진장 [출처: C0060303-002] |")
lines.append("| 굴진 1회당 길이 | 있음 | 「굴진 (1회당 0.8m)」 [출처: C0060303-008] |")
lines.append("| 라이닝 구조도 적용 구간 | 일부 있음 | 「지보패턴 입출구부 A~C-2 구간의 구조적 취약구간에 적용」 [출처: C0060304-004, 주석 5] — 전 구간 적용 여부 확인 못함 |")
lines.append("| 지보패턴 명칭 | 있음 | NP-4AD-1-D-2, NP-4BD-1-D-2, NP-5AE-1-WL-4, NP-5BE-1-WL-4, NP-6A, NP-6B [출처: 03-003~008 파일명] |")
lines.append("| 평면·종단 지질·구배 표기 | 있음 | 구배 S=2.4913%, 지층명(붕적층·풍화토·풍화암·보통암) [출처: C0060302-001 텍스트] |")
lines.append("| 구간 측점(STA) 정합 | 확인 못함 | 평면·종단·굴착 도면 간 측점 대응표 없음 — Task 3에서 대조 필요 [추론] |")
lines.append("")
lines.append("## 4. 확인·보류 항목 (OPEN 등재 대상)\n")
lines.append("- **C0060304-003** (공동구 및 배수계획도 3): DXF 구조 오류(`Invalid sort handle code 331`), 복구 모드도 실패. 원본 DWG 재변환 필요. 본선 3D 골격에는 필수가 아니므로 배수 요소만 보류.")
lines.append("- 대산방향 단면 형상 (C0060303-001 주석 2): 3D 대산방향 재구성 여부 결정 필요 [CLARIFICATION NEEDED].")
lines.append("- 라이닝 두께 수치 출처 (표준지보패턴도 미보유): 두께 값 확정 전까지 `[미확정]`으로 표기.")
lines.append("")
lines.append("## 5. 다음 단계\n")
lines.append("Task 3(평면·종단 선형 추출, HITL) 착수 전 위 4항의 확인 응답을 받는다. 본 인벤토리의 수치는 `Code/3d-drawing/data/inventory_mainline.json`에서 자동 생성되었다.")
open(OUT_MD, "w", encoding="utf-8").write("\n".join(lines) + "\n")
print("written", OUT_MD, "lines", len(lines))
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Render the lining-reinforcement reference report (DOC/03_설계) from
data/lining_spec.json written by extract_lining_spec.py. Values are read from
the JSON, not typed. Usage: make_lining_doc.py <lining_spec.json> <out.md>"""
import json
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
d = json.load(open(SRC, encoding="utf-8"))
S = d["sheets"]
note5 = S[0]["note5"]
conc = S[0]["concrete"][0].split("=")[-1]
rebar = S[0]["rebar_grade"][0].split("=")[-1]
agg = S[0]["aggregate"][0].split("Ø")[-1]
r1_drawn = next(x for x in S[0]["radius_dims"] if x.startswith("R1="))
r1_mm = float(r1_drawn.split("=")[1])
lines = ["# 콘크리트 라이닝 구조도 판독 및 재료 사양 (초안)\n",
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
"| v0.1.00 | 2026-10-21 | 최초 작성. 콘크리트라이닝구조도 C0060304-004~009 판독 (HITL 검토 대상) |\n",
"## 1. 개요",
"- 6장은 **지보패턴 입출구부 취약구간**의 철근 상세와 공통 사양이다. 도면에 측점(STA) 표기가 없어 어느 측점 구간에 적용되는지는 판독되지 않았다 ([미확정]).",
"- 적용 범위(주기 5, 원문): " + note5 + "\n",
"## 2. 시트별 패턴군",
"| 도면 | 패턴군 | 상세 종류 | 철근 라벨(H13~H29 개수) |",
"|---|---|---|---|"]
for r in S:
bars = ", ".join(f"{k} {v}" for k, v in r["rebar_sizes_label_count"].items())
for i, g in enumerate(r["pattern_details"]):
lab = bars if i == 0 else ""
lines.append(f"| {r['file'][:14]} | {g['group']} | {g['kind']} | {lab} |")
lines += ["",
"- 철근 라벨 개수는 도면 시트 단위 합계다 (패턴군별 분리 불가). 첫 행에만 표기했다.\n",
"## 3. 공통 재료 사양 (6장 동일)",
f"- 콘크리트: {conc} ({S[0]['concrete'][0]})",
f"- 철근: {rebar}, 도면 표기 SD400 ({S[0]['rebar_grade'][0]})",
f"- 굵은골재 최대치수: Ø{agg} mm",
"- 설계방법: 강도설계법, 피복두께 6 cm 이상 확보 (주기 1)",
"- 이음장 600 mm (도면 치수), 겹침이음 1 cycle (도면 상세)",
"- 라이닝 뒷면 주입재 강도 1.0 MPa 기대 (주기 11)",
"- 천정부 방수쉬트 부착형 앵커, 철근처짐방지 앵커 적용 (주기 4, 12)\n",
"## 4. 기준선과 치수",
f"- 도면 표기 R1 = {r1_drawn} (6장 모두 동일 표기)",
"- 표준단면 치수표(설계_표준단면치수표) R1 = 6,982 mm, 골격 모델은 6,982 mm 사용",
f"- 라이닝 외면 R 7.282 m 에서 내면 R 6.982 m 를 빼면 두께 0.300 m, 표기 6.892 m 를 쓰면 0.390 m 이다. 표준단면 두께 300 mm 와 맞는 값은 6.982 m 이다.",
"- R2 = 3,712 mm, R2′ = 3,910 mm (도면 표기, 표준단면 치수표와 일치)",
"- EL. 0.000 = 노면 기준 (계획고 F = 0.000 과 일치 기록)",
"- EL. -0.367 = 아치 중심 높이 (골격의 중심 c = -0.367 m 와 일치)\n",
"## 5. 충돌·미확정",
"- [확정 2026-10-21, 사용자 결정] R1 = 6.982 m 유지. 도면의 6.892 m 표기는 확인 대상 오기로 기록한다.",
"- [미확정] 시트별 패턴군이 어느 측점 구간인지 (도면에 측점 없음)",
"- [확인 필요] 라이닝 패턴군(A~C-2, D-1~2, E-1~2, WL-1~4, RP-1~2, P-2~3)과 굴착 순서도 패턴명(NP-4AD-1-D-2 등)의 대응. 사용자는 파일명에 기록되어 있다고 답했다. 예시 풀이(NP-5BE-1-WL-4 = NP-5B 의 D-1·D-2)는 파일명 속 WL-4 및 시트 패턴군(WL-3~4)과 맞지 않아 재확인이 필요하다.",
"- 재료표 수량(총 중량)은 이번 판독에서 추출하지 않았다. 필요하면 재료표 추출을 별도 Task로 한다.",
"- 철근 배치는 골격 LOD 범위 밖이라 모델에 넣지 않았다 (사용자 승인 3D 상세 수준: 축선·표준단면 스윕 골격).",
"- 설계 조건과 상이하면 구조해석 검토 후 철근 보강 가능 (주기 5, 시공 중 판단 대상)\n",
"## 6. 확인 요청 (HITL)",
"1. NP-5BE-1-WL-4 의 D-1·D-2 풀이가 맞는지, 아니면 WL-4 가 어느 라이닝 패턴군인지",
"2. 측점별 지보패턴 구간표가 확보되면 그 값으로 placeholder 구간을 교체할지",
""]
open(OUT, "w", encoding="utf-8").write("\n".join(lines))
print("written", OUT, len(lines), "lines")
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Render the station-by-station lining pattern report (DOC/03_설계) from the
CSVs written by extract_pattern_table.py. Values are read, not typed.
Usage: make_pattern_doc.py <dangjin.csv> <daesan_ref.csv> <lining_spec.json> <out.md>"""
import csv
import json
import sys
DJ, DS, LS, OUT = sys.argv[1:5]
dj = list(csv.DictReader(open(DJ, encoding="utf-8")))
ds = list(csv.DictReader(open(DS, encoding="utf-8")))
lin = json.load(open(LS, encoding="utf-8"))["sheets"]
drawing_groups = sorted({g["group"] for s in lin for g in s["pattern_details"] if g["group"]})
dj_names = {r["lining_pattern"] for r in dj}
ds_names = {r["lining_pattern"] for r in ds}
# Drawing group labels (read from the sheet notes, 04-004..009) -> member patterns.
# Explicit table: the labels mix letter ranges (A~C-2) and number ranges (WL-1~2).
DRAWING_MEMBERS = {"A~C-2": ["A", "B-1", "B-2", "C-1", "C-2"], "D-1~2": ["D-1", "D-2"],
"E-1~2": ["E-1", "E-2"], "WL-1~2": ["WL-1", "WL-2"], "WL-1~2,RP-1~2": ["WL-1", "WL-2", "RP-1", "RP-2"],
"WL-3~4": ["WL-3", "WL-4"], "P-2~3": ["P-2", "P-3"], "RP-1~2": ["RP-1", "RP-2"]}
members = set()
for g in drawing_groups:
members |= set(DRAWING_MEMBERS.get(g, [g]))
missing_dj = sorted(m for m in members if m not in dj_names)
only_ds = sorted(m for m in missing_dj if m in ds_names)
none_any = sorted(m for m in missing_dj if m not in ds_names)
L = ["# 측점별 콘크리트 라이닝 지보패턴표 (당진방향) (초안)\n",
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
"| v0.1.00 | 2026-10-21 | 최초 작성. 엑셀 `참고_지보패턴현황` 당진방향 표를 측점별로 정리 (사용자 결정: 모델 placeholder 대체) |\n",
"## 1. 출처와 범위",
"- 원본: `RAG/03_터널명(대산2터널)/지보패턴현황_20260918.xlsx`, 시트 `참고_지보패턴현황`, 당진방향 표 (원본 행 55~96)",
f"- 행 {len(dj)}개, 측점 {dj[0]['sta_from']} ~ {dj[-1]['sta_to']}, 빈틈 없음 (각 행 끝 = 다음 행 시작)",
"- 모델 적용 범위 4+745 ~ 5+480 은 이 표로 전 구간 덮임 (735 m)",
"- 지보패턴(M열) = 라이닝 패턴, 실시설계(O열) = 굴착 등급. 실시설계 칸이 빈 행은 위 행의 값을 이어 받음 (병합 셀 해석, [추론])\n",
"## 2. 측점별 표",
"| 측점 from | 측점 to | 연장(m) | 지보패턴 | 실시설계 | 굴착공법 | 비고 |",
"|---|---|---|---|---|---|---|"]
def sta(x):
v = int(float(x)); return f"{v // 1000}+{v % 1000:03d}"
for r in dj:
exc = r["excavation"] + (" (이어받음)" if r["excavation_inherited"] == "True" and r["excavation"] else "")
L.append(f"| {sta(r['sta_from'])} | {sta(r['sta_to'])} | {float(r['length_m']):g} | {r['lining_pattern']} | {exc} | {r['method'].replace(chr(10), ' ')} | {r['remark']} |")
L += ["", "## 3. 도면과 표의 대응",
f"- 라이닝 도면(04-004~009)의 패턴 {len(members)}개 중 당진 표에 있는 것: {len(members) - len(missing_dj)}개",
f"- 당진 표에 없고 대산 표에만 있는 것: {', '.join(only_ds) if only_ds else '없음'} (대산 표 값 참고만, 적용 안 함 — 아래 4항)",
f"- 두 표 모두 없는 것: {', '.join(none_any) if none_any else '없음'} → 측점 지정 불가, [미확정]\n",
"## 4. 확인 필요 (HITL)",
"- [확인 필요] 대산방향 표의 WL-4 (4+758.85~4+768.85, NP-5) 를 당진 측점에 적용할지. 두 방향이 별도 튜브라는 점은 [추론]이며 확인되지 않았다. 당진 표는 같은 측점에 WL-2/WL-1 을 보여 준다.",
"- [확인 필요] 말씀하신 `NP-5BE-1-WL-4` 의 풀이 (NP-5B 의 E-1 ~ WL-4). 당진 NP-5 구간은 WL-2·WL-1 뿐이고, E-1 은 두 표 어디에도 없다.",
"- [확인 필요] 엑셀 `개요2`·`1` 시트: 굴착 등급은 NP-4/5/6 (A·B 접미사 포함), 괄호 안은 지보 하위 패턴이라는 표기. 이 해석은 엑셀 표기와 일치하는 것으로 보이나 도면 명칭 대응은 확정하지 않았다.\n",
"## 5. 적용 결과 (골격 모델)",
"- placeholder 2 m 구간은 폐기하고 이 표로 대체 (사용자 결정 2026-10-21)",
"- 뷰어의 라이닝 클릭 값: 측점, 지보패턴(본 표), 라이닝 사양 (C0060304-004~009)",
""]
open(OUT, "w", encoding="utf-8").write("\n".join(L))
print("written", OUT, "rows", len(dj), "drawing groups", len(members), "missing in dangjin", missing_dj)
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Render the confirmed station table (지반고 = A열, 계획고 = B열, 20 m station
numbering per user decision 2026-10-21) into DOC/03_설계 and check the design
grade against the drawn label S=2.4913% (정상 구배 검산). No value is adjusted."""
import csv
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
rows = []
with open(SRC, encoding="utf-8") as fh:
rd = list(csv.DictReader(fh))
gl, dg = {}, {}
for r in rd:
sta = int(r["station_anchor"])
if float(r["across_m"]) < -25:
gl[sta] = float(r["value"])
else:
dg[sta] = float(r["value"])
stas = sorted(set(gl) & set(dg))
n = len(stas)
xs = [s for s in stas]
mx = sum(xs) / n
my = sum(dg[s] for s in stas) / n
slope = sum((s - mx) * (dg[s] - my) for s in stas) / sum((s - mx) ** 2 for s in stas)
grade_pct = slope * 100
resid = [dg[s] - (my + slope * (s - mx)) for s in stas]
max_res = max(abs(r) for r in resid)
lines = ["# 본선 종단 측점값 — 지반고·계획고 (초안, 확정 해석 반영)\n",
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
"| v0.1.00 | 2026-10-21 | 최초 작성. 02-002 종단 눈금 4+740~5+480, 지반고(A열)·계획고(B열) 확정 해석 반영 |\n",
"## 1. 확정 사항 (사용자 답변 2026-10-21)",
"- 소수 A열(중심선 측 약 -33m) = **지반고**, B열(약 -16m) = **계획고** [출처: C0060302-002, CR-TABL-TEX2]",
"- 측점은 **측점 번호 20m 기준**. 도면상 눈금 거리 21.27m는 사용하지 않음 (축척 1.0635배 불일치 기록)",
"- 공백 구간(4+700~4+740, 5+480~5+500)은 **[미확정]**, 확보 구간만 모델링\n",
"## 2. 측점별 지반고·계획고",
"| 측점(m) | 표기 | 지반고 GL(m) | 계획고(m) |", "|---|---|---|---|"]
for s in stas:
lines.append(f"| {s} | {s // 1000}+{s % 1000:03d} | {gl[s]:.3f} | {dg[s]:.3f} |")
lines += ["",
"## 3. 검산",
f"- 전구간 직선 회귀 구배 {grade_pct:.4f}%는 **단일 구배가 아님**을 뜻하므로 채택하지 않음 (잔차 최대 {max_res:.3f} m)",
"- 구간별 구배 (계획고 2점 기울기, 도면 표기 S=2.4913%와 비교):"]
for a, b in [(4740, 4800), (4800, 4900), (4900, 5000), (5000, 5100), (5100, 5200), (5200, 5300), (5300, 5400), (5400, 5480)]:
seg = [k for k in stas if a <= k <= b]
if len(seg) >= 2:
g = (dg[seg[-1]] - dg[seg[0]]) / (seg[-1] - seg[0]) * 100
lines.append(f" - {a // 1000}+{a % 1000:03d} ~ {b // 1000}+{b % 1000:03d}: {g:.4f}%")
lines += ["- 해석: 4+740~5+300은 2.491%로 S=2.4913%와 일치. 5+300 이후 구배 감소는 종곡선 구간으로 보이나 [추론], 종곡선 제원은 도면에서 확인 못함 [확인 못함]",
"- 지반고는 구배 검산 대상이 아님 (지형 값)\n",
"## 4. 미확정·후속 (OPEN 등재 대상)",
"- 4+700~4+740, 5+480~5+500 종단 값 없음 → [미확정]",
"- 허용오차 ±50mm 판정: 본 초안은 도면 표기 값 그대로이며, 3D 대조 판정은 Task 7 이후 수행",
"- 라이닝 두께 수치: 04-004~009 콘크리트라이닝 구조도 확인 예정 (사용자 지시 2026-10-21)\n"]
open(OUT, "w", encoding="utf-8").write("\n".join(lines) + "\n")
print(f"stations={n} grade={grade_pct:.4f}% max_resid={max_res:.3f}")
+42
View File
@@ -0,0 +1,42 @@
import sys, time, pickle
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
from build123d import import_step, Face
import numpy as np
shape = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step")
solids = shape.solids()
pickle_out = {}
import json
meta=json.load(open('/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids_meta.json'))
comps=[c for c in meta['components'] if c['group']!='시설한계(참조선)']
print("non-clearance comps", len(comps))
# match by volume greedily in meta order
used=set(); mapping={}
for c in comps:
best=None;bd=1e9
for j,s in enumerate(solids):
if j in used: continue
d=abs(s.volume-c['volume_m3'])
if d<bd: bd=d;best=j
if bd<0.02:
used.add(best); mapping[c['id']]=best
else:
mapping[c['id']]=None
print("matched", sum(v is not None for v in mapping.values()), "of", len(comps), "unused solids", len(solids)-len(used))
print("first 10 mapping", list(mapping.items())[:10])
pickle.dump({'mapping':mapping}, open('/tmp/verify_daesan2_run/mapping.pkl','wb'))
# faces of solid 0 (LIN_01)
for idx in [mapping['LIN_01_P-3']]:
s=solids[idx]
fs=s.faces()
print("solid",idx,"nfaces",len(fs), "vol", round(s.volume,3))
rows=[]
for f in fs:
gt=f.geom_type
c=f.center(); a=f.area
try:
n=f.normal_at(c)
except Exception as ex:
n=None
rows.append((gt, round(a,1), [round(x,3) for x in (c.X,c.Y,c.Z)], None if n is None else [round(x,4) for x in (n.X,n.Y,n.Z)]))
rows.sort(key=lambda r: -r[1])
for r in rows[:25]: print(r)
+69
View File
@@ -0,0 +1,69 @@
import sys
exec(open('/tmp/verify_daesan2_run/scripts/hd2.py').read().split('allres={}')[0])
from scipy.spatial import cKDTree
import numpy as np
extra={
'PAV_sideL': seg(9526.3,15764.0,9526.3,15484.0), 'PAV_sideR': seg(18726.3,15580.0,18726.3,15300.0),
'DRN_topL': seg(9026.3,15694.0,9476.3,15703.0), 'DRN_topR': seg(18776.3,15519.0,19226.3,15510.0),
}
REF.update(extra)
def dd(A,B):
d1,_=cKDTree(A).query(B); d2,_=cKDTree(B).query(A); return d1,d2
for st in ['4745.0005','5100.0','5469.9995']:
print("=== station", st, "sigma=+1 slope(dxf)", round(FIT[st]['slope_top'],5))
L=to_dxf(pts(st,lambda g,i:g=='라이닝'),st,1); E=to_dxf(pts(st,lambda g,i:g=='굴착면'),st,1)
P=to_dxf(pts(st,lambda g,i:g=='노면(계획고)'),st,1); B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,1)
D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,1)
# LIN full arcs (no clipping) and clipped to model extent
arcs=['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']
for k in arcs:
d,_=cKDTree(L).query(REF[k]); print(f" LIN {k:18s} max={d.max():7.1f} mm (at y_min_ref={REF[k][:,1].min():.0f})")
ymin_L=L[L[:,0]<C0[0]][:,1].min(); ymin_R=L[L[:,0]>C0[0]][:,1].min()
lr=np.vstack([REF[k] for k in arcs]); d,_=cKDTree(L).query(lr)
inside=((lr[:,0]<C0[0])&(lr[:,1]>=ymin_L))|((lr[:,0]>=C0[0])&(lr[:,1]>=ymin_R))
print(f" LIN within model extent (y>={ymin_L:.0f}/{ymin_R:.0f}) max={d[inside].max():.1f} mm; drawn part below model extent (not modelled) n={int((~inside).sum())} pts, y range {lr[~inside,1].min():.0f}-{lr[~inside,1].max():.0f}")
fl=np.vstack([REF['LIN_footL'],REF['LIN_footR']]); a,b=dd(L,fl); print(f" LIN foot lines (CS-CONC-MAJR wall/foot) ref->model max={a.max():.1f} model->ref(near band) ")
# EXC
ea=['EXC_crown','EXC_haunchL','EXC_haunchR']
eymL=E[E[:,0]<C0[0]][:,1].min(); eymR=E[E[:,0]>C0[0]][:,1].min()
er=np.vstack([REF[k] for k in ea]); d,_=cKDTree(E).query(er)
ins=((er[:,0]<C0[0])&(er[:,1]>=eymL))|((er[:,0]>=C0[0])&(er[:,1]>=eymR))
print(f" EXC arcs within model extent (y>={eymL:.0f}/{eymR:.0f}) max={d[ins].max():.1f} mm; crown max={cKDTree(E).query(REF['EXC_crown'])[0].max():.1f}; below model extent n={int((~ins).sum())} y {er[~ins,1].min():.0f}-{er[~ins,1].max():.0f}")
# PAV model vs drawn top/bottom/sides
pref=np.vstack([REF['PAV_top'],REF['PAV_bot'],REF['PAV_sideL'],REF['PAV_sideR']])
Pm=P[(P[:,0]>9526.3-150)&(P[:,0]<18726.3+150)]
a,b=dd(Pm,pref); print(f" PAV ref->model max={a.max():.1f} mm ; model->ref max={b.max():.1f} mm (top/bottom/sides)")
# BASE: top envelope vs base top line; bottom envelope vs base bottom line, within road span
bw=B[(B[:,0]>=9526.3)&(B[:,0]<=18726.3)]
xs=np.linspace(9526.3,18726.3,921)
top_line=15484.0+(xs-9526.3)*(15300-15484)/9200.0; bot_line=15334.0+(xs-9526.3)*(15150-15334)/9200.0
# model envelope per x-bin (1 m bins for robust): max y (top) and min y (bottom)
tops=[];bots=[]
for x0 in np.arange(9526.3,18726.3,200):
m=(bw[:,0]>=x0)&(bw[:,0]<x0+200)
if m.sum()==0: continue
xm=bw[m,0].mean(); tops.append((xm,bw[m,1].max())); bots.append((xm,bw[m,1].min()))
tops=np.array(tops); bots=np.array(bots)
et=np.abs(tops[:,1]-np.interp(tops[:,0],xs,top_line)); eb=np.abs(bots[:,1]-np.interp(bots[:,0],xs,bot_line))
print(f" BASE top envelope vs drawn top (15484->15300): max={et.max():.1f} mm, mean={et.mean():.1f}; bottom envelope vs drawn bottom (15334->15150): max={eb.max():.1f} mm, mean={eb.mean():.1f}")
print(f" BASE model bottom envelope at span ends: x={bots[0,0]:.0f} y={bots[0,1]:.1f} / x={bots[-1,0]:.0f} y={bots[-1,1]:.1f}; top envelope ends y={tops[0,1]:.1f}/{tops[-1,1]:.1f}")
print(f" BASE model extent outside road span: x {B[:,0].min():.0f}..{B[:,0].max():.0f}, y {B[:,1].min():.0f}..{B[:,1].max():.0f}")
# DRN
dref=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR','DRN_topL','DRN_topR']])
dm=D[(D[:,0]>8900)&(D[:,0]<19300)]
a,b=dd(dm,dref); print(f" DRN ref->model max={a.max():.1f} mm ; model->ref max={b.max():.1f} mm (walls, bottoms, pipes, tops)")
# vertical datum check at tunnel centre and road centre
fit=FIT[st]; uc,vc,_=fit['outer']
top_c=fit['slope_top']*(0-uc)+0 ;
# top envelope at u for DXF tunnel centre (u=uc) and road centre (x=9776.3 -> u=uc-4.35)
uu=P[:,0];
def top_env(xdxf):
um=uc+(xdxf-C0[0])/1000.0
m=np.abs(pts(st,lambda g,i:g=='노면(계획고)')[:,0]-um)<0.05
return pts(st,lambda g,i:g=='노면(계획고)')[m,1].max()
Pm_raw=pts(st,lambda g,i:g=='노면(계획고)')
for lab,xdxf,ydxf in [('tunnel centre x=14126.3',14126.3,15672.0),('road centre x=9776.3',9776.3,15759.0)]:
um=uc+(xdxf-C0[0])/1000.0
m=np.abs(Pm_raw[:,0]-um)<0.05
yt=(Pm_raw[m,1].max()-vc)*1000; yb=(Pm_raw[m,1].min()-vc)*1000
print(f" datum {lab}: model top={yt:.1f} mm, bottom={yb:.1f} mm above lining centre | DXF top at this x = {ydxf-15305:.1f} mm above lining centre")
+23
View File
@@ -0,0 +1,23 @@
import sys
exec(open('/tmp/verify_daesan2_run/scripts/final.py').read().split("for st in ['4745.0005','5100.0','5469.9995']:")[0])
for st in ['4745.0005','5100.0']:
B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,1); D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,1)
bw=B[(B[:,0]>=9526.3+150)&(B[:,0]<=18726.3-150)]
xs=np.arange(9676.3,18576.3,50.0)
top_line=lambda x: 15484.0+(x-9526.3)*(15300-15484)/9200.0
bot_line=lambda x: 15334.0+(x-9526.3)*(15150-15334)/9200.0
tt=[];bb=[]
for x0 in xs:
m=(bw[:,0]>=x0-25)&(bw[:,0]<x0+25)
if m.sum()==0: continue
tt.append(bw[m,1].max()-top_line(x0)); bb.append(bw[m,1].min()-bot_line(x0))
tt=np.array(tt); bb=np.array(bb)
print(f"{st}: BASE top (pavement-bottom interface), interior 150mm from ends: model-drawn max|dev|={np.abs(tt).max():.1f} mm, mean={tt.mean():.1f} mm (signed mean)")
print(f"{st}: BASE bottom (drawn excavation floor 15334->15150), interior: max|dev|={np.abs(bb).max():.1f} mm, mean={bb.mean():.1f} mm (signed; negative=model below drawing)")
# drains body vs lid
body=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']])
lid=np.vstack([REF['DRN_topL'],REF['DRN_topR']])
dm=D[(D[:,0]>8900)&(D[:,0]<19300)]
a,_=cKDTree(dm).query(body); b,_=cKDTree(dm).query(lid)
print(f"{st}: DRN body (walls/bottom/pipe) ref->model max={a.max():.1f} mm; lid (450-wide top line) ref->model max={b.max():.1f} mm")
print(f"{st}: DRN model x-extent per ditch: left {dm[dm[:,0]<14126,0].min():.0f}-{dm[dm[:,0]<14126,0].max():.0f}, right {dm[dm[:,0]>14126,0].min():.0f}-{dm[dm[:,0]>14126,0].max():.0f}")
@@ -0,0 +1,102 @@
import sys, json, pickle, collections
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
import numpy as np
from build123d import import_step, Shape
from OCP.BRepAlgoAPI import BRepAlgoAPI_Section
from OCP.gp import gp_Pln, gp_Pnt, gp_Dir
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_EDGE
solids = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step").solids()
mp = pickle.load(open('/tmp/verify_daesan2_run/mapping.pkl','rb'))['mapping']
caps = json.load(open('/tmp/verify_daesan2_run/caps.json'))
ch = json.load(open('/tmp/verify_daesan2_run/chain.json'))
meta = json.load(open('/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids_meta.json'))
comps = {c['id']: c for c in meta['components'] if c['group']!='시설한계(참조선)'}
solid_comp = {v:k for k,v in mp.items()}
# 1) boundary points from lining chain
pts = []
for k in ch['lin']:
pts.append(np.array(ch['starts'][k])); pts.append(np.array(ch['ends'][k]))
P = np.array(pts)[:, :2]
uniq = []
for p in P:
if not any(np.linalg.norm(p-q) < 1e-3 for q in uniq): uniq.append(p)
uniq = np.array(uniq)
print("unique boundary points", len(uniq), "(expect 41)")
D = P[-1]-P[0] if False else (np.array(ch['ends'][ch['lin'][-1]])[:2]-np.array(ch['starts'][ch['lin'][0]])[:2])
proj = uniq@D/np.linalg.norm(D)
order = np.argsort(proj); B = uniq[order]
cum = np.concatenate([[0], np.cumsum(np.linalg.norm(np.diff(B,axis=0),axis=1))])
Sb = 4745 + cum
M = sorted(set([c['from_sta'] for c in comps.values()] + [5480]))
print("boundary count", len(Sb), "meta boundary count", len(M))
print("max |station_chain - meta_boundary| (m):", round(float(np.max(np.abs(Sb-np.array(M)))),3) if len(Sb)==len(M) else 'count mismatch')
# 2) station for each solid by centroid projection
def station_of(p):
best=None
for k in range(len(B)-1):
a,b=B[k],B[k+1]; t=b-a; L2=t@t
f=np.clip(((p-a)@t)/L2,0,1); q=a+f*t; d=np.linalg.norm(p-q)
if best is None or d<best[0]: best=(d, Sb[k]+f*(Sb[k+1]-Sb[k]), k, f)
return best
sol_info={}
for i,s in enumerate(solids):
c=s.center(); d,st,k,f=station_of(np.array([c.X,c.Y]))
cid=solid_comp[i]; sol_info[i]=dict(cid=cid, group=comps[cid]['group'], st=st, lat=d)
# 3) pair within group by sorted mid
assign={}
groups=collections.defaultdict(list)
for i,v in sol_info.items(): groups[v['group']].append(i)
maxres=0
for g,idxs in groups.items():
cs=[c for c in comps.values() if c['group']==g]
idxs_s=sorted(idxs, key=lambda i: sol_info[i]['st'])
cs_s=sorted(cs, key=lambda c: (c['from_sta']+c['to_sta'])/2)
if len(idxs_s)!=len(cs_s): print("COUNT MISMATCH", g, len(idxs_s), len(cs_s))
for i,c in zip(idxs_s, cs_s):
mid=(c['from_sta']+c['to_sta'])/2
res=abs(sol_info[i]['st']-mid); maxres=max(maxres,res)
assign[i]=dict(id=c['id'], group=g, frm=c['from_sta'], to=c['to_sta'], mid_chain=sol_info[i]['st'], lat=sol_info[i]['lat'])
print("max residual solid-centre station vs comp mid (m):", round(maxres,3))
# lateral offset sanity
print("max lateral offset of solid centroid from chain (m):", round(max(v['lat'] for v in assign.values()),3))
# 4) slice
def slice_solid(idx, Q, T):
nrm=np.array([T[0],T[1],0.0])
pl=gp_Pln(gp_Pnt(float(Q[0]),float(Q[1]),0.0), gp_Dir(float(nrm[0]),float(nrm[1]),0.0))
face=BRepBuilderAPI_MakeFace(pl,-80,80,-80,80).Face()
sec=BRepAlgoAPI_Section(solids[idx].wrapped, face); sec.Build()
polys=[]
exp=TopExp_Explorer(sec.Shape(), TopAbs_EDGE)
while exp.More():
e=Shape.cast(exp.Current()); L=e.length; N=max(2,int(L/0.02))
polys.append([[p.X,p.Y,p.Z] for p in (e.position_at(j/N) for j in range(N+1))])
exp.Next()
return polys
def plane_at(s):
k=int(np.searchsorted(Sb, s, side='right')-1); k=min(max(k,0),len(B)-2)
f=(s-Sb[k])/(Sb[k+1]-Sb[k])
Q=(1-f)*B[k]+f*B[k+1]; T=(B[k+1]-B[k]); T=T/np.linalg.norm(T)
return Q, T
out={}
for s in [4745.0005, 5100.0, 5469.9995]:
Q,T=plane_at(s); eh=np.array([-T[1],T[0]])
print(f"== station {s}: Q=({Q[0]:.3f},{Q[1]:.3f}) T=({T[0]:.5f},{T[1]:.5f})")
out[str(s)]={'Q':Q.tolist(),'T':T.tolist(),'eh':eh.tolist(),'solids':{}}
cnt=collections.Counter()
for i,a in assign.items():
if not (a['frm'] <= s <= a['to']): continue
polys=slice_solid(i,Q,T)
uv=[]
for pl_ in polys:
arr=np.array(pl_); u=(arr[:,:2]-Q)@eh; uv.append(np.stack([u,arr[:,2]],1).tolist())
out[str(s)]['solids'][str(i)]={'id':a['id'],'group':a['group'],'polys':uv}
cnt[a['group']]+=1
allp=np.vstack([np.array(x) for x in uv]) if uv else np.zeros((0,2))
if len(allp): print(f" {a['group']:<12} {a['id']:<16} nloops={len(uv)} u[{allp[:,0].min():.3f},{allp[:,0].max():.3f}] v[{allp[:,1].min():.3f},{allp[:,1].max():.3f}]")
else: print(" EMPTY", a['group'], a['id'])
print(" counts", dict(cnt))
json.dump(out, open('/tmp/verify_daesan2_run/sections_final.json','w'))
json.dump({str(i):a for i,a in assign.items()}, open('/tmp/verify_daesan2_run/assign.json','w'), ensure_ascii=False)
print("saved")
+110
View File
@@ -0,0 +1,110 @@
import sys, json, math
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
import numpy as np
from scipy.spatial import cKDTree
SEC=json.load(open('/tmp/verify_daesan2_run/sections_final.json'))
FIT=json.load(open('/tmp/verify_daesan2_run/fits.json'))
C0=(14126.3,15305.0)
def pts(st, pred):
out=[]
for sid,v in SEC[st]['solids'].items():
if pred(v['group'], v['id']):
for poly in v['polys']: out.append(np.array(poly)[:,:2])
return np.vstack(out)
def arc(cx,cy,r,a0,a1,step=1.0):
if a1<a0: a1+=360
n=max(2,int(r*math.radians(a1-a0)/step)); th=np.radians(np.linspace(a0,a1,n))
return np.c_[cx+r*np.cos(th), cy+r*np.sin(th)]
def seg(x0,y0,x1,y1,step=1.0):
n=max(2,int(math.hypot(x1-x0,y1-y0)/step)); t=np.linspace(0,1,n); return np.c_[x0+(x1-x0)*t,y0+(y1-y0)*t]
def circ(cx,cy,r,step=1.0):
n=max(8,int(2*math.pi*r/step)); th=np.linspace(0,2*math.pi,n,endpoint=False); return np.c_[cx+r*np.cos(th),cy+r*np.sin(th)]
REF={
'LIN_crown_outer': arc(*C0,7282,40,140), 'LIN_crown_inner': arc(*C0,6982,40,140),
'LIN_haunchL_outer': arc(11959.9,17122.8,4454,140,204.67), 'LIN_haunchL_inner': arc(11621.3,17406.9,3712,140,195.56),
'LIN_haunchR_outer': arc(16105.0,16965.3,4699,336.35,40), 'LIN_haunchR_inner': arc(16479.6,17279.7,3910,344.38,40),
'LIN_footL': np.vstack([seg(8171.3,16410.9,8471.3,16410.9), seg(8471.3,16410.9,8621.3,16410.9), seg(8621.3,16410.9,8651.3,16000.9), seg(8651.3,16000.9,8771.3,15825.9), seg(8771.3,15825.9,8771.3,15750.9)]),
'LIN_footR': np.vstack([seg(20181.3,16226.9,19781.3,16226.9), seg(19781.3,16226.9,19631.3,16226.9), seg(19631.3,16226.9,19601.3,15816.9), seg(19601.3,15816.9,19481.3,15641.9), seg(19481.3,15641.9,19481.3,15566.9)]),
'EXC_crown': arc(*C0,7402,40,140), 'EXC_haunchL': arc(11959.9,17122.8,4574,140,203.98), 'EXC_haunchR': arc(16105.0,16965.3,4819,336.97,40),
'PAV_top': seg(9526.3,15764.0,18726.3,15580.0), 'PAV_bot': seg(9526.3,15484.0,18726.3,15300.0), 'PAV_sideL': seg(9526.3,15764.0,9526.3,15484.0), 'PAV_sideR': seg(18726.3,15580.0,18726.3,15300.0),
'BASE_top': seg(9526.3,15484.0,18726.3,15300.0), 'BASE_bot': seg(9526.3,15334.0,18726.3,15150.0),
'DRN_wallL1': seg(9101.3,14864.0,9101.3,15695.5), 'DRN_wallL2': seg(9401.3,14864.0,9401.3,15701.5), 'DRN_botL': seg(9101.3,14864.0,9401.3,14864.0), 'DRN_pipeL': circ(9251.3,15064.0,150),
'DRN_wallR1': seg(18851.3,14680.0,18851.3,15517.5), 'DRN_wallR2': seg(19151.3,14680.0,19151.3,15511.5), 'DRN_botR': seg(18851.3,14680.0,19151.3,14680.0), 'DRN_pipeR': circ(19001.3,14880.0,150),
}
def to_dxf(P, st, sig):
uc,vc,_=FIT[st]['outer']
return np.c_[C0[0]+sig*(P[:,0]-uc)*1000.0, C0[1]+(P[:,1]-vc)*1000.0]
def dist_stats(M, R, label):
"""M model pts (dxf frame), R reference pts. Returns d(ref->model) max, d(model->ref) max within band."""
if len(M)==0 or len(R)==0: return None
tM=cKDTree(M); tR=cKDTree(R)
dR,_=tM.query(R); dM,_=tR.query(M)
return dict(ref_to_model_max=float(dR.max()), ref_to_model_mean=float(dR.mean()), model_to_ref_max=float(dM.max()), model_to_ref_mean=float(dM.mean()), n_ref=len(R), n_model=len(M))
def run(sig, st, feats):
res={}
L=to_dxf(pts(st,lambda g,i:g=='라이닝'),st,sig); E=to_dxf(pts(st,lambda g,i:g=='굴착면'),st,sig)
P=to_dxf(pts(st,lambda g,i:g=='노면(계획고)'),st,sig); B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,sig)
D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,sig)
def side_clip(R, M, side):
# keep reference points on given side (L: x<C0x, R: x>C0x) and above model's lowest y on that side minus 50mm
sm = M[(M[:,0]<C0[0])] if side=='L' else M[(M[:,0]>C0[0])]
sr = R[(R[:,0]<C0[0])] if side=='L' else R[(R[:,0]>C0[0])]
if len(sm)==0: return sr, 0, None
ymin=sm[:,1].min()-0.05*1000
keep=sr[sr[:,1]>=ymin]
dropped=len(sr)-len(keep)
rng=None if dropped==0 else [round(float(sr[sr[:,1]<ymin,1].min())),round(float(sr[sr[:,1]<ymin,1].max()))]
return keep, dropped, rng
out={}
# Lining: arcs and foot lines; clip below model extent per side
lin_ref=np.vstack([REF[k] for k in ['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']])
keepL,dL,rL=side_clip(lin_ref[lin_ref[:,0]<C0[0]],L,'L'); keepR,dR_,rR=side_clip(lin_ref[lin_ref[:,0]>C0[0]],L,'R')
crown=REF['LIN_crown_outer'];
out['LIN_arcs']=dist_stats(L, np.vstack([crown, REF['LIN_crown_inner'], keepL, keepR]), 'LIN')
out['LIN_arcs']['clipped_below_model_extent']={'L':dL,'R':dR_,'yrange_L':rL,'yrange_R':rR}
out['LIN_footlines']=dist_stats(L, np.vstack([REF['LIN_footL'],REF['LIN_footR']]), 'LIN_foot')
# per-arc max deviations (ref->model), by arc name
perarc={}
for k in ['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']:
R=REF[k]; tM=cKDTree(L); d,_=tM.query(R); perarc[k]=round(float(d.max())*1,1)
out['LIN_perarc_ref2model_max_mm']=perarc
# Excavation
exc_ref=np.vstack([REF['EXC_crown'],REF['EXC_haunchL'],REF['EXC_haunchR']])
keepE_L,eL,reL=side_clip(exc_ref,E,'L'); keepE_R,eR,reR=side_clip(exc_ref,E,'R')
out['EXC_arcs']=dist_stats(E, np.vstack([REF['EXC_crown'],keepE_L,keepE_R]),'EXC')
out['EXC_arcs']['clipped_below_model_extent']={'L':eL,'R':eR,'yrange_L':reL,'yrange_R':reR}
tE=cKDTree(E); d,_=tE.query(REF['EXC_crown']); out['EXC_crown_ref2model_max_mm']=round(float(d.max()),1)
# Pavement: top/bottom/sides
pav_ref=np.vstack([REF['PAV_top'],REF['PAV_bot'],REF['PAV_sideL'],REF['PAV_sideR']])
out['PAV']=dist_stats(P[(P[:,0]>9526.3-150)&(P[:,0]<18726.3+150)], pav_ref,'PAV')
# Base: model points restricted to road width (drawn span)
Bw=B[(B[:,0]>=9526.3-1)&(B[:,0]<=18726.3+1)]
out['BASE_drawn_span']=dist_stats(Bw, np.vstack([REF['BASE_top'],REF['BASE_bot']]),'BASE')
out['BASE_model_extent_x']=[round(float(B[:,0].min())),round(float(B[:,0].max()))]
out['BASE_model_ymin_ymax']=[round(float(B[:,1].min())),round(float(B[:,1].max()))]
# Drains: both ditches vs both references
dref=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']])
dm=D[(D[:,0]>8900)&(D[:,0]<19300)]
out['DRN']=dist_stats(dm, dref,'DRN')
# per-ditch: ref walls/bottom/pipe to model
tD=cKDTree(dm)
out['DRN_perref_ref2model_max_mm']={k: round(float(tD.query(REF[k])[0].max()),1) for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']}
out['DRN_model_xrange']=[round(float(dm[:,0].min())),round(float(dm[:,0].max()))]
out['DRN_model_yrange']=[round(float(dm[:,1].min())),round(float(dm[:,1].max()))]
return out
allres={}
for sig in [1,-1]:
for st in ['4745.0005','5100.0','5469.9995']:
r=run(sig,st,None); allres[f"{sig}|{st}"]=r
print(f"=== sigma={sig} st={st} slope={FIT[st]['slope_top']*sig:.5f}")
for k in ['LIN_arcs','EXC_arcs','PAV','BASE_drawn_span','DRN']:
v=r[k];
if v is None: print(' ',k,'none'); continue
print(f" {k:16s} ref->model max={v['ref_to_model_max']*1000:7.1f}mm mean={v['ref_to_model_mean']*1000:6.1f} | model->ref max={v['model_to_ref_max']*1000:7.1f}mm mean={v['model_to_ref_mean']*1000:6.1f}")
if 'clipped_below_model_extent' in v: print(' clipped', v['clipped_below_model_extent'])
print(' LIN foot lines model->ref / ref->model:', None if r['LIN_footlines'] is None else (round(r['LIN_footlines']['model_to_ref_max']*1000,1), round(r['LIN_footlines']['ref_to_model_max']*1000,1)))
print(' LIN per-arc ref->model max mm:', r['LIN_perarc_ref2model_max_mm'])
print(' EXC crown ref->model max mm:', r['EXC_crown_ref2model_max_mm'])
print(' BASE model x-range', r['BASE_model_extent_x'], 'y-range', r['BASE_model_ymin_ymax'])
print(' DRN per-ref ref->model max mm:', r['DRN_perref_ref2model_max_mm'], 'model x', r['DRN_model_xrange'], 'y', r['DRN_model_yrange'])
json.dump(allres, open('/tmp/verify_daesan2_run/hd2_results.json','w'), indent=1, default=str)
+44
View File
@@ -0,0 +1,44 @@
import sys, json, math
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
import numpy as np
from scipy.spatial import cKDTree
from scipy.optimize import least_squares
import matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt
SEC = json.load(open('/tmp/verify_daesan2_run/sections_final.json'))
def pts_of(st, pred):
out=[]; ids=[]
for sid,v in SEC[st]['solids'].items():
if pred(v['group'], v['id']):
for poly in v['polys']:
out.append(np.array(poly)[:, :2]); ids.append(v['id'])
return (np.vstack(out) if out else np.zeros((0,2))), ids
def fit_circle(P):
x,y=P[:,0],P[:,1]
A=np.c_[2*x,2*y,np.ones(len(x))]; b=x*x+y*y
c,_,_,_=np.linalg.lstsq(A,b,rcond=None)
xc,yc=c[0],c[1]; r=math.sqrt(c[2]+xc*xc+yc*yc)
return xc,yc,r
# ---- lining crown fit per station
fits={}
for st in SEC:
P,_=pts_of(st, lambda g,i: g=='라이닝')
u0=(P[:,0].min()+P[:,0].max())/2; v0=P[:,1].max()-7.282
d=np.hypot(P[:,0]-u0,P[:,1]-v0)
sel_o=P[(np.abs(d-7.282)<0.03)&(P[:,1]-v0>4.0)]
sel_i=P[(np.abs(d-6.982)<0.03)&(P[:,1]-v0>4.0)]
co=fit_circle(sel_o); ci=fit_circle(sel_i)
fits[st]={'outer':co,'inner':ci,'n_o':len(sel_o),'n_i':len(sel_i)}
print(f"st {st}: outer n={len(sel_o)} centre=({co[0]:.4f},{co[1]:.4f}) R={co[2]:.4f} | inner n={len(sel_i)} centre=({ci[0]:.4f},{ci[1]:.4f}) R={ci[2]:.4f}")
# ---- pavement slope (top/bottom envelopes) per station
for st in SEC:
P,_=pts_of(st, lambda g,i: g=='노면(계획고)')
bins=np.arange(P[:,0].min(),P[:,0].max()+0.25,0.25)
tops=[];bots=[]
for a,b in zip(bins[:-1],bins[1:]):
m=(P[:,0]>=a)&(P[:,0]<b)
if m.sum()>0: tops.append((P[m,0].mean(),P[m,1].max())); bots.append((P[m,0].mean(),P[m,1].min()))
tops=np.array(tops); bots=np.array(bots)
mt=np.polyfit(tops[:,0],tops[:,1],1); mb=np.polyfit(bots[:,0],bots[:,1],1)
fits[st]['slope_top']=mt[0]; fits[st]['slope_bot']=mb[0]
print(f"st {st}: PAV top slope dv/du={mt[0]:.5f} (S={mt[0]*100:.3f}%), bottom slope={mb[0]:.5f}; top@u=0 {mt[1]:.4f}, bottom@u=0 {mb[1]:.4f}; u-range [{P[:,0].min():.3f},{P[:,0].max():.3f}]")
json.dump(fits, open('/tmp/verify_daesan2_run/fits.json','w'))
+77
View File
@@ -0,0 +1,77 @@
import sys, json, pickle, math
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
from build123d import import_step, Edge, Shape
from OCP.BRepAlgoAPI import BRepAlgoAPI_Section
from OCP.gp import gp_Pln, gp_Pnt, gp_Dir
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_EDGE
from OCP.TopoDS import TopoDS
import numpy as np
solids = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step").solids()
mp = pickle.load(open('/tmp/verify_daesan2_run/mapping.pkl','rb'))['mapping']
caps = json.load(open('/tmp/verify_daesan2_run/caps.json'))
ch = json.load(open('/tmp/verify_daesan2_run/chain.json'))
lin = ch['lin']
starts = {k: np.array(v) for k,v in ch['starts'].items()}
ends = {k: np.array(v) for k,v in ch['ends'].items()}
# station knots
S = [caps[k]['from_sta'] for k in lin] + [caps[lin[-1]]['to_sta']]
P = [starts[k] for k in lin] + [ends[lin[-1]]]
def plane_at(s):
for i in range(len(lin)):
if S[i] <= s <= S[i+1]:
f=(s-S[i])/(S[i+1]-S[i])
Q=(1-f)*P[i]+f*P[i+1]
T=(P[i+1]-P[i])[:2]; T=T/np.linalg.norm(T)
return Q, T, lin[i]
raise ValueError(s)
def slice_solid(idx, Q, T):
nrm=np.array([T[0],T[1],0.0])
pl=gp_Pln(gp_Pnt(*Q), gp_Dir(*nrm))
face=BRepBuilderAPI_MakeFace(pl,-80,80,-80,80).Face()
sec=BRepAlgoAPI_Section(solids[idx].wrapped, face); sec.Build()
sh=sec.Shape()
polys=[]
exp=TopExp_Explorer(sh, TopAbs_EDGE)
while exp.More():
e=Shape.cast(exp.Current())
L=e.length; N=max(2,int(L/0.02))
pts=[e.position_at(i/N) for i in range(N+1)]
polys.append([[p.X,p.Y,p.Z] for p in pts])
exp.Next()
return polys
out={}
stations=[4745.0005, 5100.0, 5469.9995]
for s in stations:
Q,T,lk=plane_at(s)
eh=np.array([-T[1],T[0]])
print(f"== station {s}: comp {lk}, Q=({Q[0]:.4f},{Q[1]:.4f},{Q[2]:.4f}) T=({T[0]:.5f},{T[1]:.5f}) ")
out[str(s)]={'Q':Q.tolist(),'T':T.tolist(),'eh':eh.tolist(),'comps':{}}
for cid,info in caps.items():
if not (info['from_sta'] <= s <= info['to_sta']): continue
polys=slice_solid(mp[cid], Q, T)
uv=[]
for pl_ in polys:
arr=np.array(pl_)
dx=arr[:,:2]-Q[:2]
u=dx@eh; v=arr[:,2]
uv.append(np.stack([u,v],1).tolist())
out[str(s)]['comps'][cid]={'group':info['group'],'polys':uv}
allp=np.vstack([np.array(x) for x in uv]) if uv else np.zeros((0,2))
if info['group'] in ('라이닝','굴착면','노면(계획고)','기층·인버트(가정)','공동구(가정)') or cid.startswith('DRN') or 'DRN' in cid or 'HI' in cid:
pass
# summary per group
import collections
summ=collections.defaultdict(list)
for cid,v in out[str(s)]['comps'].items():
summ[v['group']].append(cid)
for g,cids in summ.items():
allp=[p for cid in cids for poly in out[str(s)]['comps'][cid]['polys'] for p in poly]
A=np.array(allp) if allp else np.zeros((0,2))
if len(A):
print(f" {g}: n_comp={len(cids)} n_pts={len(A)} u[{A[:,0].min():.3f},{A[:,0].max():.3f}] v[{A[:,1].min():.3f},{A[:,1].max():.3f}] ids={cids[:4]}")
else:
print(f" {g}: n_comp={len(cids)} EMPTY ids={cids[:4]}")
json.dump(out, open('/tmp/verify_daesan2_run/sections_raw.json','w'))
print("saved sections_raw.json")
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Step 3 cross-check of the lining deliverables (Task 6).
Re-reads the raw DXF texts (04-004..009 and 03-001) with a plain text search,
independent of extract_lining_spec.py, and compares them with the report and
the model meta. Prints PASS/FAIL per check with population counts.
Usage: verify_lining.py <RAG_ROOT> <DOC_LINING_MD> <DOC_STD_MD> <META_JSON>
"""
import glob
import json
import os
import re
import sys
import ezdxf
from ezdxf import recover
RAG, DOC_LIN, DOC_STD, META = sys.argv[1:5]
RESULTS = []
def check(name, ok, detail):
RESULTS.append((name, ok, detail))
print(f"[{'PASS' if ok else 'FAIL'}] {name}: {detail}")
def all_texts(path):
try:
doc = ezdxf.readfile(path)
except Exception:
doc, _ = recover.readfile(path)
out = []
for e in doc.modelspace():
if e.dxftype() == "TEXT":
out.append(e.dxf.text)
elif e.dxftype() == "MTEXT":
out.append(e.plain_text())
return out
sheets = sorted(glob.glob(os.path.join(RAG, "04_본선터널기타", "C0060304-00[4-9]*.dxf")))
std = glob.glob(os.path.join(RAG, "03_본선터널굴착", "C0060303-001*.dxf"))
raw = {os.path.basename(p)[:14]: all_texts(p) for p in sheets}
doc_lin = open(DOC_LIN, encoding="utf-8").read()
doc_std = open(DOC_STD, encoding="utf-8").read()
meta = json.load(open(META, encoding="utf-8"))
# C1 common values present in every sheet (population = 6 sheets)
common = {"fck=27MPa": "fck 27", "fy=400MPa": "SD400", "최대치수 Ø25": "Ø25",
"R1=6.892": "R1 6.892", "R2=3,712": "R2 3,712", "R2'=3,910": "R2' 3,910",
"EL.-0.367": "EL -0.367", "EL. 0.000": "EL 0.000"}
for token, label in common.items():
hits = [k for k, t in raw.items() if any(token in s for s in t)]
check(f"C1 {label} in all sheets", len(hits) == len(raw), f"{len(hits)}/{len(raw)} sheets (표본 아님, 전수)")
# C2 temperature-not-considered detail appears on 3 sheets (005, 006, 007)
temp = [k for k, t in raw.items() if any("온도미고려" in s for s in t)]
check("C2 온도미고려 detail sheets", sorted(temp) == sorted([k for k in raw if k[9:12] in ("005", "006", "007")]),
f"{len(temp)} sheets: {[k[9:12] for k in sorted(temp)]} (전수)")
# C3 pattern-group count in raw texts equals report rows (9)
groups = [s for t in raw.values() for s in t if "패턴" in s and "주철근 조립도" in s or (("패턴)" in s) and ("입출구부" in s or "온도미고려" in s))]
rows = len([l for l in doc_lin.splitlines() if l.startswith("| C0060304-")])
check("C3 pattern-detail count = report rows", len(groups) == rows,
f"raw {len(groups)} vs report {rows} (전수)")
# C4 R1 cross-check against the standard section drawing and the report table
std_txt = all_texts(std[0]) if std else []
std_has = any(("6.982" in s or "6982" in s or "6,982" in s) for s in std_txt)
check("C4 R1 6.982 in standard section (03-001 raw)", std_has, f"03-001 raw texts scanned: {len(std_txt)}")
check("C4b standard table DOC states R1 6,982", "6,982" in doc_std, "설계_표준단면치수표 본문 대조")
check("C4c report records decision", "6.982 m 유지" in doc_lin, "R1 결정 문구 존재")
# C5 thickness arithmetic: outer R 7.282 m minus inner R
t982 = round(7.282 - 6.982, 3); t892 = round(7.282 - 6.892, 3)
check("C5 thickness arithmetic", t982 == 0.300 and t892 == 0.390,
f"7.282-6.982={t982} m (matches 300 mm), 7.282-6.892={t892} m (does not match)")
# C6 V9 existence + name version + key content of OUTPUT
name = os.path.basename(DOC_LIN)
m = re.match(r"^설계_콘크리트라이닝상세_(\d{4}-\d{2}-\d{2})_v(\d+\.\d+\.\d{2})\.md$", name)
ver_hdr = re.search(r"\| (v\d+\.\d+\.\d{2}) \|", doc_lin)
check("C6 file name pattern and version", bool(m) and ver_hdr is not None and ver_hdr.group(1) == "v" + m.group(2),
f"name v{m.group(2) if m else '?'} vs header {ver_hdr.group(1) if ver_hdr else '?'}")
check("C6b OUTPUT key items present", all(k in doc_lin for k in ["fck", "SD400", "입출구부", "확정 2026-10-21"]),
"fck, SD400, 입출구부, 결정 문구")
# C7 meta: lining reference and open items
lr = meta.get("lining_reference", {})
check("C7 meta lining_reference 9 groups", sum(len(s["groups"]) for s in lr.get("sheets", [])) == 9,
f"{sum(len(s['groups']) for s in lr.get('sheets', []))} groups, status={lr.get('summary', '')[:40]}...")
check("C7b meta open records decision", any("확정 2026-10-21" in o for o in meta.get("open", [])), "open[] 결정 문구")
# C8 scripts never write to the RAG source tree
writes = []
for sc in glob.glob("scripts/*lining*.py"):
txt = open(sc, encoding="utf-8").read()
if re.search(r"open\([^)]*RAG[^)]*['\"]w", txt):
writes.append(sc)
check("C8 no script writes into RAG", not writes, f"scanned {len(glob.glob('scripts/*lining*.py'))} scripts")
n_fail = sum(1 for r in RESULTS if not r[1])
print(f"SUMMARY: {len(RESULTS) - n_fail} PASS / {n_fail} FAIL of {len(RESULTS)}")
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Step 3 deterministic checks for the solid model (Task 8, 대산2본선3D변환).
Usage: python3 verify_solids.py <project_dir> <pattern_csv>
project_dir : Code/3d-drawing folder (contains model/ and data/)
pattern_csv : data/pattern_stations_dangjin.csv
Checks (each prints PASS/FAIL with counts, method and population):
M1 meta counts per group M2 pattern runs contiguous 4745..5480
M3 runs == CSV rows (in range) M4 component ids and from/to/pattern match runs
M5 meta volume sum == verify json M6 pattern-run ids (PAV/BASE/DRN/HBOX/IBOX) present
S1 STEP re-import: solid count, validity, total volume vs meta (0.1 %)
S2 lining vs excavation overlap per run (volume, threshold 0.05 m2 x run length)
G1 GLB reload: geometry count and non-empty meshes
"""
import csv
import json
import os
import sys
from collections import Counter, defaultdict
PROJ, CSV = sys.argv[1], sys.argv[2]
S0, S1 = 4745, 5480
results = []
def rec(code, verdict, method, note=""):
results.append((code, verdict, method, note))
print(f"{code} {verdict} | {method} | {note}")
meta = json.load(open(os.path.join(PROJ, "model/tunnel_solids_meta.json"), encoding="utf-8"))
verify = json.load(open(os.path.join(PROJ, "data/verify_solids.json"), encoding="utf-8"))
comps = meta["components"]
runs = meta["pattern_runs"]
solids_meta = [c for c in comps if not c.get("reference")]
refs = [c for c in comps if c.get("reference")]
# M1 counts per group
cnt = Counter(c["group"] for c in solids_meta)
exp = {"라이닝": 40, "굴착면": 40, "노면(계획고)": 40, "기층·인버트(가정)": 40, "배수(측구)": 80, "공동구(가정)": 80}
bad = {g: (cnt.get(g, 0), n) for g, n in exp.items() if cnt.get(g, 0) != n}
rec("M1", "PASS" if not bad and len(solids_meta) == 320 and len(refs) == 6 else "FAIL",
f"meta components 전수 {len(comps)}", f"solids {len(solids_meta)} (expect 320), refs {len(refs)} (expect 6), group mismatch {bad}")
# M2 contiguity
runs_sorted = sorted(runs, key=lambda r: r["from"])
gaps = [(a["to"], b["from"]) for a, b in zip(runs_sorted, runs_sorted[1:]) if a["to"] != b["from"]]
ok2 = runs_sorted[0]["from"] == S0 and runs_sorted[-1]["to"] == S1 and not gaps
rec("M2", "PASS" if ok2 else "FAIL", f"pattern_runs 전수 {len(runs)}",
f"range {runs_sorted[0]['from']}..{runs_sorted[-1]['to']}, gaps/overlaps {gaps}")
# M3 runs vs CSV rows inside the model range
rows = list(csv.DictReader(open(CSV, encoding="utf-8")))
csv_in = [r for r in rows if float(r["sta_to"]) > S0 and float(r["sta_from"]) < S1]
# clip each CSV interval to the model range (row 95 runs 5468..5495 and is cut at 5480 by design)
csv_set = Counter((max(int(float(r["sta_from"])), S0), min(int(float(r["sta_to"])), S1), r["lining_pattern"]) for r in csv_in)
run_set = Counter((r["from"], r["to"], r["pattern"]) for r in runs)
mism = (csv_set - run_set) + (run_set - csv_set)
rec("M3", "PASS" if not mism else "FAIL",
f"CSV 전수 {len(rows)} 행 중 범위 내 {len(csv_in)} 행과 pattern_runs {len(runs)} 행 대조",
f"mismatch {dict(mism)}")
# M4 component from/to/pattern equal their run
run_by_id = {r["id"]: r for r in runs}
bad4 = []
for c in solids_meta:
if c["group"] == "라이닝":
r = run_by_id.get(c["id"])
if not r or (r["from"], r["to"], r["pattern"]) != (c["from_sta"], c["to_sta"], c["pattern"]):
bad4.append(c["id"])
rec("M4", "PASS" if not bad4 else "FAIL", f"라이닝 전수 {cnt['라이닝']} 대 run",
f"mismatch {bad4[:5]}")
# M5 volume sum
vol = sum(c["volume_m3"] for c in solids_meta)
ok5 = abs(vol - verify["volume_sum_m3"]) < 0.001
rec("M5", "PASS" if ok5 else "FAIL", f"meta 부피 합 전수 {len(solids_meta)} 개",
f"sum {vol:.3f} vs verify {verify['volume_sum_m3']}")
# M6 per-run component ids
need = ["LIN", "EXC", "PAV", "BASE", "DRNL", "DRNR", "HBOX", "IBOX"]
ids = {c["id"] for c in solids_meta}
miss = []
for i, r in enumerate(runs_sorted):
for pre in need:
cid = f"{pre}_{i+1:02d}_{r['pattern']}"
if pre in ("DRNL", "DRNR"):
cid = f"{pre}_{i+1:02d}_{r['pattern']}"
if cid not in ids and pre not in ("DRNL", "DRNR"):
miss.append(cid)
drn = [c for c in solids_meta if c["group"] == "배수(측구)"]
rec("M6", "PASS" if not miss and len(drn) == 80 else "FAIL",
f"run {len(runs_sorted)} x 부품 종류 {len(need)} 대조 (배수 L/R 포함 {len(drn)})",
f"missing {miss[:5]}")
# S1 STEP re-import
import build123d as bd # noqa: E402
step_path = os.path.join(PROJ, "model/tunnel_solids.step")
shape = bd.importers.import_step(step_path) if hasattr(bd, "importers") else bd.import_step(step_path)
solids = shape.solids()
n_valid = sum(1 for s in solids if s.is_valid)
vol_step = sum(s.volume for s in solids)
rel = abs(vol_step - vol) / vol
rec("S1", "PASS" if len(solids) == 320 and n_valid == len(solids) and rel < 0.001 else "FAIL",
f"STEP 재임포트 솔리드 전수 {len(solids)}, is_valid 전수 {n_valid}, 부피 재계산 합 vs meta",
f"count {len(solids)}, valid {n_valid}, vol_step {vol_step:.3f} vs meta {vol:.3f} (rel {rel:.2e})")
# S2 overlap lining vs excavation per run, using the STEP solids
def centroid(s):
return s.center()
lin = [s for s in solids]
# classify STEP solids by volume match to meta (only for overlap sampling)
meta_vol = sorted([(c["volume_m3"], c["group"], c["id"], c["from_sta"], c["to_sta"]) for c in solids_meta])
sample_ids = [r["id"] for r in runs_sorted[:: max(1, len(runs_sorted) // 4)]][:4]
lines = []
ok_overlap = True
for rid in sample_ids:
r = run_by_id[rid]
k = rid.split("_", 1)[1]
idx = [i for i, rr in enumerate(runs_sorted) if rr["id"] == rid][0] + 1
lin_c = next(c for c in solids_meta if c["id"] == f"LIN_{idx:02d}_{r['pattern']}")
exc_c = next(c for c in solids_meta if c["id"] == f"EXC_{idx:02d}_{r['pattern']}")
def find(vol_target):
best = min(solids, key=lambda s: abs(s.volume - vol_target))
assert abs(best.volume - vol_target) < 1e-3, "no STEP solid matches meta volume"
return best
sl = find(lin_c["volume_m3"]); se = find(exc_c["volume_m3"])
inter = sl.intersect(se)
iv = sum(x.volume for x in inter) if inter is not None else 0.0
length = r["to"] - r["from"]
thr = 0.05 * length
lines.append(f"{rid}: ovl {iv:.4f} m3 (thr {thr:.2f})")
if iv > thr:
ok_overlap = False
rec("S2", "PASS" if ok_overlap else "FAIL", f"샘플 run {len(sample_ids)}건 라이닝∩굴착 부피 (부피 매칭 솔리드)",
"; ".join(lines))
# G1 GLB reload
import trimesh # noqa: E402
glb = trimesh.load(os.path.join(PROJ, "model/tunnel_solids.glb"), force="scene")
geoms = list(glb.geometry.values())
empty = [g for g in geoms if len(g.faces) == 0]
rec("G1", "PASS" if len(geoms) == verify["glb_geometries"] and not empty else "FAIL",
f"GLB 로드 지오메트리 전수 {len(geoms)}", f"empty {len(empty)}, expect {verify['glb_geometries']}")
fails = [r for r in results if r[1] == "FAIL"]
print("SUMMARY", len(results), "checks, FAIL", len(fails))
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Headless browser check of the 3D viewer (Task 6, V11).
Needs Playwright + Chromium and the viewer/model served over HTTP, e.g.
python3 -m http.server 8765 (from Code/3d-drawing)
python3 verify_viewer_headless.py http://127.0.0.1:8765/viewer/index.html <chromium_path>
Checks: page loads without page errors, canvas exists, meta text is shown,
and a click on the lining (other layers hidden) returns station and pattern.
"""
import sys
from playwright.sync_api import sync_playwright
URL, CHROME = sys.argv[1], sys.argv[2]
with sync_playwright() as p:
b = p.chromium.launch(executable_path=CHROME, args=["--use-gl=swiftshader", "--enable-webgl", "--ignore-gpu-blocklist", "--no-sandbox"])
pg = b.new_page(viewport={"width": 1280, "height": 800})
errors = []
pg.on("pageerror", lambda e: errors.append(str(e)))
pg.goto(URL, wait_until="networkidle", timeout=90000)
pg.wait_for_timeout(6000)
canvas = pg.evaluate("() => !!document.querySelector('canvas')")
body = pg.evaluate("() => document.body.innerText")
print("page errors:", errors)
print("canvas present:", canvas, "| meta shown:", "패턴:" in body and "범위:" in body)
for label in ["excavation_R7402", "road_surface", "ground_GL"]:
pg.locator("label", has_text=label).locator("input").uncheck()
pg.wait_for_timeout(500)
readout = None
for fx in [0.5, 0.45, 0.55, 0.4, 0.6, 0.35, 0.65]:
for fy in [0.5, 0.45, 0.55, 0.4, 0.6]:
pg.mouse.click(1280 * fx, 800 * fy)
pg.wait_for_timeout(200)
t = pg.evaluate("() => { const els=[...document.querySelectorAll('div,pre,p')]; return els.map(e=>e.innerText).filter(s=>s && s.includes('교차점')).pop()||null }")
if t and "측점" in t:
readout = t
break
if readout:
break
print("lining readout:", (readout or "NO LINING HIT").replace("\n", " | ")[:700])
b.close()
+670
View File
@@ -0,0 +1,670 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>대산2터널 본선 3D 모델</title>
<style>
/* ── Prism CSS 변수 정본 (webapp/module.webapp.md §3 탑재, 수정 금지) ── */
:root {
/* ── color (light) ─────────────────────────────── */
--prism-bg-canvas: #F4F3F0; /* sand G4 — 순백 전면 금지 */
--prism-bg-card: #FFFFFF; /* white G0 */
--prism-text-body: #474235; /* brown G72 — on sand 9.02 AAA */
--prism-text-heading: #1F524B; /* green G69 — on sand 8.00 AAA */
--prism-text-subtle: #474235; /* 저강조는 색이 아니라 크기·웨이트로 */
--prism-text-accent: #D05C26; /* orange G47 — 대형 전용(24px+/19px+Bold) */
--prism-border-default: #474235;
--prism-border-strong: #1F524B;
--prism-status-do: #005A4E; --prism-status-do-bg: #DCEAE6;
--prism-status-dont: #8F2A11; --prism-status-dont-bg: #F6DFDA;
--prism-status-caution: #6B4A00; --prism-status-caution-bg: #F5E7CC;
--prism-status-tip: #0A4E86; --prism-status-tip-bg: #DDE8F3;
--prism-dv-1: #C08A2E; --prism-dv-2: #6E8F3C; --prism-dv-3: #3C6E9F;
--prism-dv-4: #8C3B14; --prism-dv-5: #1B4A43; --prism-dv-6: #5E4370;
/* ── space (base 8px = 0.5rem) ──────────────────── */
--prism-space-025: 0.125rem; /* 2px */
--prism-space-050: 0.25rem; /* 4px */
--prism-space-100: 0.5rem; /* 8px */
--prism-space-150: 0.75rem; /* 12px */
--prism-space-200: 1rem; /* 16px — card padding */
--prism-space-300: 1.5rem; /* 24px — block gap */
--prism-space-400: 2rem; /* 32px — zone gap */
--prism-space-600: 3rem; /* 48px */
--prism-space-800: 4rem; /* 64px */
/* ── type (root 16px 미고정 — html font-size 선언 금지) ── */
--prism-type-caption: 0.8125rem; --prism-lh-caption: 1.55;
--prism-type-body-sm: 0.875rem; --prism-lh-body: 1.70;
--prism-type-body: 1rem;
--prism-type-body-lg: 1.125rem;
--prism-type-title-sm: 1.25rem; --prism-lh-title: 1.50;
--prism-type-title: 1.625rem;
--prism-type-title-lg: 2rem;
--prism-type-display: 2.875rem; --prism-lh-display: 1.25;
--prism-type-display-lg: 4.125rem;
/* ── radius · stroke · z · motion ───────────────── */
--prism-radius-sm: 4px; --prism-radius-md: 8px; --prism-radius-lg: 12px;
--prism-radius-pill: 9999px;
--prism-stroke-hairline: 1px; --prism-stroke-regular: 2px; --prism-stroke-bold: 3px;
--prism-z-raised: 100; --prism-z-sticky: 200; --prism-z-dropdown: 300;
--prism-z-scrim: 400; --prism-z-modal: 500; --prism-z-popover: 600;
--prism-z-toast: 700; --prism-z-tooltip: 800;
--prism-scrim: rgba(0, 0, 0, 0.32);
--prism-motion-fast: 100ms; --prism-motion-moderate: 200ms; --prism-motion-slow: 300ms;
--prism-ease-standard: cubic-bezier(0.2, 0, 0, 1);
--prism-ease-enter: cubic-bezier(0, 0, 0, 1);
--prism-ease-exit: cubic-bezier(0.3, 0, 1, 1);
--prism-focus-ring: 3px; --prism-focus-offset: 2px;
--prism-measure-max: 38em;
}
/* ── dark theme (data-theme + OS 연동) ─────────────── */
:root[data-theme="dark"] {
--prism-bg-canvas: #111111; /* obsidian G95 */
--prism-bg-card: #111111; /* 카드는 보더로 분리 */
--prism-text-body: #F4F3F0; /* sand on obsidian 17.02 AAA */
--prism-text-heading: #F4F3F0; /* ⚠ green 텍스트 금지 (2.13:1) — 면·도형만 */
--prism-text-subtle: #D3D3D3; /* subtle 12.61 AAA */
--prism-text-accent: #D05C26; /* 4.71 AA — 대형 권장 */
--prism-border-default: #D3D3D3;
--prism-border-strong: #F4F3F0;
/* 상태색 다크: container↔on 반전 (tokens v0.1.1, 실측 동일) */
--prism-status-do: #DCEAE6; --prism-status-do-bg: #005A4E;
--prism-status-dont: #F6DFDA; --prism-status-dont-bg: #8F2A11;
--prism-status-caution: #F5E7CC; --prism-status-caution-bg: #6B4A00;
--prism-status-tip: #DDE8F3; --prism-status-tip-bg: #0A4E86;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { /* 위 dark 값셋 동일 적용 */
--prism-bg-canvas: #111111; --prism-bg-card: #111111;
--prism-text-body: #F4F3F0; --prism-text-heading: #F4F3F0;
--prism-text-subtle: #D3D3D3; --prism-text-accent: #D05C26;
--prism-border-default: #D3D3D3; --prism-border-strong: #F4F3F0;
--prism-status-do: #DCEAE6; --prism-status-do-bg: #005A4E;
--prism-status-dont: #F6DFDA; --prism-status-dont-bg: #8F2A11;
--prism-status-caution: #F5E7CC; --prism-status-caution-bg: #6B4A00;
--prism-status-tip: #DDE8F3; --prism-status-tip-bg: #0A4E86;
}
}
/* ── 베이스 (CSS-1·CSS-2: root font-size 미지정, 타입·간격 rem) ── */
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { height: 100%; margin: 0; }
body {
display: flex; flex-direction: column; overflow: hidden;
background: var(--prism-bg-canvas); color: var(--prism-text-body);
font-family: Pretendard, -apple-system, BlinkMacSystemFont, "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;
font-size: var(--prism-type-body); line-height: var(--prism-lh-body);
}
h1, h2, p, dl, dd, dt { margin: 0; }
h1 { font-size: var(--prism-type-title-sm); line-height: var(--prism-lh-title); font-weight: 700; color: var(--prism-text-heading); }
h2 { font-size: var(--prism-type-body-sm); line-height: var(--prism-lh-title); font-weight: 700; color: var(--prism-text-heading); margin-block: var(--prism-space-150) var(--prism-space-050); }
.rail h2:first-child { margin-block-start: 0; }
/* 포커스: 3px 링 + 오프셋 2px, :focus-visible 전용 (FC-1·FC-2) */
:focus-visible { outline: var(--prism-focus-ring) solid var(--prism-text-heading); outline-offset: var(--prism-focus-offset); }
.skip-link {
position: absolute; inset-inline-start: var(--prism-space-200); inset-block-start: var(--prism-space-100);
transform: translateY(-200%); z-index: var(--prism-z-toast);
padding: var(--prism-space-100) var(--prism-space-200); border-radius: var(--prism-radius-md);
background: var(--prism-bg-card); color: var(--prism-text-heading); border: var(--prism-stroke-hairline) solid var(--prism-border-strong);
}
.skip-link:focus { transform: none; }
/* 헤더 — W1 셸 메타 슬롯 */
.hdr {
display: flex; flex-wrap: wrap; align-items: center; gap: var(--prism-space-100) var(--prism-space-400);
padding: var(--prism-space-150) var(--prism-space-300);
background: var(--prism-bg-card); border-bottom: var(--prism-stroke-hairline) solid var(--prism-border-default);
}
.meta {
display: flex; flex-wrap: wrap; gap: var(--prism-space-050) var(--prism-space-300);
margin-inline-start: auto; font-size: var(--prism-type-caption); line-height: var(--prism-lh-caption);
}
.meta div { display: flex; gap: var(--prism-space-050); }
.meta dt { color: var(--prism-text-subtle); }
.meta dd { color: var(--prism-text-body); font-weight: 500; font-variant-numeric: tabular-nums; }
/* 테마 3택 (TC-2) */
.seg { display: inline-flex; overflow: hidden; border-radius: var(--prism-radius-md); border: var(--prism-stroke-hairline) solid var(--prism-border-default); }
.seg button { border: 0; border-radius: 0; background: transparent; }
.seg button + button { border-inline-start: var(--prism-stroke-hairline) solid var(--prism-border-default); }
.seg button[aria-pressed="true"] { background: var(--prism-text-heading); color: var(--prism-bg-card); }
/* 셸: 좌측 내비 레일 + 뷰포트 (W1) */
.shell { flex: 1; min-height: 0; display: grid; grid-template-columns: 17.5rem minmax(0, 1fr); }
.rail {
overflow: auto; padding: var(--prism-space-150);
background: var(--prism-bg-card); border-inline-end: var(--prism-stroke-hairline) solid var(--prism-border-default);
}
.stage { position: relative; min-width: 0; min-height: 0; }
#viewport { position: absolute; inset: 0; }
#viewport canvas { display: block; }
/* 버튼 — primary 화면당 1개 (BT-1), 히트 영역 44px (HT-1) */
button {
font: inherit; font-size: var(--prism-type-body-sm); font-weight: 500; line-height: 1.2;
min-height: 2.25rem; padding: var(--prism-space-050) var(--prism-space-100); white-space: nowrap;
border: var(--prism-stroke-hairline) solid var(--prism-border-default); border-radius: var(--prism-radius-md);
background: var(--prism-bg-card); color: var(--prism-text-body); cursor: pointer;
}
button.primary { background: var(--prism-text-heading); border-color: var(--prism-text-heading); color: var(--prism-bg-card); }
button.quiet { background: transparent; border-color: transparent; color: var(--prism-text-heading); }
.btn-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--prism-space-100); }
.btn-grid .wide { grid-column: 1 / -1; }
/* 체크박스 행 — 히트 영역 44px */
.opt {
display: flex; align-items: center; gap: var(--prism-space-100);
min-height: 2.75rem; cursor: pointer; color: var(--prism-text-body); font-size: var(--prism-type-body-sm);
}
.opt input[type="checkbox"] { width: 1.25rem; height: 1.25rem; margin: 0; flex: none; accent-color: var(--prism-text-heading); }
.pats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: var(--prism-space-050); }
.pats .opt { gap: var(--prism-space-050); white-space: nowrap; }
.sw {
width: var(--prism-space-200); height: var(--prism-space-200); flex: none;
border-radius: var(--prism-radius-sm); border: var(--prism-stroke-hairline) solid var(--prism-border-default);
}
.sw.ghost { opacity: 0.5; }
/* 구간 슬라이더 — 두 핸들, 히트 영역 44px */
.dual { position: relative; height: 2.75rem; margin-block: var(--prism-space-050); }
.dual .track {
position: absolute; inset-inline: var(--prism-space-150); top: 50%; height: var(--prism-stroke-regular);
transform: translateY(-50%); border-radius: var(--prism-radius-sm); background: var(--prism-border-default);
}
.dual input[type="range"] {
position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
background: transparent; pointer-events: none; appearance: none; -webkit-appearance: none;
}
.dual input[type="range"]::-webkit-slider-runnable-track { background: transparent; height: 100%; }
.dual input[type="range"]::-moz-range-track { background: transparent; }
.dual input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; pointer-events: auto; cursor: pointer;
width: 1.25rem; height: 1.25rem; margin-block-start: var(--prism-space-150); border-radius: 50%;
background: var(--prism-text-heading); border: var(--prism-stroke-regular) solid var(--prism-bg-card);
}
.dual input[type="range"]::-moz-range-thumb {
pointer-events: auto; cursor: pointer; width: 1.25rem; height: 1.25rem; border-radius: 50%;
background: var(--prism-text-heading); border: var(--prism-stroke-regular) solid var(--prism-bg-card);
}
.sta { font-size: var(--prism-type-body-sm); font-weight: 700; font-variant-numeric: tabular-nums; margin-block: var(--prism-space-050) var(--prism-space-100); }
/* 개폐 (component.disclosure) — 부품 선택은 기본 접힘 */
.disc { margin-block-start: var(--prism-space-150); border-top: var(--prism-stroke-hairline) solid var(--prism-border-default); padding-block-start: var(--prism-space-050); }
.disc > summary {
display: flex; align-items: center; justify-content: space-between; gap: var(--prism-space-100);
min-height: 2.75rem; cursor: pointer; list-style: none; font-size: var(--prism-type-body-sm); font-weight: 700; color: var(--prism-text-heading);
}
.disc > summary::-webkit-details-marker { display: none; }
.disc > summary > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.disc > summary::after { content: "+"; flex: none; font-weight: 500; color: var(--prism-text-subtle); }
.disc[open] > summary::after { content: "−"; }
.disc > summary:focus-visible { outline: var(--prism-focus-ring) solid var(--prism-text-heading); outline-offset: var(--prism-focus-offset); }
.disc > *:not(summary) { margin-block-start: var(--prism-space-100); }
/* 선택 목록 */
select {
font: inherit; font-size: var(--prism-type-body-sm); width: 100%; min-height: 2.75rem;
padding: var(--prism-space-050) var(--prism-space-100); border-radius: var(--prism-radius-md);
border: var(--prism-stroke-hairline) solid var(--prism-border-default);
background: var(--prism-bg-card); color: var(--prism-text-body);
}
.label { display: block; margin-block-end: var(--prism-space-050); color: var(--prism-text-subtle); font-size: var(--prism-type-caption); line-height: var(--prism-lh-caption); }
.note { color: var(--prism-text-subtle); font-size: var(--prism-type-caption); line-height: var(--prism-lh-caption); max-inline-size: var(--prism-measure-max); margin-block-start: var(--prism-space-100); }
.empty { color: var(--prism-text-subtle); font-size: var(--prism-type-caption); margin-block-start: var(--prism-space-100); }
/* 명세표 (specsheet) — 항목 말미 고정 위치, 열 구성 동일 (SS-1·SS-2) */
.spec {
display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--prism-space-050) var(--prism-space-150);
font-size: var(--prism-type-caption); line-height: var(--prism-lh-caption); font-variant-numeric: tabular-nums;
}
.spec dt { color: var(--prism-text-subtle); }
.spec dd { overflow-wrap: anywhere; color: var(--prism-text-body); }
/* 툴팁 — overlay 단계 (z.tooltip 800, 보더 우선) */
#tip {
position: fixed; display: none; pointer-events: none; z-index: var(--prism-z-tooltip);
max-inline-size: 20rem; padding: var(--prism-space-150) var(--prism-space-200);
background: var(--prism-bg-card); color: var(--prism-text-body);
border: var(--prism-stroke-hairline) solid var(--prism-border-strong); border-radius: var(--prism-radius-md);
box-shadow: 0 var(--prism-space-050) var(--prism-space-150) var(--prism-scrim);
}
#tip .tip-title { font-weight: 700; color: var(--prism-text-heading); margin-block-end: var(--prism-space-100); }
/* 상호작용 — hover는 보강 수단, hover 기기에서만 (HV-3·HV-4) */
@media (hover: hover) {
button:not(.primary):hover { border-color: var(--prism-border-strong); }
button.primary:hover { background: color-mix(in srgb, var(--prism-text-heading) 88%, var(--prism-text-body)); border-color: transparent; }
button.quiet:hover { background: var(--prism-bg-canvas); }
.opt:hover { color: var(--prism-text-heading); }
}
/* 좁은 화면: 레일을 본문 아래로 (BP md 904 미만) */
@media (max-width: 903px) {
body { height: auto; min-height: 100%; overflow: auto; }
.hdr { padding: var(--prism-space-150) var(--prism-space-200); }
.meta { margin-inline-start: 0; }
.shell { display: flex; flex-direction: column; flex: none; }
.stage { order: -1; flex: none; height: 60vh; min-height: 20rem; }
.rail { overflow: visible; border-inline-end: 0; border-block-start: var(--prism-stroke-hairline) solid var(--prism-border-default); }
}
/* 모션: 축소 요청 시 전환 제거 (MT-3·RM-1) */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}
</style>
<script type="importmap">
{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"}}
</script>
</head>
<body>
<a class="skip-link" href="#main">본문 바로가기</a>
<header class="hdr">
<h1>대산2터널 본선 3D 모델</h1>
<dl class="meta">
<div><dt>측점</dt><dd>4+745 ~ 5+480</dd></div>
<div><dt>부품</dt><dd id="metaCount">—</dd></div>
<div><dt>단위</dt><dd>m</dd></div>
<div><dt>상태</dt><dd id="status" role="status">불러오는 중</dd></div>
</dl>
<div class="seg" role="group" aria-label="화면 테마">
<button type="button" data-theme-choice="system" aria-pressed="true">시스템</button>
<button type="button" data-theme-choice="light" aria-pressed="false">라이트</button>
<button type="button" data-theme-choice="dark" aria-pressed="false">다크</button>
</div>
</header>
<div class="shell">
<nav class="rail" aria-label="모델 제어">
<div class="btn-grid" role="group" aria-label="시점">
<button type="button" class="primary" id="btnStart">종단 보기</button>
<button type="button" id="btnAll">전체 맞추기</button>
</div>
<h2>구간</h2>
<div class="dual">
<div class="track"></div>
<input type="range" id="s0" min="4745" max="5480" step="5" value="4745" aria-label="시작 측점">
<input type="range" id="s1" min="4745" max="5480" step="5" value="5480" aria-label="끝 측점">
</div>
<p class="sta" id="staText" aria-live="polite">4+745 ~ 5+480</p>
<div class="btn-grid">
<button type="button" id="btnWin">구간 맞추기</button>
<button type="button" id="btnWinReset" class="quiet">구간 전체</button>
</div>
<h2>지보패턴</h2>
<div class="pats" id="pats"></div>
<h2>구성요소</h2>
<div id="comps"></div>
<details class="disc" id="partDisc">
<summary><span id="partSummary">부품 선택</span></summary>
<select id="pickSel" aria-label="부품 목록에서 고르기">
<option value="">선택 없음</option>
</select>
<dl class="spec" id="detailSpec" hidden></dl>
<p class="empty" id="detailEmpty">부품을 클릭하거나 목록에서 고르면 속성이 표시됩니다.</p>
<div class="btn-grid" style="margin-block-start:var(--prism-space-100)">
<button type="button" id="btnFitSel">선택 맞추기</button>
<button type="button" id="btnDesel" class="quiet">선택 해제</button>
<button type="button" id="btnHideSel">선택 숨기기</button>
<button type="button" id="btnIsoSel">선택만 보기</button>
<button type="button" id="btnShowAll" class="quiet wide">모두 보기</button>
</div>
<label class="opt" style="margin-block-start:var(--prism-space-050)"><input type="checkbox" id="wire"> 와이어프레임으로 보기</label>
</details>
</nav>
<main id="main" class="stage">
<div id="viewport" role="img" aria-label="대산2터널 본선 3D 모델, 측점 4+745부터 5+480까지. 드래그로 회전하고 클릭하면 부품이 선택됩니다. 키보드는 부품 선택 목록을 사용합니다."></div>
<div id="tip" aria-hidden="true"></div>
</main>
</div>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const MODEL = '../model/tunnel_solids.glb';
const META = '../model/tunnel_solids_meta.json';
const S_MIN = 4745, S_MAX = 5480;
const THEME_KEY = 'prism-viewer-theme';
// 부품 그룹 → 디자인 토큰 (색은 모두 토큰에서 읽는다, 리터럴 없음)
const PART = {
'라이닝': { token: '--prism-dv-1', order: 0 },
'굴착면': { token: '--prism-text-subtle', order: 1, ghost: true },
'노면(계획고)': { token: '--prism-dv-4', order: 2 },
'기층·인버트(가정)': { token: '--prism-dv-2', order: 3 },
'배수(측구)': { token: '--prism-dv-3', order: 4 },
'공동구(가정)': { token: '--prism-dv-6', order: 5 },
'시설한계(참조선)': { token: '--prism-text-accent', order: 6 }
};
const TOGGLES = [
['굴착면', '굴착 여유대'], ['노면(계획고)', '노면 (계획고)'], ['기층·인버트(가정)', '기층·인버트 (가정)'],
['배수(측구)', '배수 (측구)'], ['공동구(가정)', '공동구 (가정)'], ['시설한계(참조선)', '시설한계 50 mm 참조선']
];
const NOTE = {
'노면(계획고)': '노면 포장 280 mm · 측점별 편경사 적용 (도면 계획고 기준)',
'기층·인버트(가정)': '가정 형상 · 도면에 표현되지 않아 가정하여 생성',
'배수(측구)': '측구 300×836 mm 및 관 r150 (단면 도면 기준)',
'공동구(가정)': '가정 형상 · H/I 박스를 공동구로 가정',
'시설한계(참조선)': '시설한계 50 mm 기준선 · 참조용 선 (솔리드 아님)',
'굴착면': '굴착 여유대 R 7.402 m 기준 · 노면 위',
'라이닝': '라이닝 R 6.982 / 7.282 m (지보패턴별 구간)'
};
const $ = (id) => document.getElementById(id);
const fmtSta = (m) => Math.floor(m / 1000) + '+' + String(Math.round(m % 1000)).padStart(3, '0');
const tok = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
const statusEl = $('status'), tipEl = $('tip');
// ── 테마 (TC-1~TC-4): 시스템 기본, 사용자 재정의 영속 ──
let scene = null, renderer = null;
let currentChoice = 'system';
try { currentChoice = localStorage.getItem(THEME_KEY) || 'system'; } catch (e) { /* 저장소 불가 시 시스템 값 */ }
function setThemeAttr(choice) {
const root = document.documentElement;
if (choice === 'light' || choice === 'dark') root.setAttribute('data-theme', choice);
else root.removeAttribute('data-theme');
document.querySelectorAll('[data-theme-choice]').forEach((b) =>
b.setAttribute('aria-pressed', String(b.dataset.themeChoice === choice)));
}
function applyTheme(choice, persist = true) {
currentChoice = choice;
setThemeAttr(choice);
if (persist) { try { localStorage.setItem(THEME_KEY, choice); } catch (e) { /* 무시 */ } }
if (scene) refreshColors();
}
setThemeAttr(currentChoice);
document.querySelectorAll('[data-theme-choice]').forEach((b) =>
b.addEventListener('click', () => applyTheme(b.dataset.themeChoice)));
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (scene) refreshColors(); });
// ── 3D 장면 ──
const vp = $('viewport');
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(vp.clientWidth, vp.clientHeight);
renderer.domElement.setAttribute('aria-hidden', 'true');
vp.appendChild(renderer.domElement);
scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(30, vp.clientWidth / Math.max(vp.clientHeight, 1), 0.5, 200000);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
scene.add(new THREE.HemisphereLight(new THREE.Color(1, 1, 1), new THREE.Color(0.333, 0.376, 0.439), 1.0)); // 조명(장면 광원, UI 색 아님)
const sun = new THREE.DirectionalLight(0xffffff, 0.9); sun.position.set(1, 2, 1); scene.add(sun);
const fill = new THREE.DirectionalLight(0xffffff, 0.6); fill.position.set(-1, -0.5, -1.2); scene.add(fill);
new ResizeObserver(() => {
const w = vp.clientWidth, h = Math.max(vp.clientHeight, 1);
renderer.setSize(w, h); camera.aspect = w / h; camera.updateProjectionMatrix();
}).observe(vp);
const meshes = [];
let META_BY_ID = {};
const patternOn = {}, groupOn = {};
let winLo = S_MIN, winHi = S_MAX;
let selected = null, isolate = false;
const hiddenSel = new Set();
let world = null;
// 지보패턴별 색: 패턴 순번(측점 순)마다 dv 토큰을 쓰고, 7번째부터는 배경과 50% 혼합한 명도 차로 구분
const patIdx = {};
function patCss(p) { const k = patIdx[p] ?? 0; return 'color-mix(in srgb, var(--prism-dv-' + (k % 6 + 1) + ') ' + (k >= 6 ? 50 : 100) + '%, var(--prism-bg-canvas))'; }
function patColor(p) {
const k = patIdx[p] ?? 0;
const c = new THREE.Color(tok('--prism-dv-' + (k % 6 + 1)));
if (k >= 6) c.lerp(new THREE.Color(tok('--prism-bg-canvas')), 0.5);
return c;
}
function refreshColors() {
scene.background = new THREE.Color(tok('--prism-bg-canvas'));
for (const m of meshes) {
const u = m.userData, p = PART[u.group];
if (u.group === '라이닝' && u.pattern) m.material.color.copy(patColor(u.pattern));
else m.material.color.set(tok(p ? p.token : '--prism-text-subtle'));
}
if (selected) selected.material.emissive.set(tok('--prism-text-heading'));
}
function visibleOf(m) {
const u = m.userData;
if (hiddenSel.has(m)) return false;
if (isolate && selected && m !== selected) return false;
if (u.from != null && (u.to < winLo || u.from > winHi)) return false;
if (u.group === '시설한계(참조선)' && (winLo > S_MIN || winHi < S_MAX)) return false; // 전체 길이 참조선: 전체 구간에서만
if (u.pattern) { if (!patternOn[u.pattern]) return false; }
if (u.group in groupOn && !groupOn[u.group]) return false;
return true;
}
function applyVisibility() { for (const m of meshes) m.visible = visibleOf(m); }
function frame(center, dist, dir) {
const d = (dir || new THREE.Vector3(0.6, 0.45, 0.9)).clone().normalize();
controls.target.copy(center);
camera.position.copy(center).addScaledVector(d, dist);
camera.near = Math.max(dist / 3000, 0.05); camera.far = dist * 40;
camera.updateProjectionMatrix();
controls.update();
}
function boxOf(list) {
const b = new THREE.Box3(); let any = false;
for (const m of list) { b.union(new THREE.Box3().setFromObject(m)); any = true; }
return any ? b : null;
}
function frameBox(b, pad) {
const c = b.getCenter(new THREE.Vector3()), s = b.getSize(new THREE.Vector3());
frame(c, Math.max(s.x, s.y, s.z) * (pad || 0.9) + 20);
}
function frameAll() { applyVisibility(); const b = boxOf(meshes.filter((m) => m.visible)); if (b) frameBox(b, 0.75); }
function frameWindow() { applyVisibility(); const b = boxOf(meshes.filter((m) => m.visible)); if (b) frameBox(b, 0.9); }
// 종단 시점: 측점이 좌→우로 나열, 높이는 위
function startView() {
const first = meshes.find((m) => m.userData.group === '라이닝' && m.userData.from === S_MIN);
const last = meshes.find((m) => m.userData.group === '라이닝' && m.userData.to === S_MAX);
if (!first || !last) return frameAll();
const c0 = new THREE.Box3().setFromObject(first).getCenter(new THREE.Vector3());
const c1 = new THREE.Box3().setFromObject(last).getCenter(new THREE.Vector3());
const u = new THREE.Vector3(c1.x - c0.x, 0, c1.z - c0.z);
const L = u.length(); u.normalize();
const mid = c0.clone().add(c1).multiplyScalar(0.5);
const w = new THREE.Vector3(u.z, 0, -u.x); // 카메라 시선 방향 (측점 u가 화면 우측)
controls.target.copy(mid);
camera.up.set(0, 1, 0);
camera.position.copy(mid).addScaledVector(w, -L * 2.1).add(new THREE.Vector3(0, L * 0.02, 0));
camera.near = 1; camera.far = L * 40; camera.updateProjectionMatrix();
controls.update();
}
// 부품 선택 개폐 상태 저장 (DC-3)
const DISC_KEY = 'prism-viewer-part-open';
try { $('partDisc').open = localStorage.getItem(DISC_KEY) === '1'; } catch (e) { /* 무시 */ }
$('partDisc').addEventListener('toggle', () => { try { localStorage.setItem(DISC_KEY, $('partDisc').open ? '1' : '0'); } catch (e) { /* 무시 */ } });
// ── 부품 상세 (명세표) ──
function specRows(u) {
return [
['구분', u.group || '—'],
['지보패턴', u.pattern || '—'],
['측점', u.from != null ? fmtSta(u.from) + ' ~ ' + fmtSta(u.to) + ' (' + Math.round(u.to - u.from) + ' m)' : '—'],
['부피', u.volume != null ? Number(u.volume).toFixed(2) + ' m³' : '—'],
['비고', NOTE[u.group] || '—'],
['부품 ID', u.id || '—']
];
}
function fillSpec(dl, rows) {
dl.replaceChildren();
for (const [k, v] of rows) {
const dt = document.createElement('dt'); dt.textContent = k;
const dd = document.createElement('dd'); dd.textContent = v;
dl.append(dt, dd);
}
}
function renderDetail(u) {
const dl = $('detailSpec'), empty = $('detailEmpty');
if (!u) { dl.hidden = true; empty.hidden = false; dl.replaceChildren(); return; }
fillSpec(dl, specRows(u)); dl.hidden = false; empty.hidden = true;
}
function setSelected(m) {
if (selected) selected.material.emissive.setRGB(0, 0, 0);
selected = m;
if (m) m.material.emissive.set(tok('--prism-text-heading'));
$('pickSel').value = m ? String(m.userData.idx) : '';
$('partSummary').textContent = m ? '부품 선택 · ' + m.userData.id : '부품 선택 · 선택 없음';
applyVisibility();
renderDetail(m ? m.userData : null);
}
function showTip(u, x, y) {
tipEl.replaceChildren();
const t = document.createElement('p'); t.className = 'tip-title';
t.textContent = u.group + (u.pattern ? ' · 지보패턴 ' + u.pattern : '');
const dl = document.createElement('dl'); dl.className = 'spec';
fillSpec(dl, specRows(u).filter((r) => r[0] !== '구분' && r[0] !== '지보패턴'));
tipEl.append(t, dl);
tipEl.style.display = 'block';
const gap = 14;
let left = x + gap, top = y + gap;
if (left + tipEl.offsetWidth > innerWidth) left = x - tipEl.offsetWidth - gap;
if (top + tipEl.offsetHeight > innerHeight) top = y - tipEl.offsetHeight - gap;
tipEl.style.left = Math.max(8, left) + 'px';
tipEl.style.top = Math.max(8, top) + 'px';
}
function hideTip() { tipEl.style.display = 'none'; }
Promise.all([
fetch(META).then((r) => r.json()),
new Promise((res, rej) => new GLTFLoader().load(MODEL, res, undefined, rej))
]).then(([meta, gltf]) => {
for (const c of meta.components) META_BY_ID[c.id] = c;
world = new THREE.Group();
world.rotation.x = -Math.PI / 2; // 모델은 Z-up, 화면은 Y-up
world.add(gltf.scene);
scene.add(world);
gltf.scene.traverse((o) => {
if (!o.isMesh) return;
const c = META_BY_ID[o.name] || {};
o.userData = { id: o.name, group: c.group || '', pattern: c.pattern || null,
from: c.from_sta ?? null, to: c.to_sta ?? null, volume: c.volume_m3 ?? null,
idx: meshes.length };
o.geometry = o.geometry.toNonIndexed();
o.geometry.computeVertexNormals();
const p = PART[o.userData.group];
const mat = new THREE.MeshStandardMaterial({ roughness: 0.8, metalness: 0.0, side: THREE.DoubleSide, flatShading: true });
if (p && p.ghost) { mat.transparent = true; mat.opacity = 0.28; mat.depthWrite = false; }
o.material = mat;
meshes.push(o);
});
world.updateMatrixWorld(true);
refreshColors();
// 지보패턴 체크박스
const firstFrom = {};
for (const c of meta.components) if (c.pattern && c.from_sta != null) firstFrom[c.pattern] = Math.min(firstFrom[c.pattern] ?? Infinity, c.from_sta);
const pats = Object.keys(firstFrom).sort((a, b) => firstFrom[a] - firstFrom[b]);
pats.forEach((p, i) => { patIdx[p] = i; });
refreshColors();
for (const p of pats) {
patternOn[p] = true;
const lab = document.createElement('label'); lab.className = 'opt';
const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = true;
cb.style.accentColor = patCss(p);
cb.addEventListener('change', () => { patternOn[p] = cb.checked; applyVisibility(); });
const span = document.createElement('span'); span.textContent = p;
lab.append(cb, span); $('pats').appendChild(lab);
}
// 구성요소 체크박스 (색 견본 = 부품 토큰)
for (const [key, text] of TOGGLES) {
groupOn[key] = true;
const lab = document.createElement('label'); lab.className = 'opt';
const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = true;
cb.addEventListener('change', () => { groupOn[key] = cb.checked; applyVisibility(); });
const sw = document.createElement('span'); sw.className = 'sw' + (PART[key].ghost ? ' ghost' : '');
sw.setAttribute('aria-hidden', 'true'); sw.style.background = 'var(' + PART[key].token + ')';
const span = document.createElement('span'); span.textContent = text;
lab.append(cb, sw, span); $('comps').appendChild(lab);
}
// 키보드용 부품 목록 (그룹 → 측점 순)
const order = [...meshes].sort((a, b) =>
(PART[a.userData.group]?.order ?? 9) - (PART[b.userData.group]?.order ?? 9) ||
(a.userData.from ?? 0) - (b.userData.from ?? 0) || a.userData.id.localeCompare(b.userData.id));
for (const m of order) {
const u = m.userData;
const o = document.createElement('option');
o.value = String(u.idx);
o.textContent = u.group + (u.pattern ? ' · ' + u.pattern : '') +
(u.from != null ? ' · ' + fmtSta(u.from) + '~' + fmtSta(u.to) : '') + ' · ' + u.id;
$('pickSel').appendChild(o);
}
startView();
$('metaCount').textContent = String(meshes.length);
statusEl.textContent = '로드 완료';
}).catch((e) => { statusEl.textContent = '로드 실패: HTTP 서버에서 여세요 (' + e.message + ')'; });
// ── 구간 슬라이더 (두 핸들) ──
function syncWin(from) {
let a = Number($('s0').value), b = Number($('s1').value);
if (a > b) { if (from === 0) { b = a; $('s1').value = b; } else { a = b; $('s0').value = a; } }
winLo = a; winHi = b;
$('staText').textContent = fmtSta(a) + ' ~ ' + fmtSta(b);
$('s0').setAttribute('aria-valuetext', fmtSta(a));
$('s1').setAttribute('aria-valuetext', fmtSta(b));
applyVisibility();
}
$('s0').addEventListener('input', () => syncWin(0));
$('s1').addEventListener('input', () => syncWin(1));
$('btnWin').addEventListener('click', () => frameWindow());
$('btnWinReset').addEventListener('click', () => { $('s0').value = S_MIN; $('s1').value = S_MAX; syncWin(0); frameAll(); });
$('btnStart').addEventListener('click', () => startView());
$('btnAll').addEventListener('click', () => frameAll());
// ── 선택 도구 ──
$('btnFitSel').addEventListener('click', () => { if (selected) frameBox(new THREE.Box3().setFromObject(selected), 0.9); });
$('btnDesel').addEventListener('click', () => { setSelected(null); hideTip(); });
$('btnHideSel').addEventListener('click', () => { if (selected) { hiddenSel.add(selected); setSelected(null); hideTip(); } });
$('btnIsoSel').addEventListener('click', () => { if (selected) { isolate = true; applyVisibility(); } });
$('btnShowAll').addEventListener('click', () => { hiddenSel.clear(); isolate = false; applyVisibility(); });
$('wire').addEventListener('change', () => { for (const m of meshes) m.material.wireframe = $('wire').checked; });
$('pickSel').addEventListener('change', () => {
const v = $('pickSel').value;
setSelected(v === '' ? null : meshes[Number(v)]);
});
addEventListener('keydown', (e) => { if (e.key === 'Escape') { setSelected(null); hideTip(); } });
// ── 포인터 선택 ──
const ray = new THREE.Raycaster(); const ptr = new THREE.Vector2();
let downAt = null;
renderer.domElement.addEventListener('pointerdown', (e) => { downAt = [e.clientX, e.clientY]; });
renderer.domElement.addEventListener('pointerup', (e) => {
if (!downAt || Math.hypot(e.clientX - downAt[0], e.clientY - downAt[1]) > 4) return;
const r = renderer.domElement.getBoundingClientRect();
ptr.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ptr, camera);
const hit = ray.intersectObjects(meshes.filter((m) => m.visible), false)[0];
if (!hit) { setSelected(null); hideTip(); return; }
setSelected(hit.object);
showTip(hit.object.userData, e.clientX, e.clientY);
});
renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });
</script>
</body>
</html>