Files
drawing3d-web/SPEC.md
T
오문성andClaude Haiku 5.5 f33e00bce4 KIT: F13 도면 커밋 기준을 정리본 3개 허용으로 (SPEC 34장 보완)
첫 공개 커밋 9068b6c 뒤 F13 의 「도면 파일 커밋 금지」 항목이 FAIL 이던 기준 누락(코치 결함)을
리허설에서 발견해 보완. 커밋된 도면은 정리본 3개(drawing.dxf, drawing.svg, drawing_gen.svg)만 허용하도록
check.sh 와 SPEC 34장을 함께 고쳤다.

check.sh 결과: PASS 151 / FAIL 0 / SKIP 3 (검사 범위 F01~F46)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-27 15:58:28 +09:00

616 lines
74 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC — 전력구 수직구 3D 뷰어 (MVP)
> 🔒 사내교육용 — 열람 외 재배포 등의 활동을 금함 (회사 도면의 치수를 옮겨 적음)
> 설계 면담(수석 + 코치 세션, 2026-10-19~20)으로 확정. 이 파일은 하이쿠가 고치지 않는다.
## 1. 목적
원형 수직구 일반도 한 장을 **라이브러리 없이** 브라우저 3D 화면으로 보여 준다.
청중은 교육 담당자와 동료이고, 3분 시연에서 아래 세 문장을 화면을 돌려 가며 말할 수 있으면 끝이다.
1. 외경 11m, 깊이 70.3m 원통이다.
2. 안에 슬래브 19장 + 하부 슬래브가 있고, 층마다 계단이 지그재그로 내려간다.
3. 바닥 근처 19B 층에서 4시 방향으로 TBM 터널(내경 2.6m)이 나가고, 터널 바닥 패드와 19B 패드 상면 높이가 같다.
## 2. 범위
| 넣는 것 | 빼는 것 (범위 밖) |
|---|---|
| 벽체, 중간 슬래브 19장, 19B 패드, 하부 슬래브, 바닥콘크리트 | 상부 분기구·환기구·관로, 지표→1B 구간의 계단·설비 |
| 계단 18런(1B→19B) | 슬래브 개구부(계단·케이블 slot·펌프 배출구 구멍), 방호 난간 |
| TBM 터널 세그먼트(속 빈 원통) + 터널 안 잡석·패드 2단 | 집수정 사다리, 집수조·침사조·월류벽, 배관 |
| 등각 초기 화면, 드래그 회전, 휠 확대, 벽 반투명 | 프리셋 뷰, 클릭 선택·속성 패널, 표시/숨김, 축·그리드, 단면, 치수 라벨, PNG 저장 |
## 3. 좌표계와 단위
- 데이터의 길이는 모두 **mm**, 각도는 **도**.
- 원점 = 수직구 축 중심 × 지표면(구조물 상단). **Z 위로 +**, 땅속은 음수.
- X = 평면도 오른쪽, Y = 평면도 위쪽. 수평 각도는 **X축에서 반시계**(시계의 3시 = 0°, 12시 = 90°, 4시 = 330°).
## 4. 부재와 치수
### 4.1 벽체·바닥
| 부재 | 형태 | 치수 |
|---|---|---|
| 벽체 | 속 빈 원통 | 외경 11,000(r 5,500), 내경 9,700(r 4,850), z −70,200 ~ 0 |
| 하부 슬래브(BOTTOM, 20B) | 원판 | r 4,850, z −70,200 ~ −69,000 (두께 1,200, 벽 안쪽) |
| 바닥콘크리트 | 원판 | r 5,500(= 벽 외경, 슬러리월 안에서 타설), z −70,300 ~ −70,200 (두께 100) |
### 4.2 중간 슬래브 (원판, r 4,850, 두께 300)
슬래브 상면 z. 하면 = 상면 − 300.
| 층 | 상면 z | 층 | 상면 z | 층 | 상면 z |
|---|---|---|---|---|---|
| 1B | −7,400 | 8B | −30,500 | 15B | −53,600 |
| 2B | −10,700 | 9B | −33,800 | 16B | −56,900 |
| 3B | −14,000 | 10B | −37,100 | 17B | −60,200 |
| 4B | −17,300 | 11B | −40,400 | 18B | −63,500 |
| 5B | −20,600 | 12B | −43,700 | 19B | −66,200 |
| 6B | −23,900 | 13B | −47,000 | | |
| 7B | −27,200 | 14B | −50,300 | | |
규칙: 1B = −7,400(지표에서 5,400+500+1,300+200). 1B~18B 는 3,300 간격. 18B→19B 2,700. 19B→하부 슬래브 상면 2,800. 총높이 70,300.
### 4.3 19B 패드
원판, 벽 안쪽면에서 100 떨어짐 → r 4,750(지름 9,500), z −66,200 ~ −66,100 (두께 100).
### 4.4 계단 (18런)
- 런 i 는 i 층 슬래브(위)에서 i+1 층 슬래브(아래)로 내려간다. i = 1~18 (마지막 런은 18B→19B).
- 단 높이 200, **맨 아래 첫 단만 100**. 디딤판 깊이 300. 폭 1,000.
- 층고 3,300 → 단 수 17(= 100 + 200×16), 디딤판 16장, 런 길이 4,800
- 층고 2,700(런 18) → 단 수 14(= 100 + 200×13), 디딤판 13장, 런 길이 3,900
- 계단은 디딤판마다 **블록**(아래 슬래브 상면부터 그 디딤판 높이까지 꽉 찬 상자)으로 쌓는다.
- 위치 (🔶 확인 필요 — 코치 추론):
- 런의 윗끝 = 위 슬래브의 계단 개구부 가장자리. 개구부(4,200)는 X 가운데 −2,100 ~ +2,100 으로 본다.
- 홀수 런: 윗끝 x = −2,100, 내려가며 +X 방향, Y 띠 −1,000 ~ 0.
- 짝수 런: 윗끝 x = +2,100, 내려가며 −X 방향, Y 띠 0 ~ +1,000.
- 블록 k(아래부터 1, 2, …, 단 수−1)의 윗면 z = 아래 슬래브 상면 + 100 + 200×(k−1). 맨 위 단(200)은 위 슬래브로 올라선다.
- 블록 k 의 X 구간: 윗끝 x 에서 (단 수−1−k)×300 ~ (단 수−k)×300 만큼 내려가는 방향으로 떨어진 곳.
- 개구부를 그리지 않으므로 맨 위 블록이 위 슬래브 두께에 100 겹친다. MVP 에서는 받아들인다.
### 4.5 TBM 터널
- 방향 330°(평면도 4시). 축 높이 z −65,050.
- 세그먼트: 속 빈 원통, 두께 200, 내경 2,600(r 1,300), 외경 3,000(r 1,500).
- 원통은 **벽 안쪽면(r 4,850)에서 시작**해 벽을 관통하고 벽 바깥면에서 20,000 더 나간다 → 길이 20,650. 시작점 = (4,850·cos330°, 4,850·sin330°, −65,050).
- 근거(수석): 세그먼트가 벽체 개구부의 거푸집 역할을 해 18B 슬래브와 19B 벽체부를 동시 타설한다. 그래서 세그먼트 천장 외면(−63,550)이 18B 슬래브 두께(−63,800 ~ −63,500) 안에 들어가는 것은 실제 형태다.
- 높이 검산: 세그먼트 바닥 외면 −66,550(= 19B 슬래브 하면 −66,500 − 50) → 내면 −66,350 → 잡석 상면 −66,225 → 패드 상면 −66,100 = **19B 패드 상면**.
- 터널 안 채움(누운 원통 속을 높이까지 채움). 높이 v 는 터널 축에서 잰 값(위 +).
| 채움 | v 범위 | 폭 제한 |
|---|---|---|
| 잡석 | −1,300 ~ −1,175 (125) | 세그먼트 안쪽 벽까지 |
| 패드 2단(아래) | −1,175 ~ −1,100 (75) 🔶 | 세그먼트 안쪽 벽까지 |
| 패드 1단(위) | −1,100 ~ −1,050 (50) 🔶 | 축 중심 폭 800 |
🔶 확인 필요: 패드 합계 125 를 「양측 트렌치 깊이 50」에 맞춰 75 + 50 으로 나눈 것은 코치 추론.
## 5. 데이터 파일 `model.js`
- 첫 줄은 `const MODEL = ` 로 시작하고 그 뒤는 **순수 JSON**(따옴표 있는 키, 주석·끝 쉼표 없음), 마지막은 `;`. check.sh 가 이 꼴을 python 으로 읽는다.
- 최상위: `units`("mm"), `parts`(배열).
- 모든 부재 공통 필드: `id`(겹치지 않음), `name`(한글 이름), `kind`, `type`, `color`("#rrggbb"), `alpha`(0~1, 없으면 1).
- `kind` 값: `wall`, `slab`, `pad`, `bottom`, `lean`, `stair`, `tunnel`, `fill`.
| type | 쓰는 kind | 필드 |
|---|---|---|
| `ring` (세운 속 빈 원통) | wall | `cx, cy, z0, z1, r_out, r_in` |
| `disc` (세운 원판) | slab, pad, bottom, lean | `cx, cy, z0, z1, r` (z0 아래, z1 위) |
| `pipe` (누운 속 빈 원통) | tunnel | `x, y, z`(축 시작점), `angle_deg, length, r_out, r_in` |
| `stair` (계단 런) | stair | `z_low, z_high, risers, first_riser, riser, tread, x_top, dir(+1/−1), y0, y1` |
| `fill` (누운 원통 속 채움) | fill | `x, y, z, angle_deg, length`(pipe 와 같은 축), `r`(세그먼트 안쪽 반지름), `v0, v1, half_width`(0 이면 제한 없음) |
부재 수: wall 1 · slab 19 · pad 1 · bottom 1 · lean 1 · stair 18 · tunnel 1 · fill 3 = 45.
색은 자유. 단 벽은 `alpha` 0.25.
## 6. 화면
- `index.html` 을 브라우저로 **더블클릭해서 열면** 바로 보인다(서버·인터넷 불필요). 캔버스 하나가 창을 채운다.
- 처음 화면: **등각 정사 투영**, 모델 전체(지하 70m 원통과 20m 터널)가 화면에 들어온다.
- 마우스 드래그: 궤도 회전(좌우 = Z축 둘레, 위아래 = 기울기). 휠: 확대/축소.
- 면은 방향에 따라 밝기가 다르다(면 음영). 먼 면부터 그린다(화가 알고리즘).
- 원은 32각형. 벽만 반투명(0.25), 나머지는 불투명.
## 7. 그리기 방식 (함수 이름 목록 — 구현은 하이쿠)
- 부재 → 면: `facesRing`, `facesDisc`, `facesPipe`, `facesStair`, `facesFill`, `facesBox`
- 모든 면 모으기: `buildFaces`
- 카메라: `rotatePoint`, `project`
- 그리기: `shadeColor`, `sortFaces`, `draw`
- 입력: `onMouseDown`, `onMouseMove`, `onMouseUp`, `onWheel`
- 라이브러리·외부 파일 불러오기 금지. 파일은 `index.html`, `viewer.js`, `model.js` 셋(+ README.md).
## 8. 합격 기준
### 8.1 check.sh (기계 판정)
`bash check.sh` → 항목마다 `[PASS]`/`[FAIL]`, 전부 PASS 면 종료 코드 0. 항목 이름 앞에 기능 ID(F01…)가 붙는다.
- 파일 셋이 있고 `index.html` 이 `model.js` 다음에 `viewer.js` 를 읽는다.
- 외부 라이브러리 흔적이 없다(`http://`, `https://`, `import `, `require(`).
- `model.js` 가 위 꼴의 JSON 이고, 부재 수·치수가 4장 표와 같다(허용 오차 1mm).
### 8.2 수석 육안 체크리스트 (check.sh 로 못 재는 것)
- [ ] 더블클릭으로 열린다(Chrome, Edge)
- [ ] 첫 화면이 등각이고 모델 전체가 보인다
- [ ] 드래그 회전·휠 확대가 된다
- [ ] 벽 너머로 슬래브·계단·터널이 보인다
- [ ] 계단이 층마다 방향을 바꾼다
- [ ] 터널이 평면 4시 방향, 19B 높이에 붙어 있다
- [ ] 1장의 세 문장을 화면으로 말할 수 있다
## 9. 나중 (MVP 뒤, 시간이 남으면)
프리셋 뷰·리셋 → 클릭 선택·속성 패널 → 부재별 표시/숨김 → 축·그리드·단위 → 슬래브 개구부 → 단면·거리·치수 라벨·PNG.
## 10. 추가 — 도면 분할 화면과 도면 클릭 연결 (2026-10-20 수석 지시, F13~F15)
### 10.1 화면 배치 (F13)
- `index.html` 본문: 왼쪽 `#drawPane` · 가운데 세로 막대 `#splitter`(폭 6px) · 오른쪽 `#viewPane`(기존 캔버스).
- 처음 비율 50:50. 막대를 끌면 왼쪽 폭이 바뀐다(창 폭의 15%~85%). 바뀔 때마다 캔버스 크기를 오른쪽 칸에 맞추고 다시 그린다.
- 도면 파일은 `private/drawing.svg` 고정. `private/` 는 `.gitignore` 로 커밋하지 않는다(제출 압축에만 넣는다). 파일이 없으면 왼쪽 칸에 「도면 파일 없음 — private/drawing.svg 를 넣으세요」.
### 10.2 도면 표시·확대·이동 (F14)
- `#drawPane` 안에 `#drawStage` 하나, 그 안에 `<img id="drawing">`(도면)과 같은 크기로 겹친 인라인 `<svg id="hotspots" viewBox="0 0 443 443">`(클릭 영역). 확대·이동은 `#drawStage` 의 CSS transform 으로 한다.
- 처음 화면: 도면 SVG 좌표 상자 `fit`(아래 MODEL.drawing) 이 왼쪽 칸에 꽉 차게(`fitDrawing`).
- 휠 = 마우스 위치 기준 확대/축소(0.5~40배), 드래그 = 이동.
### 10.3 도면 ↔ 부재 연결 (F15)
- `model.js` 최상위에 `drawing` 추가:
`src` "private/drawing.svg" · `viewBox` [0, 0, 443, 443] · `fit` [118, 160, 245, 250](u0, v0, u1, v1) · `section` {x0 139.0, y0 174.08, mm_per_unit 1000} · `plan` {cx 227.51, cy 177.1, mm_per_unit 1000}
- 좌표 (u = 도면 가로, v = 도면 세로, 아래로 +):
- 단면 A-A: u = x0 + X/1000, v = y0 − z/1000 (도면 선으로 확인: 벽 바깥 133.5/144.5, 안쪽 134.15/143.85, 1B 상면 181.48)
- 평면도: u = cx + x/1000, v = cy − y/1000
- 부재별 클릭 영역(다각형, 도면 단위). 아래 순서대로 만들고, 겹치면 나중 것이 위:
1. stair: 단면, u = x0 ± 4.85, v = y0 − (z_high − 300)/1000 ~ y0 − z_low/1000
2. wall: 단면 두 띠, u = x0 − 5.5 ~ x0 − 4.85 와 x0 + 4.85 ~ x0 + 5.5, v = y0 − z1/1000 ~ y0 − z0/1000
3. tunnel: 평면, 축 시작점에서 angle_deg 방향으로 length, 폭 ±r_out 인 사각형
4. slab, pad, bottom, lean (disc): 단면, u = x0 ± r/1000, v = y0 − z1/1000 ~ y0 − z0/1000
5. fill: 영역 없음
- 동작: 영역 위에 마우스 → 노란 테두리. 클릭 → 그 부재 선택: 도면 영역을 #ffd400(불투명도 0.35)으로 칠하고, 3D 에서 그 부재를 #ffd400·alpha 1 로 그리고, 3D 칸 왼쪽 위에 「선택: 부재 이름」. 영역 밖 클릭 → 선택 해제.
- 함수 이름: `onSplitDown`, `onSplitMove`, `onSplitUp`, `fitDrawing`, `onDrawWheel`, `onDrawDown`, `onDrawMove`, `onDrawUp`, `sectionPoly`, `planPoly`, `buildHotspots`, `selectPart`
- 3D → 도면 방향(3D 클릭으로 도면 표시)은 하지 않는다(나중).
## 11. 추가 — 제출 문서 두 개 (2026-10-20 수석 지시, F16~F17)
제출물은 git log, 코드, 문서. 문서는 둘로 나눈다. 둘 다 첫머리에 🔒 한 줄. 사실만 쓰고, git log·PROGRESS.md·SPEC.md 에 근거가 있는 내용만 쓴다.
### 11.1 `구동설명서.md` — 무엇을 어떤 기획으로 만들었고 어떻게 돌리나 (F16)
제목 순서(## 단계 제목에 아래 낱말을 그대로 넣는다):
1. 목적 — 무엇을 누구에게 보여 주는가(SPEC 1장 세 문장)
2. 기획 — 도면 선정 → 범위 결정 → 설계 면담으로 치수·규칙 확정(SPEC 3~5장 요지) → SPEC·FEATURES·check.sh → 기능 하나씩 커밋. 결정된 것 중 핵심 5개 이상(예: 지표 = z 0, 터널 패드 = 19B 패드 높이, 계단 첫 단 100)
3. 범위 — 넣은 것 / 뺀 것 (SPEC 2장, 9장)
4. 구동 — 더블클릭으로 연다, 도면은 `private/drawing.svg` 에 둔다, 브라우저
5. 조작 — 3D 드래그·휠, 가운데 막대, 도면 확대·이동, 도면 클릭 → 3D 강조
6. 작업 방식 — WSL2, Claude Code(Haiku 5.5)가 코드 작성·수정·커밋, 합격 판정은 check.sh, 설계 면담·검토와 F02 이후 하이쿠 호출은 수석 위임으로 상위 모델 코치 세션이 맡음, 기능 1개 = 커밋 1개
7. 한계 — 계단은 위에서 보면 슬래브에 가려 옆에서 본다, 슬래브 개구부 없음, 3D → 도면 방향 없음 등
8. 이력 — `git log --oneline` 결과 표(해시·첫 줄)
### 11.2 `README.md` — 코드 설명 (F17, 기존 README 를 고쳐 쓴다)
제목 순서: 파일 구조 · 실행 · 데이터(model.js) · 코드(viewer.js) · 그리기 방식 · 도면 연결 · 검사
- 데이터: type 별 필드 표(SPEC 5장), drawing 항목(SPEC 10.3)
- 코드: SPEC 7장·10.3 의 함수 이름을 **모두** 적고 각각 한 줄 설명
- 그리기 방식: 회전·정사 투영·면 음영·먼 면부터(화가 알고리즘)·벽 반투명
- 도면 연결: 단면·평면 좌표 변환식
- 검사: `bash check.sh`, `bash check.sh F0n`
## 12. 변경 — 구동설명서를 HTML 위키로 (2026-10-20 수석 지시, F18)
- 11.1 의 `구동설명서.md` 를 HTML 여러 쪽으로 옮기고, md 파일은 지운다(내용은 그대로 옮기고 git 기록에는 남는다).
- 폴더 `guide/`: 목차 `index.html` + 8쪽. 파일 이름(쪽 제목): `purpose.html`(목적) · `plan.html`(기획) · `scope.html`(범위) · `run.html`(구동) · `control.html`(조작) · `method.html`(작업 방식) · `limits.html`(한계) · `history.html`(이력). 모양은 `guide/guide.css` 하나.
- 모든 쪽(목차 포함):
- 맨 위에 「← 3D 화면으로 돌아가기」 버튼 — `../index.html` 로 가는 링크
- 🔒 한 줄, `<h1>` 쪽 제목
- 위키처럼 본문 낱말에 다른 쪽 링크를 건다(예: 기획 쪽의 「범위」 → scope.html). 쪽마다 다른 쪽 링크 2개 이상
- 아래에 「목차」 링크와 이전/다음 쪽 링크
- 3D 화면(`index.html`): 오른쪽 3D 칸 **오른쪽 위**에 「구동 설명서」 버튼 `<a id="guideLink" href="guide/index.html">`. 3D 드래그를 방해하지 않게 작게.
- 라이브러리·외부 URL 금지는 그대로. 파일 목록에 `guide/` 추가.
## 13. 추가 — 선명한 도면과 모든 그림 연결 (2026-10-20 수석 지시, F19~F20)
### 13.1 선명한 도면 (F19)
- 원인: 도면 SVG 의 선 굵기가 0.75 도면단위로 박혀 있어 확대할수록 굵어지고, `#drawStage` 를 CSS scale 로 키워 그림이 뭉개진다.
- `tools/prep_drawing.py`(python3, 표준 라이브러리만): `python3 tools/prep_drawing.py <원본.svg> private/drawing.svg`. 원본은 고치지 않고, 출력 SVG 는 0 이 아닌 stroke-width 를 모두 1 로 바꾸고 루트에 `<style>` 로 모든 요소에 `vector-effect: non-scaling-stroke` 를 건다(화면에서 1px 선). 원본 파일 이름·경로를 코드나 문서에 적지 않는다.
- 확대·이동은 CSS scale 대신 `#drawing` 과 `#hotspots` 의 **실제 width/height** 를 배율에 맞춰 바꾸고(브라우저가 벡터를 다시 그림), 위치만 translate 로 옮긴다. 함수 `applyDrawView`.
### 13.2 모든 그림 연결 (F20)
- `MODEL.drawing.views` 배열을 추가한다(10.3 의 section·plan 은 그대로 둔다). 값은 아래 표 그대로.
| id | type | 그림 | 기준 |
|---|---|---|---|
| secA | section | 단면 A-A | x0 139.0, y0 174.08 |
| secB | section | 단면 B-B | x0 179.01, y0 174.08 |
| secA2 | section_break | 단면 A-A (4B~15B 생략) | x0 263.41, y0_upper 173.04, y0_lower 140.04, z_upper_min −17600, z_lower_max −53600 |
| secB2 | section_break | 단면 B-B (4B~15B 생략) | x0 284.23, 나머지 secA2 와 같음 |
| plan1 | plan | 평면도 | cx 227.51, cy 177.1, tunnel true |
| plan2 | plan | 평면도 2 | cx 227.51, cy 197.1, tunnel false |
| slabOdd | slabplan | 중간슬래브 (홀수층) | cx 227.51, cy 226.31, kind slab, z1 = 1B·3B·…·17B 상면 |
| slabEven | slabplan | 중간슬래브 (짝수층) | cx 252.27, cy 226.31, kind slab, z1 = 2B·4B·…·16B 상면 |
| slab18 | slabplan | 중간슬래브 (18B) | cx 277.02, cy 226.31, kind slab, z1 [−63500] |
| slab19 | slabplan | 중간슬래브 (19B) | cx 227.51, cy 247.32, kind slab, z1 [−66200], tunnel true |
| slab20 | slabplan | 하부슬래브 (20B) | cx 252.27, cy 247.32, kind bottom, z1 [−69000] |
- 그림별 클릭 영역(모두 도면 단위, 각 view 의 기준으로 계산):
- section: 10.3 규칙 그대로(stair → wall → slab·pad·bottom·lean). x0·y0 만 view 값.
- section_break: section 과 같되, 부재의 z 범위가 전부 z_upper_min 이상이면 y0_upper, 전부 z_lower_max 이하이면 y0_lower 로 계산하고, 그 밖(생략 구간에 걸친 것)은 영역 없음. 벽은 위 조각(0 ~ z_upper_min)과 아래 조각(z_lower_max ~ 바닥) 두 개로 나눈다.
- plan: 고리(반지름 4.85 ~ 5.5, 32각형 두 개를 evenodd 로) → 벽. tunnel true 면 10.3 의 터널 사각형도(그 view 의 cx·cy 기준).
- slabplan: 안쪽 원판(반지름 4.85) → z1 상면이 일치하는 kind 부재 **전부를 한 번에** 선택. 고리 → 벽. slab19 는 19B 패드도 함께, tunnel true 면 터널 사각형.
- 선택은 여러 부재를 한 번에 할 수 있다: `selectPart` 가 부재 id 배열을 받는다. 「선택:」 글자는 view 이름과 부재 이름(여럿이면 「1B, 3B, … 17B」처럼).
- 함수: `viewHotspots`(view 하나의 영역 목록), `buildHotspots` 는 views 를 돈다.
- 참고(구현하지 않음): 슬래브 평면에 있는 개구부(계단·케이블 slot·펌프 배출구·전기 배선구)는 3D 에 없다(2장 범위 밖). 그래서 3D 슬래브는 꽉 찬 원판이고, 계단 맨 윗단이 슬래브를 뚫고 보인다. 개구부를 넣으려면 슬래브 평면에서 개구 위치·크기를 읽어 원판 윗면·아랫면을 구멍 난 다각형(evenodd)으로 그려야 한다 — 다음 단계 후보.
## 14. 변경 — 구동설명서는 사용 설명서 (2026-10-20 수석 지시)
- `guide/` 는 누구나 이 웹을 작동할 수 있게 하는 **사용 설명서**다. 수석 지시로 코치 세션이 직접 쓴다. 파일 이름·목차·돌아가기 버튼 규칙(12장)과 check.sh 검사 항목은 그대로 지킨다.
## 15. 추가 — 슬래브 구멍, 계단 방향 정정, 카메라 이동, 사람 애니메이션 (2026-10-20 수석 지시, F21~F23)
### 15.1 슬래브 구멍 (F21)
- 슬래브 평면 도면에서 읽은 구멍(슬래브 중심 기준 mm, X 오른쪽·Y 위쪽). 노란 선(방호 난간)이 비어 있는 쪽이 계단 입구.
- disc 부재에 `holes` 필드: 다각형 목록, 다각형은 [x, y] 점 목록(mm). 구멍이 없으면 필드를 생략하거나 빈 목록.
| 구멍 | 다각형 (mm) |
|---|---|
| T_odd (계단 + 위 slot) | (−2100,0) (2100,0) (2100,1000) (1250,1000) (1250,2200) (−1250,2200) (−1250,1000) (−2100,1000) |
| T_even (계단 + 아래 slot) | (−2100,0) (2100,0) (2100,−1000) (1250,−1000) (1250,−2200) (−1250,−2200) (−1250,−1000) (−2100,−1000) |
| slot_up | 사각형 x −1250~1250, y 1000~2200 |
| slot_dn | 사각형 x −1250~1250, y −2200~−1000 |
| stair_even | 사각형 x −2100~2100, y −1000~0 |
| pump | 사각형 x −750~750, y 3650~4790 |
| pump_pad | 사각형 x −750~750, y 3650~4750 |
| elec | 사각형 x 2080~3070, y 3530~3750 (벽에 붙은 1000×300, 벽 안쪽까지만) |
| 부재 | holes |
|---|---|
| 홀수층 슬래브 1B·3B·…·17B | T_odd, slot_dn, pump, elec |
| 짝수층 슬래브 2B·4B·…·16B | T_even, slot_up, pump, elec |
| 18B 슬래브 | stair_even, pump, elec (slot 없음 — 수석 확인 Q1) |
| 19B 슬래브 | pump (집수정 사다리도 이 구멍으로 — 수석 확인 Q2) |
| 19B 패드 | pump_pad |
| 하부 슬래브·바닥콘크리트 | 없음 |
- 그리기: 원판 윗면·아랫면 = 바깥 32각형 + 구멍 다각형을 한 면으로 묶어 evenodd 로 칠한다. 구멍 둘레마다 세로 옆면을 더한다. 함수 `facesDiscHoles`.
### 15.2 계단 방향 정정 (F21, 4.4 의 🔶 추론을 도면으로 바로잡음)
- 홀수 런(1·3·…·17, 홀수층 슬래브에서 내려감): x_top **+2100**, dir **−1**, Y 띠 **0 ~ +1000**
- 짝수 런(2·4·…·18, 짝수층·18B 에서 내려감): x_top **−2100**, dir **+1**, Y 띠 **−1000 ~ 0**
- 블록 계산식(4.4)은 그대로.
### 15.3 카메라 이동 (F22)
- 3D 는 보는 중심 `CAM.target`[x, y, z](처음 = 모델 중심)을 기준으로 그린다.
- 왼쪽 드래그 = 회전(그대로). **오른쪽 드래그 또는 Shift + 왼쪽 드래그 = 화면 이동**(target 을 화면 평면에서 옮김). 캔버스에서 오른쪽 클릭 메뉴는 막는다.
- 휠 = **마우스 아래 점을 고정한 채** 확대·축소, 배율 0.5~**40**.
- 더블클릭 또는 3D 칸 오른쪽 위 `<button id="resetView">처음 화면</button>` = 처음 등각 화면(yaw·pitch·배율·target 초기값).
- 함수 `panView`, `zoomAt`, `resetView`.
### 15.4 사람 애니메이션 (F23)
- 사람 = 상자 4개(mm): 다리 2개 150×150×800(좌우 100 간격), 몸통 400×250×600(z 800~1400), 머리 220×220×220(z 1450~1670). 색 #ff8c1a, 불투명. 데이터 파일에는 넣지 않고 viewer.js 가 그린다.
- 경로 `buildWalkPath`: 계단 데이터(stair 18개)로 만든다. 런마다
1. 위 슬래브 위, 입구 앞 300 지점 (x_top − dir·300, 띠 중심 y, z_high)
2. 디딤판마다 가운데 점: k = 단 수−1 … 1 순서로 x = x_top + dir·300·(단 수 − 1 − k + 0.5), z = z_low + 100 + 200·(k − 1)
3. 아래 슬래브에 내려선 점 (x_top + dir·300·단 수, 띠 중심 y, z_low)
4. 다음 런 입구 앞 점으로 옆걸음(같은 높이)
- 1B 런 입구에서 19B 까지 내려가고, 같은 길로 1B 까지 올라오기를 쉬지 않고 반복(속도 1000 mm/s, 점 사이 직선 보간). 함수 `walkerAt`(시각 → 위치), `facesPerson`, `animate`(requestAnimationFrame).
- 3D 칸 오른쪽 위 `<button id="walkToggle">` — 움직일 때 「사람 ❚❚」, 멈췄을 때 「사람 ▶」. 처음에는 움직인다.
- 멈춰 있을 때는 지금처럼 필요할 때만 다시 그린다. 느리면 초당 30번으로 줄인다.
## 16. 추가 — 바닥부: 월류벽·집수조·침사조·침사구·사다리 (2026-10-20 수석 사전 승인, F24~F25)
도면 단면 A-A 하부와 19B·20B 슬래브 평면에서 읽음(mm, 슬래브 중심 기준).
- **정정**: 15.1 의 「19B 집수정 사다리는 펌프 배출구로(Q2)」는 도면과 다르다. 19B 동쪽 벽 쪽에 **침사구 1000×1000** 구멍이 따로 있고, 사다리는 그 아래 동쪽 벽에 붙어 있다. (수석 부재 중 도면 근거로 코치가 바로잡음)
### 16.1 부재 (F24)
| 부재 | kind | type | 값 |
|---|---|---|---|
| 월류벽 (넘침 홈 양옆 2, 홈 아래 1) | weir | prism | ① 다각형 사각형 X 1400~1600, Y −4580~−500, z −69000~−67000 ② Y 500~4580, 같은 z ③ Y −500~500, z −69000~−67500 (홈 깊이 500) |
| 집수조 (펌프 쪽, 서쪽) | pit | zone | r 4850, x_min −4850, x_max 1400, z −69000~−67000, 반투명 0.25 |
| 침사조 (터널 쪽, 동쪽) | pit | zone | r 4850, x_min 1600, x_max 4850, z −69000~−67000, 반투명 0.25 |
| 사다리 | ladder | ladder | x0 4668, x1 4708(기둥 두께), yc 0, width 400, z0 −68700, z1 −66200, rung 300 |
| 19B 슬래브 구멍 추가 | — | — | 침사구 사각형 X 3850~4820, Y −500~500 |
| 19B 패드 구멍 추가 | — | — | 침사구 사각형 X 3850~4720, Y −500~500 |
- `prism`: `polygon`([x, y] 점 목록), `z0`, `z1` — 다각형을 세로로 늘인 기둥(윗면·아랫면·옆면). 함수 `facesPrism`.
- `zone`: 원(반지름 r) 안에서 x_min ≤ X ≤ x_max 인 부분을 z0~z1 로 세운 덩어리(활꼴 기둥). 함수 `facesZone`.
- `ladder`: 기둥 2개(X x0~x1, Y = yc ± width/2 에 두께 40), 가로대(rung 간격, 두께 25)를 상자로. 함수 `facesLadder`.
- 색은 자유(물칸은 서로 다른 옅은 색). 🔶 사다리 폭 400·가로대 300 은 도면에 없어 코치가 정한 값(산업안전보건기준 사다리 폭 30cm 이상 참고).
### 16.2 도면 연결 (F25)
- 단면 A-A 두 그림(view id `secA`, `secA2`)에서: 월류벽·집수조·침사조·사다리를 단면 규칙(u = x0 + X/1000, v = y0 − z/1000)으로 X 범위 × z 범위 사각형 영역. 순서 집수조 → 침사조 → 월류벽 → 사다리(나중 것이 위).
- 하부슬래브(20B) 평면(`slab20`)에서: 위 네 부재를 평면 규칙(u = cx + X/1000, v = cy − Y/1000)으로. 집수조·침사조는 원과 세로선으로 자른 활꼴 다각형.
- 19B 슬래브 평면(`slab19`)의 침사구 자리를 누르면 사다리를 선택.
- 함수 `bottomHotspots`.
## 17. 추가 — 상부 구조·환기구·방호 난간·배관 (2026-10-20 수석 사전 승인, F26~F29)
목표: 2D 도면에 있는 것을 모두 3D 로(수석 지시). 단면 A-A 상부, 단면 B-B(환기 덕트를 펼친 단면), 평면도 1·2, 슬래브 평면 노란 선에서 읽음. 🔶 = 도면에 수치가 없어 코치가 정한 값.
### 17.1 상부 슬래브·맨홀 목·벽 상단 (F26)
- **정정**: 벽체 윗면은 지표가 아니라 상부 슬래브 밑면 **z −2000** (벽 위로는 흙 1500 과 맨홀 목). 4.1 의 벽체 z1 0 → −2000.
- 상부 슬래브: kind `topslab`, type disc, r 5500(벽 바깥면까지), z −2000 ~ −1500, holes: 원 (0, 0, 450), 원 (2223, 3803, 450).
- holes 항목은 다각형 외에 `{"circle": [x, y, r]}` 도 된다(16각형으로 그림). prism 에도 `holes` 를 쓸 수 있다. 함수 `holePolygon`.
- 맨홀 목 2개: kind `neck`, type prism, z −1500 ~ 0, 각각 1300 정사각형을 30° 돌린 다각형 + 가운데 원 구멍 r 450.
- 목 1: [[238, 888], [-888, 238], [-238, -888], [888, -238]], holes [{"circle": [0, 0, 450]}]
- 목 2: [[2461, 4691], [1335, 4041], [1985, 2915], [3111, 3565]], holes [{"circle": [2223, 3803, 450]}]
- **정정**: 전기 배선구 구멍은 벽을 따라 돈 1000×300 사각형이다(난간 선으로 확인). 15.1 의 elec 를 다각형 [[2079, 4079], [2922, 3528], [3072, 3753], [2229, 4304]] 로 바꾼다(홀수층·짝수층·18B).
### 17.2 분기구·환기구 (F27)
- `obox`(돌린 상자): `cx, cy, angle_deg, s0, s1, t0, t1, z0, z1`. 점 = (cx, cy) + s·(cosθ, sinθ) + t·(−sinθ, cosθ). 함수 `facesObox`.
- 분기구 kind `branch`, cx = cy = 0, 안 2200×2100, 벽 350, 바닥 450:
| 이름 | angle | s | t | z |
|---|---|---|---|---|
| 바닥 | 167 | 4850 ~ 8350 | -1450 ~ 1450 | -4550 ~ -4100 |
| 지붕 | 167 | 4850 ~ 8350 | -1450 ~ 1450 | -2000 ~ -1650 |
| 옆벽 1 | 167 | 4850 ~ 8350 | 1100 ~ 1450 | -4100 ~ -2000 |
| 옆벽 2 | 167 | 4850 ~ 8350 | -1450 ~ -1100 | -4100 ~ -2000 |
| 끝벽 | 167 | 8000 ~ 8350 | -1100 ~ 1100 | -4100 ~ -2000 |
- 환기구(강제) 덕트·환기탑 kind `vent`, cx = cy = 0, angle 0 (s = X, t = Y). 덕트 안 3000×2100, 남쪽으로 내려가 동쪽으로 꺾여 환기탑(안 3000 정사각형, 지표 z 0 까지)에 닿는다:
| 이름 | angle | s | t | z |
|---|---|---|---|---|
| 덕트 바닥 1 | 0 | -1850 ~ 1850 | -10500 ~ -4610 | -4600 ~ -4100 |
| 덕트 바닥 2 | 0 | 1850 ~ 7504 | -10500 ~ -6800 | -4600 ~ -4100 |
| 덕트 지붕 1 | 0 | -1850 ~ 1850 | -10500 ~ -4610 | -2000 ~ -1650 |
| 덕트 지붕 2 | 0 | 1850 ~ 7504 | -10500 ~ -6800 | -2000 ~ -1650 |
| 덕트 서벽 | 0 | -1850 ~ -1500 | -10500 ~ -4610 | -4100 ~ -2000 |
| 덕트 동벽 | 0 | 1500 ~ 1850 | -6800 ~ -4610 | -4100 ~ -2000 |
| 덕트 북벽 | 0 | 1500 ~ 7504 | -7150 ~ -6800 | -4100 ~ -2000 |
| 덕트 남벽 | 0 | -1850 ~ 7504 | -10500 ~ -10150 | -4100 ~ -2000 |
| 환기탑 바닥 | 0 | 7504 ~ 11205 | -10500 ~ -6799 | -4600 ~ -4100 |
| 환기탑 서벽 위 | 0 | 7504 ~ 7854 | -10149 ~ -7149 | -2000 ~ 0 |
| 환기탑 동벽 | 0 | 10855 ~ 11205 | -10500 ~ -6799 | -4100 ~ 0 |
| 환기탑 북벽 | 0 | 7504 ~ 11205 | -7149 ~ -6799 | -4100 ~ 0 |
| 환기탑 남벽 | 0 | 7504 ~ 11205 | -10500 ~ -10149 | -4100 ~ 0 |
### 17.3 방호 난간·배관·위쪽 사다리 (F28)
- 방호 난간 kind `railing`, type `railing`: `z`(설치 슬래브 윗면), `h` 1100 🔶, `paths`(꺾은선 목록, 각 [x, y] 점). 꺾은선을 따라 높이 h 의 얇은 면(난간 판)을 세운다. 함수 `facesRailing`.
- 홀수층 9장(1B·3B·…·17B 윗면): paths [[[2140, 4350], [1940, 4050], [2950, 3390], [3150, 3690]], [[1350, -900], [1350, -2300], [-1350, -2300], [-1350, -900], [1350, -900]], [[2100, -100], [-2200, -100], [-2200, 1100], [-1350, 1100], [-1350, 2300], [1350, 2300], [1350, 1100], [2100, 1100]], [[-850, 4780], [-850, 3550], [850, 3550], [850, 4780]]]
- 짝수층 8장(2B·…·16B 윗면): paths [[[-850, 4780], [-850, 3550], [850, 3550], [850, 4780]], [[-2100, 100], [2200, 100], [2200, -1100], [1350, -1100], [1350, -2300], [-1350, -2300], [-1350, -1100], [-2100, -1100]], [[1350, 2300], [1350, 900], [-1350, 900], [-1350, 2300], [1350, 2300]], [[2140, 4350], [1940, 4050], [2950, 3390], [3150, 3690]]]
- 19B(z −66100, 패드 위): paths [[[-850, 4780], [-850, 3550], [850, 3550], [850, 4780]]]
- 18B: 도면에 난간 없음.
- 펌프 배출관(STS Ø100) kind `pipe`, type ring: cx 0, cy 4220, r_out 57, r_in 50, z −68800 ~ −2000 🔶(도면 L=64.0m 는 경로 길이로 보고 집수조 바닥 근처부터 상부 슬래브 밑까지).
- 위쪽 사다리 kind `ladder_up`, type ladder:
- 서쪽(1B → 분기구 바닥): x0 −4720, x1 −4680, yc 1080, width 400, z0 −7400, z1 −4100, rung 300
- 가운데(1B → 맨홀 → 지표): x0 −20, x1 20, yc 0, width 360, z0 −7400, z1 0, rung 300 🔶
### 17.4 도면 연결 (F29)
- 단면 A-A(secA, secA2 위쪽): topslab 은 disc 규칙, neck·branch·vent·ladder_up 은 꼭짓점들의 X 범위 × z 범위 사각형.
- 평면도 1·2(plan1, plan2): 원판 안(r 4850)을 먼저 topslab 영역으로, 그 위에 neck 다각형, branch·vent obox 바닥 윤곽(꼭짓점 4개), 그 위에 기존 벽 고리·터널.
- 함수 `upperHotspots`.
## 18. 변경 — 사람 두 명, 콘크리트 계단, 구동설명서 3쪽 (2026-10-21 수석 지시, F30~F32)
### 18.1 사람 두 명 (F30)
- 지하시설물에 들어갈 때는 2명 이상이 조를 이루어 이동한다. 사람을 두 명으로 하고, 두 번째 사람은 첫 사람과 같은 경로를 **2초 뒤에** 따라온다(속도 1000 mm/s → 약 2m 간격). 경로 끝에서 돌아설 때 잠깐 스쳐 지나가는 것은 받아들인다.
- 색: 첫 사람 #ff8c1a, 두 번째 #e07010. 함수 `teamAt`(시각 → 두 위치). 「사람 ▶/❚❚」 버튼은 두 사람을 함께 멈춘다.
### 18.2 콘크리트 계단 (F31)
- 철근콘크리트 계단: 경사판(계단 슬래브) 위에 단면이 직각삼각형인 단이 올라간 꼴이고, **밑면은 경사지며 계단 아래는 빈 공간**이다(수석 제공 측면도로 비례 확인: 안쪽 모서리 선에서 수직 200 아래에 밑면, 위끝은 위 슬래브 밑보다 100 낮음, 아래끝은 마지막 디딤판보다 약 450 앞에서 아래 슬래브에 닿음).
- 런마다 단면 다각형(s = x_top 에서 내려가는 방향 거리, 실제 x = x_top + dir·s)을 만들어 Y 띠(y0~y1) 폭만큼 늘인다. 함수 `stairProfile`(런 → 점 목록), `facesStair` 가 이것으로 면을 만든다(옆면 2개 + 둘레 변마다 사각형).
1. (0, z_high) 에서 시작
2. j = 0 … n−2: 챌판 (j·300, z_high − 200·(j+1)) 로 내려가고, 디딤판 ((j+1)·300, z_high − 200·(j+1)) 로 간다
3. 첫 단 챌판: ((n−1)·300, z_low)
4. 아래 슬래브 윗면을 따라 (s_b, z_low), s_b = (z_high − z_low − 400) × 1.5
5. 경사 밑면: (0, z_high − 400)
6. 다시 (0, z_high) 로 닫는다
- 디딤판 높이·위치는 4.4·15.2 와 같다(사람 경로 그대로). 색은 그대로.
### 18.3 구동설명서 3쪽 (F32)
- `guide/` 는 목차(index) + **목적(purpose)·구동(run)·조작(control)** 3쪽만 남긴다. plan·scope·limits·method·history 쪽은 지운다(git 기록에는 남는다).
- 🔒 줄은 구동설명서에 넣지 않는다(수석 지시, 외부에 내보낼 문서). README·SPEC 의 표기는 그대로.
- 사람 문장: 「주황색 사람 두 명 — 지하시설물에 들어갈 때는 2명 이상이 조를 이루어 이동해야 한다 — 이 시설물 계획 중 유지관리 동선계획을 따라 이동하는 과정을 보여 준다.」
- 「3D 를 눌러 도면을 찾는 방향은 아직 없다」 문장은 뺀다. 지운 쪽을 가리키는 링크·문구는 남은 쪽으로 바꾸거나 지운다. 도면 연결 표는 조작 쪽으로 옮긴다.
- 쪽마다: 맨 위 「← 3D 화면으로 돌아가기」, h1, 다른 두 쪽 링크, 목차·이전/다음 링크. 수석 지시로 코치 세션이 쓰고, 하이쿠가 커밋한다.
## 19. 추가 — 도면 추출 도구와 대조 검사 (2026-10-21 수석 지시, F33~F34)
목적: 「도면 → 데이터」 단계를 저장소 안 코드로 옮긴다. 범용 변환기가 아니라 **이 도면에 대한 반자동 변환**: 도면에서 기계적으로 읽을 수 있는 값은 도구가 읽고, 사람이 정한 규칙(패드 높이, 계단 첫 단 등)은 SPEC 의 설계 결정으로 남는다.
### 19.1 추출 도구 (F33)
- `python3 tools/extract_from_svg.py <도면.svg> model_extracted.js` — 표준 라이브러리만. 출력은 model.js 와 같은 꼴(`const MODEL = ` + 순수 JSON + `;`), `units` "mm".
- **도면 좌표를 코드에 숫자로 적지 않는다**(그림 위치·기준점은 모두 도면에서 찾는다). 규칙:
1. 축척: 단면도·평면도 모두 1 도면단위 = 1000 mm. 평면은 X = (u − 중심u)·1000, Y = −(v − 중심v)·1000. 단면은 X = (u − 축u)·1000, z = −(v − 지표v)·1000.
2. 그림 찾기: `<text>` 의 글자(공백 무시)와 transform 의 평행이동 값으로 그림 이름 위치를 얻는다. 「단면A-A」 중 가장 왼쪽 것이 전체 단면이다. 슬래브 평면은 「중간슬래브(홀수층)」「(짝수층)」「(18B)」「(19B)」 글자 바로 아래에 있는 원(반지름 5.5 와 4.85 두 개)의 중심.
3. 단면 A-A(전체): 글자 아래에 있는 하늘색 rgb(0,255,255) 세로선 중 가장 긴 두 쌍이 벽 — 바깥 두 선 간격 = 외경, 안 두 선 간격 = 내경, 축u = 바깥 두 선의 가운데. 지표v = 그 근처 녹색 rgb(0,255,0) 수평선(지반선)의 v.
4. 슬래브 윗면: 단면 A-A 의 하늘색 수평선 중 길이가 내경과 같은(±0.05) 선. 바로 0.3 아래에 같은 길이 선이 또 있으면 위 선만 윗면이다. 결과는 위에서부터 19개(1B~19B).
5. 슬래브 구멍: 슬래브 평면 원(반지름 4.85) 안의 검은색 rgb(0,0,0)·회색 rgb(128,128,128) 꺾은선 중 닫힌 사각형은 그대로, 세 변만 있는 꺾은선(벽에 붙은 구멍)은 두 끝점을 이어 닫은 다각형을 구멍으로 한다. 다른 후보를 감싸는 후보(난간 윤곽, 18B 처럼 회색으로 그려진 경우)는 뺀다. 계단 구멍은 5b 로 뽑는다.
6. 방호 난간: 슬래브 평면 원 안의 노란색 rgb(255,255,0) 꺾은선을 그대로 paths 로.
5b. 계단 구멍(난간으로만 그려짐): 노란 난간 꺾은선 중 두 끝점이 서로 다르고 둘 다 벽(반지름 4.85)에서 0.3 넘게 떨어진 것이 계단 입구 난간이다. 꺾은선을 두 끝점으로 닫은 다각형에서, 닫는 변(입구)은 그대로 두고 나머지 변을 0.1(=100 mm) 안쪽으로 옮긴 다각형을 구멍으로 한다. 홀수층·짝수층 평면에서 하나씩 나온다.
7. 평면 → 층: 홀수층 평면 = 1B·3B·…·17B, 짝수층 = 2B·…·16B, 18B, 19B (4 의 윗면 목록 순서로).
- 색은 요소 자신 또는 가장 가까운 조상 `<g>` 의 stroke 를 따른다.
- 출력 부재: 벽 1(type ring, r_out·r_in, z1 −2000, z0 −70200 — z 는 고정값 허용), 슬래브 19(type disc, z1 = 윗면, z0 = 윗면 − 300, r = 내경/2, holes = 5 의 구멍), 난간(type railing, 층마다 1, z = 슬래브 윗면, h 1100, paths = 6). 각 부재 `name` 에 층 이름.
- 함수(파이썬): `load_svg`, `find_views`, `section_walls`, `slab_tops`, `plan_holes`, `plan_railings`, `build_model`.
### 19.2 대조 검사와 비교 화면 (F34)
- `compare.html`: index.html 과 같은 화면인데 `model.js` 대신 `model_extracted.js` 를 읽는다(도면 연결 데이터가 없으면 왼쪽은 안내 글자). 3D 칸 위에 「추출 데이터로 그린 화면」 글자. 추출 결과와 수작업 모델을 눈으로 비교하는 용도.
- check.sh 가 도구를 직접 돌려 model.js 와 대조한다(벽 반지름 ±10, 슬래브 윗면 ±60, 구멍은 추출된 각 구멍이 model.js 의 어느 구멍 범위 안(±40)에 들고, 난간 꺾은선 수·꼭짓점이 ±40 안에서 같다). 또 도면 좌표를 통째로 옮긴 사본으로 다시 돌려 결과가 같아야 한다(숫자를 코드에 박으면 실패). 그리고 도면 내용을 바꾼 사본 두 개로 돌려 결과가 그만큼 바뀌어야 한다 — 1B 슬래브 윗면 선만 0.5 내린 사본(1B 만 약 500 낮아짐), 홀수층 아래 slot 하나를 지운 사본(홀수층 9장에서 구멍이 하나씩 줄어듦). 공식이나 정답 값을 코드에 넣으면 실패한다.
- `model_extracted.js` 는 도구의 출력이며 커밋한다(도면 자체는 들어 있지 않다).
## 20. 변경 — 추출 입력을 DXF 로 (19장 대체, 2026-10-21 수석 지시, F33~F34)
- 수석이 원본 DWG 를 주었고, 코치가 GNU LibreDWG `dwg2dxf` 로 DXF 로 바꾼 뒤 **우리 도면 세트의 그림(전체 단면·평면도·생략 단면·슬래브 평면)만** 남겼다(표제란·다른 도면 제외). 파일 `private/drawing.dxf`(ASCII, git 제외). 19장의 SVG 규칙은 이 장으로 대체한다(SVG 는 왼쪽 도면 표시에만 쓴다).
- 이 SVG 규칙은 도면에 맞지 않았다(18B 윗면이 구멍에 잘려 18개만 잡힘, T자 구멍의 출처 불명확) — 하이쿠가 하드코딩 없이 보고해 드러남.
### 20.1 추출 도구 (F33)
- `python3 tools/extract_from_dxf.py <도면.dxf> model_extracted.js` — 표준 라이브러리만(DXF 는 「그룹 코드 줄 + 값 줄」의 텍스트다). 출력 꼴은 19.1 과 같다. **도면 좌표·정답 값·층 높이 공식을 코드에 적지 않는다.**
- DXF 읽기: ENTITIES 구역의 LINE·LWPOLYLINE(코드 70 의 1 비트 = 닫힘)·POLYLINE(+VERTEX…SEQEND, 머리의 10/20 은 기준점이라 쓰지 않음)·CIRCLE·TEXT. 레이어 = 코드 8, 색 = 코드 62(없으면 레이어 색).
- 규칙(단위: 모델 1 = 1 m → ×1000 mm, Y 위 +):
1. 전체 단면: 레이어 `구조물` 의 세로 선분 중 길이 60 m 넘는 것들. X 가 가장 작은 네 선 = 단면 A-A 의 벽(바깥 둘·안 둘): 외경 = 바깥 두 선 간격, 내경 = 안 두 선 간격, 축 X = 바깥 두 선 가운데.
2. 지표: 색 3(녹색) 선 중 단면 A-A 의 축 X 를 지나는 것의 그 자리 Y. z = (Y − 지표 Y)·1000.
3. 슬래브 윗면: 레이어 `구조물` 의 수평 선분 중 **왼쪽 안쪽 벽 X 에서 시작**하고(±0.01), 0.3(±0.01) 아래에 역시 왼쪽 안쪽 벽 근처(0.2 안)에서 시작하는 `구조물` 수평 선분이 있는 것. 위에서부터 19개 = 1B~19B (구멍에 잘린 층도 왼쪽 조각이 남는다).
4. 슬래브 평면: 레이어 `S-TITLE` 글자 「중간슬래브(홀수층)」「(짝수층)」「(18B)」「(19B)」(공백 무시) 바로 아래에서 가장 가까운, 반지름 4.85 와 5.5 원이 겹친 중심.
5. 구멍: 그 평면의 안쪽 원 안에 있는 레이어 `오프닝`·`구조물 계단` 객체로 만든다. (가) 꼭짓점 4개 이상인 LWPOLYLINE/POLYLINE 하나는 그대로 후보(닫혀 있지 않으면 두 끝점을 이어 닫는다 — 벽에 붙은 세 변 구멍). (나) 꼭짓점 3개인 열린 LWPOLYLINE 은 같은 레이어의 **수평·수직** LINE 중 그 끝점에서 이어지는 것을 차례로 붙여 닫히면 후보. 기울어진 LINE(대각선)과 끝점에 붙지 않는 LINE(디딤판 선)은 쓰지 않는다. 같은 모양 중복은 하나만, 다른 후보를 감싸는 후보(난간 윤곽)는 뺀다.
6. 방호 난간: 그 평면 원 안의 레이어 `계단`, 색 2 인 꼭짓점 3개 이상 꺾은선 → paths (닫힌 것은 첫 점을 끝에 한 번 더).
7. 평면 → 층: 홀수층 = 1B·3B·…·17B, 짝수층 = 2B·…·16B, 18B, 19B (3 의 순서).
- 출력 부재: 19.1 과 같음(벽 1, 슬래브 19 + holes, 난간 층마다 1).
- 함수(파이썬): `load_dxf`, `section_walls`, `ground_y`, `slab_tops`, `find_slab_plans`, `plan_holes`, `plan_railings`, `build_model`.
- 코치는 이 규칙대로만 짠 별도 구현으로 채점이 모두 통과함을 미리 확인했다(그 구현은 주지 않는다). 막히면 규칙 결함일 수 있으니 숫자를 넣지 말고 보고한다.
### 20.2 대조 검사 (F34)
- 19.2 와 같은 대조(벽 ±5, 윗면 ±5, 구멍 ⊆ model.js 구멍 ±10, 난간 ±10)와 compare.html.
- 계단 구멍(폭 4 m 넘는 구멍) 18개(홀수 9·짝수 8·18B 1).
- 변조 사본 셋: 좌표 전체 (+7, +5) m 이동 → 같은 결과 / 1B 슬래브(윗면·아랫면 선)만 0.5 m 내림 → 1B 만 500 낮아짐 / 홀수층 아래 slot 지움 → 구멍 9개 감소.
## 21. 추가 — 계단 추출 (2026-10-21 수석 지시, F35) — 계단이 생기면 사람 두 명도 추출 모델에서 걷는다
도면(DXF)에서 읽는다. 20.1 의 함수에 더한다. 함수 `section_stairs`, `stair_parts`.
1. 단면 계단 윤곽: 레이어 `구조물`, 색 8, **닫힌** 꺾은선, 꼭짓점 6개 이상, 단면 A-A 바깥 벽 두 선 사이.
2. 단 높이·디딤판: 윤곽들의 세로 변 길이(mm) 중 가장 많이 나온 값 = riser, 가장 작은 값 = first_riser, 가로 변 중 가장 많이 나온 값 = tread.
3. 런 i(1~18): z_high = i 층 윗면, z_low = i+1 층 윗면(20.1 ③). risers = (z_high − z_low − first_riser) / riser + 1 (반올림).
4. 방향 dir: 가장 낮은 꼭짓점의 z 가 z_low 인 단면 윤곽에서, 그 가장 낮은 꼭짓점들이 윤곽의 왼쪽 끝에 붙어 있으면 −1, 오른쪽 끝이면 +1. 단면에 윤곽이 없는 런(18)은 바로 위 런의 반대.
5. x_top, y0, y1: i 층 슬래브 평면의 계단 구멍(20.1 ⑤ 의 구멍 중 폭 4 m 넘는 것). dir −1 이면 구멍의 오른쪽(X 최대) 가장자리, +1 이면 왼쪽(X 최소) 가장자리가 x_top. y0·y1 = 그 구멍의 Y 범위.
6. 출력: kind·type `stair` 18개, 필드는 model.js 와 같다(z_low, z_high, risers, first_riser, riser, tread, x_top, dir, y0, y1, 이름·색은 자유).
- 코치는 이 규칙대로만 짠 별도 구현으로 F35 채점(대조·변조 시험)이 통과함을 미리 확인했다(그 구현은 주지 않는다).
## 22. 정정 — 평면도 좌표틀 (2026-10-21 수석 지시, F36)
- 발견(코치, 도면 DXF 재확인): 슬래브 평면(홀수·짝수·18B·19B·20B)과 단면 A-A 는 같은 틀이다(계단 구멍 긴 변 = X). 평면도 1·2 는 이 틀을 **−30.3°** 돌려 그렸다(평면도의 계단 구멍 긴 변·터널 중심선이 −30.3°). 4.5 와 17장의 터널·채움·분기구·환기구·상부 맨홀·목은 평면도에서 읽어 그 틀 그대로 넣었기 때문에, 모델 안에서 안쪽 부재와 30.3° 어긋났다. 예: 19B 평면에서 터널은 +X(계단과 나란히)인데 모델은 330°.
- 원칙: **모델 좌표 = 슬래브 평면 틀.** 평면도에서 온 점 (x, y) 는 +30.3° 돌린다: (x cos30.3° − y sin30.3°, x sin30.3° + y cos30.3°). 그렇게 돌리면 맨홀 2 가 (0, 4405) = 펌프 구멍 바로 위, 목 두 개가 축에 맞는 1300 정사각형이 된다.
- 새 값(나머지 필드는 그대로):
| 부재 | 바꿀 것 |
|---|---|
| tunnel_TBM | x 4850, y 0, angle_deg 0 (19B 평면의 터널 선이 X 축과 나란하고 Y ±1300·±1500) |
| fill 3개 | x 4850, y 0, angle_deg 0 (터널과 같은 축) |
| topslab | holes [{"circle": [0, 0, 450]}, {"circle": [0, 4405, 450]}] |
| neck_1 | polygon [[-650, -650], [650, -650], [650, 650], [-650, 650]], holes [{"circle": [0, 0, 450]}] |
| neck_2 | polygon [[-650, 3755], [650, 3755], [650, 5055], [-650, 5055]], holes [{"circle": [0, 4405, 450]}] |
| branch 5개 | angle_deg 167 → 197.3 (s, t, z 그대로) |
| vent 13개 | angle_deg 0 → 30.3 (s, t, z 그대로) |
| ladder_up_W | x0 −4622, x1 −4582 (단면 A-A 의 사다리 중심 X −4602), yc −1260 🔶 (분기구 입구 쪽, 폭 400 이 벽 안에 드는 한도) |
- 17.1 의 목 다각형은 돌린 방향도 틀렸다(위 표로 대체). 슬래브 구멍·계단·난간·패드·바닥부·배관·가운데 사다리는 이미 슬래브 평면 틀이라 그대로.
- 도면 연결: `MODEL.drawing.views` 의 plan1·plan2 에 `"rot_deg": -30.3` 을 더한다. 평면 그림에 놓을 때 모델 (X, Y) 를 rot_deg 만큼 돌린 뒤(X' = X cos − Y sin, Y' = X sin + Y cos) u = cx + X'/k, v = cy − Y'/k. rot_deg 가 없는 그림은 0. `planXY` 와 `planPoly`(터널 사각형)에 넣는다(원은 그대로).
- 결과: 3D 에서 터널이 계단과 나란히 +X 로 나가 침사구 쪽 벽으로 들어오고, 평면도 클릭 영역은 지금처럼 도면 그림 위에 놓인다.
## 23. 추가 — 터널 추출 (2026-10-21 수석 지시, F37)
도면(DXF)에서 TBM 터널과 터널 안 채움 3개를 읽는다. 20.1·21장 함수에 더한다. 함수 `section_tunnel`, `plan_tunnel`, `tunnel_parts`.
1. 단면(높이·지름): 단면 A-A **오른쪽 바깥 벽선**(20.1 ① 의 X 가 가장 큰 선)에서 시작해 바깥으로 1 m 넘게 뻗는 레이어 `구조물` 수평선(기울기 5% 미만, 터널 경사가 조금 있다). 벽 쪽 끝의 z(20.1 ② 방식) 다섯 개를 위에서부터 [바깥 위, 안 위, 패드 윗면, 안 아래, 바깥 아래] 로 본다.
- 축 z = (바깥 위 + 바깥 아래)/2, r_out = (바깥 위 − 바깥 아래)/2, r_in = (안 위 − 안 아래)/2.
2. 평면(방향): 「중간슬래브(19B)」 평면 중심(20.1 ④)에서 두 끝이 4.8 m 이상 8 m 이하인 레이어 `-평면계획` 색 8 선분. 각 선분을 중심에 가까운 끝 → 먼 끝으로 본 방향의 합 = 터널 방향 d. 축 어긋남 = 그 선분들 가까운 끝의 d 에 수직인 거리 평균(왼쪽 +).
3. 터널(kind `tunnel`, type `pipe`): angle_deg = d 의 각도(X축에서 반시계, 0~360), 시작점 = 축 어긋남 + 벽 안쪽 반지름(20.1 ①)만큼 d 방향, 길이 = 벽 두께 + 20000(4.5 의 설계값: 벽 바깥에서 20 m).
4. 채움 3개(kind `fill`, 터널과 같은 축·길이, r = r_in). v 는 축에서 잰 높이: 패드 윗면 v = ① 의 패드 윗면 − 축 z. 4.5 의 설계값(도면에 없음): 잡석 두께 125, 패드 1단 두께 50, 1단 폭 800.
- 잡석 −r_in ~ −r_in + 125 / 패드 2단 −r_in + 125 ~ 패드 윗면 − 50 / 패드 1단 패드 윗면 − 50 ~ 패드 윗면(half_width 400, 나머지 0).
5. 도면 좌표·정답 값(축 높이, 지름, 패드 높이, 방향 각도)은 코드에 적지 않는다. 고정값은 위 설계값(20000, 125, 50, 400)과 20.1 의 허용값만.
- 대조(F37): 터널 x·y·z·r·길이 ±10, 방향 ±0.5°, 채움 v·폭 ±5. 변조 사본 둘: 단면 A-A 터널 선만 0.5 m 내림 → 터널 축 500 낮아짐 / 19B 평면 터널 선을 좌우로 뒤집음 → 방향 180°.
- 코치는 이 규칙대로만 짠 별도 구현으로 F37 채점이 통과함을 미리 확인했다(그 구현은 주지 않는다).
## 24. 추가 — 분기구·환기구 추출 (2026-10-21 수석 지시, F38)
도면(DXF)에서 분기구(kind `branch`)와 환기구(덕트·환기탑, kind `vent`)를 obox(17.2) 로 읽는다. 함수 `plan_center`, `frame_angle`, `section_heights`, `branch_parts`, `vent_parts`.
- 평면 모양은 평면도 1, 높이는 단면(분기구 = 단면 A-A, 환기구 = 단면 B-B「환기 덕트를 펼친 단면」)에서 읽는다. 평면도는 슬래브 평면 틀과 각도 φ 만큼 다르다(22장) — φ 도 도면에서 구한다.
### 24.1 평면도 1 과 φ
1. 평면도 중심 C: 레이어 `S-TITLE` 글자 「평면도」(공백 무시) 아래에서 가장 가까운 반지름 4.85 원의 중심.
2. 레이어 `-평면계획` 색 1, 꼭짓점 3개이고 가운데 꼭짓점이 C(±0.01)인 꺾은선 → C 에서 나가는 두 다리.
3. 터널 다리 = 그 다리와 나란하고(±0.01 rad) 다리에서 r_out(23장, ±0.02 m) 떨어진 `-평면계획` 색 8 선분이 2개 이상 있는 다리. 다른 다리 = 분기구 다리.
4. φ = 23장 터널 angle_deg − 터널 다리의 각도. 평면도의 점·방향은 모두 C 기준 mm 로 바꾼 뒤 φ 만큼 돌려 모델 좌표로 쓴다(obox 는 cx·cy 를 돌리고 angle_deg 에 φ 를 더하면 된다).
### 24.2 단면 높이 (`section_heights`)
- 단면 B-B 의 벽 네 선 = 20.1 ① 처럼 길이 60 m 넘는 `구조물` 세로선을 X 순으로 놓았을 때 5~8번째(1~4번째가 A-A).
- 벽 개구부 X 표시: 한쪽(왼쪽 = 바깥 왼·안 왼, 오른쪽 = 안 오·바깥 오) 벽 두 면 사이를 잇는 길이 1 m 넘는 `구조물` 사선, 지표 아래 10 m 안. 그 z 범위의 아래끝 = **바닥 윗면**, 위끝 = **지붕 아랫면**. X 가 있는 쪽이 그 구조물 쪽.
- 헌치: 그 쪽 벽 바깥면(±0.01)에 한 끝이 있고 다른 끝이 벽 바깥으로 나간 짧은 `구조물` 사선(길이 0.1~0.5 m). 바깥 끝이 지붕 아랫면보다 위이고 아래로 내려가는 것의 바깥 끝 z = **지붕 윗면**, 바닥 윗면보다 아래이고 위로 올라가는 것의 바깥 끝 z = **바닥 아랫면**.
- 도면 값(참고): 분기구 바닥 −4,550 ~ −4,100, 환기구 바닥 −4,600 ~ −4,100, 둘 다 지붕 −2,000 ~ −1,650. 이 숫자를 코드에 쓰지 않는다.
### 24.3 분기구 (`branch_parts`)
- 분기구 다리 방향 = s, 왼쪽 수직 = t. 평면도의 닫히지 않은 `구조물` 꺾은선(꼭짓점 4개 이상) 중 모든 꼭짓점이 s > 4.55 m, |t| < 2 m, s < 다리 길이 + 0.5 m 인 것 둘: |t| 최대가 작은 것 = 안 윤곽(t_in, 끝 s_in = 최대 s), 큰 것 = 바깥 윤곽(t_out, s_out).
- obox 5개(cx = cy = 0, angle = 다리 각도 + φ, s 는 벽 안쪽면 4850 부터): 바닥·지붕(t ±t_out, s ~ s_out), 옆벽 둘(t_in ~ t_out 양쪽, 높이 바닥 윗면 ~ 지붕 아랫면), 끝벽(s_in ~ s_out, t ±t_in).
### 24.4 환기구 (`vent_parts`)
- 중심선: 레이어 `환기구`, 꼭짓점 3개, 첫 꼭짓점이 C 인 꺾은선 → 꺾인 점 V1, 끝점 T. 다리 1 = C→V1, 다리 2 = V1→T.
- 덕트 벽선: 닫히지 않은 꼭짓점 3개 `구조물` 꺾은선 중 가운데 꼭짓점이 V1 에서 3 m 안인 것 4개. 다리 1 에서 잰 |t| 의 작은 둘 평균 = t_in, 큰 둘 평균 = t_out.
- 환기탑: T 를 품는 닫힌 `구조물` 꺾은선 — 꼭짓점 4개 = 바깥 사각형, 더 많은 것 = 안 윤곽. 다리 2 축으로 잰 범위(s, t)를 쓴다.
- 덕트 obox(높이는 24.2 의 B-B 값):
- 다리 1: 바닥·지붕 s 4850 ~ L1 + t_out, t ±t_out. 꺾이는 안쪽 벽(다리 2 가 나가는 쪽) s 4850 ~ L1 − t_out, 바깥 벽 s 4850 ~ L1 + t_out (t_in ~ t_out).
- 다리 2(V1 에서): 바닥·지붕 s t_out ~ 탑 바깥 사각형의 가까운 변, t ±t_out. 안쪽 벽 s t_in ~ 탑 변, 바깥 벽 s −t_out ~ 탑 변.
- 환기탑 obox: 바닥 = 바깥 사각형(덕트 바닥 높이). 벽 넷 = 바깥 사각형과 안 윤곽 범위 사이의 띠, 높이 바닥 윗면 ~ 지표 z 0. 단 덕트가 들어오는 쪽 벽은 지붕 아랫면 ~ 0 만(B-B 에서 탑 벽이 덕트 지붕 위에만 있음). 탑에는 지붕이 없다.
- 덕트·탑을 obox 로 나누는 방법은 위와 다르게 해도 된다 — 채점은 **입체(차지하는 공간)** 로 비교한다.
### 24.5 대조 (F38)
- 분기구·환기구 각각: 높이 5곳(−4575, −4300, −3000, −1800, −1000) × 100 mm 격자(수직구 바깥 r 5520 넘는 곳)에서 「추출 부재 안인가」가 model.js 와 같은지. 경계 ±20 mm 는 봐준다. 어긋난 점이 안쪽 점의 1% 이하.
- 변조 사본 둘: 평면도 1 의 환기구(중심선·덕트·탑)를 C 기준 좌우로 뒤집음 → 추출 환기구도 뒤집혀 나옴 / 단면 A-A 의 분기구 지붕 헌치 끝을 0.2 m 내림 → 분기구 지붕 윗면만 200 낮아짐.
- 코치는 이 규칙대로만 짠 별도 구현으로 F38 채점이 통과함을 미리 확인했다(그 구현은 주지 않는다).
## 25. 추가 — 나머지 부재 추출 (2026-10-21 수석 지시, F39)
도면(DXF)에서 상부 슬래브·맨홀 목·19B 패드·하부 슬래브·바닥 콘크리트·월류벽·집수조·침사조를 읽고, 19B 슬래브에 침사구 구멍을 더한다. 함수 `top_parts`, `pad_part`, `bottom_parts`, `weir_parts`. id·kind·type·필드는 model.js 와 같게(topslab, neck_1·neck_2, pad_19B, bottom_20B, lean_concrete, weir_L·weir_R·weir_mid, pit_pump·pit_tbm).
1. 단면 A-A 의 「벽 전체 수평선」: 레이어 `구조물` 수평선 중 바깥 왼 벽선에서 바깥 오른 벽선까지(양 끝 ±0.01) 걸친 것. 가장 높은 것 = **상부 슬래브 윗면**, 가장 낮은 것 = **바닥 콘크리트 밑면**, 두 번째로 낮은 것 = **하부 슬래브 밑면**(= 바닥 콘크리트 윗면).
2. 상부 슬래브 밑면 = 윗면보다 낮은 `구조물` 수평선 중 바깥 벽 사이에 있고 길이가 안쪽 지름의 0.9 배 넘는 것 중 가장 높은 것. 상부 슬래브: type disc, r = 바깥 반지름, holes = 평면도 1(24.1) 안의 반지름 1 m 미만 `구조물` 원(중복 하나로) → C 기준 mm 를 φ 만큼 돌린 `{"circle": [x, y, r]}`.
3. 맨홀 목: 평면도 1 의 닫힌 꼭짓점 4개 `구조물` 꺾은선 중 2 의 원 중심을 품는 것(중복 하나로). polygon = 꼭짓점을 φ 만큼 돌린 것, holes = 그 원, z 상부 슬래브 윗면 ~ 지표 0. 구멍 y 가 작은 쪽이 neck_1.
4. 하부 슬래브 윗면 = 단면 A-A 에서 안쪽 벽면에 한 끝이 있고 안쪽·아래로 내려가는 짧은 `구조물` 사선(길이 0.1~0.6 m) 아래 끝 중 가장 낮은 것. 하부 슬래브 disc r = 안쪽 반지름, 바닥 콘크리트 disc r = 바깥 반지름.
5. 19B 패드: 19B 슬래브 윗면(20.1) 위 0~0.3 m 안에 있는, 안쪽 벽 사이 길이 9 m 넘는 `구조물` 수평선 → 윗면, r = 길이/2. 구멍 = 19B 평면 안쪽 원 안의 **X 표시**(같은 레이어(`오프닝` 또는 `구조물`)의 엇갈린 사선 LINE 두 개, 길이 0.5 m 넘고 X 범위가 같은 것)의 끝점 범위 사각형, 패드 반지름 밖은 ±r 로 자른다. 같은 X 표시 사각형(자르지 않은 것)이 19B 슬래브 구멍에 없으면 더한다(침사구).
6. 월류벽: 「하부슬래브(20B)」 평면(제목 아래 가장 가까운 반지름 4.85 원) 안쪽 원 안의 길이 5 m 넘는 `구조물` 세로 LINE 두 개 → x0, x1. 엇갈린 사선 두 개(X 표시)의 Y 범위 → 낮은 칸 y0~y1. 양쪽 칸은 낮은 칸 끝 ~ ±√(안쪽 반지름² − x1²).
- 높이: 단면 A-A 에서 안쪽 벽 사이(벽선에서 0.05 m 넘게 떨어진) 길이 1 m 넘는 세로 `구조물` 선 중 아래 끝이 하부 슬래브 윗면 위 0~0.3 m 안인 것들 → 위끝 = 월류벽 윗면. 그 선들 사이에 걸친 수평선 중 윗면보다 낮은 것 = 낮은 칸 윗면. z0 = 하부 슬래브 윗면.
7. 집수조·침사조(type zone, alpha 0.25, r = 안쪽 반지름): 월류벽 왼쪽 x −r ~ x0, 오른쪽 x1 ~ r, z 하부 슬래브 윗면 ~ 월류벽 윗면. 단면 A-A 글자 「집수조」가 축 왼쪽이면 왼쪽이 pit_pump, 오른쪽이 pit_tbm.
- 도면 값(참고): 상부 슬래브 −2,000~−1,500, 19B 패드 −66,200~−66,100 r 4,750, 하부 슬래브 −70,200~−69,000, 바닥 콘크리트 −70,300~−70,200, 월류벽 x 1,400~1,600·윗면 −67,000·낮은 칸 −67,500. 이 숫자를 코드에 쓰지 않는다.
- 대조(F39): model.js 와 r·z ±5, 원 구멍·윤곽 ±10, 패드 구멍 ±40(정답지는 원 경계에 맞춰 손으로 줄였다). 변조 사본 둘: 20B 평면 월류벽 선을 중심 쪽으로 0.3 m 옮김 → 월류벽·조 경계가 300 옮겨짐 / 평면도 맨홀 2(원·목)를 0.5 m 옮김 → 구멍 2·목 2 만 φ 돌린 만큼 옮겨짐.
- 벽체 높이도 이제 도면에서 읽는다(19.1 의 고정값 허용을 끝냄): wall z0 = 하부 슬래브 밑면, z1 = 상부 슬래브 밑면.
- 사다리·펌프 배출관·사람 등 도면에 위치가 다 나오지 않는 것(🔶)은 이 장에서 다루지 않는다(다음 장의 설계 규칙).
- 코치는 이 규칙대로만 짠 별도 구현으로 F39 채점이 통과함을 미리 확인했다(그 구현은 주지 않는다).
## 26. 추가 — 정답지 보강: 환기구 사다리 2개 (2026-10-21 수석 지시 「2D 도면에 있는 것 모두」, F40)
- 발견(코치, DXF 재확인): 단면 B-B(환기 덕트를 펼친 단면)에 레이어 `사다리` 로 **벽 사다리**(축에서 +4.6 m, 1B ~ 덕트 바닥 위)와 **환기탑 안 사다리**(펼친 거리 18.75 m 근처, 덕트 바닥 ~ 지표 위)가 그려져 있는데 정답지에 없다. 단면 A-A 의 서쪽 벽 사다리(ladder_up_W)와 짝이다.
- `ladder` 에 선택 필드 `angle_deg` 를 더한다: 지금처럼 (x0~x1, yc ± width/2) 로 만든 면의 모든 점을 **원점 (0, 0) 기준으로 angle_deg 만큼** 돌린다(법선도 같이). 없으면 0(지금과 같음). 함수 `facesLadder`.
- 새 부재(kind `ladder_vent`, type `ladder`, color "#d0a040"). kind 가 새것이라 도면 클릭 영역(17.4)에는 넣지 않는다:
| id | name | angle_deg | x0 | x1 | yc | width | z0 | z1 | rung |
|---|---|---|---|---|---|---|---|---|---|
| ladder_vent_wall | 환기구 벽 사다리 (1B→덕트 바닥) | 300.3 | 4582 | 4622 | 0 | 400 | −7400 | −4100 | 300 |
| ladder_vent_tower | 환기탑 사다리 (덕트 바닥→지표) | 30.3 | 10100 | 10140 🔶 | −8650 🔶 | 400 | −4100 | 0 | 300 |
- 300.3° = 환기 덕트 다리 1 방향(24장, 평면도 −90° + 30.3°). 그 방향으로 4.6 m(B-B 의 사다리 앞 기둥) = 벽 안쪽 4.85 m 에서 0.25 m.
- 30.3° 틀의 (x, y) = 평면도의 (x, y). 탑 안 사다리 앞 기둥 = 펼친 거리 18.75 − 덕트 다리 1 길이 8.65 ≈ 10.1 m, yc = 탑 안 가운데(평면도 y −8,650) 🔶.
## 27. 추가 — 설계 규칙 파일, 추출 모델 = 정답지 전체 (2026-10-21 수석 지시, F41)
목표: 추출 모델(model_extracted.js)이 정답지(model.js)의 부재를 **모두** 갖게 한다. 도면에서 읽는 것은 20~25장, 도면에 위치가 다 나오지 않아 사람이 정한 것(🔶)과 화면용 값은 **설계 규칙 파일**에서 온다.
- 파일 `tools/design_rules.json` (하이쿠 작성, 순수 JSON):
- `"parts"`: 🔶 부재 6개를 model.js 의 객체 그대로(id·name·kind·type·color 와 모든 수치) — ladder_19B, ladder_up_W, ladder_up_C, ladder_vent_wall, ladder_vent_tower, pump_pipe. 근거는 16.1·17.3·22·26장(사다리 위치·배관 경로는 도면 수치가 없거나 일부만 있어 설계 결정).
- `"style"`: kind 별 `{"color": ..., "alpha": ...(있을 때만)}` 와 `"ids"`: id 별 덮어쓰기(채움 3개·집수조·침사조처럼 같은 kind 안에서 색이 다른 것). 값은 model.js 의 색·투명도와 같게.
- 도면에서 읽는 부재(벽·슬래브·계단·터널·채움·분기구·환기구·상부 슬래브·목·패드·하부 슬래브·바닥 콘크리트·월류벽·집수조·침사조·난간)의 **치수·좌표는 넣지 않는다.**
- 추출 도구: 도면에서 읽은 부재 뒤에 `parts` 를 더하고, 모든 부재에 style(kind → ids 순으로 덮어씀)의 color·alpha 를 넣는다. 모든 부재에 `name` 이 있다(한국어, 자유). 함수 `apply_design_rules`.
- id: 도면에서 읽는 부재도 model.js 와 같은 id 를 쓴다(분기구·환기구는 나누는 방법이 달라도 되므로 예외).
- 대조(F41): design_rules.json 의 6개 = model.js(±1), 그 밖의 kind 는 넣지 않음, 도면 값 숫자 없음 / 추출 모델의 kind 집합 = model.js, 분기구·환기구 말고 id 집합 = model.js / 같은 id 의 color·alpha = model.js / 모든 부재에 name·color.
## 28. 추가 — 변환 패키지: DWG → DXF → 도면 범위 (2026-10-21 수석 지시, F42)
설계도면은 DWG 로 주고받는다. DWG 는 바이너리라 GNU LibreDWG(`dwg2dxf`)로 DXF(글자 파일)로 바꾸고, 그 안에서 이 과제가 쓰는 그림만 남겨 추출 도구(20~27장)에 넣는다. 지금까지 코치가 리포 밖에서 하던 전처리를 리포 안 도구로 옮긴다.
- 입력: `private/drawing.dwg`(원본 DWG 사본, git 제외 — 코치가 넣었다).
1. `tools/dwg_to_dxf.sh <입력.dwg> <출력.dxf>`: `dwg2dxf -y -o <출력> <입력>`. dwg2dxf 를 PATH 와 `~/.local/bin` 에서 찾고, 없으면 README 의 설치 절을 가리키는 한 줄을 내고 종료 코드 2.
2. `tools/views.json`(사람 입력, 이 장과 함께 커밋): DWG 한 파일에 수직구 두 개와 옛 개정판이 함께 있어 **어느 그림을 쓸지는 사람이 정한다**. `boxes`(그림 상자 4개, 모형 공간 m), `drop_layers`, `drop_block_word`.
3. `tools/select_views.py <전체.dxf> <views.json> <출력.dxf>` (표준 라이브러리만, 좌표 숫자 없음 — 상자는 views.json 에서만):
- ENTITIES 구역을 객체 묶음으로 나눈다: POLYLINE 은 SEQEND 까지, 코드 66 이 1 인 INSERT 는 ATTRIB…SEQEND 까지 한 묶음.
- 뺄 것: 배치 공간(코드 67 이 1), 레이어가 drop_layers 인 것, 블록 이름(INSERT 의 코드 2)에 drop_block_word 가 들어 있는 INSERT, 좌표가 없는 것.
- 좌표: 코드 10·11 의 X 와 그 뒤 20·21 의 Y. POLYLINE 은 머리의 10/20(기준점)을 빼고 VERTEX 부터, INSERT 는 ATTRIB 도 포함. CIRCLE·ARC 는 중심 ± 반지름(코드 40)도 넣는다.
- 남길 것: 좌표 범위(최소~최대)가 상자 하나 안에 다 들어가는 묶음. 순서는 원본 그대로.
- 출력: ENTITIES 앞(HEADER·TABLES·BLOCKS 등)은 원본 그대로, 단 $EXTMIN·$EXTMAX 의 10/20 은 남긴 좌표의 최소·최대. 남긴 묶음, 그 뒤 원본의 나머지. 줄 끝 CRLF.
4. `tools/make_model.sh [입력.dwg] [출력 폴더]` (기본 private/drawing.dwg, private/): 1 → `full.dxf`, 3 → `drawing.dxf`, 추출 도구 → `model_extracted.js`(출력 폴더). 출력 폴더가 private/ 이면 리포 루트의 model_extracted.js 도 새로 쓴다.
5. README 에 「변환 패키지」 절: 설치(LibreDWG 0.14 소스 빌드 — `PKG_CONFIG=/bin/true ./configure --prefix=$HOME/.local --disable-bindings --disable-docs && make && make install`, WSL Ubuntu), 실행(`bash tools/make_model.sh`), 각 파일의 용도(DWG = 주고받는 원본, DXF = 추출 입력, SVG = 웹 표시(다음 장)).
- 대조(F42): make_model.sh 를 임시 폴더로 돌려 나온 drawing.dxf 의 객체가 private/drawing.dxf 와 같고(숫자 1e-6 까지 — LibreDWG 가 z 에 1e-11 잡음을 넣을 때가 있다), 나온 model_extracted.js 의 부재가 리포의 것과 같다. 상자 하나만 남긴 views.json 으로 돌리면 그 상자 안 객체만 남는다.
- 코치는 이 규칙대로만 짠 별도 구현으로 원본 DWG → 3,356 객체가 private/drawing.dxf 와 같게 나옴을 미리 확인했다.
## 29. 추가 — DXF → SVG 와 도면 연결값을 도면에서 (2026-10-21 수석 지시 「웹에서 DXF 를 못 보이면 SVG 로 변환까지 패키지로」, F43)
웹 왼쪽 칸의 도면 SVG 를 DXF 에서 직접 만들고, 그림 연결값(MODEL.drawing)도 추출 도구가 도면에서 계산한다. 화면은 지금과 같아야 한다 — 수석이 준 SVG 는 DXF 의 그림 상자 4개를 **축척 없이 평행이동**해 놓은 배치였다(오차 0.01 m).
- `tools/views.json` 에 더함(사람 입력 — 배치): 상자마다 `place` [dx, dy] — 그 상자의 점 (X, Y) 를 SVG (X − dx, dy − Y) 에 놓는다(1 = 1 m). `viewBox` [0, 0, 443, 443], `fit` [118, 160, 245, 250](처음 화면).
### 29.1 `tools/dxf_to_svg.py <drawing.dxf> <views.json> <출력.svg>` (표준 라이브러리만)
- 객체마다 그 좌표 범위가 들어가는 상자(28장과 같은 방법)의 place 로 옮긴다. 상자에 안 들어가는 객체는 그리지 않는다.
- 레이어 표(TABLES 의 LAYER: 2 이름, 62 색 — 음수면 꺼짐, 70 의 1 비트 = 얼림)에서 꺼짐·얼림·`Defpoints` 레이어는 그리지 않는다.
- 색: 객체 62 가 없거나 256 = 레이어 색, 0 = 블록을 넣은 INSERT 의 색. ACI → RGB: 1 빨강 (255,0,0), 2 노랑, 3 초록, 4 하늘 (0,255,255), 5 파랑, 6 자홍, 7 검정 (0,0,0)(원본 SVG 와 같게), 8 (128,128,128), 9 (192,192,192), 250~255 회색 6단(51, 91, 132, 173, 214, 255), 10~249 는 색상 (i−10)//10 × 15°, i%10 짝수 = 채도 1·홀수 = 0.5, 밝기 [1, 1, .8, .8, .6, .6, .5, .5, .3, .3][i%10] (HSV).
- 그리기: LINE·LWPOLYLINE(코드 42 볼록 → 원호)·POLYLINE(VERTEX, 42)·LEADER(꼭짓점 꺾은선)·CIRCLE·ARC(50·51 도, 반시계)·SOLID(10·11·13·12 순 채움)·TEXT·MTEXT·INSERT(BLOCKS 구역의 블록을 기준점 10/20 빼고, 41·42 배율, 50 회전, 10/20 위치로; 겹친 INSERT 도)·DIMENSION(코드 2 의 블록을 그대로). Y 를 뒤집으므로 원호 방향도 뒤집힌다.
- 글자: 높이 40, 회전 50, 72/73 이 0 이 아니면 11/21 을 기준점, 72 → text-anchor(0 start·1 middle·2 end·4 middle). %%c → Ø, %%d → °, %%p → ±, %%u 지움. MTEXT 는 3·1 을 이어 붙이고 서식 코드(\P → 공백, \f…; 등, 중괄호)를 지운다. font-family "Dotum,'sans-serif'".
- 출력: 루트 `<svg viewBox="0 0 443 443">` + `<style>* { vector-effect: non-scaling-stroke; }</style>`, 색마다 `<g fill="none" stroke="rgb(…)" stroke-width="1">`. 원본처럼 선은 화면 1 px.
- `make_model.sh` 가 이것도 돌려 출력 폴더에 `drawing_gen.svg` 를 만든다(private/ 이면 private/drawing_gen.svg).
### 29.2 도면 연결값 (추출 도구 `drawing_views`)
- 추출 도구는 `tools/views.json`(세 번째 인수로 다른 파일을 줄 수 있다)을 읽어 `MODEL.drawing` 을 만든다: src "private/drawing_gen.svg", viewBox·fit = views.json, section {x0, y0, mm_per_unit 1000} = secA 값, plan {cx, cy, mm_per_unit 1000} = plan1 값, views 11개(id·type·label·필드는 13.2·22장과 같다). 모든 좌표 = 도면 점을 그 상자 place 로 옮긴 값(소수 둘째 자리).
- secA·secB: 전체 단면 A-A(20.1 ① 벽 1~4)·B-B(24.2 벽 5~8)의 축 X 와 그 축의 지반선(20.1 ②) → x0, y0.
- secA2·secB2(section_break): `S-TITLE` 「단면 A-A」·「단면 B-B」(공백 무시) 글자 X 에서 가까운, 길이 10 m 넘는 `구조물` 세로선 X(1 cm 로 반올림, 중복 하나로) 4개 = 그 생략 단면의 벽. 축 = 바깥 두 선 가운데, 지반선(초록, ±0.05 m 로 축을 지나는 선) → x0, y0_upper. 층 글자(「숫자B」, 축에서 8 m 안)마다, 안쪽 왼쪽 벽선에서 시작하고 글자 아래 0.3 m 안에 있는 `구조물` 수평선 Y 로 그 층의 「지표에 해당하는 Y」 = Y − 그 층 윗면 z(20.1 ③)/1000 을 구한다. 지반선과 같은(±0.05) 층들 = 위 구간, 나머지 = 아래 구간: y0_lower = 아래 구간 값의 평균을 같은 place 로 옮긴 것, z_upper_min = 위 구간 가장 깊은 층 윗면 − 300, z_lower_max = 아래 구간 가장 높은 층 윗면.
- plan1 = 평면도 1 중심(24.1 ①), plan2 = 그 아래 30 m 안의 가장 가까운 반지름 4.85 원. rot_deg = −φ(24.1, 소수 첫째 자리). tunnel: plan1 true, plan2 false.
- 슬래브 평면 5개: 20.1 ④ 의 중심과 「하부슬래브(20B)」 중심. z1 = 홀수층(1·3·…·17B 윗면)·짝수층(2·…·16B)·[18B]·[19B]·[하부 슬래브 윗면], kind slab(20B 만 bottom), tunnel 은 19B 만 true.
- 대조(F43): 생성 SVG 가 XML 로 읽히고 viewBox 443, 요소 7000 개 이상·글자 400 개 이상, **원본 SVG(private/drawing.svg)와 선이 지나는 0.5 칸 비교(상자 안)에서 재현율·정밀도 0.85 이상**(원본의 해치는 DXF 에서 기준점이 (0,0) 이라 범위에서 빠져 1 이 되진 않는다) / 추출 drawing = model.js drawing(views 11개 숫자 ±0.05, z1 ±5, 플래그 같음, rot_deg ±0.1, viewBox·fit 같음) / views.json 의 평면도 상자 place 를 10 m 바꾼 사본으로 추출 → plan1·plan2 cx 만 10 바뀜.
- 코치는 이 규칙대로만 짠 별도 구현으로 SVG(재현율 0.885·정밀도 0.928)와 views 11개(정답지와 ±0.01)를 미리 확인했다.
## 30. 변경 — 화면을 도면에서 만든 모델로 전환 (2026-10-21 수석 지시, F44)
이제 3D 와 도면 연결이 모두 도면(DWG)에서 만들어진다(20~29장). 첫 화면을 그 모델로 바꾸고, 손으로 만든 정답 모델은 비교용 화면으로 남긴다.
- `index.html`: `model_extracted.js` 다음 `viewer.js` 를 읽는다(model.js 는 읽지 않는다). **1.x 의 F01 「model.js 다음 viewer.js」 를 이것으로 바꾼다.** 왼쪽 도면 = MODEL.drawing.src(private/drawing_gen.svg).
- `answer.html`(새 파일, 정답지 화면): index.html 과 같은 화면이되 `model.js` 다음 `viewer.js`. 제목에 「정답지」, 맨 위 안내 한 줄: 「손으로 만든 정답 모델 — 도면에서 만든 모델(index.html)을 검사하는 기준」. 왼쪽 도면 = model.js 의 drawing.src(private/drawing.svg, 수석이 준 SVG 처리본).
- `viewer.js`: 도면 그림 파일을 `MODEL.drawing.src` 에서 읽는다(없으면 'private/drawing.svg'). 「도면 파일 없음」 안내도 그 경로를 보인다.
- compare.html 은 그대로 둔다(F34 — 추출 모델 비교 화면, 이제 index.html 과 같은 데이터).
- README: 첫 화면 = 도면에서 만든 모델, answer.html = 정답지, 다시 만들기 = `bash tools/make_model.sh`.
- 구동 설명서(코치 작성, 이 장과 함께 커밋): 구동 쪽 — 여는 방법·도면에서 다시 만들기(형식별 용도 표)·잘 안 열릴 때 / 목적 쪽 — 3D 가 도면에서 만들어진다는 한 줄, 사다리 4개(위 2 + 환기구 쪽 2).
## 31. 추가 — README 「사용 도구」 절 (제출 점검, F45)
- 11.2 로 README 를 코드 설명으로 고쳐 쓸 때 빠진 「사용 도구」 절을 되살린다(제출 가산점: WSL2·git).
- README 에 `## 사용 도구` 절: WSL2 Ubuntu(그 아래 `wsl.exe -l -v` 출력을 코드 블록에 그대로 — WSL 안에서는 `wsl.exe -l -v`, 글자 사이 빈 칸은 지워도 된다), Claude Code(Haiku 5.5)가 코드 작성·수정·커밋(커밋마다 Co-Authored-By 줄), 설계 문서·검사(SPEC.md·FEATURES.json·check.sh·구동 설명서)는 수석 지시로 코치 세션이 쓰고 하이쿠가 KIT 커밋, GNU LibreDWG 0.14(DWG → DXF), Python 3 표준 라이브러리, git, 브라우저(Chrome·Edge).
- 대조(F45): README 에 「사용 도구」, `wsl -l -v` 출력(Ubuntu 와 VERSION 2), Haiku, LibreDWG, check.sh.
## 32. 변경 — 환경에 따라 못 하는 검사는 SKIP (2026-10-21 수석 지시 「압축에 DXF 를 넣어라, 실패가 나오는 건 싫어」)
- 제출 압축에는 `private/drawing.dxf`·`drawing.svg`·`drawing_gen.svg` 가 들어가고 원본 DWG 는 빠진다. 평가자 PC 에는 GNU LibreDWG(dwg2dxf)가 없을 수 있다.
- 그래서 F42 의 「원본 DWG 로 make_model.sh 를 돌려 보는」 검사 3개는 dwg2dxf 나 private/drawing.dwg 가 없으면 FAIL 이 아니라 **SKIP**(이유 표시)으로 센다. 둘이 다 있는 개발 환경에서는 지금처럼 돈다. 다른 검사는 그대로.
## 33. 추가 — WSL2 설치 증빙 파일 (2026-10-22 수석 지시 「WSL2 를 설치했다는 증빙자료가 없다」, F46)
- `evidence/wsl2.txt`: 명령 출력 그대로 모은 증빙.
- 앞부분 「Windows 쪽」: 코치 세션이 Windows PowerShell 에서 실행해 모은 출력(wsl --version, wsl -l -v, wsl --status, 등록된 배포판과 가상 디스크 ext4.vhdx 를 만든 날 = 설치일). 이 장과 함께 커밋한다.
- 뒷부분 「WSL 안쪽」: 하이쿠가 WSL 안에서 직접 실행해 덧붙인다 — 수집 시각(date), `uname -r`(커널 이름에 microsoft-standard-WSL2), `cat /proc/version`, `/etc/os-release` 의 PRETTY_NAME, `pwd`(작업 폴더 /mnt/d/…), `claude --version`, `git --version`, `python3 --version`, `dwg2dxf --version` 첫 줄. 명령마다 `$ 명령` 줄 다음에 출력 그대로.
- README 「사용 도구」: 예전 커밋에서 옮겨 적은 출력 대신 `evidence/wsl2.txt` 를 가리키고, 설치일(가상 디스크 만든 날)과 오늘 `wsl -l -v` 출력을 적는다.
- 대조(F46): evidence/wsl2.txt 에 `wsl -l -v` 출력(Ubuntu, VERSION 2)·설치일·`microsoft-standard-WSL2`·Ubuntu 버전·claude 버전, README 가 그 파일을 가리킨다.
## 34. 공개용 정리본 (2026-10-27 수석 지시 — 사내 Gitea `tdc_engineer/drawing3d-web` 에 올림)
- 이 저장소는 1차 제출본(커밋 81개, 로컬 보존)을 **공개용으로 정리해 새 이력으로 시작**한 것이다. 옛 이력에는 도면 실제 좌표가 들어 있어 그대로 올리지 않았다. 옛 커밋 기록(메시지)은 `evidence/gitlog_v1.txt`.
- 도면 정리(코치, 저장소 밖 도구): 우리가 쓰는 그림 객체와 거기 딸린 블록·레이어·선종류만 남기고, 과업명·지명·외부 참조 이름·저장자·XDATA·OBJECTS 를 지웠다. **모든 좌표를 같은 양만큼 평행이동**해 실제 위치를 알 수 없게 했다(옮긴 양은 기록하지 않는다).
- 같은 양을 `tools/views.json`(그림 상자·배치)과 `check.sh`(채점기만 아는 기준점·변조 시험 좌표)에도 적용했다. 추출 결과(3D 데이터 95부재)·도면 연결 11개·생성 SVG 는 옮기기 전과 같다(SVG 바이트 동일, 부재는 1e-9 mm 잡음 하나).
- `private/` 에 올리는 것: `drawing.dxf`(정리본), `drawing.svg`(정답지 화면용 그림), `drawing_gen.svg`(첫 화면 그림). 원본 DWG 는 올리지 않으므로 F42 의 DWG 재변환 검사 3건은 SKIP(32장). `.gitignore` 는 `private/` 전체 대신 `private/*.dwg`·`private/full.dxf` 등을 뺀다 — F13 의 「.gitignore 에 private/」 기준을 이것으로 바꾼다. F13 의 「도면 파일 커밋 금지」 항목도 「커밋된 도면은 정리본 3개뿐」으로 바꾼다(처음 바꿀 때 빠뜨림 — 첫 커밋 뒤 이 항목이 FAIL 이었다).