# CLAUDE.md — dwg2model 작업 가이드 이 저장소에서 작업할 때 지켜야 할 것. 사람이 읽어도 된다. --- ## 이 프로젝트가 하는 일 2D 도면(평면 · 입면 · 단면 · 좌/우측면)을 읽어 3D 솔리드와 IFC 로 옮긴다. **도면마다 설정 파일 하나만 바꾸면 도는 구조**가 목표다. 문서 셋을 먼저 읽어라. | 문서 | 읽어야 할 때 | |---|---| | [`README.md`](README.md) | 구조 · 실행 방법 | | [`docs/2d-3d-process.md`](docs/2d-3d-process.md) | **절차** — 새 도면 추가, 검증 순서 | | [`docs/2d-3d-conversion.md`](docs/2d-3d-conversion.md) | **노하우** — 함정, 실패 사례, 왜 그렇게 하는지 | | [`docs/roadmap.md`](docs/roadmap.md) | 남은 일 · 우선순위 | | [`docs/parametric.md`](docs/parametric.md) | **파라메트릭 설계 층** — 설계 노트, 미구현 | `2d-3d-conversion.md` 의 함정 목록은 실제로 다 밟아본 것이다. 새로 시작하기 전에 훑어보면 같은 곳에서 두 번 넘어지지 않는다. --- ## 절대 어기면 안 되는 경계 ``` src/core/* · src/ir.mjs → 도면 고유값이 들어가면 안 된다 src/drawings/*.mjs → 레이어명 · 제목 · 임계값 · 좌표 · 단위 · 축척은 전부 여기 src/emit/* → 여기서만 3D 툴의 사정을 안다 ``` 이 경계가 무너지면 두 번째 도면에서 **파이프라인이 아니라 복사본**이 생긴다. `core/` 에 `'-전력구'` 같은 문자열을 넣고 싶어지면 설계가 잘못된 것이다. **이 경계는 이미 두 번 새 있었다.** 교량 도면을 넣어보고서야 드러났다. - `core/datum.mjs` 의 `UNIT_MM = 1000` — 숫자라서 눈에 안 띄었을 뿐 도면 고유값이다. 같은 파일의 라벨 대역(`±3`)·뷰 구분 문턱(`2`)도 「1단위=1m」를 전제한 길이였다. - `rules.mjs` 는 주석에 "도면도 3D도 모른다"고 적혀 있었지만 `챔퍼_시작X`·`샤프트` 같은 **첫 도면의 파라미터 이름**에 묶여 있었다. 두 번째 도면에서 `undefined` 비교가 전부 위상 위반으로 떨어져 `build` 가 죽었다. 지금은 `needs` 로 적용 범위를 명시한다. 교훈: **문서에 "안 들어간다"고 쓰는 것으로는 안 지켜진다.** 두 번째 도면을 넣기 전에는 안 보인다. 경로는 `src/paths.mjs` 한 곳에서만 정한다. 스크립트에서 `../..` 을 세지 말 것. --- ## 작업 순서 — 이 순서를 건너뛰지 마라 3D 부터 세우면 **어디가 틀렸는지 알 수 없다.** 반드시 아래 순서로 통과시킨다. ``` 0. egm guide | wc -l 뷰어 모드가 아닌가 (39 면 뷰어 — 모델링 불가) 1. node src/run.mjs <도면> 기준점이 도면 치수 2개 이상과 맞는가 뷰 폭·높이가 도면 치수와 맞는가 2. node src/gaps.mjs <도면> 프로파일이 안 닫히는 곳 · 그 원인 3. node src/build.mjs <도면> --loops 폐곡선 치수가 도면 표기와 맞는가 꼭짓점 수가 예상과 맞는가 4. node src/build.mjs <도면> IR 검증 5. python src/run_egm.py … --dry 좌표 검산 6. python src/run_egm.py … --export=out/<도면>.obj 실행 + OBJ 7. egm status modelCount 가 최종 파트 수와 같은가 8. node src/measure.mjs <도면> --ir 의도(IR) vs 결과(모델러) 대조 + 기준선 ``` `--ir` 이 핵심이다. 부피가 이상할 때 **우리 조립이 틀렸는지 모델러가 흘렸는지**를 한 줄로 가른다. 손으로 기대값을 조립하면 커터 범위를 놓쳐 세 번 다 틀렸다. `run.mjs` 는 검증 실패 시 **exit 1** 을 낸다. 그 상태로 다음 단계에 가지 말 것. 7 이 어긋나면 boolean 이 인자를 흘린 것이다. 「모델러 조작」 절을 볼 것. --- ## 자주 틀리는 것 (전부 실제로 겪었다) | 함정 | 올바른 방법 | |---|---| | 표고 기준을 **텍스트 위치**로 잡음 | **선**에서 잡는다. 713mm 틀렸다 | | 계획고 선 후보가 둘 (도로/토공) | 라벨 **바로 아래** 선을 고른다. 420mm 틀렸다 | | 횡단면 하나를 전 구간에 압출 | 종단면이 **구간 배정표**다. 구간마다 단면이 다르다 | | 폐곡선을 `loops[0]` 로 집음 | **치수**로 집는다. `loop(뷰, 2700, 2750)` | | 자기교차 링을 그냥 압출 | 솔리드가 쓰레기가 되고 이후 boolean 이 **조용히** 무력화된다. `--loops` 의 `✗ 자기교차` 를 먼저 볼 것 | | 원 좌표·반지름을 손으로 적음 | `circles(뷰, 900, /^-전력구$/)`. 손으로 적은 수는 도면이 바뀌어도 안 바뀐다 | | 원을 선분에 섞어 넣음 | 따로 모은다. 폐곡선이 원 윤곽을 따라 돌아 오염된다 | | 평면도 세로축을 표고로 봄 | 세로=Y. `v0` 앵커 필요. 치수만 보는 동안은 안 걸린다 | | 개구부 폐곡선을 그대로 씀 | X 기호 때문에 링이 자기교차. **bbox → `rect()`** 로 재구성 | | 오목한 외곽을 그대로 뚫음 | 노치를 합쳐 **봉투부터** 만들고 그 다음에 뚫는다 | | 구간별로 따로 difference | 구간 **몸통**을 union 한 뒤에 뚫는다. 경계에 구멍 난다 | | 커터를 union 해서 한 번에 | 떨어진 커터끼리는 교차하지 않아 union 이 죽는다. 커터는 **하나씩** | | 커터가 자기 부피보다 많이 뺌 | boolean 이 죽은 것이다. `--primitives` 로 커터 부피를 보면 즉시 확정 | | 면으로만 맞닿은 솔리드를 union | 조각으로 붙어 있다가 다음 difference 가 통째로 흘린다. **부피로 겹치게** | | union 결과를 뚫음 | 조각마다 제 몫을 먼저 뚫고 합친다. 커터는 조각 수만큼 따로 만든다 | | 커터를 대상 바깥면에서 시작 | `OVER` 만큼 밖으로 뺀다. 압출축뿐 아니라 **모든 방향** | | 수평 단면의 세로축을 Z 로 봄 | 가로=Y, 세로=X. `swapUV` | | 압출축을 의식 안 함 | YZ→X 압출은 **Y 방향 벽만** 만든다 | | 전이부를 loft 하나로 이음 | loft 는 **꺾인 자리를 못 만든다**. 꺾이는 X 마다 단면을 놓고 끊는다 | | loft 단면을 그냥 넣음 | 시작 꼭짓점이 어긋나면 면이 비틀린다. 링 정규화 + 꼭짓점 수 일치 | | DWG 1단위 = 1m 로 가정 | **도면군마다 다르다**. 맨홀군 1m, 교량 삽도군 1mm. `cfg.unitMm` 로 준다. 안 맞추면 전 치수가 1000× 로 **조용히** 틀린다 | | 모델스페이스가 1:1 이라고 가정 | 삽도는 **도형마다 배율이 다르다** (5.805배 사례). 표기 치수 2개로 역산해 교차검증 | | 표고 기준선이 있다고 가정 | 없는 도면이 있다. `datum.mode:'none'` + 뷰별 `anchors.v0`. 아무 수평선이나 자동 선택하면 안 된다 | | 뷰 제목으로 뷰를 가름 | 제목이 없거나 **N벌 중복**인 도면이 있다. `cfg.viewBoxes` 로 좌표를 짚는다 | | 레이어로 치수선을 거름 | 구조선과 같은 레이어인 도면이 있다. `colorIndex`(0=ByBlock=치수블록)가 가른다 → `cfg.entityFilter` | | 끊김 0인데 폐곡선 0 | 겹치는 **동일선상 선분**. T자 분할 뒤 중복 변이 남는다. `loops.mjs` 가 분할 뒤 한 번 더 지운다 | | 파싱 캐시 파일명에 `[^\w.-]→_` | `\w` 가 한글을 안 먹어 다른 도면이 **같은 캐시**를 쓴다. 인덱스·해시로 | | 도면을 바로 게이트에 넣음 | 먼저 **숫자 치수 vs 기호 치수**를 센다. 다만 「그 도면 안에 대조 상대가 없다」일 뿐 — 다른 도면·표준 부재 치수로 축척을 되찾으면 쓸 수 있다 | | 그림 안 보고 뷰 상자를 잡음 | **한 시트에 날짜 다른 작업본이 겹쳐** 있을 수 있다. 두 벌이 섞여도 폐곡선·치수가 그럴듯해 **숫자로는 안 걸린다.** 시트 전체 → 도형 하나 → 확인 → 상자 | | 같은 뷰에 확대 상세가 섞임 | 축척이 다르다(받침 153×182 ↔ 상세 461×196). 치수로 집으면 갈리지만 **갈렸는지 검사**를 넣는다 | | 같은 부재가 1mm 다르게 나옴 | 꼭짓점마다 따로 반올림한다. `loops(…, tol)` 로 열되 기본값 0 | | 면적은 맞는데 폭·높이가 틀림 | **매달린 변**(끝이 안 닿는 선) 왕복이 bbox 를 부풀린다. 면적은 0 이라 안 걸린다 | | 진단 도구가 본편과 다른 값 | 도구가 `projectViews` 를 안 거쳤다. 본편과 **같은 함수**를 써야 한다 | | 형상이 없으면 정보도 없다고 봄 | 말뚝은 **해치 + 중심선**이다. 중심선 x = 위치, 윤곽 두 줄 간격 = 지름 | `--loops` 의 **꼭짓점 수**가 가장 빠른 이상 신호다. ``` 4 정상 직사각형 10 헌치가 있는 정상 형상 8 개구 기호(X)가 링을 오염시킴 114 케이블랙 같은 가구가 프로파일 레이어에 섞임 ``` --- ## 모델러(EGModeler) 조작 ``` Extrude C=True 단독은 항상 +Z 로 민다. → 항상 D F {기준점} {방향점} 로 방향 명시 C=True 는 D F 앞에. → 뒤에 오면 무시되고 서피스가 나온다 닫힌 Polyline 에 Enter 금지 → 남은 신호가 다음 명령으로 샌다 SelByUids 는 Esc 로 닫는다 → 안 닫으면 이후 명령이 전부 무시된다 명령은 비동기 → command 가 null 될 때까지 대기 보이드 객체 없음 → 커터 솔리드 + BooleanDifference 뿐 Boolean 은 두 개만 먹는다 → 셋 이상 선택하면 나머지는 조용히 무시된다 Loft 는 기본이 서피스 → C=True 로 막아야 솔리드. Enter 는 한 번 Loft 의 L(원본삭제)은 기본 true → 끄면 커브가 남아 modelCount 가 어긋난다 Cap 은 제자리 수정 → 새 uid 가 안 나와 파이프라인이 못 잡는다 ExportByPath 경로에 _ 가 있으면 → 큰따옴표 필수. 없으면 조용히 파일이 안 생긴다 SelAll 먼저. .obj 만 나온다 (.stl 은 안 됨) ``` `SelAll`→`Delete` 가 파이프를 죽인 적이 있다. 프로세스도 창도 멀쩡한데 (`Responding=True`, CPU 유휴) 명령만 전부 "응답하지 않습니다"가 된다. CLI 로는 못 살린다 — 모델러를 재시작한다. ### 시작 전에 뷰어 모드부터 확인한다 뷰어 모드면 **모델링 명령이 등록조차 안 된다.** 파이프는 살아 있어서 `egm status` 는 정상 응답하고, `~Polyline` 은 아무 말 없이 `created:[]` 를 낸다. 원인을 찾는 데 제일 오래 걸린 함정이다. ```sh egm guide | python -c "import sys,json;print(len(json.load(sys.stdin)))" # 39 → 뷰어 모드. 보기·선택·계측만 있다. 모델링 불가 # 207 → 정상 egm guide Polyline # "not found" 면 뷰어 모드 ``` CLI 로는 못 바꾼다. 모델러 UI 에서 **Assistive Features → Working Mode Setting** 을 `Viewer & Modeler Use` 로 놓는다. 모델링 플러그인은 뷰포트가 한 번 렌더된 뒤 `LoadProjectPluginsAsync()` 로 올라오므로, 창이 화면 밖에 있으면 같은 증상이 난다. ### Boolean 은 이항이다 `BooleanDifference` · `BooleanUnion` 플러그인은 선택 개수와 무관하게 `TargetModels[0]`, `[1]` **두 개만** 쓴다. 커터 9개를 한 번에 넘겼더니 컷 1개만 적용되고 커터 8개가 씬에 그대로 남았다 (`modelCount` 11 이어야 할 게 19). `emit/egm.mjs` 의 `fold()` 가 둘씩 접는다. 중간 결과는 `id~1`, `id~2` … 로 잡고 마지막만 `id` 가 된다. **`modelCount` 가 최종 파트 수와 같은지 매번 확인한다.** 가장 빠른 이상 신호다. ### 씬 초기화 `!Cancel` 을 쓰면 모델러가 명령을 안 받는 상태에 빠진 적이 있다. 초기화는 `Esc` → `SelAll` → `Delete` → `Esc` 로 하고 `egm status` 로 확인한다. --- ## 파라메트릭 층 ``` DWG ──fit──→ params/<이름>.json ──build --params──→ IR ──→ 3D ↑ 사람은 여기만 만진다. build 는 도면을 안 본다 ``` | | | |---|---| | `src/fit.mjs` | 도면 → 파라미터. 재현 안 되는 링은 `grade: opaque` 로 좌표째 얼린다 | | `src/params.mjs` | 스키마·식(위상정렬)·검증·도면 이탈 감지 | | `src/rules.mjs` | 위상 규칙(**막는다**) · 설계 규칙(경고) | | `src/volume.mjs` | IR 을 격자로 평가 — 모델러 없이 "의도" 부피 | | `src/ui.mjs` | 편집 + 갱신 버튼. `node src/ui.mjs manhole1` | - `drawings/*.mjs` 는 `fit()` 과 `assemble()` 둘로 나뉜다. **`assemble()` 에는 도면 조회 도우미를 주지 않는다** — 주면 다시 도면에 묶인다. - **모델러에서 직접 편집하지 않는다.** 갱신하면 UID 가 전부 바뀌어 날아간다. ## 코드 스타일 - 주석은 **왜** 를 적는다. 무엇을 하는지는 코드가 말한다. - 실패했던 방법과 그 이유를 주석에 남긴다. 다음 사람이 같은 걸 시도한다. - 좌표·치수는 mm 정수. `egm` 에 넘기기 직전에만 m 로 나눈다. - 조용히 틀리느니 예외를 던진다 (`loop()` 가 0개나 2개 이상이면 throw). --- ## 라이선스 `vendor/acadrust-dwg/` 는 **MPL-2.0 수정본**이다. 이 wasm 을 다른 저장소로 옮길 때 `THIRD-PARTY-NOTICES.md` 와 `licenses/` 두 파일을 반드시 함께 옮겨야 한다. 패치 파일이 MPL-2.0 §3.2 의무의 근거다.