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

74 KiB
Raw Blame History

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 −12501250, y 10002200
slot_dn 사각형 x −12501250, y −2200−1000
stair_even 사각형 x −21002100, y −10000
pump 사각형 x −750750, y 36504790
pump_pad 사각형 x −750750, y 36504750
elec 사각형 x 20803070, y 35303750 (벽에 붙은 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 8001400), 머리 220×220×220(z 14501670). 색 #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 14001600, Y −4580−500, z −69000~−67000 ② Y 5004580, 같은 z ③ Y −500500, 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 38504820, Y −500500
19B 패드 구멍 추가 — — 침사구 사각형 X 38504720, Y −500500
  • 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.
  • 정정: 전기 배선구 구멍은 벽을 따라 돈 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)

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 순으로 놓았을 때 58번째(14번째가 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), 250255 회색 6단(51, 91, 132, 173, 214, 255), 10249 는 색상 (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 ① 벽 14)·B-B(24.2 벽 58)의 축 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 이었다).