step3-4: 프로젝트 기본 구조 & 상세 계획 수립

- CLAUDE.md: 프로젝트 전체 가이드 및 워크플로우 정의
- CONSIDERATIONS.md: 8개 영역 고려사항 분석 (기술, 품질, 배포, UX)
- PROGRESS.md: 진행 상황 추적 및 체크리스트
- PLAN.md: 단계 5~9 상세 계획 및 에이전트 역할 정의
- 메모리: 프로젝트 정보 저장

다음: 단계 5 (에이전트/커맨드/스킬/훅 아이디어 제안)
This commit is contained in:
Claude Haiku committed 2026-10-16 15:06:18 +09:00
commit 363c2a2a25
16 files changed
+9018

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+524
View File
@@ -0,0 +1,524 @@
# 2D → 3D 제작 과정
DWG 도면 한 벌을 3D 솔리드로 세우기까지 **실제로 무엇을 어떤 순서로 했는지**를
기록한다. 다른 프로젝트로 옮길 때 이 문서만 따라가면 재현된다.
- 판단의 근거·함정·교훈은 [`2d-3d-conversion.md`](2d-3d-conversion.md) 에 따로 있다.
이 문서는 **절차**, 그 문서는 **왜 그렇게 하는가**.
- 코드: [`src/`](../src/)
- 대상: `drawings-src/04manhole1-simplify.dwg` (맨홀 #1 및 출입구 일반도, 4매)
---
## 0. 준비물
| 항목 | 이 프로젝트에서의 위치 | 옮길 때 확인할 것 |
|---|---|---|
| DWG 파서 | `vendor/acadrust-dwg/` (wasm, read-only) | wasm 파일 2개(`.js`, `_bg.wasm`)를 같이 옮긴다 |
| Node | 18+ (ESM, top-level await 사용) | — |
| 3D 툴 | EGModeler + `egm` CLI (`D:\GiteaAll\EG-BIM_Modeler`) | 앱이 **실행 중**이어야 한다. `egm status` 로 확인 |
| Python | 3.10+ (`src/run_egm.py`) | 표준 라이브러리만 쓴다 |
`egm` 은 Named Pipe 로 실행 중인 EGModeler 에 명령을 보내는 CLI 다.
연결 확인:
```sh
egm status # {"command":null,"modelCount":0,"selectedCount":0}
```
`EGModeler가 실행 중이지 않거나 응답하지 않습니다.` 가 나오면 앱을 먼저 띄운다.
**`status` 가 응답한다고 모델링이 되는 건 아니다.** 뷰어 모드면 모델링 명령이
등록조차 안 된 채로 파이프만 살아 있다. 등록된 명령 개수로 가른다.
```sh
egm guide | python -c "import sys,json;print(len(json.load(sys.stdin)))"
# 39 → 뷰어 모드. Polyline·Extrude·Boolean* 이 통째로 없다. 모델링 불가
# 207 → 정상
```
39 이 나오면 모델러 UI 에서 **Edit Mode** 로 바꾼다 (View Mode / Edit Mode 토글).
Assistive Features 의 `ModelerMode` 와는 다른 값이다. 자세한 배경은
[`2d-3d-conversion.md`](2d-3d-conversion.md) 교훈 #15.
---
## 1. 전체 흐름
```
DWG ─parse─→ 엔티티 ─segment─→ 뷰 ─datum─→ 프로젝트좌표 ─loops─→ 프로파일
│
조립 스펙 (IR, JSON)
↙ ↓ ↘
egm IFC three.js
```
명령 4개로 끝난다.
```sh
cd src
node src/run.mjs manhole1 # ① 파싱 · 기준점 · 뷰 분리 → out/manhole1.views.json
node src/build.mjs manhole1 # ② 프로파일 추출 · 조립 → out/manhole1.ir.json
node src/emit/egm.mjs out/manhole1.ir.json # ③ 모델러 명령으로 번역 → out/manhole1.egm.json
python src/run_egm.py out/manhole1.egm.json # ④ 실행
```
`src/run.mjs` 는 검증이 깨지면 **종료 코드 1** 을 낸다. CI 게이트로 바로 쓸 수 있다.
---
## 2. 단계별 절차
### ① 파싱 · 기준점 · 뷰 분리 — `src/run.mjs`
```sh
node src/run.mjs manhole1 # 산출물까지 저장
node src/run.mjs manhole1 --report # 검증표만 출력
```
출력 예:
```
도면 manhole1 — R2018, 엔티티 7035
표고 기준선 DWG y = 483623.47666155506 (뷰 11곳에 반복, 총연장 61492mm)
✓ B-B 구체 상단 실측 -4907 도면 -4907
✓ B-B 기초 하단 실측 -7757 도면 -7757
✓ B-B 잡석 하단 실측 -8057 도면 -8057
✓ D-D 구체 상단 실측 -4907 도면 -4907
뷰 kind 조각 선 폭mm 높이mm Z 범위
평 면 도 plan 1 72 20200 10200 -1478 ~ -11678 ✓
단 면 A-A long 2 123 20200 10839 232 ~ -10607
...
✓ 검증 통과 — 뷰 분리와 표고 기준이 도면 치수와 일치
```
**이 단계를 통과하지 못하면 다음으로 가지 않는다.** 여기서 틀린 채로 3D 를
세우면 어디가 원인인지 알 수 없게 된다.
문제가 나오면:
| 증상 | 확인 도구 |
|---|---|
| 뷰가 없다 / 크기가 안 맞는다 | `node src/inspect.mjs manhole1 "<뷰>"` — 그 영역의 모든 엔티티를 레이어 무관하게 훑는다 |
| 기준점 검증 실패 | `datum.checks` 의 레이어 이름이 맞는지, 계획고 선이 `layers.ground` 에 잡히는지 |
### ② 프로파일 추출 · 조립 — `src/build.mjs`
```sh
node src/build.mjs manhole1 --loops # 뷰별 폐곡선 목록 (조립 스펙 작성용)
node src/build.mjs manhole1 # IR 생성
```
`--loops` 출력이 조립 스펙을 쓰는 **재료 목록**이다.
```
단 면 B-B (cross)
# 0 2700× 2750 bbox[-1350..1350, -7657..-4907] 꼭짓점 4 부모 -
# 1 2100× 2150 bbox[-1050..1050, -7357..-5207] 꼭짓점 10 부모 #0
# 2 4300× 300 bbox[-2150..2150, -8057..-7757] 꼭짓점 4 부모 -
# 3 2900× 100 bbox[-1450..1450, -7757..-7657] 꼭짓점 4 부모 -
```
폭×높이가 도면 표기 치수와 일치하는지 먼저 눈으로 확인한다. 여기가 맞으면
그 뒤는 거의 틀리지 않는다.
한 뷰만 자세히 보려면:
```sh
node src/probe.mjs manhole1 "단 면 B-B" # 표
node src/probe.mjs manhole1 "단 면 B-B" --ring # 꼭짓점 좌표까지
```
### ③ 모델러 명령으로 번역 — `src/emit/egm.mjs`
IR 의 연산을 egm 명령으로 바꾼다. 여기서만 egm 의 사정을 안다.
```
extrude → Polyline(닫힌 커브) → SelByUids → Extrude C=True D F {벡터}
loft → 단면마다 Polyline → SelByUids(단면 순서대로) → Loft C=True Enter
box → Box {대각1} {대각2} {높이}
difference → SelByUids(base) → SelByUids(A=true, cutters) → BooleanDifference
union → SelByUids(첫째) → SelByUids(A=true, 나머지) → BooleanUnion
```
`Loft` 는 기본이 **서피스**라 `C=True`(Cap)로 막아야 솔리드가 나온다.
단면 커브는 `L`(DeleteOriginal, 기본 true)로 자동 삭제된다. 별도 `Cap` 명령은
제자리 수정이라 새 uid 가 안 나오므로 여기서는 못 쓴다.
### ④ 실행 — `src/run_egm.py`
```sh
python src/run_egm.py out/manhole1.egm.json # 실행
python src/run_egm.py out/manhole1.egm.json --dry # 보내지 않고 명령만 출력
```
`--dry` 로 좌표를 먼저 검산하는 습관을 들이면 모델러를 왕복하지 않아도 된다.
```
[ 0] duct.solid — extrude
egm ~Polyline 0,-1.9,-7.757 0,1.9,-7.757 0,1.9,-3.157 0,-1.9,-3.157 0,-1.9,-7.757
egm ~SelByUids A=false <duct.solid$crv> Esc
egm ~Extrude C=True D F 0,0,0 15.4,0,0
```
---
## 3. 새 도면을 추가하는 절차
**코드는 건드리지 않는다.** `src/drawings/` 에 파일 하나만 추가한다.
### 3.1 뼈대 만들기
`src/drawings/manhole1.mjs` 를 복사해 이름을 바꾸고 `dwg` 경로를 고친다.
`layers` / `titles.views` 는 일단 대충 채우고 ①을 돌린다.
### 3.2 레이어 알아내기
```sh
node src/run.mjs <도면> --report # 일단 실패해도 좋다
node src/inspect.mjs <도면> "<뷰>" # 그 영역의 레이어 실태
```
`src/inspect.mjs` 는 레이어 필터를 무시하고 전부 보여준다. 여기서
구조체 외곽선이 어느 레이어에 있는지 눈으로 고른다.
### 3.3 레이어를 세 갈래로 나눈다
```js
layers: {
struct: /…/, // 뷰 분리용. 넉넉하게. 가구·주기까지 들어가도 된다
body: /…/, // 구체 프로파일용. 좁게. 케이블랙 같은 가구는 빼야 한다
material: /…/, // 기초·잡석·보도 등 별개 재료. 프로파일 추출엔 함께 넣는다
ground: /…/, // 계획고·GL 선. 표고 기준 검출용
}
```
`body ⊆ struct` 는 코드가 강제한다(`src/run.mjs`). 어겨도 조용히 틀리지 않는다.
### 3.4 표고 기준 잡기
```js
datum: {
levelLabel: /도로\s*계획고/, // 기준선을 찾는 힌트. 값은 선에서 가져온다
checks: [ // 도면 치수와 대조. 최소 2개
['B-B 구체 상단', (v) => v['단 면 B-B']?.layerZ['-전력구']?.[1], -4907],
['B-B 기초 하단', (v) => v['단 면 B-B']?.layerZ['!-기초']?.[0], -7757],
],
}
```
`layerZ[레이어] = [최저 Z, 최고 Z]`. 도면에 적힌 치수를 그대로 넣는다.
### 3.5 뷰별 앵커 잡기
각 뷰의 u(가로) 좌표 중 무엇이 프로젝트 0 인지 정한다.
`--loops` 로 폐곡선 bbox 를 보고 도면 치수와 맞춰가며 찾는다.
```js
anchors: {
'평 면 도': { u0: 1000 }, // u → X
'단 면 B-B': { u0: 2150 }, // u → Y (중심선)
'단 면 F-F': { u0: -14700 }, // XZ 단면
'단 면 H-H': { u0: 1900, v0: -17125, swapUV: true }, // 수평 단면
}
```
| 옵션 | 뜻 |
|---|---|
| `u0` | 가로축 원점. `프로젝트 = u - u0` |
| `v0` | 세로축 원점. 수직 단면은 생략(표고가 이미 기준) |
| `flip` / `flipV` | 축 방향이 반대일 때 |
| `swapUV` | 도면 가로축이 프로젝트 Y, 세로축이 X 일 때 (수평 단면) |
### 3.6 조립 스펙 쓰기
파이프라인에서 **사람 판단이 들어가는 유일한 자리**다.
```js
assemble({ loop, profile, part, rect, expect }) {
profile('duct.outer', 'YZ', loop('단 면 B-B', 2700, 2750).ring);
part({ id: 'duct.solid', op: 'extrude', profile: 'duct.outer', axis: 'X',
from: 0, to: 15400, cap: true, keep: false });
...
}
```
| 도우미 | 용도 |
|---|---|
| `loop(뷰, 폭, 높이)` | 그 치수의 폐곡선을 집는다. 0개나 2개 이상이면 예외 |
| `profile(id, 평면, ring)` | 프로파일 등록. 평면은 `'XY' \| 'XZ' \| 'YZ'` |
| `part({...})` | 파트 추가 |
| `rect(u0,v0,u1,v1)` | 직사각 프로파일. 폐곡선이 인접 형상과 붙어 못 쓸 때 |
| `expect(뷰, 폭, 높이, 이유)` | 그 폐곡선이 실재하는지 확인만. `rect` 값 교차검증용 |
IR 연산:
| op | 필드 |
|---|---|
| `box` | `min:[x,y,z]`, `max:[x,y,z]` |
| `extrude` | `profile`, `axis`, `from`, `to`, `cap` |
| `union` | `members:[id,…]` |
| `difference` | `base`, `cutters:[id,…]` |
| `loft` | `sections:[{profile, at},…]` — `at` 은 평면 법선축 위 위치 |
**loft 는 꺾인 자리를 못 만든다.** 두 단면 사이를 곧게 이을 뿐이라, 형상이
꺾이는 X 마다 단면을 놓고 구간을 끊어야 한다. 단면은 2개씩만 쓴다 — 3개
이상이면 스플라인 보간으로 부푼다. 두 단면은 **꼭짓점 수가 같아야** 하고,
링 정규화는 `addProfile` 이 자동으로 한다. 자세한 배경은
[`2d-3d-conversion.md`](2d-3d-conversion.md) 교훈 #16.
`keep: false` 는 boolean 에 소모될 중간 파트. 최종 산출물에만 `name`/`color` 를 준다.
---
## 4. 이 도면에서 실제로 만든 것
### 4.1 좌표계
```
X = 전력구 길이방향, 0 = 구조물 최좌단
Y = 폭방향, 0 = 전력구 중심선
Z = 표고, 0 = 도로 계획고 (DWG y = 483623.477, 자동 검출)
단위 = 내부 mm 정수, egm 넘길 때만 m
```
### 4.2 구간 구성 (A-A 종단면에서 읽음)
```
X -1,000 ~ 0 입구 스터브 B-B 구체 2,700×2,750
X 0 ~ 15,400 전력구 본체 C-C 구체 3,800×4,600 / 내부 3,000×3,800
X 1,900 ~ 3,200 사다리 샤프트 1 Z -3,157 ~ 0
X 12,600 ~ 13,900 사다리 샤프트 2 Z -3,157 ~ -39
X 15,400 ~ 18,200 맨홀 #2 Z -10,007 ~ +182, Y -1,900 ~ 8,300
X 18,200 ~ 19,200 출구 스터브 B-B
```
### 4.3 뷰 ↔ 부재 대응
| 뷰 | 평면 | 쓰인 곳 |
|---|---|---|
| 평 면 도 | XY | X 구간 확정, 샤프트 위치, 환기구 footprint |
| 단 면 A-A | XZ | **구간 배정표** — 어디서 어디까지 어느 단면인지 |
| 단 면 B-B / D-D | YZ | 스터브 단면 (2,700×2,750) |
| 단 면 C-C | YZ | 전력구 본체 단면 (3,800×4,600) |
| 단 면 E-E | YZ | 맨홀 전체 (챔버·격실·환기구 다리) |
| 단 면 F-F / G-G | XZ | 환기구 X 벽, 기초·잡석 |
| 단 면 H-H | XY | 환기 OPEN부 개구 (1.2×2.0) |
| 단 면 I-I | XY | 집수정 점검구 개구 (1.2×1.0) |
| 단 면 J-J | XY | 침사조·집수조 평면 (E-E 교차검증) |
### 4.4 최종 산출 파트
```
● 맨홀1_전력구_구체 스터브+본체 union 후 공동 차감
● 맨홀1_전력구_기초 4,000 × 100 fck=18MPa
● 맨홀1_전력구_잡석 5,400 × 500
● 맨홀1_전력구_출구스터브
● 맨홀1_사다리샤프트_1 / _2
● 맨홀1_맨홀2_구체 봉투 union 후 공동·개구부 차감
● 맨홀1_맨홀2_기초 / _잡석
● 맨홀1_환기구_기초 / _잡석
```
트리 이름은 `대분류_중분류_소분류` 형식이라 ObjectTreeViewer 에서 자동 그룹화된다.
---
## 5. 검증 절차
3D 를 세우기 **전에** 아래를 순서대로 통과시킨다. 한 단계라도 건너뛰면
나중에 어디가 원인인지 추적이 불가능해진다.
```
0. egm guide | wc -l 뷰어 모드가 아닌가 (39 면 뷰어 — 모델링 불가)
1. node src/run.mjs <도면> 지반선 Z 가 전 뷰에서 일치하는가
기준점이 도면 치수 2개 이상과 맞는가
뷰 폭·높이가 도면 치수와 맞는가
2. node src/gaps.mjs <도면> 프로파일이 안 닫히는 곳이 있는가
원인이 레이어 오분류인가 선 누락인가
3. node src/build.mjs <도면> --loops
폐곡선 치수가 도면 표기와 맞는가
node src/build.mjs <도면> --circles
원 지름이 도면 표기(Ø900·Ø100)와 맞는가
node src/report.mjs <도면> → out/<도면>.html
**자기교차 링이 있는가** (있으면 압출부터 쓰레기다)
링을 그림으로 확인. 표로는 못 보이는 것이 보인다
4. node src/build.mjs <도면> IR 검증 (참조 무결성, 소모 파트 keep 플래그)
5. python run_egm.py ... --dry
좌표가 예상대로 나오는가
6. python run_egm.py ... --export=out/<도면>.obj
실행 + OBJ 내보내기
7. egm status modelCount 가 최종 파트 수와 같은가
8. node src/measure.mjs <도면> --ir
의도(IR 격자 평가) vs 결과(모델러 OBJ) 대조
+ 기준선 (실패 시 exit 1)
```
7 이 어긋나면 boolean 이 인자를 흘린 것이다 (교훈 #14). 형상만 봐서는 못 잡는다 —
남은 커터 솔리드가 구체와 겹쳐 있어 겉보기엔 멀쩡하다.
### 꼭짓점 수를 먼저 본다
`--loops` 출력에서 **직사각형이어야 할 것이 4가 아니면 의심**한다. 링이 오염됐다는
신호다. 이 도면에서 실제로 나온 값:
```
꼭짓점 4 정상 직사각형
꼭짓점 10 헌치(150×150)가 있는 정상 형상
꼭짓점 8 개구 기호(X 표시)가 사각형 안을 가로질러 링이 자기교차
꼭짓점 114 케이블 지지대가 프로파일 레이어에 섞임
```
링이 오염됐으면 **bbox 는 여전히 정확하므로** `rect()` 로 재구성하고
`expect()` 로 치수만 확인한다.
### 부피를 대조한다 — `src/measure.mjs`
비틀린 loft 는 형상만 봐서는 못 잡는다. 부피가 어긋나는 것으로만 드러난다.
```sh
python src/run_egm.py out/manhole1-trans.egm.json --export=out/manhole1-trans.obj
node src/measure.mjs manhole1-trans # 기준선 대조 (실패 시 exit 1)
node src/measure.mjs manhole1-trans --save # 지금 값을 기준선으로 얼린다
node src/measure.mjs manhole1-trans --primitives # IR 원시 파트 부피표도
```
OBJ 삼각형을 발산 정리로 잰다. `~SetObjectName` 으로 붙인 이름이 OBJ 의 `g` 로
나오므로 파트 대조가 이름 하나로 끝난다. 나오는 것:
```
파트 부피 m³ 표면적 m² 삼각형 열린변 재질
맨홀1_전력구_구체 100.5403 507.471 10862 28 구체 콘크리트 fck=27MPa
맨홀1_사다리샤프트_1 3.3217 28.356 3127 13 …
합계 107.1425
```
**IR 에 없는 그룹이 나오면 boolean 이 인자를 흘린 것이다** — `modelCount` 대조와
같은 신호인데 어느 커터가 남았는지까지 알려준다.
`열린변`은 다양체 여부다. 이음매를 맞대어 붙이면 양쪽 삼각분할이 달라 T 정점이
남는다(교훈 #16). 부피·렌더링에는 문제가 없지만 IFC 로 갈 때 걸릴 수 있다.
#### 기준선은 `baselines/` 에 두고 **커밋한다**
`out/` 과 달리 재생성으로 복구되지 않는다. 사람이 한 번 확인한 부피가 남아야
다음 변경이 형상을 건드렸는지 알 수 있다.
#### 기대값은 자동으로 안 나온다 — 그리고 그게 맞다
`measure.mjs` 는 **boolean 을 합성하지 않는다.** 커터는 관통을 확실히 하려고
대상 밖으로 `OVER` 만큼 튀어나와 있고(교훈 #13), 샤프트 보어는 본체 공동과
겹친다. 그래서 `Σ멤버` 도 `base − Σ커터` 도 틀린 값이 된다. **틀린 기대값은
기대값이 없는 것보다 나쁘다.**
새 형상의 기대값은 `--primitives` 표를 재료로 사람이 한 번 조립하고, 맞으면
`--save` 로 얼린다. 두 단면 사이 면적은 t 의 2차식이라 심프슨
`L·(A0 + 4·A(½) + A1)/6` 이 **근사가 아니라 정확**하다.
```
duct.solid 5.1975 + 1.6540 + 0.9760 + 269.1920 = 277.0195
샤프트 1 원시합 5.3299 − 보어(솔리드 범위로 클립) 2.0084 = 3.3215 실측 3.3217
└ 원기둥이 다면체로 나와 미세하게 크다
```
결과의 **정점 X 값에 중간 행이 없는지**도 같이 본다. 없으면 직선 룰드다.
### `src/gaps.mjs` 읽는 법
```
뷰 채택선 경계 레이어 누락 고치면 되는 레이어
단 면 J-J 22 0 0 0 —
단 면 E-E 69 1 3 0 잡석, 선형
```
| 열 | 뜻 | 대응 |
|---|---|---|
| 경계 | 뷰 밖으로 이어져 잘린 것 | 결함 아님. 무시 |
| 레이어 | 선은 있는데 다른 레이어에 있음 | `layers.body` 에 추가 (설정 한 줄) |
| 누락 | 도면에 선이 없음 | 도면 수정 또는 스냅 오차 확인 |
이 도면은 끊김 50곳 중 29곳이 뷰 경계, 20곳이 레이어 오분류, **선이 실제로
없는 곳은 1곳**(그것도 1mm 스냅 오차)이었다.
---
## 6. 도구 목록
| 파일 | 역할 |
|---|---|
| `src/run.mjs` | 파싱 · 기준점 검출 · 뷰 분리 · 검증 |
| `src/build.mjs` | 프로파일 추출 · 조립 스펙(IR) 생성 |
| `src/emit/egm.mjs` | IR → egm 명령 목록 |
| `src/run_egm.py` | 명령 실행 (`--dry`, `--export=<obj>` 지원) |
| `src/measure.mjs` | OBJ 부피 실측 · 기준선 대조 · 재질별 물량 |
| `src/report.mjs` | 추출 결과 뷰어 (한 장짜리 HTML). 링 그림 · 자기교차 · 원 · IR |
| `src/capture.mjs` | 뷰별 **도면** 캡처 + 좌표 사이드카 (전 레이어, `--crop`·`--png`) |
| `src/render.mjs` | 만든 **3D** 를 정투영 렌더 (`--axis`·`--part`·`--clip`·`--png`) |
| `src/volume.mjs` | IR 을 격자로 평가 — 모델러 없이 "의도" 부피 |
| `src/fit.mjs` | 도면 → `params/<이름>.json` |
| `src/params.mjs` | 파라미터 스키마·식·검증 |
| `src/rules.mjs` | 설계 규칙 (위상은 막고, 설계는 경고) |
| `src/ui.mjs` | 파라미터 편집 + 갱신 UI (로컬 서버) |
| `src/core/views.mjs` | 뷰 → 프로젝트 좌표. `build.mjs` 와 `report.mjs` 가 **같이 쓴다** |
| `src/probe.mjs` | 뷰 하나의 폐곡선을 표로 (`--ring` 으로 좌표까지) |
| `src/gaps.mjs` | 프로파일 끊김 진단 + 원인 분류 |
| `src/inspect.mjs` | 뷰 영역의 모든 엔티티를 레이어 무관하게 |
| `src/materials.mjs` | 뷰별 재질 주기 수집 · 레이어 ↔ 재질 대응 확인 |
| `src/core/parse.mjs` | DWG → 엔티티, 선분 펴기, 표제 수집 |
| `src/core/datum.mjs` | 표고 기준 검출 · 좌표 변환 |
| `src/core/segment.mjs` | 뷰 분리 (교대 간격분할 + 제목 귀속) |
| `src/core/loops.mjs` | 선분 → 닫힌 프로파일 (평면 분할) |
| `src/ir.mjs` | 조립 스펙 스키마 · 빌더 · 검증 |
| `src/drawings/*.mjs` | **도면별 설정.** 새 도면은 여기에 파일 하나 |
| `src/drawings/manhole1-trans.mjs` | 전이부만 세우는 최소 모델. `manhole1` 설정을 상속하고 `assemble()` 만 교체 — 한 부분을 떼어 검증할 때의 본보기 |
---
## 7. 모델러 조작 시 주의
`Modeler_AI/docs/egm-usage.md` 중 이 작업에서 특히 물리기 쉬운 것.
- **`Extrude C=True` 단독은 항상 +Z 로 민다.** 커브 평면의 법선을 따르지 않는다.
`src/emit/egm.mjs` 는 항상 `D F {기준점} {방향점}` 으로 방향을 명시한다.
- **`C=True` 는 `D F` 앞에** 와야 한다. 뒤에 오면 무시되고 서피스가 나온다.
- **닫힌 `Polyline` 에 `Enter` 를 붙이지 말 것.** 자동 종료되므로 남은 신호가
다음 명령으로 새어나간다.
- **`SelByUids` 는 `Esc` 로 닫는다.** 안 닫으면 이후 명령이 전부 무시된다.
- 명령은 비동기다. `egm status` 의 `command` 가 `null` 이 될 때까지 기다린다.
`src/run_egm.py` 의 `wait()` 가 그 일을 한다.
- **`BooleanDifference`/`BooleanUnion` 은 두 개만 먹는다.** 플러그인이
`TargetModels[0]`, `[1]` 만 쓰므로 셋 이상 선택하면 나머지는 조용히 무시된다.
`src/emit/egm.mjs` 의 `fold()` 가 둘씩 접는다.
- **뷰어 모드면 모델링 명령이 등록조차 안 된다.** 0절의 `egm guide` 확인을
건너뛰지 말 것.
- **`ExportByPath` 의 경로는 반드시 큰따옴표로 감싼다.** `_` 가 egm 스크립트
특수문자라, 경로에 밑줄이 있으면 오류도 경고도 없이 **아무 파일도 안 생긴다**.
`result` 는 정상으로 보이고 `created:[]` 도 원래 그런 명령이라 신호가 없다.
내보내기 전에 `SelAll` 이 필요하고, `.obj` 만 나온다(`.stl` 은 파일이 안 생긴다).
`src/run_egm.py` 의 `export_obj()` 가 이걸 다 하고 파일 생성까지 확인한다.
### 씬 초기화가 실패할 때
`SelAll` → `Delete` 직후 상태가 남아 다음 명령이 씹히는 경우가 있다.
```sh
egm "Esc"; egm "!Cancel"; egm "SelAll"; egm "Delete"; egm "Esc"; egm "!Cancel"
egm status # command 가 null 인지 확인
```
---
## 8. 현재 미완
| 항목 | 상태 | 필요한 것 |
|---|---|---|
| 전이부 챔퍼 | `manhole1-trans` 에서 완성 (챔퍼·단차·종방향 헌치) | `manhole1` 본편에 이관 |
| 이음매 T 정점 | 열린 변 21개 (기하는 닫힘) | IFC 내보낼 때 재검토 |
| Φ100 관통 ×5 | 미구현 | 평면도에서 전선인입 2 · 우수관연결 2 · 유도배수관 1 위치 특정 |
| 나선계단 | 미구현 | 현 IR 연산으로 표현 불가. `Sweep1` 또는 전용 연산 추가 |
| 방호난간 L=7.2m | 미구현 | H-H 에 표기 |
| IFC 출력 | 미구현 | `emit/ifc.mjs`. 레이어 ↔ 재질 매핑은 이미 확보 |
| 브라우저 연동 | 미구현 | `src/core/*` 순수화 + localhost 브리지 |
+217
View File
@@ -0,0 +1,217 @@
# 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 의무의 근거다.
File diff suppressed because it is too large. Load diff
+194
View File
@@ -0,0 +1,194 @@
# 다음 세션 프롬프트 — 교각 P11 (꺾임)
> 그대로 복사해 붙이면 된다. 아래 `---` 부터가 프롬프트다.
---
# 교각 P11 — 꺾인 코핑
## 지난 세션에 P9 를 끝냈다
`산외교_파이형_교각일반도(울산P9).dwg` 로 **교각 한 기를 끝까지** 세웠다.
코핑 · 원형기둥 2 · 확대기초 2 · 버림 2 · 강관말뚝 44 · 받침 6기(교좌대·모르타르·
받침·솔플레이트) · 미관문양 2 = **77 파트 · 660.4154 m³**.
모델러 실측 대비 **불일치 0건**, 열린 변 0, IFC 내보내기까지 왕복 검증됐다.
이번엔 **P11** 이다. `산외교_파이형_교각일반도(울산P11-꺾임).dwg`.
P9 와 뷰 구성이 같지만 **코핑 축선이 꺾여 있다.** 그게 이번 세션의 전부다.
## 먼저 읽어라 (순서대로)
1. `CLAUDE.md` — 경계 · 작업 순서 · 함정표
2. `docs/methodology.html` **11.4 절** — P9 실증 결과. 판정표 · 코어 변경 · 새 함정
3. `src/drawings/pier-p9.mjs` — **이번 작업의 원본이다.** 통째로 읽어라
4. `docs/2d-3d-conversion.md` 교훈 #29~#33 (지난 교량 세션)
## 대상
```
drawings-src/산외교_파이형_교각일반도(울산P11-꺾임).dwg
도곽 CXBLKA1 INSERT (1,406,622, 390,000) × scale 100 × A1(841×594)
→ sheet x[1,406,622 … 1,490,722] y[390,000 … 449,400]
```
**도곽 밖은 전부 다른 벌이다.** 이 파일도 도면집 스택이라 17벌이 y 방향 65,000
간격으로 쌓여 있고, 그중 P9 자리는 치수선만 남아 있다. 상자를 눈으로 잡지 마라 —
도곽 블록에서 계산한다.
## 이미 확인한 것 (DWG 데이터에서 직접 잰 값)
```
측점 STA.0+594.570
표고 EL. 82.799 · 82.306 · 81.713 (코핑 상면 3단으로 추정)
EL. 44.413 (기초 저면)
기둥 Ø2,200 원형 3본 · CS-CONC-MINR CIRCLE
평면도 (1,416,488, 437,383) (1,425,088, 437,383) (1,433,616, 436,442)
기초평면 (1,416,488, 403,537) (1,425,088, 403,537) (1,433,616, 402,596)
받침 블록 500x500 ×3 · 530x530 ×2 · 470x470 ×1
연단거리 L=170.600 · H=36.203 · Theta=0
```
### 꺾임각 = **6.30°**
```
기둥 1→2 Δx 8,600 Δy 0 → 0.00° 간격 8,600
기둥 2→3 Δx 8,528 Δy −941 → −6.30° 간격 8,580
```
**두 뷰(평면도·기초평면도)에서 같은 941 이 나온다.**
> ⚠ 지난 세션에 이걸 한 번 「8.7°」로 잘못 말했다. 전 색 각도 분포에 있던
> `81.3°/171.3°` 를 꺾임각으로 읽은 것인데, 구조 외곽선만(`ci=256`) 세면 그
> 대역이 없다 — **보조선 각도였다.** 기둥 축선에서 직접 잰 6.30° 가 맞다.
> **각도를 선 분포에서 읽지 말고 부재 좌표에서 재라.**
`(1,453,587, 436,249)` 의 Ø2,200 은 `CS-CONC-MAJR` 이고 x 가 19,971 떨어져 있다 —
**단면 A-A** 의 기둥 단면이지 평면도 기둥이 아니다.
## 1단계 — 꺾임의 정체 (여기서 멈추고 보고)
**캡처를 먼저 봐라.** 좌표만으로는 못 가른다.
1. **평면도** — 꺾이는 자리가 **기둥 3번 위**인가 그 **사이**인가.
코핑이 «각진 두 토막»인가 «한 곡선»인가. 꺾임부 폭·필렛
2. **정면도** — 파단선 위치 · 코핑 상면 단수 · EL 지시 위치
3. **단면도** — 코핑 교축폭 · 헌치
4. **기초 평면도** — 기초 3기인가 · 말뚝 배치(P9 는 22본×2)
## 2단계 — 형상 전략 (보고 후 진행)
P9 코핑은 「정면 윤곽(YZ) × 교축 압출」 ∩ 「평면 윤곽 빗각기둥 2장」으로 섰다.
**꺾이면 그 전제가 깨진다** — 정면도가 투영이라 실제 길이를 안 준다.
후보 셋. 캡처 보고 고른다.
| 방법 | 되는 조건 | 걸림돌 |
|---|---|---|
| 꺾임점에서 **끊어** 두 토막을 각자 압출 | 두 토막이 각각 직선 | 이음매 union — 면으로만 맞닿으면 다음 boolean 이 흘린다 |
| 토막마다 **로컬 좌표계** | 가장 정직 | IR 에 배치 개념이 없다. 코어 변경 |
| 평면 압출 ∩ 정면 압출 | 꺾임이 평면에만 있고 정면이 실제 길이를 줄 때 | 대개 안 준다 |
**끊는 쪽이 유력하다.** loft 는 쓰지 마라 — 결과가 안 닫혀 boolean 에서 샌다(아래).
## 절대 규칙
- `src/core/*` · `src/ir.mjs` 에 도면 고유값 금지. 레이어명 · 제목 · 좌표 · 단위 ·
축척은 전부 `src/drawings/pier-p11.mjs`
- **코어 수정이 필요해 보이면 먼저 보고해라.** 그게 방법론의 구멍이다
- 못 찾거나 여러 개면 예외를 던져라. 자동 선택 금지
- **손으로 읽은 수를 코드에 적지 마라.** 도면에서 집고 다른 값과 대조해라
- **그림을 먼저 봐라.** 뷰 상자를 잡기 전에 시트 → 도형 → 확인 → 상자
## 게이트 (P9 에서 전부 통과시킨 순서)
```
0. egm guide | 명령 수 208 이면 편집 모드 · 39 면 뷰어 모드(모델링 불가)
1. node src/run.mjs pier-p11 뷰 폭·높이가 도면 표기와 맞는가
2. node src/gaps.mjs pier-p11 끊김의 원인이 형상인가 파단선인가
3. node src/build.mjs pier-p11 --loops 꼭짓점 수·자기교차
node src/build.mjs pier-p11 --circles / --blocks
node src/table.mjs pier-p11 --title "교량받침 ELEVATION TABLE"
4. node src/build.mjs pier-p11 IR 검증
5. node src/volume.mjs pier-p11 해석 부피 (모델러 없이)
6. node src/emit/egm.mjs out/pier-p11.ir.json
python src/run_egm.py out/pier-p11.egm.json --export=out/pier-p11.obj
7. egm status modelCount == 최종 파트 수
8. node src/measure.mjs pier-p11 --ir 의도 vs 결과 (합격선 절대 0.05 m³)
9. node src/emit/ifc.mjs out/pier-p11.ir.json
```
## 지난 세션에 코어를 이만큼 고쳤다 (되돌리지 마라)
```
C12 core/loops.mjs dropCollinear 중복점 제거
매달린 변 왕복이 길이 0 인 변을 남긴다. 압출하면 퇴화 면이 나온다
C13 ir.mjs · emit/egm.mjs · volume.mjs intersect 연산
BooleanIntersection 은 이항. 「두 방향이 동시에 맞아야 하는 형상」용
C14 ir.mjs · emit/egm.mjs · volume.mjs extrude.drift (빗각 기둥)
Extrude C=True D F 가 자유방향을 받고 결과가 닫힌다
C15 core/parse.mjs collectBlocks · run.mjs · core/views.mjs · core/pick.mjs blocks()
말뚝이 INSERT 블록. 삽입점=위치, 배율=치수
C16 core/table.mjs (신규) · src/table.mjs (CLI) · run.mjs 색인 · pick.tables
표를 (이름표, CSV) 쌍으로 색인. 이름표 아래 글자높이 ×2 로 표를 찾는다
C17 run.mjs 뷰별 entityFilter
뷰마다 무엇이 구조선인지 다르다 (문양은 ci=2, 본체는 ci=256)
C18 volume.mjs 교집합 해석 부피
① 압출∩압출 닫힌 식 ② 종단∩평면(높이 가변) Z 슬라이스 가우스
C19 emit/ifc.mjs (신규) IFC4 출력. 해석 솔리드 우선 + IfcMapConversion
C20 emit/egm.mjs loft → `~Loft Enter` + `~Cap` (그래도 boolean 엔 못 쓴다)
```
## 새로 밟은 함정 (또 밟지 마라)
| 증상 | 원인 | 대응 |
|---|---|---|
| **부피는 맞는데 boolean 이 샌다** | **`Loft` 결과가 안 닫힌다.** 4각형 단면도 열린 변 12개. 발산 정리 부피는 정확해 **부피 검사로 절대 안 걸린다** | loft 를 boolean 인자로 쓰지 마라. 평행이동뿐이면 `drift` 압출 |
| 그려진 길이가 실제가 아니다 | **파단선.** 기둥이 6,100 인데 실제 33,800 | `CS-CUTL` 을 본다. 생략량 = 실제 − 도형 (derived) |
| 한 파일에 도형 17벌 | 도면집 스택 | **도곽 블록에서 상자를 계산** |
| 「단면도」 안쪽 선을 부재로 읽음 | **절단면이 아니라 투영** | 치수가 `외곽 − 2×R` 이면 투영 |
| 평면 모서리가 45° 모따기 | 파서가 `bulges` 를 버려 90° 호가 현으로 눌림 | 호 끝점은 정확 → R 은 되찾는다. 링 면적은 못 믿는다 |
| 적분값이 0.2 m³ 샘 | 구간 끝점이 링 꼭짓점 → 스캔라인 교차 수 어긋남 | 내부 노드 구적(가우스). 심프슨 금지 |
| 구조 형상이 **표**로 잡힘 | 평면도 격자가 73행×83열 「표」 | 표 색인에 `layerFilter` |
| `actualMeasurement` 로 단위 판정 실패 | 주요 m 표기 · 상세 mm 표기 (**표시값**이 온다) | 도곽 배율 · 블록 scale · 격자 간격으로 교차검증 |
## P9 에서 통한 방식 — 그대로 가져다 써라
```
단위 도곽 A1×100 · 블록 scale ↔ Φ 텍스트 · 격자 ↔ 「4@1.300」 → 넷 다 1단위=1mm
공통중심 부재 대칭으로 자기검증 (코핑 ±7,000 · 기초 ±3,250 · 기둥 ±4,100)
표고 공통 기준선이 없어도 **표가 준다** — EL.'X' = 코핑상면 + h1 + h2
6기가 모두 같은 절대표고를 내면 그게 원점이다
받침 평면도(위치·평면치수) × 표(적층고·표고·교축위치) 세 겹 검산
파단 생략량 = 기둥_높이 − 기둥_도형길이 (derived. 손으로 적지 마라)
```
## 보고 형식 (매 단계)
```
쓴 도면·도형: 무엇을, 왜 그것을
통과한 게이트:
막힌 곳: 게이트 번호 + 증상 + 원인
설정으로 해결: 무엇을
코드가 필요: 무엇이 왜 (← 방법론의 구멍)
방법론에서 틀린 것: 어느 절의 어느 주장이
새 함정: 증상 → 원인 → 대응
```
## 아직 안 한 일 (P11 과 별개, 여력 되면)
1. **`manhole1` 재검증** — loft 결과를 union·difference 인자로 쓴다.
C20 의 함정 위에 서 있을 수 있다. 부피가 맞아 보여도 샜을 수 있다
2. **P9 미관문양 형상 재검토** — 사용자가 「제대로 안 된다」고 했다.
지금은 코핑 양 측면의 50 두께 띠 + 하단 250 구간 100 돌출로 세워져 있고,
정면도의 매달린 대각선 6줄(문양 내부 능선)은 재현 안 한다
3. **`docs/2d-3d-conversion.md` 에 교훈 #34~ 추가** — 위 함정표가 아직 안 옮겨졌다
4. **텍스트 셀렉터** — 기둥 높이·말뚝 길이가 아직 `design` 이다.
치수/표고 **텍스트**를 집는 수단이 없다. `blocks()` 를 만든 것과 같은 종류
## 마지막
1. `docs/2d-3d-conversion.md` 에 교훈 추가
2. `docs/methodology.html` **11.5 절** 신설 — P11 이 무엇을 밝혔는지.
판정표를 그대로 싣고, 배지를 올리되 **틀린 주장은 지우지 말고 왜 틀렸는지 적어라**
3. `node src/ui.mjs pier-p11 --port 7333` 로 파라메트릭 UI 까지 확인
4. `node src/emit/ifc.mjs out/pier-p11.ir.json` 로 IFC 까지
+143
View File
@@ -0,0 +1,143 @@
# 다음 세션 프롬프트 — 교각(Pier) 하나로 방법론 실증
> 그대로 복사해 붙이면 된다. 아래 `---` 부터가 프롬프트다.
---
# 교각(Pier) 한 부재로 방법론 실증
## 왜 부재를 쪼개나
지난 세션에 교량 도면 7장으로 한꺼번에 갔다가 **뷰 상자를 두 번 잘못 잡았다.**
한 시트에 날짜가 다른 작업본이 겹쳐 있는 걸 못 보고 두 벌을 섞어 뽑았고,
교대라고 부른 것이 실은 옆 도형의 날개벽이었다. **폐곡선도 치수도 그럴듯해서
숫자로는 안 걸렸다.**
이번엔 **교각 하나만** 한다. 부재 하나를 끝까지 밀어 방법론의 어느 절이
그 부재에 대해 참인지 거짓인지를 확정하는 것이 목적이다. 모델 완성이 아니다.
## 먼저 읽어라 (순서대로)
1. `CLAUDE.md` — 경계 · 작업 순서 · 함정표(마지막 7행이 지난 세션 것)
2. `docs/2d-3d-conversion.md` — **교훈 #29~#33 을 반드시 읽어라.** 지난 세션에
내가 밟은 것들이다
3. `docs/methodology.html` — **11.0.4 · 11.0.5** 가 교량 실증 결과다
4. `src/drawings/bridge-end.mjs` — 지난 세션 산출물. 교각을 이미 세워봤다
(두부 2,244×1,334 · 기둥 1,586×3,904 · 확대기초 5,081×1,334)
## 대상
`drawings-src/` 의 **교각 도면**.
- 새 DWG 가 들어와 있으면 그것을 쓴다
- 없으면 기존 7장 중 교각이 그려진 것 (`교량시종점` 의 2지간 도형,
`C-BRDG-PIER` 레이어를 가진 `교량폭원` · `횡단경사`)
**어느 것을 쓰는지 먼저 보고해라.**
## 절대 규칙 (지난 세션에 어긴 것 포함)
- `src/core/*` · `src/ir.mjs` 에 도면 고유값 금지. 레이어명 · 제목 · 좌표 · **단위** ·
**축척**은 전부 `src/drawings/<이름>.mjs`
- **코어 수정이 필요해 보이면 먼저 보고해라.** 그게 방법론의 구멍이다
- 못 찾거나 여러 개면 예외를 던져라. 자동 선택 금지
- 손으로 읽은 수를 코드에 적지 마라. 도면에서 집고 **다른 값과 대조**해라
- **그림을 먼저 봐라.** 뷰 상자를 잡기 전에 반드시:
① 시트 전체 캡처 → 도형이 몇 벌인지, 겹치는지
② 도형 하나만 캡처 → 무엇이 들어 있는지 눈으로 확인
③ 그 다음에 상자
**이 순서를 건너뛰면 지난 세션과 같은 실수가 난다**
- 캡처는 `src/capture.mjs` 를 써라 (`--crop`, `--scale`, `--png`).
프로젝트 격자와 사이드카가 같이 나온다. 즉석 렌더를 던지지 마라
## 1단계 — 도면 파악 (여기서 멈추고 보고)
1. **시트 전수 캡처.** 도형이 몇 벌인가. 날짜 도장이 몇 개이고 **작업본이
겹쳐 있는가** (교훈 #29)
2. **교각이 그려진 도형을 전부 나열**하고 각각 무엇을 주는지:
- 종단면(측면) / 횡단면 / 평면 중 무엇인가
- 두부 · 기둥 · 확대기초 · 말뚝 중 무엇이 부재로 그려졌나
- 확대 상세가 섞여 있나 (축척이 다르다 — 교훈 #31)
3. **단위와 축척.** `DIMENSION.actualMeasurement` ↔ 치수 글자로 `unitMm` 확정.
치수 글자가 없으면 **크기를 아는 표준 부재**로 축척을 되찾아라 (교훈 #30).
두 값 이상으로 교차검증할 것
4. **표기 치수가 숫자인가 기호인가** 세라. 기호뿐이면 그 도면 안에는 대조
상대가 없다는 뜻일 뿐 — 다른 도면·표준 치수는 여전히 상대가 된다
## 2단계 — 교각 하나 세우기 (보고 후 진행)
**부재를 하나씩** 올려라. 한 번에 다 넣지 말 것.
```
1. 기둥 가장 단순한 것부터. 폐곡선 → 압출 → 부피 대조
2. 두부 기둥과 맞닿는 자리 확인
3. 확대기초
4. 말뚝 원기둥. 위치·지름을 도면에서 어떻게 얻는가 (교훈 #33)
5. 받침 두부 위. 확대 상세와 섞이지 않는지
```
각 부재마다 **게이트를 다 통과한 뒤** 다음으로 간다.
`--loops` → `gaps` → `build` → `measure --ir` 이 0.0000 인지.
## 3단계 — 이 부재에서 방법론이 어디까지 참인가
교각에 대해 **명시적으로 판정**해라. 「해봤다」가 아니라 「참/거짓/해당없음」으로.
| 방법론 주장 | 교각에서 |
|---|---|
| 2.2 표고 기준은 전 뷰 공통 | ? |
| 3.4 제목으로 뷰를 가른다 | ? |
| 5.2 연산 결정 트리 | 교각에 필요한 연산이 트리에 있나 |
| 11.1 기준 체인 (받침 → 교각 로컬) | **여기서 처음 검증 가능하다.** 하부공 도면이 받침 좌표·표고로 교각을 잡나 |
| 11.2 revolve 필요 | 원형 교각·두부가 있나 |
| 11.2 임의축 cylinder | 경사 말뚝이 있나 |
## 알려진 구멍 (지난 세션에 찾았고 아직 안 메웠다)
- **`pick` 이 선을 못 집는다.** 말뚝 위치는 `CS-CNTL` 중심선인데 집을 수단이
없어 스크립트로 재서 설정에 적었다. `circles()` 처럼 `centerlines()` 가 필요하다.
교각에서 또 필요해지면 **그때 코어에 넣자고 보고**해라
- 말뚝 **폭방향 배치**와 길이를 주는 도면이 없었다. 교각 도면에 평면이 있으면
거기서 나올 것이다 — **있는지 먼저 확인**해라
- 개별 거더 치수 없음 (거더배치는 위상만 준다)
## 지난 세션에 코어를 이만큼 고쳤다 (되돌리지 마라)
```
C1 cfg.unitMm DWG 모델단위가 도면군마다 다르다
C2 groupByBox 제목 없는/중복인 도면의 뷰 영역
C3 datum.mode:'none' 공통 기준선이 없는 도면
C4 toSegments entityFilter 레이어 아닌 필터 (색·선종)
C5 viewBoxes[뷰].unitMm 도형별 축척
C6 buildLoops 분할 뒤 중복 제거 겹치는 동일선상 선분
C7 rules[].needs 규칙 적용 범위
C8 findLevelDatum 라벨 무리 검사 예외 없이 틀린 답을 막는다
C9 pick.loops(w,h,tol) 같은 부재 여러 벌
C10 volume.mjs 해석 부피 격자 거짓 경보 제거
C11 buildLoops bbox collinear 제거 뒤로 (매달린 변)
capture.mjs 단위·격자·사이드카
probe.mjs → projectViews 경유
render.mjs --part 정규식 · --clip 걸침 허용
```
## 보고 형식 (매 단계)
```
쓴 도면·도형: 무엇을, 왜 그것을
통과한 게이트:
막힌 곳: 게이트 번호 + 증상 + 원인
설정으로 해결: 무엇을
코드가 필요: 무엇이 왜 (← 방법론의 구멍)
방법론에서 틀린 것: 어느 절의 어느 주장이
새 함정: 증상 → 원인 → 대응
```
## 마지막
1. `docs/2d-3d-conversion.md` 에 교훈 추가 (번호는 #34 부터)
2. `docs/methodology.html`
- 11장에 **교각 절** 신설 — 부재 단위 실증이 무엇을 밝혔는지
- 3단계 판정표를 그대로 싣는다
- 배지를 `가설` → `검증`/`부분` 으로 올리되 **틀린 주장은 지우지 말고 왜
틀렸는지 적어라**
3. `node src/ui.mjs <도면> --port 7333` 으로 파라메트릭 UI 까지 확인
(`dims()` 를 만들어야 치수 상자가 나온다)
+516
View File
@@ -0,0 +1,516 @@
# 파라메트릭 설계 층 — 설계 노트
2026-08-06 토론 정리 → **구현 완료.** 12절에 실제로 만든 것과 확인된 것을 적었다.
결정 근거는 11절(순서) · 8절 (b)(대조 등급) · 8.5절(UI).
- 절차는 [`2d-3d-process.md`](2d-3d-process.md), 노하우는 [`2d-3d-conversion.md`](2d-3d-conversion.md)
- 남은 일 전체는 [`roadmap.md`](roadmap.md)
---
## 0. 대전제 — 목표는 바뀌지 않았다
**이 저장소의 목표는 여전히 `2D DWG → 3D 모델 · IFC` 다.**
파라메트릭은 그 위에 얹는 층이지 대체가 아니다. 맨홀 도면 모델링은 **파이프라인을
검증하는 과정**이지 그 자체가 목적이 아니다.
순서를 헷갈리면 안 된다.
```
DWG 를 읽어 3D 를 세운다 ← 본체. 지금 여기
그 위에
치수를 파라미터로 뽑아 바꾼다 ← 이 문서
그 위에
바꾼 결과가 맞는지 평가한다 ← 설계 도구가 되는 지점
```
---
## 1. 사용자가 원하는 것
> 설계란, 비슷한 도면을 가져와 **새 환경에 맞게 치수를 바꿔보고**, 맞는지 검토하고,
> 다시 바꾸는 **반복 작업**이다.
그래서 필요한 것은 이것뿐이다.
```
값을 바꾼다 → 모델이 다시 만들어진다
```
**속도는 요구사항이 아니다.** 전체 재생성으로 충분하다(현재 28파트 1~2분).
실시간 프리뷰·부분 재생성은 있으면 좋은 것이지 필수가 아니다 — 우선순위 뒤로.
### 모델러에 패밀리 개념이 없다는 것
EGModeler 는 Revit 패밀리 같은 것이 없다. 매번 새로 만들어야 한다.
**그래도 된다 — 패밀리를 모델러가 아니라 우리가 들고 있으면 된다.**
| Revit | 우리 |
|---|---|
| 패밀리 `.rfa` | `assemble()` 조립 로직 |
| 타입 파라미터 | `params.json` |
| 인스턴스 배치 | egm 재생성 |
모델러는 **덤 렌더러**다. 형상 지능은 전부 IR 위쪽에 둔다. 지금 계층 구조가
이미 그렇게 생겼다 — `core/*` 는 3D 를 모르고 `emit/*` 만 모델러 사정을 안다.
---
## 2. D-Cubed 검토 결과 — 쓰지 않는다
Siemens PLM 의 CAD 부품 라이브러리. 원래 영국 D-Cubed Ltd, 2004년 UGS 인수.
| 컴포넌트 | 하는 일 |
|---|---|
| **2D DCM** | 2D 스케치 구속 솔버 (치수·평행·접선·동심) |
| **3D DCM** | 3D 어셈블리 구속 솔버 |
| AEM / CDM / PGM | 기구학 · 간섭검사 · 2D 프로파일 연산 |
SolidWorks · Solid Edge · NX 가 쓴다.
**쓰지 않기로 한 이유**
- CAD 벤더용 **상용 C++ SDK**. Siemens 와 개별 계약해야 하고 공개 가격이 없다
- 우리는 Node 다. FFI/N-API 바인딩을 직접 짜야 한다
- **무엇보다, 우리 문제에 구속 솔버가 거의 필요 없다** (아래 3절)
오픈소스 대안 (필요해질 때 검토)
| | 라이선스 | 비고 |
|---|---|---|
| **planegcs** (FreeCAD 스케처 솔버) | LGPL | Eigen 기반. WASM 포팅본이 있다고 알고 있으나 **미확인** |
| **SolveSpace `slvs`** | GPL-3 | C 라이브러리 + Python 바인딩. 2D·3D |
GPL 배포 조건 확인 필요. `vendor/acadrust-dwg` MPL-2.0 을 이미 다루고 있으니 낯선 문제는 아니다.
---
## 3. 구속 솔버가 필요 없는 이유
파라메트릭에는 성격이 다른 두 가지가 있다.
```
① 명시적 (history-based) 치수 = 프로그램의 입력. 값 → 계산 → 형상
OpenSCAD · Grasshopper · 그리고 지금 우리 파이프라인
② 변분/구속 (variational) 엔티티 + 구속을 선언하고 솔버가 위치를 찾는다
SolidWorks 스케치 · D-Cubed
```
요구사항은 **①로 거의 다 된다.** 증거가 이미 코드에 있다 —
`drawings/manhole1-trans.mjs` 의 외곽 챔퍼 중간 단면은 상수가 아니라 **식**이다.
```js
const t = (CH1 - CH0) / (CH2 - CH0);
const oY = Math.round(1350 + (1650 - 1350) * t); // 1550
const oZ = Math.round(-4907 + (-4608 + 4907) * t); // -4708
```
②가 필요한 것은 **관계가 순환할 때**다. "헌치가 두 면에 접하면서 벽 두께는
유지" 같은 것. 우리 형상은 거의 직각·45° 라 순환이 잘 안 생긴다.
**결론: 파라미터 + 식 + 위상 정렬로 시작한다. 솔버는 필요한 자리를 실제로
세어본 뒤에 결정한다.**
---
## 4. 핵심 구조 변경 — `params.json` 을 파이프라인 한가운데 끼운다
이 하나가 나머지를 거의 다 푼다.
```
지금 DWG ──build.mjs──→ IR ──→ 3D 치수가 코드 안에 박혀 있다
바꿀 것 DWG ──fit──→ params.json ──build──→ IR ──→ 3D
↑ 사람이 여기만 만진다 도면을 다시 안 본다
```
**빌드가 도면을 안 보는 것이 핵심이다.**
지금 `assemble()` 은 `loop('단 면 B-B', 2700, 2750)` 로 **치수를 도면 조회 키로**
쓴다. 사용자가 2700 을 3000 으로 바꾸면 그런 폐곡선이 없어 예외가 난다.
도면에 묶여 있는 한 파라메트릭이 성립하지 않는다.
```
① fit 도면에서 값을 뽑아 params.json 을 쓴다. 출처까지 기록
② build params.json 만 읽어 IR 을 만든다. 도면 없이도 돈다
```
부수 효과가 크다.
- `git diff params.json` 이 곧 "무엇을 바꿨나"
- 설계안 A/B/C 를 파일 셋으로 두고 나란히 비교
- **새 도면 설정 비용이 첫 도면 한 번으로 끝난다** — 변형은 도면을 안 읽는다
---
## 5. 파일을 셋으로 가른다
지금 `drawings/manhole1.mjs` 한 파일이 세 가지를 섞고 있다.
```
types/전력구_박스.mjs 조립 로직. 파라미터를 받는다 ← 패밀리
params/맨홀1.json 그 도면에서 뽑은 값 ← 타입 파라미터
drawings/manhole1.mjs DWG 읽기 설정 (레이어·앵커·기준점) ← 추출 규칙
```
**"비슷한 도면 가져와 치수만 바꾼다" = 같은 type + 다른 params.json.**
```sh
cp params/맨홀1.json params/신규현장.json
# 구체_외폭 3800 → 4200, 토피 2957 → 3500 수정
node src/build.mjs --params 신규현장 # 도면 안 읽는다
```
`src/core/*` 와 `src/ir.mjs` 에 도면 고유값이 들어가면 안 된다는 기존 경계는
그대로 유지된다. `types/*` 는 도면 고유값이 아니라 **부재 종류**다.
---
## 6. 파라미터 스키마 — 값만으로는 부족하다
```jsonc
{
"구체_외폭": { "v": 3800, "src": "measured", "from": "단면 C-C #1 폭" },
"벽두께": { "v": 400, "src": "design", "min": 300, "max": 600, "step": 50 },
"구체_내폭": { "expr": "구체_외폭 - 2*벽두께" },
"헌치": { "v": 150, "src": "design" },
"샤프트": [
{ "x": 2550, "저부Z": -3157, "정상Z": 0 },
{ "x": 13250, "저부Z": -3157, "정상Z": -39 }
]
}
```
넷이 필요하다.
| 항목 | 왜 |
|---|---|
| `expr` 식 | 파생값. 위상 정렬해 평가. 솔버 불필요 |
| `src` 출처 | `measured` 는 도면과 묶여 있다. 바꾸면 "도면과 다름" 경고 |
| `min`/`max`/`step` | 유효 범위. 검증이자 나중에 UI |
| **배열** | 치수만이 아니라 **개수**가 바뀐다. 샤프트 2→3, 환기구 없음 |
배열이 특히 중요하다. `manhole1-trans.mjs` 의 샤프트를 이미
`for (const s of shafts)` 로 짜놨다 — 그대로 파라미터로 올라간다.
### `src` 3분류가 도면과의 계약이다
```
measured 도면에서 뽑았다. 바꾸면 도면과 어긋난다 → 경고하되 막지는 않는다
design 사람이 정한다. 자유롭게 바꾼다
derived 식으로 나온다. 직접 못 바꾼다
```
기존 `datum.checks` 가 이미 이 자리를 갖고 있다. 확장하면 된다.
---
## 7. 설계 도구가 되는 지점 — 규칙과 물량
치수를 바꾸는 것보다 **바꾼 결과를 즉시 평가하는 것**이 설계다.
지금 파이프라인에 없는 층이 이것이고, 실제 가치는 여기서 나온다.
```
rules.mjs 벽두께 ≥ 최소, 내공 높이 ≥ 2,100, 피복 ≥ 기준,
헌치가 부재보다 크지 않은가, 샤프트 보어가 벽 안에 있는가,
개구부 최소 치수, 케이블랙 이격
quantity.mjs 콘크리트 m³ · 굴착 m³ · 거푸집 m² · 되메움
```
**물량은 이미 공짜로 나온다.** 오늘 OBJ 를 발산 정리로 재서 0.0003% 오차로
검증했다. 그 코드가 그대로 물량 산출이다.
```
안 A 구체 3,800×4,600 콘크리트 100.5 m³ ✓ 전 규칙 통과
안 B 구체 4,200×4,600 콘크리트 108.2 m³ ✓
안 C 구체 4,200×4,600 · 벽 300 — ✗ 최소 벽두께 미달
```
이 표가 나오는 순간 도구가 쓸모를 얻는다.
---
## 8. 위험한 곳
**(a) 파라미터가 형상 위상(topology)을 바꾼다.**
벽두께를 키우다 헌치보다 커지면 꼭짓점 수가 달라져 loft 대응이 깨진다.
`validateIR` 의 꼭짓점 수 검사가 여기서 걸린다(교훈 #16). `min`/`max` 로
미리 막고, 걸리면 명확한 메시지를 낸다.
**(b) 프로파일을 파라미터로 "생성"하면 도면의 실제 형상을 잃는다.**
```
지금 loop('단 면 B-B', 2700, 2750).ring 도면이 준 실제 링
이후 box(p.스터브_외폭, p.스터브_외고, p.헌치) 파라미터로 만든 링
```
B-B 내부 링에 있는 **50mm 보도 단차** 같은 자잘한 것이 생성기로 재현이 안 되면
조용히 빠진다. **fit 단계에서 "이 링이 생성기로 재현되는가"를 대조**해야 한다.
그게 도면과의 마지막 끈이다.
#### 결정 (2026-08-06) — 3등급으로 기록한다
완전일치를 요구하지 않는다. `fit` 이 링마다 등급을 매겨 `params.json` 에 남긴다.
```
exact 꼭짓점 집합이 mm 단위로 일치 → 파라미터로 완전 대체
tolerant bbox · 면적 · 꼭짓점 수 일치 → 경고를 남기고 파라미터 사용
개별 점은 ±ε
opaque 생성기로 재현 불가 → 파라미터화하지 않는다.
도면 링을 **리터럴로 얼려** params 에 박는다
```
`opaque` 가 핵심이다. "재현 못 하면 파라미터화 안 한다"가 아니라 **"재현 못 하는
링은 값으로 얼려 넣는다"** (`src: "measured"` + `ring` 배열). 그러면
- 4절 불변식(build 가 도면을 안 본다)이 안 깨진다
- 50mm 보도 단차 같은 것이 조용히 빠지지 않는다
- 그 링은 **못 바꾸는 것**일 뿐이고, 그게 정직하다
등급 분포가 곧 "이 도면이 얼마나 파라메트릭한가" 의 척도가 된다. `opaque` 가
많으면 생성기가 부족한 것이지 도면이 이상한 게 아니다.
**(c) 재생성하면 모델러의 UID 가 전부 바뀐다.**
이름·색은 우리가 다시 붙이니 괜찮지만 사용자가 모델러에서 직접 손댄 것은 날아간다.
**"모델러에서 편집하지 않는다"를 규칙으로 못 박아야 한다.** 편집은 params 에서만.
**(d) 추출 설정은 여전히 도면당 반나절이다.**
4절의 fit/build 분리가 이 비용을 **첫 도면 한 번**으로 묶어준다. 그래서 구조
변경이 이 위험까지 같이 없앤다.
---
## 8.5. 모델러 밖 UI — 값을 바꾸고 "갱신"
요구사항으로 확정(2026-08-06). 파이프가 이미 CLI 단계 사슬이라 **새 아키텍처가
아니라 얇은 껍데기**다.
### 1단계는 이미 있다 — 읽기 전용 뷰어 (`report.mjs`)
`node src/report.mjs <도면>` → `out/<도면>.html`. 의존성 없는 한 장짜리다.
뷰별 링을 SVG 로 그리고 자기교차·꼭짓점 수·원·IR·부피를 같이 보여준다.
**만든 날 바로 값을 했다** — 맨홀 #2 의 부피가 2배로 나온 원인(`mh.outer`
자기교차)을 숫자로 찾는 데 반나절 걸렸는데, 그림에서는 빨간 X 두 개로 즉시
보인다(교훈 #18).
`build.mjs` 와 **같은 `projectViews()`** 를 쓴다. 뷰어가 자기 변환을 따로 갖고
있으면 보여주는 것과 조립되는 것이 갈라진다 — **거짓말하는 뷰어는 없는 것보다
나쁘다.**
편집은 아직 안 된다. `params.json` 이 없어서 편집할 대상이 없다.
```
UI(로컬 웹앱) → params.json → build(도면 안 봄) → IR → emit/egm → run_egm.py → 모델러
↘ three.js 프리뷰 (9절·나중)
```
- **입력 위젯이 공짜로 나온다.** 6절의 `min`/`max`/`step`/`src` 가 그대로
슬라이더·범위·경고다. 그러라고 스키마를 그렇게 잡았다.
- **갱신 버튼이 검증을 태워야 도구가 된다.** 단순 실행이면 폼이고,
`validateIR` → `rules.mjs`(7절) → `measure.mjs` 기준선까지 통과 못 하면
거부하면 설계 도구다. 여기가 값이 나오는 지점이다.
- 재생성 1~2분. 버튼 누르고 기다리는 UX 로 견딘다(1절).
못 박을 것 둘.
```
갱신 = 씬 초기화 포함. Esc → SelAll → Delete → Esc. `!Cancel` 은 쓰지 않는다
모델러에서 직접 편집 금지. 8절 (c) — 갱신하면 UID 가 전부 바뀐다. 편집은 params 에서만
```
**순서상 위치: 10절 1~4 뒤.** 지금 만들면 편집할 `params.json` 이 없다.
---
## 9. 나중에 (필수 아님)
- **브라우저 프리뷰** — IR 은 이미 도구 중립이다. three.js + CSG(`manifold-3d` WASM 이
빠르다고 알고 있으나 **미확인**)로 모델러 없이 확인. 속도가 요구사항이 아니므로 뒤로.
- **선택 재생성** — 파라미터 → 파트 의존 그래프로 바뀐 파트만 다시. 마찬가지로 뒤로.
- **구속 솔버** — 1~5 를 하고 나서 정말 필요한 자리를 세어본 뒤 결정.
- **모델 → 도면 역방향** — DWG writer 가 필요하다. 현재 범위 밖.
---
## 10. 1차 목표 — **전부 됐다**
```
✔ 1. params.json 스키마 확정 v / expr / src / grade / min·max·step / 배열
✔ 2. build.mjs 를 fit / build 로 분리 --params 면 도면을 아예 안 읽는다
✔ 3. manhole1 을 params 로 전환 egm 명령 목록이 **바이트 단위로 동일**했다
✔ 4. 파라미터 바꿔 재생성 시연 본체_외폭 3800→4200, 종헌치 150→200
✔ 5. 물량 measure.mjs 가 재질별로 낸다
```
3번이 좋은 시험대다. `manhole1-trans.mjs` 에 이미 이름이 뚜렷한 값들이 있다 —
**챔퍼 200×200, 헌치 150×150, 벽두께 200, 보어 Ø900, 칼라 900, 토피 2,957.**
5번은 [`../src/measure.mjs`](../src/measure.mjs) 로 **먼저 끝났다.** 실측·기준선
대조·재질별 물량이 돈다. 남은 것은 굴착·거푸집·되메움 같은 파생 물량이다.
---
## 11. 순서 결정 (2026-08-06)
**본체(A)를 좁혀서 먼저, 그 다음 파라메트릭(B).** 근거 셋.
**① 인스턴스 1개로 추상화를 얼리게 된다.**
5절의 가치 명제는 "같은 type + 다른 params" 인데 도면이 하나뿐이라 type/params
경계를 **검증할 두 번째 인스턴스가 없다.** 맨홀 #2(격실·환기구·침사조)가 스키마에
요구할 것 — 배열이 아니라 *선택적 존재*("환기구 없음"), 중첩 격실, 구간별 단면
배정표 — 은 지금 `manhole1-trans` 3파트에 안 보인다.
재작업이 대칭이 아니다. A 먼저 → `assemble()` 본문이 늘어 변환 비용이 커지지만
그 변환은 10절 3번 그 자체다(어차피 한 번은 한다). B 먼저 → 스키마를 **두 번** 짠다.
**② 8절 (b) 결정이 A 에 의존한다.**
"어디까지 엄격하게 대조하나"는 *불규칙한 링이 몇 개냐*에 달렸다. 지금 아는
사례(B-B 내부 50mm 보도 단차, C-C 외곽 샤프트 융합, 개구 기호로 정점 8,
케이블랙 정점 114)는 전부 `manhole1-trans` 밖에서 나왔다. 등급 기준은 정했지만
`ε` 와 등급 분포는 표본을 세어본 뒤에 확정한다.
**③ CIRCLE 파서가 B 의 전제다.**
`manhole1-trans.mjs` 의 `BORE_R = 450` 은 원본 엔티티에서 눈으로 읽어 손으로 적은
값이다. 8절 (b) 가 말하는 "도면과의 마지막 끈"이 보어에는 **아예 없다.**
```
✔ 0. measure.mjs B 에서 유일하게 당겨온 항목(10절 5번).
앞으로 만드는 모든 파트의 회귀 검사가 된다
1. CIRCLE 파서 → circle() 헬퍼 손으로 적은 R450 을 대조로. Φ100 ×5 도 풀린다
2. manhole1-trans 를 본편으로 통합 기초·잡석 이관. 지금 반쪽 모델이 둘이다
3. 맨홀 #2 + 출구 스터브 파라미터 표본을 대표성 있게
4. → 10절 1~4
5. → 8.5절 UI
```
---
## 부록 — 2026-08-06 기준 현재 상태
**되는 것**
| | |
|---|---|
| `manhole1` | 11파트. 전이부는 X=250 계단 근사, **지붕 개구 없음**(결함) |
| `manhole1-trans` | 3파트. 스터브 + 챔퍼 전이 + 본체 + 종방향 헌치 + 샤프트 2기(Ø900 원형 보어) |
| 새 IR 연산 | `loft` (2단면, `Loft C=True`), `cylinder` (Z축 한정, `Cylinder S=True`) |
| 새 유틸 | `normalizeRing` — loft 꼭짓점 대응용. `addProfile` 이 자동 적용 |
| 검증 | 부피 대조 107.1425 실측 / 107.1422 기대 (0.0003%) |
| `measure.mjs` | OBJ 실측 · 기준선 회귀 · 재질별 물량. 기준선 `baselines/manhole1-trans.volumes.json` |
| CIRCLE | `circles(뷰, 지름, 레이어)`. 보어 R450 이 손으로 적은 수에서 도면 대조로 바뀌었다 |
**안 되는 것 / 남은 것**
```
맨홀 #2 (X 15,400~18,200) 챔버·격실·환기구·침사조. 제일 크다
출구 스터브 (X 18,200~19,200) 맨홀 #2 뒤라 그전에 맨홀을 해야 자리를 얻는다
기초·잡석 manhole1.mjs 에 있다. trans 로 옮기면 됨
Ø100 관통 5개 환기구 4 · 배수관 1. 평면도의 1개는 X=17,690 으로
맨홀 #2 영역이라 그 형상을 세운 뒤에 뚫는다
이음매 T 정점 열린 변 49개. 기하는 닫힘. IFC 갈 때 재검토
IFC 출력 emit/ifc.mjs 미구현
```
---
## 12. 구현 결과 (2026-08-06)
### 만든 것
| 파일 | 하는 일 |
|---|---|
| [`../src/params.mjs`](../src/params.mjs) | 스키마 · 식 평가(위상 정렬) · 검증 · 도면 이탈 감지 |
| [`../src/fit.mjs`](../src/fit.mjs) | 도면 → `params/<이름>.json`. 출처(`from`)와 원값(`fitted`)까지 |
| [`../src/rules.mjs`](../src/rules.mjs) | 규칙 13개. 위상 8(막는다) · 설계 5(경고) |
| [`../src/volume.mjs`](../src/volume.mjs) | IR 을 격자로 평가 — **모델러 없이** "의도" 부피 |
| [`../src/ui.mjs`](../src/ui.mjs) | 편집 + 갱신. `node src/ui.mjs manhole1` → `localhost:7331` |
| [`../src/core/pick.mjs`](../src/core/pick.mjs) | 도면 조회 도우미. **`fit()` 만 쓴다** |
### 확인된 것
```
파라미터 스칼라 49 (measured 38 · design 3 · derived 8) + 배열 1(샤프트)
링 11 (opaque 8 · exact 2 · tolerant 1)
규칙 13개 중 위반 0
기하 불변 전환 전후 egm.json **바이트 동일**
도면 없이 views.json 을 치운 채 --params 로 빌드 → 역시 바이트 동일
```
### 8절 (b) 결정이 실제로 어떻게 쓰였나
`opaque` 8개가 결정의 값을 증명한다. B-B 내부(50mm 보도 단차), C-C 내부(헌치),
E-E 외곽(23꼭짓점 오목 윤곽) 같은 것은 파라미터로 재현이 안 된다. 좌표째 얼려
넣으니 **도면 없이도 형상이 그대로** 나오고, 자잘한 형상이 조용히 빠지지도 않는다.
그 링들은 못 바꿀 뿐이고, 그게 정직하다.
`exact` 2개는 개구부다 — 개구 기호의 네 모서리라 직사각형으로 완전히 재현된다.
### 규칙이 폼과 도구를 가른다
```
✗ 맨홀 내벽이 뒤집혔다: 16900 ≥ 16700 (벽두께 1500)
(맨홀 벽두께가 절반을 넘으면 내부가 사라진다)
```
벽두께를 1,500 으로 바꾸면 **빌드가 거부한다**(exit 1). 모델러까지 가서 비틀린
면으로 만나는 것보다 훨씬 싸다. 설계 규칙(내공 높이·토피·최소 벽두께)은 경고만
하고 사람이 판단한다.
### 도면 이탈은 막지 않고 남긴다
```
⚠ 도면에서 뽑은 값이 바뀌어 있다 — 이 모델은 원 도면과 다르다
본체_외폭 도면 3800 → 지금 4200 C-C 구체 외폭
```
설계를 바꾸는 게 이 층의 목적이니 막지 않는다. 다만 "도면대로 만든 모델"인 척하게
두면 안 된다.
### 그림 위에서 치수를 고친다
`render.mjs --dims` 가 **파라미터를 가리키는 치수선**을 렌더에 얹는다. 2D 도면이
치수를 적어 형상을 증명하듯이, 우리 렌더도 "이 길이가 이 파라미터다" 를 그림에서
바로 보이게 한다.
```
span 두 점 사이 **길이** 연장선 + 치수선 + 이름 상자
level 한 좌표의 **위치** 그 자리를 가로지르는 선 + 이름 상자
```
둘로 나눈 이유가 있다. 파라미터의 절반은 길이가 아니라 **좌표**다(`본체_상단Z`
같은 표고). 그런 것을 억지로 길이로 그리면 거짓말이 된다 — 표고선으로 그린다.
같이 나오는 `model-<축>.dims.json` 이 각 라벨의 자리를 **캔버스 비율**로 담는다.
UI 가 그 자리에 `<input>` 을 얹으므로 **그림에서 값을 고칠 수 있다**. 비율이라
창 크기가 바뀌어도 따라간다. 표와 그림의 입력칸은 서로 동기화된다.
라벨이 많으면 겹치므로 위에서부터 훑으며 겹치는 것을 아래로 민다. 치수선 자체는
안 움직인다 — 라벨만.
**47개**가 그림에 올라간다. 빠진 둘(`커터여유`·`면겹침_여유`)은 형상이 아니라
수치 여유값이라 패널에만 둔다.
### UI 는 정말로 얇았다
파이프가 이미 CLI 단계 사슬이라, UI 가 한 일은 `params.json` 편집 + 그 사슬을
순서대로 돌리는 것뿐이다. 갱신 사슬:
```
build --params → emit → run_egm --reset --export → measure --ir → render --dims
```
`--reset` 이 필수다. 안 비우면 갱신할 때마다 객체가 쌓여 `modelCount` 대조가
무의미해진다. 다만 **연달아 여러 번 하면 안 된다** — 초기화를 10번쯤 반복했더니
모델러 파이프가 죽었다. 갱신 1회당 1번이면 문제없다.
`min`/`max`/`step` 이 그대로 입력 위젯이 됐고, `src` 는 태그, `fitted` 는 변경
표시가 됐다. **그러라고 6절에서 그렇게 설계했던 것이 그대로 맞았다.**
### 남은 것
- **맨홀 #2 부피 9.32 손실** — 우리 IR 은 맞고(74.68) 모델러가 흘린다(65.36).
원인은 면으로만 맞닿은 union (교훈 #19). 제대로 된 해법은 맨홀을 **수평 단면에서
Z 로 쌓는 것** — 조각이 Z 로 진짜 겹치고 평면 헌치도 같이 따라온다
- 외부 헌치 5개 (평면 좌표 확정, Z 구간 미확정)
- `quantity.mjs` — 굴착·거푸집·되메움 같은 파생 물량
+310
View File
@@ -0,0 +1,310 @@
# 로드맵 — 일반 도면 세트 → 3D → IFC
지금 파이프라인은 **평면도 + 단면도** 조합에서 검증됐다(맨홀 일반도 4매, 11파트).
2026-08-07 에 **두 번째 도면군**(교량 설계지침 삽도 7장)을 넣어 1장을 관통시켰다
(중앙분리대 방호벽, 4.8058 m³ — 모델러·해석 일치).
궁극 목표는 **평면 · 입면 · 단면 · 좌측면 · 우측면**을 읽어 IFC 구조로 내보내는 것.
이 문서는 거기까지 무엇이 남았는지, 어떤 순서로 하면 되는지를 적는다.
> **두 번째 도면군이 바꾼 것.** 남은 일 목록이 「연산이 부족하다」에서
> 「**입력 해석 가정이 부족하다**」로 옮겨갔다. 교량에서 실제로 막은 것은
> sweep·revolve 가 아니라 단위·축척·뷰 제목·레이어·폐곡선이었다.
> 자세한 것은 [`methodology.html`](methodology.html) 11장, 교훈은
> [`2d-3d-conversion.md`](2d-3d-conversion.md) #20~#26.
> 그 위에 얹은 **파라메트릭 설계 층**은 [`parametric.md`](parametric.md) 에 따로 있다.
> **구현됐다**(fit/build 분리 · 규칙 · 편집 UI). 본체 목표(2D→3D)를 대체하지 않는다.
---
## 1. 뷰 종류의 일반화
지금 `titles.views` 는 뷰 제목을 네 가지 `kind` 로 분류한다.
```
plan 평면도 DWG(x,y) → 프로젝트(X, Y)
long 종단면 DWG(x,y) → 프로젝트(X, Z)
cross 횡단면 DWG(x,y) → 프로젝트(Y, Z)
horizontal 수평 단면 DWG(x,y) → 프로젝트(Y, X) (swapUV)
```
정면도·배면도·좌측면도·우측면도를 넣으면 이렇게 된다.
| 뷰 | 보는 방향 | 평면 | 비고 |
|---|---|---|---|
| 평면도 | −Z (위에서) | XY | |
| 저면도 | +Z | XY | 좌우 반전 → `flip` |
| 정면도 | +Y | XZ | |
| 배면도 | −Y | XZ | 좌우 반전 → `flip` |
| 좌측면도 | +X | YZ | |
| 우측면도 | −X | YZ | 좌우 반전 → `flip` |
| 종단면 | ±Y | XZ | 정면도와 같은 평면 |
| 횡단면 | ±X | YZ | 측면도와 같은 평면 |
| 수평 단면 | −Z | XY | 평면도와 같은 평면 |
**결국 평면은 셋(XY · XZ · YZ)뿐이고, 나머지는 반전 여부의 문제다.**
`kind` 라는 이름이 오해를 부른다 — 앞으로는 `plane` + `flip` 으로 부르는 게 맞다.
### 할 일
- `titles.views` 항목을 `{ plane, u0, v0, flip, flipV, swapUV }` 한 덩어리로 합친다.
지금은 `titles.views` 와 `anchors` 로 나뉘어 있어 뷰 하나가 두 곳에 흩어져 있다.
- 뷰가 어느 방향에서 본 것인지 **도면에서 검출**할 방법을 찾는다.
단면은 절단 기호(`SYM`)로 알 수 있지만, 입면도는 제목 말고 단서가 없다.
→ 제목 규칙(`정면도`/`좌측면도` …)을 설정에 두는 게 현실적이다.
---
## 2. 입면도가 단면도와 다른 점
단면도는 **잘린 면**만 그리지만 입면도는 **보이는 모든 것**을 그린다.
그래서 입면도의 폐곡선은 구조체 경계가 아니라 **투영 윤곽**이다.
| | 단면도 | 입면도 |
|---|---|---|
| 폐곡선의 의미 | 재료 영역 | 보이는 면의 윤곽 (여러 깊이가 겹침) |
| 압출 가능? | 그대로 가능 | **불가.** 깊이 정보가 없다 |
| 쓸모 | 형상 생성 | 형상 **검증**, 높이·폭 확인, 개구부 위치 |
**입면도로 솔리드를 만들려 하지 말 것.** 입면도는
- 다른 뷰에서 만든 형상의 **외곽 검증**
- 창·문·개구부의 **XZ(또는 YZ) 위치** 확보
- 마감선·레벨선 확인
에 쓰고, 실제 압출은 평면도(footprint) + 단면도(높이)로 한다.
### 할 일
- 입면도용 검증 도구: 만든 3D 를 그 방향으로 투영해 입면도 윤곽과 대조
- 개구부 위치만 뽑아 쓰는 경로 (지금 `rect()` + `expect()` 로 하는 것과 같은 방식)
---
## 3. 곡면 · 원호
지금은 **직선 선분만** 다룬다(`src/core/parse.mjs` 의 `toSegments`).
ARC · CIRCLE · ELLIPSE 는 버린다.
| 대상 | 필요한 것 |
|---|---|
| 모따기·필렛 | 이미 직선으로 그려져 있으면 그대로 됨 (지금 잘 되고 있다) |
| **원형 관통 (Ø900·Ø100)** | **됨** — `collectCircles` → `circles()` → `cylinder` |
| 원호 벽체 | ARC 를 선분으로 근사(tessellate)하거나 IR 에 호 지원 추가 |
| 나선계단 | 전용 연산. `Sweep1` 또는 파라메트릭 생성 |
### CIRCLE — 된다 (2026-08-06)
```sh
node src/build.mjs <도면> --circles # 뷰별 원 목록 (지름 · 레이어 · 좌표)
```
```js
circles('평 면 도', 900, /^-전력구$/) // 지름으로 집는다. 폐곡선과 같은 규칙
circle('평 면 도', 100) // 정확히 하나일 때. 여럿이면 예외
```
**선분과 섞지 않는다.** 원을 다각형으로 근사해 평면 분할에 넣으면 폐곡선이 그
윤곽을 따라 돌아 프로파일이 오염된다. 원은 그 자체가 형상이라 `cylinder` 로 바로 간다.
뷰 귀속은 중심 좌표로 한다 — 뷰 분리(조각 클러스터링)에 원을 넣으면 관통 구멍
하나가 뷰를 잇는 다리가 될 수 있다. 이 도면은 133개 중 129개가 귀속됐다.
**부수 효과: 평면도에 `v0` 앵커가 필요해졌다.** 평면도는 세로축이 표고가 아니라
폭방향(Y)인데, 지금까지 치수(폭×높이)만 봐서 드러나지 않았다. 좌표를 직접 쓰는
순간 필요해진다. 수평 단면(H/I/J)과 같은 문제였다.
남은 것: **Ø100 관통 5개**(`환기구` 4 · `배수관` 1). 평면도에 있는 1개는
X=17,690 으로 맨홀 #2 영역이라 그 형상을 세운 뒤에 뚫는다.
---
## 4. IFC 출력
### 이미 확보된 것
레이어가 재질을 인코딩한다는 사실(노하우 문서 교훈 #6). 그래서 클래스·재질
배정에 필요한 정보가 이미 IR 에 실려 있다.
```js
part({ …, name: '맨홀1_전력구_구체',
material: '구체 콘크리트 fck=27MPa' })
```
### 매핑 초안
| 부재 | IFC 클래스 | 재질 |
|---|---|---|
| 벽체 | `IfcWall` | Concrete C27 |
| 슬래브 | `IfcSlab` | Concrete C27 |
| 기초 | `IfcFooting` | Concrete C18 |
| 잡석·토목섬유 | civil 계열 (**IFC4.3 엔티티명 확인 필요**) | — |
| 집합 요소 | `IfcBuiltElement` 또는 civil 계열 | — |
- 재질 속성 → `IfcMaterial` + `Pset_MaterialConcrete`(CompressiveStrength)
- 층상 구성(잡석 → 기초 → 구체) → `IfcMaterialLayerSet`
- 설계 정보(fck · fy · 설계법 · 하중) → `IfcProject` 속성
> `IfcWall`/`IfcSlab`/`IfcFooting`/`IfcMaterial*`/`Pset_MaterialConcrete` 는 IFC4
> 표준이라 그대로 써도 된다. **잡석·토목섬유용 IFC4.3 civil 엔티티 이름은
> 스키마를 직접 확인하고 확정할 것.** 위 표의 후보는 검증 전이다.
### 할 일
- `part` 에 `ifcClass` 필드 추가 (지금은 `material` 만 있다)
- `src/emit/ifc.mjs` — IR → IFC4 STEP(.ifc)
- 형상 표현: `IfcExtrudedAreaSolid` 가 지금 IR 의 `extrude` 와 1:1 로 맞는다.
`difference`/`union` 은 `IfcBooleanResult` 로 그대로 옮겨진다.
**IR 을 IFC 를 염두에 두고 설계해둔 게 여기서 값을 한다.**
---
## 5. 우선순위 제안
2026-08-06 에 순서를 정했다 — **본체(A)를 좁혀서 먼저, 그 다음 파라메트릭(B).**
근거는 [`parametric.md`](parametric.md) 11절.
```
✔ 0. 부피 측정 자동화 (measure.mjs) 손계산 → 회귀 검사. B 의 quantity 몸통
✔ 1. CIRCLE → circles() 헬퍼 손으로 적은 R450 이 도면 대조로 바뀌었다
2. manhole1-trans 를 본편으로 통합 지금 반쪽 모델이 둘이다. 기초·잡석 이관
3. 맨홀 #2 + 출구 스터브 제일 크다. 파라미터 표본을 대표성 있게 만든다
✔ 4. 파라메트릭 (parametric.md 10·12절) fit/build 분리 · 규칙 · 편집 UI 까지 완료
5. 뷰 설정 통합 (titles.views + anchors + viewBoxes → 한 덩어리) ← 아래 참고
6. emit/ifc.mjs IR 이 이미 준비돼 있다
7. 입면도 검증 경로 새 도면 종류를 받으려면 필요
8. Sweep 백엔드 (선형 곡선부·구배) Loft 는 완료
```
**5 의 값이 올라갔다.** 뷰 하나를 서술하는 자리가 이제 **셋**이다 —
`titles.views`(평면) · `anchors`(앵커·축척) · `viewBoxes`(영역·평면·축척).
`unitMm` 이 `viewBoxes` 와 `anchors` 양쪽에 있고, 뷰 분리 방식에 따라 어느 쪽이
읽히는지가 갈린다. 뷰 종류를 늘리기 전에 정리하는 편이 싸다 —
`{ plane, box?, u0, v0, flip, flipV, swapUV, unitMm, role }` 한 덩어리로.
**교량군에서 새로 생긴 것 (전부 구현됨, 정리 필요):**
```
cfg.unitMm DWG 모델단위. 도면군마다 다르다
cfg.entityFilter 레이어 말고 색·선종으로 거른다
cfg.viewBoxes 제목 없는/중복인 도면의 뷰 영역
cfg.datum.mode='none' 공통 기준선이 없는 도면
viewBoxes[뷰].unitMm 도형별 축척
rules[].needs 규칙 적용 범위 (부재 타입 태그의 임시 대용)
```
**아직 없는 것 (교량 일반도를 받으면 바로 필요해진다):**
```
G1.5 도면 적격 판정 도구 숫자/기호 치수 계수 + 윤곽 닫힘률. 지금은 손으로 센다
축척 역산 도구 표기 치수 2개 ↔ 기하 길이 대조. 지금은 손으로 잰다
부재 타입 태그 rules 를 파라미터 이름이 아니라 부재 종류에 붙인다
받침 기반 로컬 프레임 상부(선형) ↔ 하부(연직) 좌표계 전환 (methodology 11.1)
```
1 을 3 보다 앞에 두는 이유는 로드맵상 흔해서만이 아니다. **파라메트릭의
전제이기도 하다** — 지금 샤프트 보어 R450 은 원본 엔티티에서 눈으로 읽어 손으로
적은 값이라 도면과의 끈이 아예 없다. CIRCLE 지원이 들어와야 `loop()`/`expect()`
로 대조되고, 그제서야 보어 반지름이 정당한 파라미터가 된다.
1·2·3 은 서로 독립이라 순서를 바꿔도 된다.
**Loft 백엔드는 끝났다** — 전이부(챔퍼·수직 단차·종방향 헌치)를 세워 부피
오차 0.0002% 로 검증했다. 배경은 [`2d-3d-conversion.md`](2d-3d-conversion.md)
교훈 #16. 남은 것은 **rail 을 따라가는 스윕**이다 (`Sweep1 C=True V=True` 가
IFC4.3 `IfcSectionedSolidHorizontal` 과 의미가 같다). 그러려면 IR 이 직선축
`from/to` 대신 **측점(station)** 을 다뤄야 한다.
4 는 실제 입면도가 있는 도면을 하나 받아야 설계가 확정된다.
---
## 6. 미완 목록 (맨홀 도면 기준)
| 항목 | 상태 |
|---|---|
| 스터브↔본체 전이부 | **완료.** `assembleDuct()` 를 본편·trans 가 공유한다 |
| 관로↔맨홀 관통구 | **완료.** 개구 기호에서 좌표가 나온다. 합치기 **전에** 뚫는다 |
| 구조물만 (기초·잡석 제외) | **완료.** 5파트 |
| **맨홀 #2 부피 9.32 손실** | IR 은 맞고(74.68) 모델러가 흘린다(65.36). 원인은 면으로만 맞닿은 union — 교훈 #19 |
| 외부 헌치 5개 | **완료.** 아래 참조 |
| 내부 헌치 (100×100·150×150) | 미구현. 맨홀을 **수평 단면에서 Z 로 쌓아야** 따라온다 |
| H/I/J 내부 링 끊김 | H-H 내부가 Y 1,800~3,700 만 닫힌다. 위 항목의 전제 |
| Ø100 관통 5개 | 위치 확인됨 (전선인입 ×2 · 우수관연결 ×2 · 유도배수관 ×1). 미구현 |
| 침사구 (0.8×0.8) | I-I 개구 기호로 **잡힌다**. `opening('단 면 I-I', 800, 800)` |
| 나선계단 · 방호난간 | 현 IR 연산으로 표현 불가 |
| 기초 · 잡석 · 보도 | 뺐다. 되살리는 데 필요한 값은 `manhole1.mjs` 주석에 있다 |
### 외부 헌치 — 완료 (2026-08-06)
평면도 대각선 21개 중 **외부 표면 모서리**인 5개를 세 종류로 나눠 넣었다.
Z 구간은 도면에서 뽑았지 지어내지 않았다.
| 종류 | 도면 근거 | 구현 | 부피 변화 |
|---|---|---|---|
| 출구 스터브 챔퍼 | A-A `(18200,-4607)→(18500,-4907)` 300×300 · `(18200,-8007)→(18550,-7657)` **350×350** | loft 2단 | +0.577 |
| 전력구↔맨홀 접합 | 평면 `(15100,1900)→(15400,2200)` 300×300 | XY 삼각형 → Z 압출 → union | +0.224 |
| 챔버↔환기구 다리 | 평면 `(15400,4100)→(15500,4200)` 100×100 ×2 | XY 사각 쐐기 → Z 압출 → 커터 | −0.029 |
**3D 검증 완료** — `modelCount 5`, 의도 vs 결과가 셋 다 맞는다.
```
파트 실측 IR 의도 차이
맨홀1_전력구_구체 100.7473 100.7592 −0.0119 ✓ 접합 헌치 들어감
맨홀1_전력구_출구스터브 3.5474 3.5490 −0.0016 ✓ 출구 챔퍼 2단 들어감
맨홀1_사다리샤프트_1·2 3.3217/3.2805 ✓ 건드리지 않은 것 확인
맨홀1_맨홀2_구체 66.0875 74.6527 −8.5652 ✗ 기존 손실 (수평단면 재구성 대기)
```
**출구는 입구와 대칭이 아니다.** 위·옆은 300×300(X 18,200~18,500)인데 아래는
350×350(18,200~**18,550**)이라 꺾이는 X 가 다르다. loft 하나로 이으면 아래 챔퍼가
300 짜리로 짧아진다 — 입구에서 이미 겪은 교훈 #16 이 그대로 반복됐다.
검산: 격자 +0.5765 vs 심프슨 해석 +0.5749 — **0.3% 일치**.
**평면 방향 헌치는 XZ 단면에 안 나타난다.** 다리 100×100 의 Z 범위를 F-F·G-G 에서
찾으려 했지만 없다(거기 있는 150×150 은 다리 *내부* 헌치다). 그래서 `fit()` 이
E-E 외곽 링에서 `Y>4,200` 구간의 Z 범위를 **코드로** 뽑는다 → −5,257 ~ 176.
다리 테이퍼의 부피 변화가 작은(−0.029) 데는 이유가 있다. E-E 링을 보면 하부 블록이
**Y=4,100 에서 끝나고**(`[4100,-10007]→[4100,-5557]`) 그 위로 다리가 선다. 그래서
테이퍼가 걸리는 Y 4,100~4,200 구간에는 재료가 100mm 높이밖에 없다.
### 맨홀 #2 를 수평 단면 기반으로 다시 세우면 셋이 한꺼번에 풀린다
```
부피 9.32 손실 조각이 Z 로 진짜 겹쳐 union 이 한 덩어리가 된다
내부 헌치 H/I/J 의 XY 프로파일에 모따기가 이미 들어 있다
층별 정확도 E-E 치수 사슬이 각 층의 Z 범위를 그대로 준다
```
전제는 H/I/J 의 내부 링 끊김 수리다. **조사는 끝났다.**
`gaps.mjs` 는 H-H·I-I 에 끊김 **0** 이라고 한다 — 선이 없는 게 아니라 개구부
윤곽이 안 닫혀 있어 면이 새는 것이다(교훈 #12 와 같은 종류). 선분을 직접 뽑아
실태를 확정했다.
```
H-H 내부는 격벽으로 3칸이다 (전부 X 15,800~17,800, 모서리 150×150 모따기)
A Y -1,500 ~ 1,500 3,000 ← 환기 OPEN부가 들어 있어 안 닫힘
B Y 1,800 ~ 3,700 1,900 ← 유일하게 폐곡선으로 잡힌다 (#2)
C Y 3,700 ~ 8,000 4,300 ← 점검구·방호난간이 들어 있어 안 닫힘
격벽 Y 1,500~1,800 (300)
```
A·C 가 안 닫히는 이유는 개구부 좌측 변이 `000-구조물실선` 에 **Y -1,000~1,000
로만** 그려져 있기 때문이다(개구는 Y -500~1,500). 대응은 교훈 #12 대로
`rect()`/`opening()` 으로 재구성 — 위 좌표가 그 재료다.
---
## 7. 검증 기준선
`baselines/<도면>.volumes.json` 은 **커밋한다.** `out/` 과 달리 재생성으로
복구되지 않기 때문이다 — 사람이 한 번 확인한 부피가 남아 있어야 다음 변경이
형상을 건드렸는지 알 수 있다.
| 도면 | 파트 | 부피 m³ | 확인 |
|---|---|---|---|
| `manhole1-trans` | 3 | 107.1425 | 원시 파트 합으로 교차검증 |
| `manhole1` | 11 | — | 기준선 없음. 통합 후 뜬다 |
+535
View File
@@ -0,0 +1,535 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>교각 P9 — 촬영 런시트 &amp; 대본</title>
<style>
html{color-scheme:light dark}
/* ── 팔레트는 모델 자체에서 가져왔다 ────────────────────────────
코핑 주황 · 기둥 파랑 · 기초 초록 · 말뚝 보라. 그 색이 곧 이 영상의 주인공이다. */
:root{
--ground:#F5F2EE; --surface:#FFFFFF; --slab:#181C22; --slab-ink:#D9E2ED;
--line:#DED7CE; --line-soft:#EBE5DC;
--ink:#191E25; --ink-2:#4A5462; --ink-3:#7C8794;
--cap:#C2732F; --col:#3E7CBC; --ftg:#4E9750; --pil:#8B4A9A; --brg:#C4413F;
--ok:#3E8F5C; --warn:#B07E14;
--accent:var(--cap);
--mono:ui-monospace,"Cascadia Mono","Consolas","D2Coding",monospace;
--sans:"Pretendard","Malgun Gothic","Apple SD Gothic Neo",system-ui,sans-serif;
}
@media (prefers-color-scheme:dark){
:root:not([data-theme="light"]){
--ground:#0E1116; --surface:#151A21; --slab:#0A0D11; --slab-ink:#CBD6E3;
--line:#252C36; --line-soft:#1C222B;
--ink:#DDE5EE; --ink-2:#94A2B3; --ink-3:#65717F;
--cap:#D68444; --col:#5894D0; --ftg:#74B06C; --pil:#A968B8; --brg:#DC5A58;
--ok:#5FBF7F; --warn:#E0B33C;
}
}
:root[data-theme="dark"]{
--ground:#0E1116; --surface:#151A21; --slab:#0A0D11; --slab-ink:#CBD6E3;
--line:#252C36; --line-soft:#1C222B;
--ink:#DDE5EE; --ink-2:#94A2B3; --ink-3:#65717F;
--cap:#D68444; --col:#5894D0; --ftg:#74B06C; --pil:#A968B8; --brg:#DC5A58;
--ok:#5FBF7F; --warn:#E0B33C;
}
*{box-sizing:border-box}
body{margin:0; background:var(--ground); color:var(--ink);
font:15.5px/1.7 var(--sans); -webkit-font-smoothing:antialiased}
.wrap{max-width:1080px; margin:0 auto; padding:36px 22px 90px}
/* ── 머리 ────────────────────────────────────────────────── */
header{display:flex; flex-wrap:wrap; align-items:flex-end; gap:18px 28px;
padding-bottom:20px; border-bottom:2px solid var(--ink)}
h1{margin:0; font-size:29px; line-height:1.2; letter-spacing:-.02em; text-wrap:balance}
h1 small{display:block; font:600 12px/1.6 var(--mono); letter-spacing:.16em;
color:var(--accent); text-transform:uppercase; margin-bottom:7px}
.facts{margin-left:auto; display:flex; gap:22px; font:600 12px/1.5 var(--mono);
font-variant-numeric:tabular-nums; color:var(--ink-3)}
.facts b{display:block; font-size:19px; color:var(--ink); letter-spacing:-.01em}
/* ── 탭 ──────────────────────────────────────────────────── */
.tabs{display:flex; flex-wrap:wrap; gap:2px; margin:22px 0 0;
border-bottom:1px solid var(--line)}
.tabs button{appearance:none; border:0; background:none; cursor:pointer;
font:600 13.5px/1 var(--sans); color:var(--ink-3);
padding:12px 15px; border-bottom:2px solid transparent; margin-bottom:-1px;
transition:color .12s}
.tabs button:hover{color:var(--ink-2)}
.tabs button[aria-selected="true"]{color:var(--ink); border-bottom-color:var(--accent)}
.tabs button:focus-visible{outline:2px solid var(--accent); outline-offset:-2px; border-radius:3px}
.panel{display:none; padding-top:26px}
.panel.on{display:block}
/* ── 타임라인 띠 ─────────────────────────────────────────── */
.tl{display:flex; height:34px; border:1px solid var(--line); border-radius:3px;
overflow:hidden; margin-bottom:6px}
.tl div{position:relative; display:grid; place-items:center; min-width:0;
font:600 10.5px/1 var(--mono); color:#fff; letter-spacing:.02em;
border-right:1px solid rgba(0,0,0,.28)}
.tl div:last-child{border-right:0}
.tl span{opacity:.92; white-space:nowrap; overflow:hidden}
.tl-key{display:flex; justify-content:space-between; font:600 11px/1 var(--mono);
color:var(--ink-3); font-variant-numeric:tabular-nums; margin-bottom:30px}
/* ── 컷 ──────────────────────────────────────────────────── */
.cut{display:grid; grid-template-columns:118px 1fr; gap:0 22px;
padding:22px 0; border-top:1px solid var(--line-soft)}
.cut:first-of-type{border-top:0}
.rail{font:600 12px/1.55 var(--mono); font-variant-numeric:tabular-nums;
color:var(--ink-3); padding-top:3px}
.rail .tc{display:block; font-size:15px; color:var(--ink); letter-spacing:-.02em}
.rail .dur{display:block}
.rail .clip{display:inline-block; margin-top:8px; padding:2px 7px; border-radius:3px;
background:var(--line-soft); color:var(--ink-2); font-size:11px; letter-spacing:.04em}
.cut h3{margin:0 0 3px; font-size:17.5px; letter-spacing:-.01em}
.cut .sub{margin:0 0 13px; color:var(--ink-3); font-size:13px}
.hookmark{color:var(--accent)}
pre{margin:0 0 13px; background:var(--slab); color:var(--slab-ink);
border-radius:4px; padding:12px 14px; overflow-x:auto;
font:12.5px/1.75 var(--mono)}
pre .c{color:#6E7D8E}
pre .h{color:#E9B872}
.say{margin:0; padding:14px 16px 14px 18px; background:var(--surface);
border:1px solid var(--line); border-left:3px solid var(--accent);
border-radius:0 4px 4px 0; font-size:15px}
.say b{color:var(--accent)}
.say .beat{display:block; height:9px}
.screen{margin:12px 0 0; padding:0; list-style:none;
font:12.5px/1.85 var(--mono); color:var(--ink-2)}
.screen li{padding-left:17px; position:relative}
.screen li:before{content:"›"; position:absolute; left:3px; color:var(--accent); font-weight:700}
.screen b{color:var(--ink); font-weight:600}
/* ── 표 ──────────────────────────────────────────────────── */
table{width:100%; border-collapse:collapse; margin:0 0 24px; font-size:13.5px}
th,td{text-align:left; padding:9px 12px; border-bottom:1px solid var(--line-soft);
vertical-align:top}
th{font:600 11px/1.5 var(--mono); letter-spacing:.09em; text-transform:uppercase;
color:var(--ink-3); border-bottom:1px solid var(--line)}
td code{font:12.5px/1.6 var(--mono); color:var(--accent)}
.num{font-variant-numeric:tabular-nums; text-align:right; white-space:nowrap}
.tw{overflow-x:auto}
h2{margin:34px 0 12px; font-size:20px; letter-spacing:-.01em}
h2:first-child{margin-top:0}
p{margin:0 0 14px; max-width:68ch}
.lede{color:var(--ink-2); font-size:14.5px}
.chip{display:inline-block; padding:1px 8px; border-radius:3px; font:600 11px/1.7 var(--mono);
letter-spacing:.04em}
.c-cap{background:color-mix(in srgb,var(--cap) 18%,transparent); color:var(--cap)}
.c-ok{background:color-mix(in srgb,var(--ok) 18%,transparent); color:var(--ok)}
.c-warn{background:color-mix(in srgb,var(--warn) 20%,transparent); color:var(--warn)}
.note{border:1px solid var(--line); border-radius:4px; padding:14px 16px;
background:var(--surface); margin:0 0 20px}
.note b{display:block; margin-bottom:4px}
@media (max-width:660px){
.cut{grid-template-columns:1fr; gap:10px}
.rail{display:flex; gap:12px; align-items:baseline; padding:0}
.facts{margin-left:0}
}
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1><small>촬영 런시트 · 컷별 대본</small>2D 도면 한 장에서<br>교각 하나를 세운다</h1>
<div class="facts">
<span><b>11</b>컷</span>
<span><b>5:54</b>총길이</span>
<span><b>75</b>부재</span>
<span><b>0.0000</b>오차</span>
</div>
</header>
<div class="tabs" role="tablist">
<button role="tab" aria-selected="true" data-p="p1">컷 순서 · 대본</button>
<button role="tab" aria-selected="false" data-p="p2">실행 명령</button>
<button role="tab" aria-selected="false" data-p="p3">화면 체크</button>
<button role="tab" aria-selected="false" data-p="p4">사고 대응</button>
<button role="tab" aria-selected="false" data-p="p5">업로드</button>
</div>
<!-- ══════════ 1. 컷 순서 · 대본 ══════════ -->
<section class="panel on" id="p1" role="tabpanel">
<div class="tl">
<div style="flex:14; background:var(--cap)"><span>HOOK</span></div>
<div style="flex:24.4; background:var(--ink-3)"><span>01 도면</span></div>
<div style="flex:11.1; background:var(--col)"><span>02</span></div>
<div style="flex:12.9; background:var(--col)"><span>03</span></div>
<div style="flex:48.5; background:var(--col)"><span>04 진단</span></div>
<div style="flex:24.8; background:var(--ftg)"><span>05</span></div>
<div style="flex:16.7; background:var(--ftg)"><span>06</span></div>
<div style="flex:82; background:var(--cap)"><span>07 모델링</span></div>
<div style="flex:25.3; background:var(--pil)"><span>08</span></div>
<div style="flex:44.3; background:var(--pil)"><span>09 IFC</span></div>
<div style="flex:50.1; background:var(--brg)"><span>10 마무리</span></div>
</div>
<div class="tl-key"><span>0:00</span><span>도면 → 진단 → 파라미터 → 모델링 → 검증 → IFC</span><span>5:54</span></div>
<div class="cut">
<div class="rail"><span class="tc">0:00</span><span class="dur">14.0초</span>
<span class="clip">07 발췌</span></div>
<div>
<h3 class="hookmark">HOOK — 결과부터 보여준다</h3>
<p class="sub">07번에서 교각이 서는 12~15초만 잘라 맨 앞에. 자막·설명 없이 화면만.</p>
<p class="say">이거, 사람이 만든 모델이 아닙니다.<span class="beat"></span>
2D 도면 한 장을 읽어서, 명령 한 줄로 자동으로 서는 겁니다.<span class="beat"></span>
교각 하나. 부재 <b>일흔다섯 개</b>. 오차 <b>0.0000</b>.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">0:14</span><span class="dur">24.4초</span>
<span class="clip">01.mp4</span></div>
<div>
<h3>도면 — 출발점</h3>
<p class="sub">CAD 에서 교각 일반도. 뷰를 훑고 표까지.</p>
<p class="say">시작은 이 도면입니다. 교각 일반도 한 장.<span class="beat"></span>
A1 한 장에 평면도, 정면도, 단면도, 기초 평면도, 그리고 표 세 개.
토목 하시는 분들껜 익숙한 화면이죠.<span class="beat"></span>
그런데 이 파일 하나에 <b>도면이 놓이는 자리가 열일곱 개</b> 있습니다.
옛 작업 흔적이 그대로 쌓여 있는 거죠.
<b>출력용 도곽이 씌워진 건 그중 하나뿐</b>입니다.<span class="beat"></span>
어느 게 진짜인지 고르는 것부터가 일입니다.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">0:38</span><span class="dur">11.1초</span>
<span class="clip">02.mp4</span></div>
<div>
<h3>뷰를 가른다</h3>
<pre><span class="c">1단계</span> node src/run.mjs pier-p9</pre>
<p class="say">첫 명령입니다. 도면을 읽고 뷰를 가릅니다.<span class="beat"></span>
평면도 <b>14,000 × 3,200</b>. 도면에 적힌 치수와 정확히 같습니다.
여기가 안 맞으면 뒤는 볼 것도 없습니다.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">0:50</span><span class="dur">12.9초</span>
<span class="clip">03.mp4</span></div>
<div>
<h3>끊김 진단</h3>
<pre><span class="c">2단계</span> node src/gaps.mjs pier-p9</pre>
<p class="say">다음은 끊김 진단입니다.<span class="beat"></span>
윤곽이 안 닫히는 이유가 <b>레이어를 잘못 골랐다</b>인지
<b>도면에 선이 없다</b>인지를 가릅니다.<span class="beat"></span>
앞은 설정 한 줄, 뒤는 도면 수정. 대응 비용이 백 배 차이 납니다.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">1:02</span><span class="dur">48.5초</span>
<span class="clip">04.mp4</span></div>
<div>
<h3>도면이 말하는 네 가지 방식</h3>
<pre><span class="c">3단계</span> node src/build.mjs pier-p9 --loops
node src/build.mjs pier-p9 --circles
node src/build.mjs pier-p9 --blocks
node src/table.mjs pier-p9 --title "교량받침 ELEVATION TABLE"</pre>
<p class="say">도면은 형상으로만 말하지 않습니다.<span class="beat"></span>
닫힌 윤곽, 원, 블록, 그리고 표.<span class="beat"></span>
기둥은 지름 <b>2,200 원</b>으로 들어 있고,
말뚝 <b>마흔네 본</b>은 폐곡선이 아니라 <b>블록</b>입니다.
블록 배율 <b>254</b>가 곧 반지름이고요. 도면 주기의 Φ508 과 맞습니다.<span class="beat"></span>
받침 여섯 기의 적층 높이와 절대표고는 도면 어디에도 안 그려져 있습니다.
<b>표에만</b> 있습니다. 그래서 표도 읽습니다 — 이름표 아래를 찾아서, 병합 셀까지.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">1:51</span><span class="dur">24.8초</span>
<span class="clip">05.mp4</span></div>
<div>
<h3>도면이 파라미터가 된다</h3>
<pre><span class="c">4단계</span> node src/fit.mjs pier-p9
<span class="c">5단계</span> node src/build.mjs pier-p9</pre>
<p class="say">여기서 도면이 <b>파라미터</b>가 됩니다.<span class="beat"></span>
서른 개 중 스물한 개는 도면에서 잰 값,
다섯 개는 도면이 안 주는 설계값,
네 개는 식으로 나오는 값입니다.<span class="beat"></span>
어느 게 어디서 왔는지 전부 기록됩니다.
<b>도면에서 읽은 척하지 않는 것</b> — 이게 핵심입니다.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">2:16</span><span class="dur">16.7초</span>
<span class="clip">06.mp4</span></div>
<div>
<h3>3D 툴 없이 부피부터</h3>
<pre><span class="c">6단계</span> node src/volume.mjs pier-p9</pre>
<p class="say">3D 툴을 켜기 전에 부피부터 냅니다.<span class="beat"></span>
<b>657.0569</b> 세제곱미터.<span class="beat"></span>
이게 <b>우리 의도</b>입니다. 이따 모델러가 만든 것과 대조할 기준이죠.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">2:32</span><span class="dur">82.0초</span>
<span class="clip">07.mp4</span></div>
<div>
<h3 class="hookmark">모델러에 교각이 선다</h3>
<pre><span class="c">7단계</span> node src/emit/egm.mjs out/pier-p9.ir.json
python src/run_egm.py out/pier-p9.egm.json --export=out/pier-p9.obj</pre>
<p class="say">이제 실행입니다.<span class="beat"></span>
78개 파트, <b>329단계</b> 명령이 모델러로 갑니다.<span class="beat"></span>
코핑, 원형 기둥 두 본, 확대기초 두 기, 버림,
강관말뚝 마흔네 본,
받침 여섯 기 — 받침콘크리트, 무수축 모르타르, 탄성받침, 솔플레이트까지.<span class="beat"></span>
사람 손은 안 들어갑니다.<span class="beat"></span>
<em>(말 없이 20초 — 세워지는 걸 보여준다)</em><span class="beat"></span>
중요한 건 속도가 아닙니다.<span class="beat"></span>
같은 명령을 다시 돌리면 <b>같은 모델</b>이 나온다는 것.
도면이 바뀌면 파라미터만 고쳐서 다시 세울 수 있다는 것입니다.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">3:54</span><span class="dur">25.3초</span>
<span class="clip">08.mp4</span></div>
<div>
<h3>의도 vs 결과 — 이 영상의 핵심</h3>
<pre><span class="c">8단계</span> egm status
node src/measure.mjs pier-p9 --ir</pre>
<p class="say">그리고 검증입니다.<span class="beat"></span>
<b>우리가 뜻한 부피</b>와 <b>모델러가 실제로 만든 부피</b>를 부재마다 대조합니다.<span class="beat"></span>
일흔다섯 개 전부 차이 <b>0.0000</b>. 열린 변 0.<span class="beat"></span>
부피가 맞으면 형상이 맞습니다. 눈으로 보고 「비슷하네」로 끝내지 않습니다.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">4:20</span><span class="dur">44.3초</span>
<span class="clip">09.mp4</span></div>
<div>
<h3>IFC 로 넘긴다</h3>
<pre><span class="c">9단계</span> node src/emit/ifc.mjs out/pier-p9.ir.json</pre>
<p class="say">마지막은 IFC 입니다.<span class="beat"></span>
IfcBeam, IfcColumn, IfcFooting, <b>IfcPile 마흔네 개</b>, IfcPlate, IfcDiscreteAccessory.<span class="beat"></span>
메시로 안 내보냈습니다. 원기둥은 <b>진짜 원</b>으로 나갑니다.
그래야 받는 쪽에서 「왜 지름이 508이 아니냐」를 다시 겪지 않습니다.<span class="beat"></span>
절대 위치는 <b>IfcMapConversion</b> 으로 넣었습니다.
측량좌표와 방위각은 받침배치도 표에서 가져왔고요.<span class="beat"></span>
뷰어에서 열면 이렇게 나옵니다.</p>
</div>
</div>
<div class="cut">
<div class="rail"><span class="tc">5:04</span><span class="dur">50.1초</span>
<span class="clip">10.mp4</span></div>
<div>
<h3>그림으로도 본다 · 마무리</h3>
<pre><span class="c">10단계</span> node src/render.mjs pier-p9 --png --width 900</pre>
<p class="say">부피가 맞아도 형상이 맞는 건 아닙니다. 그래서 그림으로도 봅니다.<span class="beat"></span>
정투영 세 방향. <b>검은 자리는 그 방향으로 재료가 없다</b>는 뜻입니다.
관통구나 빠진 부재가 바로 드러나죠.<span class="beat"></span>
정리하면 — 도면 한 장에서 부재 일흔다섯 개, 657 세제곱미터,
오차 0.0000, IFC 까지.<span class="beat"></span>
사람이 판단한 건 <b>「무엇을 어떻게 조립할까」 하나뿐</b>입니다.
나머지는 전부 도면이 말한 겁니다.<span class="beat"></span>
다음 편에서는 <b>꺾인 교각</b>을 해보겠습니다.</p>
</div>
</div>
</section>
<!-- ══════════ 2. 실행 명령 ══════════ -->
<section class="panel" id="p2" role="tabpanel">
<p class="lede">작업 디렉터리 <code>d:\MYCLAUDE_PROJECT\dwg2model</code> · PowerShell.
씬은 비워 뒀고 기준선은 문양 제외본으로 갱신돼 있다. 0번부터 순서대로.</p>
<h2>0 · 촬영 전 확인 <span class="chip c-warn">필수</span></h2>
<pre>egm guide | python -c "import sys,json;print('명령',len(json.loads(sys.stdin.buffer.read().decode('utf-8-sig'))))"</pre>
<p><b>208</b> 이어야 한다. <b>39</b> 면 뷰어 모드 — 모델링 명령이 등록조차 안 된다.
모델러 UI 에서 <code>Assistive Features → Working Mode Setting → Viewer &amp; Modeler Use</code>.</p>
<h2>1–10 · 본편</h2>
<pre><span class="c"># 1 도면 읽기 — 뷰 분리 · 표 색인 [02.mp4]</span>
node src/run.mjs pier-p9
<span class="c"># 2 끊김 진단 [03.mp4]</span>
node src/gaps.mjs pier-p9
<span class="c"># 3 도면이 말하는 네 방식 [04.mp4]</span>
node src/build.mjs pier-p9 --loops
node src/build.mjs pier-p9 --circles
node src/build.mjs pier-p9 --blocks
node src/table.mjs pier-p9 --title "교량받침 ELEVATION TABLE"
<span class="c"># 4+5 파라미터 → 조립 스펙 [05.mp4]</span>
node src/fit.mjs pier-p9
node src/build.mjs pier-p9
<span class="c"># 6 모델러 없이 부피 — 「의도」 [06.mp4]</span>
node src/volume.mjs pier-p9
<span class="c"># 7 번역 · 실행 — 교각이 선다 [07.mp4] ★</span>
node src/emit/egm.mjs out/pier-p9.ir.json
python src/run_egm.py out/pier-p9.egm.json --export=out/pier-p9.obj
<span class="c"># 8 의도 vs 결과 [08.mp4] ★</span>
egm status
node src/measure.mjs pier-p9 --ir
<span class="c"># 9 IFC [09.mp4]</span>
node src/emit/ifc.mjs out/pier-p9.ir.json
<span class="c"># 10 그림 [10.mp4]</span>
node src/render.mjs pier-p9 --png --width 900</pre>
<h2>덤 · 파라메트릭 (다음 편 예고용)</h2>
<pre>node src/ui.mjs pier-p9 --port 7333
<span class="c"># 브라우저 http://localhost:7333 → 값 수정 → 갱신 → 검증 사슬이 돈다</span></pre>
<div class="note">
<b>미관 문양은 뺐다</b>
단면(50 띠 + 하단 100 돌출)은 도면에서 나왔지만 정면도의 내부 능선을 아직 못 살렸다.
<code>params/pier-p9.json</code> 의 <code>미관문양_포함</code> 을 <code>1</code> 로 바꾸면 다시 선다.
지금 기준선은 <b>문양 제외 · 75파트 · 657.0569 m³</b> 기준이다.
</div>
</section>
<!-- ══════════ 3. 화면 체크 ══════════ -->
<section class="panel" id="p3" role="tabpanel">
<p class="lede">컷마다 <b>화면에 반드시 보여야 할 것</b>. 안 나오면 다시 찍는다.</p>
<div class="tw">
<table>
<tr><th>컷</th><th>화면에 나와야 하는 것</th><th class="num">길이</th></tr>
<tr><td><b>HOOK</b></td><td>모델러 뷰포트에 교각이 서는 순간. 색이 부재별로 갈려 있어야 한다 —
코핑 주황 · 기둥 파랑 · 기초 초록 · 말뚝 보라</td><td class="num">14.0</td></tr>
<tr><td><b>01</b></td><td>도곽 · 평면도 · 정면도 · 단면도 · 기초평면도 · 표. 한 화면에 다 보이는 장면 하나</td><td class="num">24.4</td></tr>
<tr><td><b>02</b></td><td><code>평면도 14000 3200 ✓</code> 와 <code>✓ 검증 통과</code>, 그리고 <code>표 3개</code></td><td class="num">11.1</td></tr>
<tr><td><b>03</b></td><td>「레이어 오분류 0 / 선 누락 n」 갈라지는 줄</td><td class="num">12.9</td></tr>
<tr><td><b>04</b></td><td><code>Ø2200</code> 원 2개 · <code>CSPILEH 배율 254</code> 44개 ·
표 6행 18열(NO./h1~h4/A/B/S1/S2)</td><td class="num">48.5</td></tr>
<tr><td><b>05</b></td><td><code>measured 21 · design 5 · derived 4</code> · <code>최종 산출 75</code></td><td class="num">24.8</td></tr>
<tr><td><b>06</b></td><td><code>합계 657.0569</code></td><td class="num">16.7</td></tr>
<tr><td><b>07</b></td><td><code>파트 78 → 단계 329</code> · 뷰포트에 부재가 쌓이는 과정 ·
끝에 <code>modelCount 75</code></td><td class="num">82.0</td></tr>
<tr><td><b>08</b></td><td><b>차이 0.0000 열이 쭉 이어지는 화면</b> · <code>열린변 0</code> · 재질별 물량</td><td class="num">25.3</td></tr>
<tr><td><b>09</b></td><td><code>IFCPILE 44</code> 등 클래스 목록 · IFC 뷰어에서 열린 모델</td><td class="num">44.3</td></tr>
<tr><td><b>10</b></td><td>정투영 X·Y·Z 세 장. 검은 배경과 부재 색이 대비되는 것</td><td class="num">50.1</td></tr>
</table>
</div>
<h2>영상에서 쓸 숫자</h2>
<div class="tw">
<table>
<tr><th>말할 것</th><th>값</th></tr>
<tr><td>도곽 한 장 안에</td><td>뷰 7 + 표 3.<br>
그런데 파일에는 도면 자리가 <b>17개</b> — 도곽이 씌워진 건 <b>1개</b>,
나머지는 구조선만 남은 옛 작업 흔적</td></tr>
<tr><td>단위 확정</td><td>독립 근거 4개로 <b>1단위 = 1 mm</b></td></tr>
<tr><td>파단선</td><td>그려진 기둥 6,100 ↔ 실제 <b>33,800</b>. 생략량 27,700 을 도면에서 되찾는다</td></tr>
<tr><td>말뚝</td><td>5×5=25 가 아니라 <b>22본</b> — 둘레 16 + 내부 3등분점 6</td></tr>
<tr><td>받침</td><td>형상 × 표 × 다른 도면 <b>세 겹 검산</b>, 6/6 일치</td></tr>
<tr><td>결과</td><td><b>75 파트 · 657.0569 m³ · 차이 0.0000 · 열린 변 0</b></td></tr>
</table>
</div>
</section>
<!-- ══════════ 4. 사고 대응 ══════════ -->
<section class="panel" id="p4" role="tabpanel">
<p class="lede">녹화 중 걸릴 만한 것만. 전부 실제로 겪었다.</p>
<div class="tw">
<table>
<tr><th>증상</th><th>원인</th><th>대응</th></tr>
<tr><td><code>run_egm</code> 이 「명령이 끝나지 않는다」</td>
<td>이전 명령이 물려 있다. <code>egm status</code> 의 <code>command</code> 가 남는다</td>
<td><code>egm "Esc"</code> — 접두 <code>~</code> <b>없이</b>. 그리고 재실행</td></tr>
<tr><td>씬에 이전 모델이 남음</td><td>—</td>
<td><code>egm "Esc"</code> → <code>"SelAll"</code> → <code>"Delete"</code> → <code>"Esc"</code></td></tr>
<tr><td><code>modelCount</code> 가 75 가 아님</td>
<td>boolean 이 인자를 흘렸다</td>
<td>씬 비우고 재실행. 반복되면 촬영 멈추고 확인</td></tr>
<tr><td>명령이 조용히 무시됨</td><td>뷰어 모드</td>
<td>0단계로 돌아간다. <b>연결 정상 ≠ 모델링 가능</b></td></tr>
<tr><td><code>Access to the path is denied</code></td>
<td>모델러가 다른 권한으로 떠 있어 파이프에 못 붙는다</td>
<td>모델러를 종료하고
<code>C:\Program Files\Baron\EG-BIM Modeler\EG-BIM_Modeler.exe</code> 로 다시 띄운다</td></tr>
<tr><td>파이프가 죽음 (창은 멀쩡)</td><td>씬 초기화를 여러 번 반복했다</td>
<td>재시작 외에 방법 없다. 초기화는 갱신당 1회</td></tr>
</table>
</div>
<div class="note">
<b><code>--reset</code> 은 붙이지 않는다</b>
지난번 그것 때문에 <code>Select</code> 가 물렸다. 씬은 촬영 전에 미리 비워 둔다.
</div>
</section>
<!-- ══════════ 5. 업로드 ══════════ -->
<section class="panel" id="p5" role="tabpanel">
<h2>제목 후보</h2>
<div class="tw">
<table>
<tr><th>안</th><th>제목</th></tr>
<tr><td>A</td><td>2D 도면 한 장으로 교각 3D 모델 자동 생성 — 부재 75개, 오차 0.0000</td></tr>
<tr><td>B</td><td>토목 도면을 BIM 으로 바꾸는 법 : 손으로 안 그리고 IFC 까지</td></tr>
<tr><td>C</td><td>도면이 말하는 네 가지 방식 — 윤곽 · 원 · 블록 · 표 | 2D→3D 전환설계</td></tr>
</table>
</div>
<h2>설명란</h2>
<pre>교각 일반도(DWG) 한 장을 읽어 3D 솔리드와 IFC 까지 만드는 과정입니다.
부재 75개 · 콘크리트 657.06 m³ · 모델러 실측 대비 오차 0.0000 · 열린 변 0
00:00 결과 먼저
00:14 도면 — 한 파일에 도형 17벌
00:38 뷰 가르기
00:50 끊김 진단 — 레이어 오분류 vs 선 누락
01:02 도면이 말하는 네 방식 : 윤곽 · 원 · 블록 · 표
01:51 도면이 파라미터가 된다
02:16 3D 툴 없이 부피부터
02:32 모델러에 교각이 선다
03:54 의도 vs 결과 — 부재마다 대조
04:20 IFC 로 넘긴다
05:04 그림으로도 본다 · 정리
다룬 것
· 도곽 블록으로 시트 경계를 계산한다 (한 파일에 여러 벌이 쌓여 있다)
· 파단선 — 그려진 길이가 실제가 아니다. 생략량을 도면에서 되찾는 법
· 말뚝은 폐곡선이 아니라 INSERT 블록. 배율이 곧 치수다
· 받침 적층·절대표고는 형상에 없고 표에만 있다
· IFC 는 메시가 아니라 해석 솔리드로. 원기둥은 진짜 원으로</pre>
<h2>태그</h2>
<pre>토목엔지니어, BIM, 3D전환설계, 토목BIM, IFC, 교량, 교각, 도면자동화,
DWG, 캐드, 파라메트릭모델링, 스마트건설, civil engineering, BIM automation</pre>
<h2>썸네일</h2>
<p>왼쪽 <b>흑백 2D 도면</b>(01.mp4 한 프레임), 오른쪽 <b>색 입은 3D 교각</b>(07.mp4 마지막 프레임).
가운데 화살표. 카피는 숫자 하나만 — <b>「오차 0.0000」</b> 또는 <b>「부재 75개, 자동」</b>.</p>
<div class="note">
<b>다음 편 훅</b>
마지막 컷에서 <b>꺾인 교각(P11)</b> 을 예고했다. 꺾임각 6.30° —
코핑 축선이 중간에 꺾여 P9 방식이 그대로는 안 통한다. 그게 다음 편 주제다.
</div>
</section>
</div>
<script>
const tabs = document.querySelectorAll('.tabs button');
tabs.forEach(b => b.addEventListener('click', () => {
tabs.forEach(x => x.setAttribute('aria-selected', String(x === b)));
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('on', p.id === b.dataset.p));
window.scrollTo({ top: 0, behavior: 'instant' });
}));
</script>
</body>
</html>