- CLAUDE.md: 프로젝트 전체 가이드 및 워크플로우 정의 - CONSIDERATIONS.md: 8개 영역 고려사항 분석 (기술, 품질, 배포, UX) - PROGRESS.md: 진행 상황 추적 및 체크리스트 - PLAN.md: 단계 5~9 상세 계획 및 에이전트 역할 정의 - 메모리: 프로젝트 정보 저장 다음: 단계 5 (에이전트/커맨드/스킬/훅 아이디어 제안)
1631 lines
81 KiB
Markdown
1631 lines
81 KiB
Markdown
# 2D → 3D 전환 노하우
|
||
|
||
DWG 도면(평면도 + 다수 단면도)을 읽어 3D 솔리드 모델로 세우는 작업의 축적 문서.
|
||
작업하면서 얻은 교훈을 계속 덧붙인다. 최신 항목이 아래로 쌓인다.
|
||
|
||
- 대상 도면: `drawings-src/04manhole1-simplify.dwg` (맨홀 #1 및 출입구 일반도, 4매)
|
||
- 파이프라인 코드: [src/](../src/)
|
||
- 3D 생성 수단: EGModeler + `egm` CLI (`D:\GiteaAll\EG-BIM_Modeler`)
|
||
|
||
> **절차**는 [`2d-3d-process.md`](2d-3d-process.md) 에 따로 있다.
|
||
> 이 문서는 **왜 그렇게 하는가**, 그 문서는 **무엇을 어떤 순서로 하는가**.
|
||
|
||
---
|
||
|
||
## 1. 교훈 #1 — 기준점을 먼저 정하면 나머지가 쉬워진다
|
||
|
||
**가장 크게 시간을 아낀 판단.** 도면마다 기준점을 미리 확정해두면, 이후 모든 좌표가
|
||
단순 뺄셈으로 나온다. 반대로 기준점 없이 형상부터 뽑으면 뷰마다 좌표계가 달라
|
||
정합 단계에서 전부 다시 해야 한다.
|
||
|
||
### 프로젝트 좌표계 (이 도면 기준)
|
||
|
||
| 축 | 방향 | 원점 |
|
||
|---|---|---|
|
||
| X | 전력구 길이방향 | 구조물 최좌단(분기구 #1 쪽 구체 외면) |
|
||
| Y | 폭방향 | 전력구 중심선 |
|
||
| Z | 표고 | 도로 계획고 (아래로 음수) |
|
||
|
||
단위는 **내부 계산 mm 정수**, egm에 넘기기 직전에만 m로 나눈다.
|
||
mm 정수로 반올림하면 DWG의 부동소수 노이즈(`-500.00000000001455`)가 자동으로 흡수된다.
|
||
|
||
### 기준점은 도면에서 "검출"해야지 눈으로 읽으면 안 된다
|
||
|
||
Z 기준을 `도로 계획고` **텍스트 위치**로 잡았다가 713mm 틀렸다.
|
||
텍스트는 라벨이고, 실제 지반선은 그 아래에 따로 있다.
|
||
|
||
```
|
||
도로 계획고 TEXT DWG y = 483624.19 ← 라벨. 쓰면 안 됨
|
||
도로 계획고 LINE DWG y = 483623.477 ← 정답 (레이어 `0지반`)
|
||
```
|
||
|
||
검증법: 기준점을 잡은 뒤 **도면의 치수 숫자와 대조**한다. 맞으면 확정.
|
||
|
||
```
|
||
구체 상단 Z = -4,907 ↔ B-B 도면 치수 4,907 ✓
|
||
기초 하단 Z = -7,757 ↔ A-A 도면 치수 7,757 ✓
|
||
```
|
||
|
||
### 기준선 후보가 둘이면 "라벨 바로 아래"로 가른다
|
||
|
||
계획고는 보통 **도로/토공 두 줄이 나란히** 그려진다. 둘 다 모든 뷰에 나타나므로
|
||
"가장 많은 뷰에 반복되는 수평선"만으로는 갈리지 않는다. 실제로 토공 계획고를
|
||
집어 420mm 어긋났다.
|
||
|
||
라벨은 자기 선을 가리킨다. **라벨 바로 아래에 있는 선**이 그 라벨의 선이다.
|
||
출현 횟수는 잡음을 거르는 문턱으로만 쓰고, 최종 선택은 라벨 근접도로 한다.
|
||
|
||
```
|
||
도로 계획고 라벨 y = 483624.19
|
||
→ 도로 계획고 선 483623.477 (0.713 아래) ← 선택
|
||
→ 토공 계획고 선 483623.057 (1.133 아래)
|
||
```
|
||
|
||
### 공통 datum이 있으면 무조건 그것부터 찾아라
|
||
|
||
이 도면은 **모든 단면이 같은 y에 계획고 선을 그렸다.** 그래서 단면별 Z 오프셋
|
||
계산이 통째로 사라졌다. 7개 단면에서 독립적으로 검출한 지반선이 전부 `Z=0`,
|
||
`Z=-420`으로 떨어져 기준점이 교차 검증됐다.
|
||
|
||
토목 도면에서는 계획고/GL/기준선(EL.)이 이 역할을 한다. 건축이면 FL/층레벨.
|
||
**단면도를 배치할 때 표고선을 맞춰 그리는 관행**이 그대로 3D 정합 데이터가 된다.
|
||
|
||
> 다음에 이 작업을 또 한다면: 도면 작성자에게 "각 뷰에 기준점 마커를 하나씩
|
||
> 찍어달라"고 요청하는 게 가장 싸다. 검출 로직 전체가 필요 없어진다.
|
||
|
||
---
|
||
|
||
## 2. 교훈 #2 — 뷰 분리는 제목 bbox가 아니라 기하 간격으로
|
||
|
||
여러 도면이 한 모델스페이스에 나열된 경우, 뷰를 먼저 갈라야 한다.
|
||
|
||
**실패한 방법**: 제목 텍스트 위치로 Voronoi 분할 → A-A와 B-B 영역이 겹쳐
|
||
B-B 추출 결과에 A-A 기하가 섞여 들어왔다.
|
||
|
||
**간격 임계값 하나로는 절대 안 된다.** 넓게 잡으면 이웃이 딸려오고, 좁게 잡으면
|
||
한 뷰가 쪼개진다. 이 도면에서 둘 다 실제로 터졌다.
|
||
|
||
| 증상 | 원인 |
|
||
|---|---|
|
||
| A-A 하단(기초·잡석)이 잘려나감 | 구체와 기초 사이가 2.1m 비어 있어 임계값 1.2m에 잘림 |
|
||
| I-I 폭이 6,000 → 12,954 로 부풀음 | 4,239m 떨어진 HOOK 상세도 조각이 붙어옴 |
|
||
|
||
### 되는 방법: 잘게 쪼갠 뒤 제목으로 다시 묶는다
|
||
|
||
```
|
||
1. 구조 레이어 기하만 모은다
|
||
2. x ↔ y 를 번갈아 가며 더 이상 갈라지지 않을 때까지 잘게 쪼갠다 (임계값 0.2m)
|
||
3. 조각마다 "바로 위에 있는 제목"을 찾아 귀속시킨다
|
||
4. 같은 제목을 가진 조각끼리 합치면 그게 뷰다
|
||
```
|
||
|
||
x→y 한 번씩만 하면 부족하다. **y로 나눈 뒤에야 드러나는 x 간격이 있다** —
|
||
I-I 옆 상세도 조각이 정확히 그 경우였다. 안정될 때까지 교대로 돌려야 한다.
|
||
|
||
제목 귀속이 핵심 장치다. 상세도·재료표도 제목 후보에 넣어두면 자기 조각을
|
||
가져가므로, 뷰 쪽으로 새어 들어오지 않는다. 뷰 제목이 아닌 것에 귀속된 조각은
|
||
그대로 버리면 된다.
|
||
|
||
### 제목 후보를 고를 때 — 글자 높이로 거른다
|
||
|
||
```
|
||
h=0.5 시트명 (맨홀 #1 및 출입구 일반도 (1)~(4))
|
||
h=0.35 뷰 제목 · 상세도 · 재료표 ← 여기까지가 표제
|
||
h=0.30 부재 방향 라벨 (분기구 #1, 맨홀 #2) ← 표제 아님. 걸러야 함
|
||
```
|
||
|
||
`h=0.30`짜리 방향 라벨을 안 걸렀더니 A-A 하단부를 가져가 뷰가 잘려나갔다.
|
||
또 "제목이 조각 상단보다 살짝 아래여도 허용"하는 여유값을 크게(0.6m) 잡았더니
|
||
옆에 붙은 `상부 SLAB용` 표제가 I-I를 통째로 훔쳐갔다. 0.2m로 줄여 해결.
|
||
|
||
### 분할 결과 검증은 폭·높이를 도면 치수와 대조
|
||
|
||
```
|
||
H-H 10,200 × 2,800 도면 표기 10,200 / 2,800 ✓
|
||
I-I 6,000 × 2,800 도면 표기 6,000 / 2,800 ✓
|
||
J-J 6,000 × 2,800 도면 표기 6,000 / 2,800 ✓
|
||
A-A 20,200 × 10,839 ↔ E-E 10,839 ← 심도 일치, 교차 검증
|
||
```
|
||
|
||
폭이 도면값보다 크면 잡석/기초처럼 바깥으로 삐져나온 요소이거나(정상),
|
||
이웃 뷰가 딸려온 것(버그)이다. 차이값이 딱 떨어지는지 확인하면 구분된다.
|
||
서로 다른 뷰의 심도가 같게 나오면 기준점이 맞다는 강한 방증이다.
|
||
|
||
---
|
||
|
||
## 3. 교훈 #3 — 닫힌 폴리라인을 DWG에서 만들 필요가 없다
|
||
|
||
처음엔 "AutoCAD에서 `BOUNDARY`로 닫힌 폴리라인을 만들어 달라"고 요청했다.
|
||
**불필요했다.** 3D 툴(egm)이 좌표 리스트로 폴리라인을 직접 만들 수 있으면,
|
||
DWG에서는 **꼭짓점 좌표만** 뽑으면 된다.
|
||
|
||
```
|
||
DWG 파싱 → 꼭짓점 순서 계산 → egm "Polyline x,y,z ..." → Extrude → Solid
|
||
↑ 여기서 닫힌 커브가 새로 생김
|
||
```
|
||
|
||
즉 문제가 "닫힌 영역 찾기(어려움)"에서 "꼭짓점 순서 정렬(쉬움)"로 바뀐다.
|
||
DWG 편집 권한도, DWG writer도 필요 없다.
|
||
|
||
### 게다가 형상이 규칙적이면 기하 대신 치수를 쓰는 게 낫다
|
||
|
||
이 도면의 구조체는 거의 전부 **직사각형 + 모따기(헌치)**다.
|
||
LINE 위상을 추적하는 것보다, 치수 텍스트에서 파라미터를 뽑아 프로파일을
|
||
파라메트릭으로 생성하는 쪽이 짧고 정확하다.
|
||
|
||
단, **기하에서 뽑은 값과 치수 표기값을 서로 대조**하는 게 핵심이다.
|
||
둘이 일치하면 그 단면은 신뢰할 수 있다. 실제로 B-B에서 순수 기하만으로 뽑은
|
||
`2,700 / 2,100 / 2,750 / 2,150 / 150x150 / THK100 / THK300`이 도면 표기와
|
||
전부 일치했다.
|
||
|
||
---
|
||
|
||
## 3.5. 교훈 #4 — DWG를 뷰별로 미리 쪼개지 마라
|
||
|
||
"뷰마다 별도 DWG로 분리해두면 편하지 않겠나"는 자연스러운 발상이지만, **손해다.**
|
||
|
||
| | 한 도면 유지 | 뷰별 분리 |
|
||
|---|---|---|
|
||
| 표고 정합 | 공짜 — 전 단면이 같은 y에 계획고선을 공유 | 파일마다 원점이 따로 놀아 관계가 사라짐 |
|
||
| 절단기호 ↔ 단면 매핑 | `SYM` 좌표로 자동 연결 | 파일 간 참조가 끊겨 수동 매핑 |
|
||
| 뷰 분리 비용 | 간격 클러스터링 ~30줄 | (불필요하나 위 손실이 더 크다) |
|
||
|
||
이 작업에서 가장 큰 이득이 **"모든 단면이 같은 표고선에 맞춰 배치돼 있다"**였는데,
|
||
이건 한 모델스페이스에 함께 있어야만 성립한다. 분리하면 3D 정합의 절반을 버린다.
|
||
|
||
분리가 이득인 경우는 제한적이다 — 파일이 너무 커서 파싱이 안 되거나(이 도면은
|
||
380KB / 7,035 엔티티로 여유), 축척·좌표계가 다른 프로젝트가 섞여 있을 때.
|
||
|
||
### 대신 도면에 해주면 좋은 것
|
||
|
||
```
|
||
레이어 "3D-ORIGIN" 을 만들고, 각 뷰에 POINT 를 하나씩 찍어 기준점을 표시
|
||
```
|
||
|
||
기준점 검출 로직이 통째로 사라지고, 텍스트/선 혼동 같은 사고도 원천 차단된다.
|
||
분리보다 훨씬 싸고 효과가 크다. 여력이 되면 구조체 외곽선 레이어도 하나로 통일.
|
||
|
||
---
|
||
|
||
## 3.6. 교훈 #5 — 캡처 이미지에 좌표 변환을 실어 보내라
|
||
|
||
기하 파싱만으로는 "이 폐곡선이 구체인가 치수보조선인가" 같은 판단이 어렵다.
|
||
반대로 이미지를 보면 사람은 즉시 안다. **둘을 합치는 장치**가 필요하다.
|
||
|
||
렌더 캡처 PNG를 만들 때 **픽셀 ↔ DWG 좌표 변환을 사이드카로 같이 저장**하면,
|
||
이미지에서 짚은 지점을 곧바로 도면 좌표(그리고 프로젝트 mm)로 환산할 수 있다.
|
||
|
||
```jsonc
|
||
// 04manhole1_단면B-B.json (같은 이름의 PNG 옆에)
|
||
{
|
||
"view": "단 면 B-B",
|
||
"image": { "w": 1188, "h": 1400 },
|
||
// 이미지 좌상단 픽셀에 대응하는 DWG 좌표와 픽셀당 DWG 단위
|
||
"originDwg": [-48352.400, 483619.100],
|
||
"unitsPerPx": 0.0043,
|
||
"yFlipped": true, // 화면 y는 아래로 증가, DWG y는 위로 증가
|
||
"roadLevelY": 483623.477 // 프로젝트 Z=0 기준
|
||
}
|
||
```
|
||
|
||
변환:
|
||
|
||
```
|
||
dwgX = originDwg[0] + px * unitsPerPx
|
||
dwgY = originDwg[1] - py * unitsPerPx // yFlipped
|
||
프로젝트 Z(mm) = round((dwgY - roadLevelY) * 1000)
|
||
```
|
||
|
||
### 변환을 "역산"하지 말고 "지정"해라 — 뷰 박스로 정확히 줌해서 캡처
|
||
|
||
렌더 결과를 놓고 축척을 되짚으면 여백·DPI·레터박싱 어딘가에서 반드시 어긋난다.
|
||
**줌 rect 를 먼저 지정하고 캔버스 크기를 축척에서 역산**하면 어긋날 구석이 없다.
|
||
|
||
```
|
||
1. 뷰 분리 결과(views.json)에 각 뷰의 DWG bbox 가 이미 있다
|
||
2. 축척을 고른다. 예: 5 mm/px
|
||
3. 캔버스 크기 = bbox 크기 / 축척
|
||
단면 B-B 4,300 × 3,150mm → 860 × 630 px
|
||
4. 그 rect 에 정확히 맞춰 줌 → 캡처
|
||
```
|
||
|
||
이러면 `unitsPerPx` 가 계산값이 아니라 **내가 정한 값 그대로**다. 뷰 박스와
|
||
캔버스의 종횡비가 자동으로 같아지므로 레터박싱 여백 보정도 필요 없다.
|
||
|
||
부수 효과로 뷰별 캡처가 자동화된다 — 분리 결과를 읽어 루프만 돌리면 된다.
|
||
|
||
이게 있으면 작업 방식이 바뀐다:
|
||
|
||
- 추출 결과를 이미지 위에 겹쳐 그려 **눈으로 검증**할 수 있다
|
||
- 애매한 형상을 이미지에서 짚으면 정확한 좌표가 나온다
|
||
- "여기 폐곡선이 안 잡힌다" 같은 피드백을 좌표로 주고받을 수 있다
|
||
|
||
이 저장소의 Viewer2D는 이미 DWG→화면 변환을 알고 있으므로, 캡처 시 그 행렬을
|
||
JSON으로 함께 떨구기만 하면 된다. 렌더러 밖에서 역산하려 들면 축척·여백·DPI에서
|
||
반드시 어긋난다 — **변환은 렌더러가 알고 있을 때 받아 적는 게 유일하게 안전하다.**
|
||
|
||
---
|
||
|
||
## 4. 도면 읽기 — 의미 추론에서 실제로 통한 단서들
|
||
|
||
| 단서 | 쓰임 |
|
||
|---|---|
|
||
| 제목 텍스트 높이 | `h=0.5` 시트명, `h=0.35` 뷰 제목, `h≤0.15` 치수/주기 |
|
||
| 레이어 `SYM` | 단면 절단 기호의 **글자 라벨만** 들어있다 (A~J 각 2개 = 절단선 양끝) |
|
||
| 절단 기호 좌표쌍 | 두 글자의 x가 같으면 **수직 절단**, y가 같으면 **수평 절단** |
|
||
| 재료 주기 텍스트 | `구체 콘크리트 fck=27MPa`, `기초 콘크리트 THK=100mm` → 두께·재질 파라미터 직독 |
|
||
| 치수 `actualMeasurement` | 실측값(mm). 도면 축척과 무관하게 정확 |
|
||
|
||
### 수평 단면을 놓치지 말 것
|
||
|
||
`H-H` / `I-I` / `J-J`는 수직 단면이 아니라 **E-E 단면 위에 표고별로 찍힌 수평 절단**이다.
|
||
평면 형상을 층별로 주는 데이터라서, 내부 격실 배치를 세울 때 가장 중요하다.
|
||
절단 기호가 *다른 단면도 위에* 찍혀 있으면 십중팔구 수평 단면이다.
|
||
|
||
---
|
||
|
||
## 4.5. 교훈 #6 — 재질은 **레이어 이름이 이미 인코딩**하고 있다
|
||
|
||
재질 주기 텍스트를 지시선 따라가며 형상에 잇는 건 어렵다. 그런데 할 필요가 없다.
|
||
|
||
```
|
||
레이어 -전력구 / 전력구 / 집수정 / A-신규선형-구조물-맨홀 … → 구체 콘크리트
|
||
레이어 !-기초 → 기초 콘크리트
|
||
레이어 잡석 → 잡석
|
||
레이어 -보도 → 보도 콘크리트
|
||
```
|
||
|
||
레이어가 재질을, 텍스트 주기가 **그 재질의 사양**(fck, THK)을 준다. 둘을 곱하면
|
||
재질 배정이 끝난다. 지시선 추적이 통째로 사라진다.
|
||
|
||
### 사양은 전역 상수로 박으면 안 된다 — 위치마다 다르다
|
||
|
||
`src/materials.mjs` 로 뷰별 사양을 뽑아보니 실제로 갈렸다.
|
||
|
||
| 재질 | 사양 | 적용 뷰 |
|
||
|---|---|---|
|
||
| 구체 콘크리트 | fck=27MPa | 전 단면 공통 |
|
||
| 기초 콘크리트 | fck=18MPa, THK=100mm | 전 단면 공통 |
|
||
| **잡석** | **THK=500mm** | A-A, C-C, E-E, F-F, G-G |
|
||
| **잡석** | **THK=300mm** | **B-B, D-D** |
|
||
| **보도 콘크리트** | **fck=27MPa** | A-A |
|
||
| **보도 콘크리트** | **fck=21MPa** | E-E, F-F, G-G |
|
||
|
||
잡석 두께와 보도 콘크리트 강도가 위치별로 다르다. "잡석 = THK 500" 으로 박으면
|
||
전력구 구간이 200mm 두꺼워진다. **반드시 뷰별로 읽을 것.**
|
||
|
||
### IFC 매핑 초안
|
||
|
||
레이어 → 재질이 잡히면 IFC 클래스·재질 배정까지 그대로 이어진다.
|
||
|
||
| 부재 | IFC 클래스 | 재질 | 비고 |
|
||
|---|---|---|---|
|
||
| 구체 벽체 | `IfcWall` | Concrete C27 | |
|
||
| 구체 상·하부 슬래브 | `IfcSlab` | Concrete C27 | |
|
||
| 기초 콘크리트 | `IfcFooting` | Concrete C18 | 버림 콘크리트 |
|
||
| 보도 슬래브 | `IfcSlab` | Concrete C21/C27 | 뷰별 상이 |
|
||
| 잡석 부설 | civil 계열 (확인 필요) | Crushed stone | `IfcCourse`/`IfcEarthworksFill` 후보 |
|
||
| 토목섬유 | civil 계열 (확인 필요) | Geotextile | |
|
||
| 전력구 전체 | `IfcBuiltElement` 또는 civil 계열 | — | 집합 요소 |
|
||
|
||
재질 속성은 `IfcMaterial` + `Pset_MaterialConcrete`(CompressiveStrength) 로,
|
||
층상 구성(잡석 → 기초 → 구체)은 `IfcMaterialLayerSet` 으로 표현한다.
|
||
|
||
도면 표제란에서 같이 읽히는 설계 정보 — `IfcProject` 속성으로 붙이면 된다.
|
||
|
||
```
|
||
콘크리트 설계기준 강도 fck = 27 MPa
|
||
철근 항복강도 fy = 400 MPa
|
||
설계법 신뢰성 기반 설계법
|
||
설계하중 한국형 표준트럭하중(KL-510)
|
||
```
|
||
|
||
> **미확인**: 잡석·토목섬유에 쓸 IFC4.3 civil 엔티티 이름은 스키마를 직접 확인하고
|
||
> 확정할 것. 위 표의 후보는 검증 전이다. 건축계 엔티티(`IfcWall`/`IfcSlab`/
|
||
> `IfcFooting`)와 재질 계열(`IfcMaterial*`, `Pset_MaterialConcrete`)은 IFC4 표준이라
|
||
> 그대로 써도 된다.
|
||
|
||
---
|
||
|
||
## 4.55. 교훈 #7 — 종단면은 "형상"이 아니라 **"구간 배정표"**로 읽어라
|
||
|
||
가장 크게 틀렸던 지점. 횡단면 하나(B-B)를 보고 "이게 이 구조물의 단면"이라
|
||
단정해 15.4m 전체에 밀었다. **실제로는 절단 위치마다 단면이 다르다.**
|
||
|
||
```
|
||
X -1,000 ~ 0 입구 스터브 B-B 구체 2,700×2,750
|
||
X 0 ~ 15,400 전력구 본체 C-C 구체 3,800×4,600 ← 본체는 이쪽
|
||
X 18,200 ~ 19,200 출구 스터브 B-B
|
||
```
|
||
|
||
**어디서 어디까지 어느 단면인지는 종단면에만 있다.** 횡단면은 "그 지점의 모습"만
|
||
알려줄 뿐, 그게 어디까지 유효한지는 말해주지 않는다.
|
||
|
||
```
|
||
잘못된 순서 횡단면 → 압출
|
||
올바른 순서 종단면에서 구간 분할 → 구간별 횡단면 배정 → 압출
|
||
```
|
||
|
||
### 종단면 읽는 법 — 수평선을 Z 내림차순으로 X 범위와 함께 늘어놓는다
|
||
|
||
각 수평선의 X 범위가 곧 "그 표고가 유효한 구간"이다. 구간 경계가 바로 단면이
|
||
바뀌는 자리다.
|
||
|
||
```
|
||
Z= -3,157 X 0 ~ 1,700 · 3,400 ~ 12,400 … 본체 구체 상단
|
||
Z= -3,557 X 550 ~ 15,250 본체 내부 천장 (슬래브 400)
|
||
Z= -4,907 X -1,000 ~ -299 스터브 구체 상단 ← 단면 전환
|
||
Z= -7,357 X -1,000 ~ 19,200 (전 구간) 내부 바닥 — 두 구간이 공유
|
||
Z= -7,757 X 0 ~ 15,100 본체 구체 하단
|
||
```
|
||
|
||
여기서 **바닥(-7,357)은 전 구간 공통이고 천장만 달라진다**는 것도 바로 보인다.
|
||
횡단면만 봐서는 절대 알 수 없는 정보다.
|
||
|
||
### 횡단면이 인접 형상과 붙어 한 덩어리로 잡히는 경우
|
||
|
||
C-C 의 구체 외곽 폐곡선은 3,800×**7,557** 로 나온다 — 위로 솟은 샤프트가
|
||
같은 윤곽에 붙어 있기 때문이다. 이럴 때는 직사각으로 따로 만들되,
|
||
**치수는 반드시 다른 뷰에서 읽은 실측치**를 쓰고 교차검증한다.
|
||
|
||
```js
|
||
expect('단 면 C-C', 3800, 7557, 'C-C 외곽은 샤프트 포함 형상이어야 한다');
|
||
profile('main.outer', 'YZ', rect(-1900, -7757, 1900, -3157)); // A-A 에서 읽은 표고
|
||
```
|
||
|
||
손으로 지어낸 숫자를 넣기 시작하면 파이프라인의 의미가 사라진다.
|
||
|
||
---
|
||
|
||
## 4.56. 교훈 #8 — 절단 기호의 좌표쌍이 단면의 **평면**을 결정한다
|
||
|
||
`SYM` 레이어의 글자쌍은 절단선 양끝이다. 두 점의 어느 좌표가 같은지가 그 단면이
|
||
어느 평면을 그리는지를 그대로 알려준다.
|
||
|
||
```
|
||
두 점의 x 가 같다 → 세로 절단선 → YZ 단면 (폭 방향을 본다) B, C, D, E
|
||
두 점의 y 가 같다 → 가로 절단선 → XZ 단면 (길이 방향을 본다) A, F, G
|
||
```
|
||
|
||
**폭 치수만으로는 구분이 안 되는 경우가 있다.** 이 환기구는 X 도 2,600, Y 도
|
||
2,600 이라 숫자가 같다. 반드시 절단 기호 좌표로 판정할 것.
|
||
|
||
### 압출축이 어느 방향 벽을 만드는지 항상 의식하라
|
||
|
||
```
|
||
YZ 단면을 X 축으로 밀면 → Y 방향 벽만 생긴다. X 방향은 열린다.
|
||
XZ 단면을 Y 축으로 밀면 → X 방향 벽만 생긴다. Y 방향은 열린다.
|
||
```
|
||
|
||
맨홀을 E-E(YZ)만으로 X 축 압출했더니 X 방향 벽이 없어 안이 들여다보였다.
|
||
|
||
### 나머지 방향 벽은 "공동의 압출 범위를 좁혀서" 만든다
|
||
|
||
두 방향 단면을 교집합할 필요가 없다. **외곽은 전 구간을 채우고 공동만 안쪽으로
|
||
줄이면** 앞뒤로 벽이 남는다.
|
||
|
||
```js
|
||
part({ id:'mh.solid', op:'extrude', from:15400, to:18200 }); // 외곽 전폭
|
||
part({ id:'mh.cut.upper',op:'extrude', from:15800, to:17800 }); // 내벽 사이만
|
||
// ↑ 좌우 400 벽이 남는다
|
||
```
|
||
|
||
공동의 끝이 솔리드 **내부**라 면이 겹치지 않는다 — 여유(OVER)도 필요 없다.
|
||
|
||
---
|
||
|
||
## 4.57. 교훈 #9 — 오목한 외곽은 "봉투"부터 만들어라
|
||
|
||
평면 분할은 오목한 윤곽의 **패인 부분을 별개 면으로** 뽑는다. 그 면이
|
||
되메움부인지 실제 공동인지는 크기로 알 수 없다.
|
||
|
||
이 맨홀에서 E-E 외곽(#0)은 상부 통로 자리를 오목하게 파고 지나간다.
|
||
**그 영역에는 애초에 솔리드가 없다.** 공동을 아무리 좁게 뚫어도 만들 벽이 없다.
|
||
|
||
```
|
||
mh.shell = 외곽(#0) 압출 ← 오목한 채로
|
||
mh.fill = 통로면(#1) 압출 ← 패인 부분을 채움
|
||
mh.solid = union(shell, fill) ← 비로소 전체 봉투
|
||
mh.body = solid − 공동들 ← 이제 벽이 남는다
|
||
```
|
||
|
||
**순서가 핵심이다.** 봉투를 먼저 만들고 그 다음에 뚫는다. 오목한 외곽을 그대로
|
||
쓰면 "뚫을 것이 없는" 상태에서 뚫는 셈이라 아무 일도 일어나지 않는다.
|
||
|
||
---
|
||
|
||
## 4.58. 교훈 #10 — 구간이 맞닿으면 union 후 **한 번만** difference
|
||
|
||
단면이 다른 구간이 이어질 때, 구간별로 따로 빼면 **경계에서 반드시 구멍이 난다.**
|
||
큰 쪽 공동이 여유(OVER)만큼 작은 쪽으로 넘어가 단차 벽면을 파먹기 때문이다.
|
||
|
||
```
|
||
✗ main.body = main.solid − main.void (void 가 -200 까지 넘어감)
|
||
stubL.body = stubL.solid − stubL.void
|
||
→ 스터브 끝벽이 사라져 안이 들여다보인다
|
||
|
||
✓ duct.solid = union(main.solid, stubL.solid)
|
||
duct.void = union(main.void, stubL.void) ← 전이점에서 딱 맞물리게
|
||
duct.body = duct.solid − duct.void
|
||
```
|
||
|
||
공동끼리는 전이점에서 **겹치지도 벌어지지도 않게** 나눈다.
|
||
`main.void: 250→15,600`, `stubL.void: -1,200→250`.
|
||
|
||
**단, 여기서 union 하는 것은 "구간 몸통"이지 "커터"가 아니다.** 서로 떨어진 커터를
|
||
하나로 합치려 들면 안 된다 — 교훈 #14 를 볼 것.
|
||
|
||
---
|
||
|
||
## 4.59. 교훈 #11 — 수평 단면은 세로축이 표고가 아니다
|
||
|
||
`H-H`/`I-I`/`J-J` 같은 수평 절단은 **도면 가로축이 프로젝트 Y, 세로축이 X** 다.
|
||
그 뷰가 도면 어느 높이에 놓였는지는 임의라, 세로 좌표를 그대로 Z 로 쓰면 안 된다.
|
||
|
||
```
|
||
H-H u 0~10,200 → Y -1,900~8,300 (평면도 맨홀 폭과 일치)
|
||
v -1,725~1,075 → X 15,400~18,200 (맨홀 X 범위와 일치)
|
||
```
|
||
|
||
앵커에 `v0` 와 `swapUV` 를 둬서 `[Y,X]` 를 `[X,Y]` 순서로 바꾼다.
|
||
**축을 뒤집거나 맞바꾸면 감김 방향이 반대가 되므로** 뒤집기 횟수가 홀수면
|
||
꼭짓점 순서를 되돌려야 면적이 양수로 유지된다.
|
||
|
||
맞게 잡혔는지는 다른 뷰와 교차검증하면 바로 안다.
|
||
|
||
```
|
||
J-J #1 X 15,800~17,800 × Y 200~3,700 집수조 3,500 ← E-E 와 일치
|
||
J-J #2 X 15,800~17,800 × Y -1,500~0 침사조 1,500 ← E-E 와 일치
|
||
H-H #3 X 15,800~17,000 × Y -500~1,500 환기 OPEN부 ← 주기 "2.0x1.2" 와 일치
|
||
I-I #4 X 15,800~17,000 × Y 1,800~2,800 점검구 ← 주기 "1.0x1.2" 와 일치
|
||
```
|
||
|
||
---
|
||
|
||
## 4.591. 교훈 #12 — 링은 못 믿어도 **bbox 는 믿을 수 있다**
|
||
|
||
개구부 폐곡선을 그대로 썼더니 자기교차하는 쓰레기가 나왔다.
|
||
|
||
```
|
||
mh.ventHole [15950,1500] [15800,1500] [17000,-500] [17000,1500] [15800,-500] …
|
||
↑ 대각선으로 튄다
|
||
```
|
||
|
||
원인이 셋 겹쳤다.
|
||
|
||
1. 개구부 윤곽이 `000-구조물실선` 에 **모서리가 안 닫힌 채로** 그려져 있다
|
||
2. 그 자리에 `-전력구` 레이어 선이 겹쳐 있다
|
||
3. 개구 기호(X 표시)와 지시선이 사각형 안을 가로지른다
|
||
|
||
평면 분할은 이 선들을 전부 경계로 삼으므로 엉뚱한 면을 따라 돈다.
|
||
|
||
**사람 눈에는 멀쩡한 사각형으로 보인다.** 끊긴 폭이 몇 mm라 화면에서 안 보이고,
|
||
X 표시는 개구 기호로 읽고 넘어간다. **기계만 걸리는 종류의 결함**이다.
|
||
|
||
### 대응 — bbox 로 재구성하고 폐곡선은 확인용으로만
|
||
|
||
끊기거나 오염돼도 **범위(bbox)는 정확하다**. 형상이 직사각형이라는 걸 알면
|
||
bbox 만으로 복원할 수 있다.
|
||
|
||
```js
|
||
expect('단 면 H-H', 1200, 2000, '환기 OPEN부 (2.0x1.2)'); // 실재 확인만
|
||
profile('mh.ventHole', 'XY', rect(15800, -500, 17000, 1500)); // 실제 프로파일
|
||
```
|
||
|
||
`expect` 를 남겨두는 게 중요하다. bbox 값이 도면과 어긋나면 거기서 걸린다.
|
||
|
||
### 이런 자리를 미리 알아채는 법
|
||
|
||
`--loops` 출력의 **꼭짓점 수**가 신호다. 직사각형이어야 할 것이 4가 아니면 의심한다.
|
||
개구 기호가 들어간 면은 8, 케이블랙이 섞인 면은 114 가 나왔다.
|
||
|
||
---
|
||
|
||
## 4.6. 교훈 #13 — 조립 스펙에서 폐곡선은 "면적 순서"가 아니라 "치수"로 집어라
|
||
|
||
`assemble()` 이 프로파일을 고를 때 `loops[0]`, `loops[1]` 처럼 순서로 집으면
|
||
도면이 조금만 바뀌어도 조용히 다른 면을 집는다. 면적 순위는 흔들린다.
|
||
|
||
**폭×높이로 집는다.** 그 값이 곧 도면 치수라서 사람이 읽고 검증할 수 있고,
|
||
못 찾거나 여러 개면 예외를 던져 조용한 오류가 안 생긴다.
|
||
|
||
```js
|
||
profile('duct.outer', 'YZ', loop('단 면 B-B', 2700, 2750).ring); // 도면 표기 그대로
|
||
profile('duct.inner', 'YZ', loop('단 면 B-B', 2100, 2150).ring);
|
||
```
|
||
|
||
### boolean 커터는 대상보다 크게 뽑아라
|
||
|
||
압출 구간이 딱 맞으면 커터와 대상의 면이 겹쳐 boolean 이 불안정해진다.
|
||
양쪽으로 조금(여기선 200mm) 빼서 확실히 관통시킨다.
|
||
|
||
```js
|
||
part({ id:'duct.solid', op:'extrude', from: 0, to: 15400 });
|
||
part({ id:'duct.void', op:'extrude', from: -200, to: 15600 }); // 양쪽 200 여유
|
||
```
|
||
|
||
### 재료 경계선을 프로파일 추출에서 빼면 면이 뭉친다
|
||
|
||
구체만 넣고 뽑으면 기초·잡석이 한 덩어리가 된다. **면이 갈리려면 경계선이
|
||
있어야 하므로** 추출에는 함께 넣고, 배정은 폐곡선의 레이어를 보고 나중에 한다.
|
||
단 케이블 지지대 같은 가구는 넣으면 안 된다 — 폐곡선이 그 윤곽을 따라 돈다.
|
||
|
||
---
|
||
|
||
## 4.7. 교훈 #14 — boolean 은 **이항**이다. 커터는 하나씩 먹인다
|
||
|
||
`BooleanDifference` · `BooleanUnion` 플러그인은 선택 개수와 무관하게
|
||
`TargetModels[0]`, `[1]` **두 개만** 쓴다. 셋 이상 넘기면 나머지는 예외도 경고도
|
||
없이 무시되고 씬에 그대로 남는다.
|
||
|
||
```
|
||
✗ select(mh.solid); select(cut1 … cut9, add)
|
||
BooleanDifference
|
||
→ 컷 1개만 적용. 커터 8개가 씬에 떠다닌다
|
||
```
|
||
|
||
`emit/egm.mjs` 의 `fold()` 가 둘씩 접는다. 중간 결과는 `id~1`, `id~2` … 로 잡고
|
||
마지막만 `id` 가 된다.
|
||
|
||
```
|
||
✓ mh.solid − cut.upper → mh.body~1
|
||
mh.body~1 − cut.lower → mh.body~2
|
||
…
|
||
mh.body~8 − cut.pumpHole → mh.body
|
||
```
|
||
|
||
**커터를 먼저 union 해서 하나로 만드는 방법은 못 쓴다.** 다리(legL/legR)와
|
||
환기 개구처럼 서로 떨어진 커터는 교차하지 않아 union 이 `ArgumentNullException`
|
||
으로 죽는다. 교훈 #10 에서 union 하는 것은 "구간 몸통"이지 커터가 아니다.
|
||
|
||
### 이 사고를 잡아내는 신호
|
||
|
||
`egm status` 의 `modelCount` 를 **최종 파트 수와 대조**한다. 가장 빠르다.
|
||
|
||
```
|
||
modelCount 11 정상 (최종 파트 11개)
|
||
modelCount 19 커터 8개가 안 먹혔다
|
||
```
|
||
|
||
실행 직후 한 줄이면 되므로 매번 확인한다. 형상만 보면 컷 하나가 빠진 걸
|
||
알아채기 어렵다 — 남은 커터 솔리드가 구체와 겹쳐 있어 겉보기엔 멀쩡하다.
|
||
|
||
---
|
||
|
||
## 4.8. 교훈 #15 — 뷰어 모드면 명령이 **조용히** 무시된다
|
||
|
||
모델러가 뷰어 모드로 떠 있으면 모델링 명령이 **등록조차 안 된다.** 그런데 파이프는
|
||
살아 있어서 `egm status` 는 정상 응답하고, `~Polyline` 은 아무 말 없이
|
||
`created: []` 를 낸다. "모델러가 죽었나" 로 시간을 크게 버린 함정이다.
|
||
|
||
원인은 `CommandManager.cs:430-434` 다. `EgEditorCommand` 를 상속한 명령은
|
||
`EGViewport.Plugins` 에 해당 플러그인이 없으면 등록에서 건너뛴다. 모델링 플러그인은
|
||
뷰포트가 한 번 렌더된 뒤 `LoadProjectPluginsAsync()` 로 올라오므로, 뷰어 모드거나
|
||
창이 화면 밖에 있으면 같은 증상이 난다.
|
||
|
||
**작업 시작 전에 이것부터 확인한다.**
|
||
|
||
```sh
|
||
egm guide | python -c "import sys,json;print(len(json.load(sys.stdin)))"
|
||
# 39 → 뷰어 모드. 보기·선택·계측만 있다
|
||
# 207 → 정상
|
||
|
||
egm guide Polyline # "Command 'Polyline' not found" 면 뷰어 모드
|
||
```
|
||
|
||
39개 쪽에 남아 있는 것은 `SelAll` · `Zoom` · `Delete` · `Distance` · `Isolate`
|
||
같은 계열뿐이다. `Polyline` · `Box` · `Extrude` · `Boolean*` 가 통째로 빠진다.
|
||
`New` · `ModelClear` 는 뷰어 모드에서도 `result` 에 찍히므로, **"명령이 하나라도
|
||
먹으니 연결은 정상"이라는 판단이 함정이다.**
|
||
|
||
CLI 로는 못 바꾼다. 모델러 UI 에서 **View Mode → Edit Mode** 로 놓는다
|
||
(`AppModeManager.ViewerMode`, 상단바 토글 또는 Display and Workspace →
|
||
Interaction Mode).
|
||
|
||
**`Settings.json` 의 `ModelerMode` 와 헷갈리지 말 것.** 그쪽은
|
||
Assistive Features → Working Mode Setting 의 별개 값이고, `0`(`ViewingOnly`) 인
|
||
채로도 Edit Mode 면 모델링이 된다. 명령 등록을 가르는 것은 `ViewerMode` 다
|
||
(`TestHost/MainWindow.xaml.cs:62` 가 같은 이유로 `ViewerMode = false` 를 세운다).
|
||
|
||
---
|
||
|
||
## 4.9. 교훈 #16 — loft 는 **꺾인 자리를 못 만든다**
|
||
|
||
전이부(스터브 B-B → 본체 C-C)를 세우면서 loft 를 처음 썼다. 스터브 단면에서
|
||
본체 단면까지 **loft 하나로** 이었더니, 결과가 도면의 "챔퍼로 꺾인 뒤 수직 단차"가
|
||
아니라 **끝까지 비스듬히 올라가는 긴 경사면 하나**가 됐다.
|
||
|
||
원인은 loft 를 "단면 A 에서 단면 B 로 가는 길"로 본 것이다. loft 는 두 단면
|
||
**사이를 곧게 이을 뿐**이다. 꺾이는 자리는 loft 가 만들어 주지 않는다 —
|
||
**꺾이는 자리마다 단면을 하나씩 놓고 구간을 끊어야** 그 자리가 생긴다.
|
||
|
||
```
|
||
챔퍼 loft 양 끝 단면이 다른 구간
|
||
수직 단차 union 앞 구간이 작은 채로 끝나고 뒤 구간이 크게 시작한다.
|
||
그 차이가 곧 단차 벽면이다. 따로 만들 것이 없다
|
||
종방향 헌치 별도 쐐기 XZ 삼각형을 Y 로 압출해 union 으로 되붙인다
|
||
```
|
||
|
||
### 꺾이는 X 가 다르면 loft 를 또 끊는다
|
||
|
||
이 전이부의 외곽은 **옆·위가 X -300…0**, **아래가 X -100…0** 에서 꺾인다.
|
||
한 번에 이으면 아래 챔퍼가 100mm 짜리인데 300mm 로 늘어난다.
|
||
|
||
```
|
||
stubL.solid extrude X -1000 → -300
|
||
ch1.solid loft stub.outer@-300 → out.ch1@-100 옆·위만 벌어진다
|
||
ch2.solid loft out.ch1@-100 → out.ch2@0 아래 챔퍼가 여기서 합류
|
||
main.solid extrude X 0 → 15400 ← X=0 수직 단차 자동 생성
|
||
```
|
||
|
||
중간 단면(X=-100)의 값은 **측정한 직선 위의 내분점**을 코드가 계산하게 둔다.
|
||
상수로 박으면 도면이 바뀔 때 조용히 틀린다.
|
||
|
||
### 단면은 2개씩만 — 3개부터 부푼다
|
||
|
||
`Loft` 는 단면을 N개 받지만 3개 이상이면 스플라인 보간으로 부풀 수 있다.
|
||
**2단면 loft 는 직선 룰드 서피스**라 그럴 일이 없다. 확인법이 간단하다 —
|
||
결과의 정점 X 값에 **중간 행이 안 생기면** 곧게 이어진 것이다.
|
||
|
||
```
|
||
정점 X: -1000 -300 -100 0 100 400 550 15400
|
||
└ 전부 단면을 놓은 자리. 사이에 중간 행이 없다 = 직선
|
||
```
|
||
|
||
### 링 정규화가 없으면 loft 가 비틀린다
|
||
|
||
loft 는 두 프로파일의 꼭짓점을 **순서대로** 잇는다. 그런데 `buildLoops` 의 시작
|
||
꼭짓점은 half-edge 순회가 어느 반변에서 출발했느냐에 달려 있어 프로파일마다
|
||
제각각이다. 그대로 이으면 엉뚱한 꼭짓점끼리 짝지어 면이 꼬인다.
|
||
|
||
**형상만 봐서는 원인을 못 찾는다** — 링 자체는 멀쩡하기 때문이다.
|
||
`addProfile` 이 등록할 때 정규화한다(반시계 · v 최소 → u 최소 꼭짓점에서 시작).
|
||
압출에는 영향이 없다. 닫힌 커브라 시작점이 어디든 같은 커브다.
|
||
|
||
정규화 뒤 두 내부 단면이 인덱스별로 정확히 짝을 이뤘다.
|
||
|
||
```
|
||
i stub.inner main.inner 의미
|
||
0 (-1050,-7357) (-1500,-7357) 바닥 좌단
|
||
6 ( 1050,-5357) ( 1500,-3707) 헌치 시작 (우)
|
||
7 ( 900,-5207) ( 1350,-3557) 헌치 끝 (우)
|
||
```
|
||
|
||
위상이 다르면(꼭짓점 수가 다르면) 정규화로도 안 된다. `validateIR` 이
|
||
**평면 일치 · 꼭짓점 수 일치 · 위치 오름차순**을 먼저 막는다.
|
||
|
||
### 검증은 부피 대조가 제일 빠르다
|
||
|
||
비틀린 loft 는 부피가 크게 어긋난다. 반대로 부피가 맞으면 꼬이지 않은 것이다.
|
||
`ExportByPath` 로 OBJ 를 받아 발산 정리로 재고, IR 프로파일에서 계산한
|
||
기대값과 맞춘다. 두 단면 사이 면적은 t 의 2차식이라 **심프슨이 정확**하다.
|
||
|
||
```
|
||
V = L·(A0 + 4·A(½) + A1) / 6 A(t) = 꼭짓점을 t 로 보간한 다각형 면적
|
||
```
|
||
|
||
```
|
||
외곽 277.0195 − 공동 176.0020 + 헌치 쐐기 0.0315 = 기대 101.0490 m³
|
||
실측 101.0492 m³ 오차 0.0002%
|
||
```
|
||
|
||
헌치 쐐기는 공동 안쪽에 들어가는 부분만 더해진다 —
|
||
높이 `d`(0…150)에서 x 폭이 `d`, y 폭이 `2(1500−d)` 이므로
|
||
`∫₀¹⁵⁰ d·2(1500−d) dd = 0.0315 m³`. 이 값까지 맞으면 union 도 제대로 먹은 것이다.
|
||
|
||
### 이음매에는 T 정점이 남는다
|
||
|
||
구간을 맞대어 붙이면 **양쪽 삼각분할이 달라** 공유 평면에서 변이 1:1로 안 맞는다.
|
||
이 모델에서 열린 변이 21개 나왔고 **전부 이음매 평면**(X=-1000 · 100 · 400)이었다.
|
||
면은 기하적으로 겹쳐 있어서 부피는 정확하다.
|
||
|
||
렌더링·부피에는 문제가 없지만 **엄밀히 manifold 가 아니다.** IFC 로 내보낼 때
|
||
걸릴 수 있다. 없애려면 이음매를 맞대지 말고 솔리드끼리 겹쳐 boolean 이 진짜
|
||
교차를 만들게 해야 하는데, 공동 쪽은 교훈 #10 때문에 그렇게 못 한다.
|
||
|
||
---
|
||
|
||
## 4.95. 교훈 #17 — 원은 선분과 **섞지 않는다**. 그리고 좌표를 쓰는 순간 앵커가 드러난다
|
||
|
||
`parse.mjs` 는 오랫동안 CIRCLE 을 버렸다. 넣을 때 두 갈래가 있었다.
|
||
|
||
```
|
||
✗ 원을 다각형으로 근사해 선분 목록에 넣는다 → 평면 분할이 그 윤곽을 따라 돈다.
|
||
폐곡선이 오염된다 (교훈 #12 와 같은 사고)
|
||
✓ 따로 모아 지름으로 집는다 → 원은 그 자체가 형상이다. cylinder 로 직행
|
||
```
|
||
|
||
원은 **관통 구멍·기둥**이라 애초에 "면을 가르는 경계선"이 아니다. 폐곡선 추적에
|
||
참여시킬 이유가 없다.
|
||
|
||
**뷰 귀속도 따로 한다.** 뷰 분리(조각 클러스터링)에 원을 넣으면 관통 구멍 하나가
|
||
두 뷰를 잇는 다리가 될 수 있다. 뷰가 다 갈린 뒤 **중심 좌표로 귀속**시킨다.
|
||
|
||
### 시트 밖을 반드시 거른다 — 블록 로컬 좌표가 섞여 있다
|
||
|
||
이 도면의 CIRCLE 253개 중 시트 안은 133개다. 나머지는 두 종류다.
|
||
|
||
```
|
||
x ≈ -565,472 유령 기하 (`000-구조물점선`. 선분에서 이미 겪은 것)
|
||
x ≈ 25.4 INSERT 블록 **로컬 좌표**. 원점 근처에 몰려 있다
|
||
```
|
||
|
||
두 번째가 새로 나온 것이다. 안 거르면 원점 근처 뷰에 남의 원이 무더기로 붙는다.
|
||
|
||
### 지름은 강한 식별자지만, 레이어까지 봐야 하는 경우가 있다
|
||
|
||
```
|
||
Ø900 -전력구 2 사다리 샤프트 보어
|
||
Ø100 환기구 4 · 배수관 1 · 금구류 2 ← 앞의 둘만 관통이다. 금구류는 철물
|
||
```
|
||
|
||
그래서 `circles(뷰, 지름, 레이어정규식)` 로 좁힐 수 있게 뒀다. 폐곡선을 치수로
|
||
집는 것과 같은 규칙이다 — 순서로 집으면 조용히 다른 것을 집는다.
|
||
|
||
### 좌표를 쓰기 시작하면 숨어 있던 앵커가 드러난다
|
||
|
||
평면도는 **세로축이 표고가 아니라 폭방향(Y)** 이다. 그런데 지금까지는 평면도에서
|
||
`expect(뷰, 폭, 높이)` 로 치수만 봤기 때문에 `v0` 앵커가 없어도 아무 문제가 없었다.
|
||
CIRCLE 좌표를 쓰는 순간 보어가 `Y = -9,778` 로 나왔다.
|
||
|
||
**치수만 보는 동안은 틀린 앵커도 안 걸린다.** 수평 단면(H/I/J)에서 이미 겪은
|
||
문제인데, 평면도는 "표고 축이 맞겠거니" 하고 넘어갔던 것이다.
|
||
|
||
앵커는 지어내지 말고 **뷰 범위에서 유도하고 다른 것으로 교차검증**한다.
|
||
|
||
```
|
||
뷰 세로 -11,678 ~ -1,478 (10,200) `-전력구` 전폭. 아래 끝이 Y=-1,900
|
||
→ v0 = -11,678 + 1,900 = -9,778
|
||
교차검증
|
||
샤프트 사각 1,300×1,300 v -10,428..-9,128 → Y -650..+650 중심선 대칭 ✓
|
||
Ø900 보어 2개 v -9,778 → Y 0 ✓
|
||
맨홀 내부 2,000×7,200 v -11,278..-4,078 → Y -1,500..5,700 ✓
|
||
```
|
||
|
||
### 손으로 적은 수는 도면이 바뀌어도 안 바뀐다
|
||
|
||
이 작업의 실제 소득은 아래 한 줄이 사라진 것이다.
|
||
|
||
```js
|
||
const BORE_R = 450; // 원본 엔티티에서 눈으로 읽어 적음
|
||
```
|
||
|
||
지금은 도면에서 집고 샤프트 중심과 대조한다. 어긋나면 빌드가 죽는다.
|
||
|
||
```js
|
||
const bores = circles('평 면 도', 900, /^-전력구$/);
|
||
if (bore.u !== cx || bore.v !== 0) throw new Error(`${s.id} 보어가 샤프트 중심과 다르다 …`);
|
||
```
|
||
|
||
파라메트릭으로 갈 때 **이 끈이 없는 값은 파라미터로 올릴 수 없다** —
|
||
바꿔도 도면과 대조할 방법이 없기 때문이다.
|
||
|
||
---
|
||
|
||
## 4.96. 교훈 #18 — 자기교차 링은 **조용히** boolean 을 무력화한다
|
||
|
||
교훈 #12 는 개구 기호(X 표시)가 링을 오염시킨다는 것이었고, 대응은 "bbox 로
|
||
재구성"이었다. 그런데 **그 사고가 개구부에서만 나는 게 아니다.**
|
||
|
||
맨홀 #2 구체를 재봤더니 이랬다.
|
||
|
||
```
|
||
기대 (IR 을 격자로 CSG 평가) 71.758 m³
|
||
실측 (모델러) 141.207 m³ ← 챔버 커터 4개가 아무것도 못 뺐다
|
||
```
|
||
|
||
원인은 외곽 프로파일 `mh.outer` (54꼭짓점) 의 **자기교차 2곳**이었다.
|
||
|
||
```
|
||
[2900,-7357]→[1800,-7607] × [2900,-7607]→[1800,-7357] 하부 슬래브 개구 기호
|
||
[ 0,-7357]→[-800,-7607] × [ 0,-7607]→[-800,-7357]
|
||
```
|
||
|
||
자기교차 링을 압출하면 솔리드가 쓰레기다. 그 뒤 `BooleanDifference` 는
|
||
**예외도 경고도 없이** 아무것도 못 뺀다. `modelCount` 는 정확히 맞는다 —
|
||
커터는 소모됐기 때문이다. **부피를 재기 전에는 알 방법이 없었다.**
|
||
|
||
### 모델러를 의심하기 전에 격리 실험을 하라
|
||
|
||
처음엔 "커터가 대상 안에 완전히 갇히면 boolean 이 실패한다"고 의심했다.
|
||
프로브를 만들어 확인하니 **정상이었다.**
|
||
|
||
```
|
||
2×2×2 상자 − 안쪽 1×1×1 상자 → 7.0000 갇힌 커터도 정상
|
||
2×2×2 상자 − 위로 뚫는 상자 → 6.5000 겹친 만큼만 정확히
|
||
```
|
||
|
||
모델러가 아니라 **우리가 넣은 링**이 문제였다. 프로브 3줄이 잘못된 방향으로
|
||
갈 뻔한 것을 막았다.
|
||
|
||
### 왜 T 자 분할만으로는 부족한가
|
||
|
||
`core/loops.mjs` 는 **T 자 교차**(노드가 다른 선분 위에 얹힌 경우)만 쪼갠다.
|
||
두 선분이 중간에서 **X 자로 교차**하는데 그 자리에 노드가 없으면 안 쪼갠다.
|
||
개구 기호의 대각선 두 개가 정확히 그 모양이다.
|
||
|
||
### 이 도면의 실태 — 링 10개가 오염돼 있었다
|
||
|
||
`build.mjs --loops` 에 자기교차 검사를 붙이자 한 번에 드러났다.
|
||
|
||
```
|
||
단 면 E-E #0 10200×10183 꼭짓점 54 ✗ 자기교차 2 ← 맨홀 외곽. 부피 2배
|
||
단 면 H-H #3 1200× 2000 꼭짓점 8 ✗ 자기교차 6 ← 환기 OPEN부 (이미 알던 것)
|
||
단 면 I-I #4 1200× 1000 꼭짓점 8 ✗ 자기교차 6 ← 점검구 (이미 알던 것)
|
||
단 면 A-A #8 900× 400 꼭짓점 8 ✗ 자기교차 8 ← 샤프트 지붕 개구
|
||
…
|
||
```
|
||
|
||
**꼭짓점 수보다 확실한 신호다.** 4가 아니면 "의심"이지만, 자기교차는 그 자체로
|
||
결함이다. `selfIntersections()` 는 `core/views.mjs` 에 있고 O(n²) 라 뷰당 링
|
||
수십 개 수준에서는 공짜다.
|
||
|
||
### 순서
|
||
|
||
```
|
||
1. --loops 에서 자기교차부터 본다 오염된 링을 전부 나열
|
||
2. 레이어로 걸러지나 확인 이 도면은 X 표시가 `-전력구` 라 안 됐다
|
||
3. 직사각형이면 bbox → rect() 재구성 교훈 #12
|
||
4. 오목한 외곽이면 X 교차 분할이 필요 rect() 로 못 만든다
|
||
```
|
||
|
||
---
|
||
|
||
## 4.97. 교훈 #19 — **의도**와 **결과**를 따로 재라
|
||
|
||
부피가 이상할 때 물어야 할 것은 둘이다.
|
||
|
||
```
|
||
우리 조립(IR)이 틀렸나? ← 의도
|
||
모델러가 IR 대로 안 만들었나? ← 결과
|
||
```
|
||
|
||
이걸 못 가르면 엉뚱한 데를 판다. 실제로 맨홀 부피를 쫓느라 손으로 기대값을
|
||
세 번 조립했는데 세 번 다 커터 범위를 놓쳐서 틀렸다. 그 값으로 모델러를 의심하고,
|
||
격리 프로브를 만들고, 커터 순서를 바꾸고 — 전부 헛수고였다.
|
||
|
||
**IR 을 격자로 그대로 평가하면 손계산이 필요 없다.** `src/volume.mjs` 가
|
||
연산 트리(union=or, difference=and-not, loft=선형보간)를 따라가며 점을 세어
|
||
"IR 이 뜻하는 부피"를 낸다. 25mm 격자에서 0.1% 안팎이라 소수 셋째 자리는 못 보지만,
|
||
**boolean 이 죽는 사고는 언제나 m³ 단위로 어긋나므로 충분하다.**
|
||
|
||
```sh
|
||
node src/measure.mjs manhole1 --ir
|
||
```
|
||
|
||
```
|
||
파트 부피 m³ IR 의도 차이
|
||
맨홀1_전력구_구체 100.5403 100.5350 0.0053 ✓
|
||
맨홀1_사다리샤프트_1 3.3217 3.3211 0.0006 ✓
|
||
맨홀1_전력구_출구스터브 2.9725 2.9725 0.0000 ✓
|
||
맨홀1_맨홀2_구체 65.3577 74.6819 −9.3242 ✗ ← 여기만 모델러가 흘렸다
|
||
```
|
||
|
||
한 줄로 "우리 조립은 맞고 모델러가 흘린다"가 확정된다. 그 전까지는 몇 시간이 걸렸다.
|
||
|
||
### 커터가 자기 부피보다 많이 빼면 boolean 이 죽은 것이다
|
||
|
||
가장 확실한 단일 신호다. 커터의 해석 부피는 `--primitives` 로 바로 나온다.
|
||
|
||
```
|
||
mh.cut.duct 자기 부피 9.04 실제로 뺀 것 15.28 ✗
|
||
mh.cut.lower 자기 부피 20.08 실제로 뺀 것 29.53 ✗
|
||
```
|
||
|
||
물리적으로 불가능하므로 형상 논쟁이 필요 없다.
|
||
|
||
### 면으로만 맞닿은 솔리드를 union 하지 마라
|
||
|
||
이 사고의 원인이 여기 있다. 맨홀 외곽(`mh.outer`)의 윗변과 상부 통로
|
||
(`mh.upper`)의 밑변이 **둘 다 Z=-4,957** 이다. union 하면 한 덩어리처럼 보이지만
|
||
실은 **면으로 붙은 조각들**이고, 이어지는 difference 가 조각 하나를 통째로 흘린다.
|
||
손실이 어느 커터에서든 **9.43 으로 일정한 것**이 그 증거다 — 커터 탓이면 값이
|
||
커터마다 달라야 한다.
|
||
|
||
```
|
||
✓ 솔리드끼리 **부피로 겹치게** 만든 뒤 union
|
||
✗ 면만 맞대고 union
|
||
```
|
||
|
||
단, 겹치게 만들 때 **압출축 방향으로만** 늘려야 한다. 프로파일을 통째로 1mm
|
||
부풀렸더니 옆으로도 커져 새 겹침이 생기고 결과가 더 나빠졌다(65.4 → 41.6).
|
||
|
||
### 관통구는 합치기 **전에**, 단순한 조각에서 뚫는다
|
||
|
||
union 결과를 뚫으면 위처럼 흘린다. 조각마다 제 몫만 뚫고 나서 합치면 정확하다.
|
||
|
||
```
|
||
✗ solid = union(shell, endL, endR) → solid − ductHole 15.28 빠짐 (기대 5.83)
|
||
✓ shell − ductHole, endL − ductHole → union mh.solid 118.008 / 의도 118.034 ✓
|
||
```
|
||
|
||
boolean 이 피연산자를 소모하므로 **같은 커터를 두 조각에 못 쓴다.** 커터를 조각
|
||
수만큼 따로 만든다. 안 겹치는 조각에는 아예 넣지 말 것 — 교차가 없으면
|
||
`BooleanDifference` 가 `created:[]` 로 죽는다.
|
||
|
||
### 커터는 대상 **바깥면에서 시작하면 안 된다**
|
||
|
||
환기구 다리 커터를 맨홀 외면(X 15,400)에서 정확히 시작했더니 46 m³ 를 파먹고
|
||
(기대 2.48), 이어진 반대쪽 커터가 부피를 59 m³ **늘렸다**. 바깥으로 `OVER` 만큼
|
||
빼자 둘 다 1.80 으로 정확해졌다. 교훈 #13 의 "커터는 대상보다 크게"가 압출축뿐
|
||
아니라 **모든 방향**에 적용된다.
|
||
|
||
---
|
||
|
||
## 4.98. 교훈 #20 — **모델스페이스가 실척이라고 믿지 마라**
|
||
|
||
교량 도면군을 넣으면서 처음 깨진 가정이다. 두 층이 따로 논다.
|
||
|
||
```
|
||
① DWG 모델단위 1 단위가 몇 mm 인가 도면군마다 다르다
|
||
② 도형 축척 그 도형이 실척으로 그려졌나 같은 도면 안에서도 다르다
|
||
```
|
||
|
||
**① 모델단위.** 맨홀군은 1 단위 = 1 m 인데 교량 삽도군은 1 단위 = 1 mm 다.
|
||
`core/datum.mjs` 에 `UNIT_MM = 1000` 이 상수로 박혀 있었다. 숫자라서 「도면
|
||
고유값」으로 안 보였을 뿐이고, 그대로 두면 **모든 치수가 1000× 로 틀린다.**
|
||
폐곡선도 나오고 부피도 나와서 알아채기 어렵다.
|
||
|
||
확정하는 법은 도면 안에 있다 — `DIMENSION` 엔티티의 `actualMeasurement` 와
|
||
그 치수의 **글자**를 비교한다.
|
||
|
||
```
|
||
actualMeasurement 3000.00 ↔ 글자 "3.000" (m) → 1 단위 = 1 mm
|
||
```
|
||
|
||
같은 파일의 다른 상수들도 같은 병을 앓고 있었다. 라벨 대역 `±3`, 뷰 구분 문턱
|
||
`2` — 전부 「1단위=1m」를 전제한 **길이**다. 길이는 mm 로 적고 단위로 나눠 되돌린다.
|
||
|
||
**② 도형 축척.** 중앙분리대 삽도의 방호벽 단면은 **5.805배**로 그려져 있었다.
|
||
같은 파일의 교량폭원 단면은 1:1 이다. 도형마다 다르다.
|
||
|
||
되찾는 법 — **표기 치수 2개**로 역산해서 서로 맞는지 본다. 하나만 쓰면 우연히 맞는다.
|
||
|
||
```
|
||
높이 포장면 y=48,812.1 → 마루 y=56,184.4 = 7,372.3 단위 ↔ 표기 1,270
|
||
바닥폭 x 58,176.9 → 61,570.8 = 3,393.9 단위 ↔ 표기 585
|
||
|
||
7372.3 / 1270 = 5.8049 3393.9 / 585 = 5.8015 차이 0.06%
|
||
```
|
||
|
||
옆 단면(단면B)도 같은 배율이 나온 것이 세 번째 검증이었다.
|
||
|
||
> 뷰의 서술이 `{좌표평면 · 반전 · 앵커 · 용도}` 넷에서 **`축척`을 더해 다섯**이 됐다.
|
||
|
||
---
|
||
|
||
## 4.99. 교훈 #21 — 뷰 제목은 **유일 키가 아니다**
|
||
|
||
뷰 분리는 「잘게 쪼갠 뒤 제목으로 다시 묶는다」(교훈 #2)였다. 그 절차는 두 가지를
|
||
전제한다. **교량 삽도에서는 둘 다 거짓이었다.**
|
||
|
||
**(가) 뷰 사이에 간격이 있다** — 없었다.
|
||
|
||
```
|
||
gap 10 25 50 100 200 400 800 1600 3200 (mm)
|
||
조각 1 1 1 1 1 1 1 1 1
|
||
```
|
||
|
||
임계값을 10mm 까지 좁혀도 조각이 하나다. 범인은 **상세 확대 테두리 상자**다
|
||
(LWPOLYLINE, Δy=5,117, y 15,965…21,082 과 20,375…25,492 가 겹친다). 상세행과
|
||
단면행을 세로로 잇는데, 구조선과 **같은 레이어·같은 색**이라 걸러낼 수도 없다.
|
||
|
||
**(나) 제목이 뷰마다 유일하다** — 아니었다.
|
||
|
||
```
|
||
교량폭원 뷰 제목이 아예 없다. 표제는 시트명 하나, 나머지는 A · B · L of Road
|
||
중앙분리대 단면A · 단면B 가 각 4벌 (사례 3종 × 위치)
|
||
교량시종점 「교량 시·종점」이 6벌 (TYPE1 반일체식 ~ TYPE4 골조식)
|
||
```
|
||
|
||
유형별 삽도는 **같은 것의 변형을 N벌 늘어놓는 형식**이라 제목이 반복되는 게 정상이다.
|
||
표제 중복률을 세보면 7장 전부 47~84% 였다.
|
||
|
||
**대응 — 좌표로 짚는다.** `cfg.viewBoxes` 에 뷰마다 `{box:[x0,y0,x1,y1], kind}`.
|
||
|
||
```js
|
||
viewBoxes: {
|
||
'단면A': { box: [55000, 44000, 66000, 58000], kind: 'cross', unitMm: 1 / 5.805 },
|
||
'단면B': { box: [70000, 44000, 82000, 58000], kind: 'cross', unitMm: 1 / 5.805 },
|
||
},
|
||
```
|
||
|
||
두 가지를 지킨다.
|
||
|
||
- **조각이 아니라 선분 단위로 나눈다.** 조각 단위로 나누면 뷰를 잇는 다리 조각이
|
||
통째로 한쪽에 붙는다. 선분이면 다리가 어느 상자에도 안 들어가 자연히 버려진다.
|
||
- **양 끝이 다 상자 안에 들어와야** 취한다. 한쪽만 걸치는 선은 뷰를 넘나드는 선이라
|
||
넣으면 뷰 폭·높이가 조용히 부푼다.
|
||
|
||
상자가 겹치면 같은 선이 두 뷰에 들어가므로 **예외를 던진다.**
|
||
|
||
box 좌표는 눈대중이 아니다. 구조선 y 를 정렬해 400mm 이상 벌어진 곳에서 끊은
|
||
**뭉치 경계**를 뽑고, 상자 변은 그 **빈틈 안**에 둔다.
|
||
|
||
---
|
||
|
||
## 4.991. 교훈 #22 — 레이어가 못 가르면 **색이 가른다**
|
||
|
||
`toSegments` 는 걸 수 있는 필터가 `layerFilter` 하나였다. 그런데 구조선과 치수선이
|
||
**같은 레이어**에 있는 도면이 있다 — 교량폭원은 둘 다 `CS-CONC-MAJR` 이다.
|
||
|
||
```
|
||
CS-CONC-MAJR 안에서
|
||
colorIndex 0 (ByBlock) LINE 576 ← 치수 블록 안의 선
|
||
colorIndex 7 (ByLayer) LINE 316 + LWPOLYLINE 161 ← 구체 윤곽
|
||
```
|
||
|
||
`colorIndex 0` 은 ByBlock 이다. 즉 **블록(치수) 안에 든 기하**라는 뜻이고,
|
||
그게 그대로 치수선/화살표/보조선을 가른다. 걸러내니 횡단면 3벌과 방호울타리
|
||
상세가 깨끗하게 드러났다.
|
||
|
||
무엇으로 가를지는 도면마다 다르므로 **판단은 `drawings/*.mjs` 에 두고 코어는
|
||
술어만 받는다.**
|
||
|
||
```js
|
||
entityFilter: (e) => (e.entityHeader?.colorIndex ?? -1) === 7,
|
||
```
|
||
|
||
> 일반화 — **거를 수 있는 축을 레이어 하나로 두지 마라.** 색·선종·선굵기가
|
||
> 레이어가 못 가르는 자리를 가른다. `gaps.mjs` 같은 진단 도구도 같은 필터를
|
||
> 걸어야 한다. 안 걸면 걸러낸 치수선이 「레이어 오분류」로 잘못 보고돼
|
||
> 원인 판정이 통째로 어긋난다.
|
||
|
||
---
|
||
|
||
## 4.992. 교훈 #23 — **끊김이 0인데 폐곡선도 0**
|
||
|
||
중앙분리대 단면A 가 그랬다. `gaps.mjs` 는 방호벽 구간에 끊김을 하나도 못 찾는데
|
||
`--loops` 는 아무것도 안 낸다. 증상만 보면 「닫혀 있는데 안 나온다」라서
|
||
끊김 진단으로는 **절대 못 잡는다.**
|
||
|
||
원인은 **같은 직선 위에서 겹치는 두 선**이었다. 끝점이 다르므로 1단계 중복 제거를
|
||
통과한다.
|
||
|
||
```
|
||
방호벽 왼쪽 면 x = -292 에 두 선이 그려져 있었다
|
||
z −48 … 30
|
||
z 0 … 49 ← 0…30 이 겹친다
|
||
```
|
||
|
||
T 자 교차 분할이 노드(−48, 0, 30, 49)마다 자르면서 겹친 구간을 **양쪽에서 똑같이**
|
||
잘라낸다.
|
||
|
||
```
|
||
−48…0 0…30 (첫 선에서)
|
||
0…30 30…49 (둘째 선에서)
|
||
↑ 같은 변이 두 벌
|
||
```
|
||
|
||
그 변에 반변이 4개가 되어 면 순회가 엉키고, 폐곡선이 **통째로** 안 나온다.
|
||
|
||
**대응 — 분할 뒤에 한 번 더 중복 제거.** `buildLoops` 는 원래 분할 **전에만**
|
||
중복을 지웠다. 순서가 문제였다.
|
||
|
||
```
|
||
1. 중복 제거 끝점이 똑같은 선만 지운다
|
||
2. T 자 분할
|
||
2b. 중복 제거 ← 분할이 만든 중복 변을 지운다 (추가)
|
||
3. half-edge 구성
|
||
```
|
||
|
||
---
|
||
|
||
## 4.993. 교훈 #24 — 게이트에 넣기 전에 **「이 도면이 대상인가」**를 판정한다
|
||
|
||
교량 도면 7장 중 **6장이 어느 게이트도 통과 못 했다.** 그런데 그걸 알아내는 데
|
||
게이트를 다 돌릴 필요가 없었다 — 파싱 직후 몇 초면 판정된다.
|
||
|
||
**① 표기 치수가 숫자인가 기호인가.** 방법론 전체가 「도면 표기 치수와 대조」에
|
||
걸려 있다. 치수가 기호면 **대조할 상대가 없다.**
|
||
|
||
| 도면 | 숫자 치수 | 기호 치수 | 판정 |
|
||
|---|---|---|---|
|
||
| 거더배치 | **2** | 62 | `L · β · L' · β1..β4` — 불가 |
|
||
| 교량시종점 | **0** | 6 | 측면도 6벌인데 전부 라벨 — 불가 |
|
||
| 교대배치 | 51 | 37 | `θ` 가변 — 낮음 |
|
||
| 교면포장 | 14 | 2 | 층상 구성표 — 형상 아님 |
|
||
| 횡단경사 | 156 | 139 | `S% · TL` 가변 |
|
||
| 교량폭원 | 151 | 10 | 높음 → **G5 에서 막힘** |
|
||
| **중앙분리대** | **108** | 7 | 높음 → **관통** |
|
||
|
||
**② 구조 윤곽이 닫히는가.** 교량폭원은 치수가 충분한데도 못 세웠다.
|
||
끊김 56곳 중 **54곳이 「도면에 선 없음」**이었기 때문이다. 폭원 **구성 도해**라
|
||
애초에 닫힌 단면을 그리지 않는다 — 띠와 열린 외곽선뿐이다.
|
||
|
||
```
|
||
분리교량 폭원 경계 1 레이어 0 누락 16
|
||
폭원 띠 중 경계 0 레이어 2 누락 2
|
||
횡단면 하 경계 0 레이어 0 누락 10
|
||
폭원 띠 상 경계 1 레이어 0 누락 12
|
||
횡단면 중 경계 2 레이어 0 누락 14
|
||
```
|
||
|
||
「레이어 오분류」면 설정 한 줄이지만(교훈 #3), **「선 없음」이 압도적이면 그 도면은
|
||
솔리드 대상이 아니다.** 이 비율이 그대로 판정 기준이다.
|
||
|
||
> 게이트 목록에 **G1.5 도면 적격**이 들어간다. 여기서 걸러야 나머지를 헛되이 채우지 않는다.
|
||
|
||
---
|
||
|
||
## 4.994. 교훈 #25 — 경계 위반은 **두 번째 도면을 넣기 전엔 안 보인다**
|
||
|
||
`CLAUDE.md` 에도 `README` 에도 「`core/*` 에 도면 고유값을 넣지 마라」가 큰 글씨로
|
||
적혀 있었다. 그런데 두 번째 도면군을 넣자 **두 곳에서 이미 새 있었다.**
|
||
|
||
- `core/datum.mjs` 의 `UNIT_MM = 1000` — 문자열이 아니라 **숫자**라서 안 보였다.
|
||
「`'-전력구'` 같은 문자열이 들어가면 잘못된 것」이라고 써둔 탓에 숫자는 통과했다.
|
||
- `rules.mjs` 는 주석에 「도면도 3D도 모른다」고 적혀 있었지만 실제로는
|
||
`챔퍼_시작X · 샤프트 · 맨홀_벽두께` 처럼 **첫 도면의 파라미터 이름**에 묶여 있었다.
|
||
두 번째 도면에서 그 값이 없어 `undefined` 비교가 전부 실패로 떨어졌고,
|
||
**위상 위반**으로 판정돼 `build.mjs` 가 exit 1 로 죽었다 — 남의 도면 규칙에 걸려서.
|
||
|
||
대응은 규칙마다 **요구 파라미터**를 명시하고, 없으면 **해당 없음**으로 건너뛰는 것이다.
|
||
|
||
```js
|
||
{ id: 'chamfer-order', kind: 'topology',
|
||
needs: ['챔퍼_시작X', '챔퍼_중간X', '본체_시작X'], … }
|
||
```
|
||
|
||
부재 종류 태그로 규칙을 붙이는 제대로 된 해법은 아직 없다. 그때까지의 최소 조치다.
|
||
|
||
> **문서에 「안 들어간다」고 쓰는 것으로는 안 지켜진다.** 두 번째 도면이
|
||
> 유일한 검사 장치다. 그래서 성격이 다른 도면을 일찍 한 장 넣어보는 값이 크다.
|
||
|
||
---
|
||
|
||
## 4.995. 교훈 #26 — 기준선 검출이 **예외 없이 틀린 답**을 낼 수 있다
|
||
|
||
교량 삽도 7장 중 `계획고` 라벨이 있는 것은 횡단경사 하나뿐이다. 그래서 여기서는
|
||
기준선 검출(교훈 #1)이 통할 줄 알았다. **통하지 않았고, 더 나쁘게 통했다.**
|
||
|
||
```
|
||
교량 계획고 → y=54,885.8 뷰 10곳
|
||
토공 계획고 → y=54,885.8 뷰 10곳 ← 서로 다른 표고인데 같은 답
|
||
```
|
||
|
||
`교량 계획고` 와 `토공 계획고` 는 **정의상 다른 표고**다. 그 둘의 **차이(단차)**가
|
||
이 도면의 주제다. 그런데 같은 선이 나왔다. 게다가 라벨마다 국소 탐색해보면
|
||
그 y 는 **어느 라벨의 선도 아니다.**
|
||
|
||
```
|
||
라벨 자기 아래 가장 가까운 수평선
|
||
교량 계획고 (73603, 81177) → y=80,105.8
|
||
토공 계획고 (73221, 79089) → y=77,983.5 ← 같은 도형, 2,122 차이 (이게 단차다)
|
||
교량 계획고 (70915, 75535) → y=75,433.7
|
||
토공 계획고 (70888, 70084) → y=69,836.0
|
||
… 라벨 10개 → 국소 선 9개, 전부 다르다
|
||
```
|
||
|
||
### 왜 틀렸나 — 전제가 숨어 있었다
|
||
|
||
`findLevelDatum` 은 라벨 y 를 **평균**낸 뒤 「그 평균 바로 아래 선」을 고른다.
|
||
숨은 전제는 **「라벨 하나가 선 하나를 가리킨다」**, 즉 라벨이 한 곳에 모여 있다는 것이다.
|
||
|
||
유형 삽도는 같은 라벨을 **도형마다 반복**한다. 라벨 10개가 y 27,533~81,177 에
|
||
흩어지면 평균 56,453 은 **어느 도형에도 속하지 않는 값**이다. 그 값 아래에서
|
||
「출현 횟수가 많은 선」을 고르니 엉뚱한 선이 나온다.
|
||
|
||
맨홀 도면에서는 이게 안 드러났다. 11개 뷰가 같은 계획고 선을 공유해서
|
||
라벨도 한 무리로 모여 있었기 때문이다. **표본 하나로는 전제가 안 보인다.**
|
||
|
||
### 대응 — 무리가 여럿이면 던진다
|
||
|
||
```
|
||
라벨 y 를 정렬해 2m 이상 벌어진 곳에서 끊는다
|
||
무리가 2개 이상 → 공통 기준선이 없는 도면이다 → 예외
|
||
```
|
||
|
||
```
|
||
Error: 표고 라벨이 4무리로 흩어져 있다 — 공통 기준선이 없는 도면이다:
|
||
y≈29934(2개), y≈69041(1개), y≈75535(1개), y≈81177(1개).
|
||
datum.mode:'none' 으로 두고 뷰별 anchors.v0 를 쓸 것
|
||
```
|
||
|
||
맨홀은 그대로 통과한다 (뷰 11곳, 총연장 61,492mm).
|
||
|
||
> **「조용히 틀리느니 예외를 던진다」가 검출기 자신에게도 적용된다.**
|
||
> 검출기는 답을 내는 게 일이라 답을 내려고 한다. 전제가 깨졌는지 스스로 검사하지
|
||
> 않으면, 실패가 **예외가 아니라 그럴듯한 숫자**로 나온다. 그게 제일 비싸다.
|
||
|
||
---
|
||
|
||
## 4.996. 교훈 #27 — **검사기가 거짓 경보를 내면 검사기를 안 믿게 된다**
|
||
|
||
교량 상부공을 세우고 `measure --ir` 을 돌렸더니 6파트 중 5개가 **✗** 였다.
|
||
|
||
```
|
||
교량_상부공_좌측상판 59.0400 IR 의도 61.5000 −2.4600 ✗
|
||
교량_방호울타리_1 8.3990 IR 의도 8.4625 −0.0635 ✗
|
||
```
|
||
|
||
모델이 틀린 게 아니었다. **모델러 값이 해석값과 정확히 같았다** —
|
||
12,300 × 240 × 20,000 = 59.04 m³. 틀린 쪽은 「의도」를 재는 `volume.mjs` 였다.
|
||
|
||
원인은 격자다. 25mm 격자로 **두께 240** 을 재면 셀 중심이 12.5, 37.5, … 로
|
||
**10개** 잡혀 250 두께로 계산된다 — **+4.2%**. 맨홀은 부재가 두꺼워 이 오차가
|
||
0.1% 수준으로 묻혔고, 얇은 교량 부재에서 처음 드러났다.
|
||
|
||
같은 방호울타리 3기가 8.4625 / 8.7125 / 8.4625 로 **서로 다르게** 나온 것도
|
||
같은 이유다 — Y 위치가 달라 격자에 걸리는 모양이 달랐다.
|
||
|
||
### 대응 — boolean 이 없으면 격자를 안 쓴다
|
||
|
||
```
|
||
box 변의 곱
|
||
extrude 신발끈 면적 × 길이
|
||
cylinder πr²·길이
|
||
loft 심프슨. 꼭짓점을 선형 보간하면 단면적이 2차식이라 **정확**하다
|
||
union 멤버 bbox 가 안 겹치면 합 (면으로만 맞닿은 건 안 겹친 것)
|
||
그 밖 격자
|
||
```
|
||
|
||
결과가 전부 `0.0000 ✓` 로 떨어졌고, 중앙분리대(loft 포함)도 4.7970(격자) →
|
||
**4.8059(해석)** 로 바뀌어 모델러 4.8058 과 붙었다.
|
||
|
||
출력에 **어느 방법으로 나온 값인지** 같이 찍는다. 격자 값을 해석 값인 양
|
||
읽으면 다음 사람이 또 몇 % 를 쫓는다.
|
||
|
||
> 「의도 vs 결과」 대조는 **의도 쪽이 정확해야** 성립한다. 근사로 재는 쪽이
|
||
> 경보를 내기 시작하면 사람이 ✗ 를 무시하게 되고, 그때 진짜 사고가 지나간다.
|
||
|
||
---
|
||
|
||
## 4.997. 교훈 #28 — 같은 부재가 여러 벌이면 **복수형 선택기**가 필요하다
|
||
|
||
`loop(뷰, 폭, 높이)` 는 여럿이면 던진다. 하나를 집는 게 목적이니 그게 맞다.
|
||
그런데 **여러 벌인 것이 정상**인 경우가 있다 — 교량 폭원 단면의 방호울타리
|
||
440×1,320 이 3기였다.
|
||
|
||
하나씩 좌표로 집으면 그 순간 **손으로 적은 수**가 된다(규칙 9 위반).
|
||
`circles()` 가 이미 같은 문제를 풀고 있었으므로 같은 꼴로 `loops()` 를 뒀다.
|
||
개수 확인은 부르는 쪽이 한다 — 몇 기여야 하는지는 도면이 아니라 조립하는
|
||
사람이 아는 것이다.
|
||
|
||
```js
|
||
const bars = loops('분리교량 폭원', 440, 1320);
|
||
if (bars.length !== 3) throw new Error(`방호울타리가 3기여야 하는데 ${bars.length}기`);
|
||
```
|
||
|
||
### 안 닫힌 4기째를 어떻게 되살렸나
|
||
|
||
우측 상판 바깥 방호울타리는 선 하나가 빠져 폐곡선이 안 됐다
|
||
(`gaps` 가 `u=28,454` 를 「선 없음」으로 짚는다). **없는 게 아니라 안 닫힌 것**이다.
|
||
|
||
지어내지 않고 되살리는 방법 — **규칙을 실측으로 확인한 뒤 그 규칙을 쓴다.**
|
||
|
||
```
|
||
들임(inset) 좌측 상판 좌단 bars[0].u0 − deck.u0 = 10
|
||
좌측 상판 우단 deck.u1 − bars[1].u1 = 10 ← 같다. 규칙 확인
|
||
→ 우측 상판 우단도 10. 외면 u = 28,464 − 10 = 28,454
|
||
↑ 위 끊김 좌표와 **같은 값**. 이게 재구성의 검증이다
|
||
```
|
||
|
||
모양은 `bars[1]`(같은 방향 부재)을 그 자리로 옮겨 쓴다. 등급은 `opaque` 가
|
||
아니라 **`tolerant`** 로 둔다 — 도면 좌표를 그대로 옮긴 게 아니라 **만든** 것이라
|
||
그 사실이 남아야 한다.
|
||
|
||
---
|
||
|
||
## 4.998. 교훈 #29 — **한 시트에 날짜 다른 작업본이 겹쳐 그려져 있다**
|
||
|
||
이번에 제일 크게 틀린 것이다. 교량시종점 삽도의 뷰 상자를
|
||
`[5000, 64000, 31000, 77100]` 로 잡고 뽑았더니 나온 형상이 **어느 도면에도 없는
|
||
조합**이었다 — 교대라고 부른 것이 실은 옆 도형의 날개벽이었고, 기초와 교각이
|
||
통째로 빠졌다.
|
||
|
||
전체 배치를 뜨고서야 봤다. 한 시트에 **두 벌**이 있다.
|
||
|
||
```
|
||
주황 테두리 TYPE1 반일체식 · TYPE2 단부슬래브 · TYPE3 일체식 · TYPE4 골조식
|
||
자홍 교량 시·종점 (벽체 전면) · (흉벽 전면) · (슬래브 끝단) 각 2벌
|
||
```
|
||
|
||
두 벌이 **서로 어긋나게 포개져** 있어서, 상자를 그 경계에 걸치면 두 도형의
|
||
선이 같이 들어온다. 폐곡선은 멀쩡히 나오고 치수도 그럴듯해서 **틀렸다는 신호가
|
||
전혀 없다.** 그림을 보고서야 알았다.
|
||
|
||
표제란의 날짜 도장 두 개(`240820_교량시종점` · `240925_교량시종점`)가 힌트였는데,
|
||
처음엔 **개정 이력**으로 읽고 넘겼다. 실제로는 **작업한 날짜**이고 날짜마다
|
||
내용이 다르다.
|
||
|
||
### 대응
|
||
|
||
```
|
||
1. 시트 전체를 먼저 뜬다 도형이 몇 벌인지 · 겹치는지
|
||
2. 도형 테두리·표제로 벌을 가른다 테두리가 있으면 그게 경계다
|
||
3. 도형 하나를 뜨고 눈으로 확인한 뒤 무엇이 들어 있는지
|
||
4. 그 다음에 상자를 잡는다
|
||
```
|
||
|
||
> **그림 없이 상자를 잡지 마라.** 숫자만 보면 「폐곡선 25개, 치수 그럴듯」이라
|
||
> 통과한 것처럼 보인다. 이 실수는 검증표로 안 잡힌다.
|
||
|
||
---
|
||
|
||
## 4.999. 교훈 #30 — 「표기 치수가 없다」와 「못 쓴다」는 다르다
|
||
|
||
교량시종점 삽도를 **숫자 치수 0개**라는 이유로 「솔리드 대상 아님」으로 잘랐다.
|
||
**오판이었다.** 표기 치수가 없으면 *검증*을 못 하는 것이지 *추출*을 못 하는 게
|
||
아니다. 모델스페이스가 1:1 이면 좌표가 곧 mm 다.
|
||
|
||
그럼 1:1 인지 어떻게 아나 — **크기를 아는 표준 부재**로 확정한다. 도면 밖에서
|
||
온 눈금이다.
|
||
|
||
```
|
||
폐곡선 153×182 이 4개 ← 탄성받침 150×180. 다른 게 이 치수로 4개 나란할 이유가 없다
|
||
교대 벽체 4,680×6,506 교량 교대 자릿수
|
||
상부공 형고 1,421 지간 12,752 에 L/h≈9 — 반일체식으로 타당
|
||
접속슬래브 두께 383 표준 300~400
|
||
```
|
||
|
||
넷이 **동시에** 타당해지는 배율은 1 뿐이다. 10배만 달라도 받침이 1.5 m 나
|
||
15 mm 가 된다. 이 도면 하나로 교대·교각·받침·기초·말뚝이 전부 열렸다.
|
||
|
||
> 「대조할 상대가 없다」는 **그 도면 안에** 없다는 뜻이다. 다른 도면과 표준 치수는
|
||
> 여전히 상대가 된다. 도면군 전체를 봐야 판정이 선다.
|
||
|
||
---
|
||
|
||
## 4.9991. 교훈 #31 — 확대 상세는 **같은 뷰 안에 다른 축척으로** 들어온다
|
||
|
||
교량 종단면 안에 교좌받침을 크게 그린 상세가 있다(파란 테두리, `지간—지간`
|
||
표기). 같은 뷰 상자 안이라 폐곡선이 같이 나온다.
|
||
|
||
```
|
||
본 도형의 받침 153 × 182 ← 실제 치수
|
||
확대 상세의 받침 461 × 196 ← 축척이 다르다. 게다가 **가로·세로 배율이 다르다**
|
||
```
|
||
|
||
치수로 집으면 저절로 갈리지만(그게 「치수로 집는다」의 값이다), **갈린다는
|
||
사실을 확인해야** 안심할 수 있다. `fit()` 에 X 범위 검사를 넣었다.
|
||
|
||
```js
|
||
if (brg.some((b) => b.bbox[0] > 15000 && b.bbox[0] < 19000)) {
|
||
throw new Error('확대 상세 안의 받침이 섞였다 — 축척이 다르다');
|
||
}
|
||
```
|
||
|
||
같은 부재가 **1 mm 다르게** 나오는 것도 여기서 처음 봤다 — 교대 5,192 / 5,191,
|
||
받침 154 / 153×3. 도면이 다른 게 아니라 DWG 부동소수를 꼭짓점마다 **따로**
|
||
반올림하기 때문이다. 대칭 배치된 부재에서만 드러난다. `loops(…, tol)` 로 열되
|
||
기본값은 0 이다 — 여는 것은 부르는 쪽의 판단이다.
|
||
|
||
---
|
||
|
||
## 4.9992. 교훈 #32 — 진단 도구가 **본편과 다른 값**을 내면 도구를 못 믿는다
|
||
|
||
`probe` 가 상부공 폭을 12,951 로, `build` 가 12,752 로 냈다. 199 차이.
|
||
|
||
원인 둘이 겹쳐 있었다.
|
||
|
||
**(가) `probe.mjs` 가 `projectViews` 를 안 거쳤다.** `buildLoops` 를 직접 불러
|
||
개구 기호 제거·앵커·평면 변환을 건너뛰었다. `core/views.mjs` 머리말이
|
||
「복사해두면 뷰어가 거짓말한다」고 경고해둔 그대로다.
|
||
|
||
**(나) `buildLoops` 가 bbox 를 collinear 제거 *전* 링으로 쟀다.** 상부공 윗선이
|
||
199 mm 튀어나와 있는데(끝이 아무 데도 안 닿는 **매달린 변**), 면 순회가 거기까지
|
||
갔다 되돌아온다. 왕복이라 **면적은 0** 이지만 bbox 는 부풀었다.
|
||
|
||
```
|
||
ring(왕복 포함) 으로 잰 bbox 12,951 ← area 는 멀쩡해서 안 걸린다
|
||
dropCollinear 뒤로 잰 bbox 12,752 ← 이게 맞다
|
||
```
|
||
|
||
`area` 만 보면 절대 못 잡는다. **폭·높이만 틀리는 오류**가 있다.
|
||
|
||
---
|
||
|
||
## 4.9993. 교훈 #33 — 도면은 위치를 **중심선**으로도 준다
|
||
|
||
말뚝은 폐곡선이 안 된다(해치뿐). 그런데 위치는 도면에 있다 — `CS-CNTL` 레이어의
|
||
수직선이 말뚝 중심선이고, 윤곽 수직선 두 줄 간격이 지름이다.
|
||
|
||
```
|
||
지름 518 윤곽 수직선 두 줄 간격
|
||
위치 교대 3본 · 교각 4본 · 교대 3본
|
||
−1,744 · 213 · 1,574 / 11,007 · 12,195 · 13,538 · 14,726 / 24,604 · 25,965 · 27,922
|
||
간격 1,957 / 1,361 — 시점과 종점이 대칭이다 (이게 검증이다)
|
||
```
|
||
|
||
**`pick` 에 이걸 집는 수단이 없다.** 폐곡선·원·개구부만 집는다. 그래서 스크립트로
|
||
재서 설정에 적었고, 출처는 남겼지만 「도면에서 집는다」는 아니다.
|
||
`circles()` 를 만든 것과 같은 이유로 `centerlines()` 가 필요하다.
|
||
|
||
그리고 **폭방향 배치는 어느 도면도 안 준다.** 교대배치 삽도가 평면이지만
|
||
말뚝을 안 그리고, 종단면의 말뚝은 파단선으로 잘려 길이도 없다. 열 수·열 간격·
|
||
길이는 설계값으로 두고 그 사실을 남겼다.
|
||
|
||
> 형상이 없다고 정보가 없는 게 아니다. **해치·중심선·기호도 도면이 말하는 방식**이다.
|
||
> 개구 기호(교훈 #12)를 되살린 것과 같은 종류의 이득이 남아 있다.
|
||
|
||
---
|
||
|
||
## 5. 함정 모음
|
||
|
||
### DWG 쪽
|
||
|
||
- **유령 기하**: 정상 도면이 `x ≈ -48,400` 대에 몰려 있는데 레이어
|
||
`000-구조물점선`/`000-구조물실선` 일부가 `x ≈ -565,692`에 떨어져 있다.
|
||
bbox·축척 계산 전에 시트 영역 밖 좌표를 먼저 버려야 한다.
|
||
- **레이어 일관성 없음**: 구조체 외곽선이 뷰마다 다른 레이어에 있다.
|
||
`-전력구`가 대부분이지만 C-C는 `기본`, J-J는 `집수정`에 있다.
|
||
**레이어 하나로 필터하면 뷰가 통째로 빠진다.** 후보 레이어를 넉넉히 잡을 것.
|
||
- **끊긴 곳은 대부분 "선이 없어서"가 아니라 "레이어가 틀려서"다.** 이 도면에서
|
||
프로파일이 안 닫히는 지점 50곳을 조사했더니 **전부** 레이어 오분류였고,
|
||
도면에 실제로 없는 선은 하나도 없었다. 예:
|
||
|
||
```
|
||
단면 J-J 침사조·집수조 격실이 안 닫힘
|
||
(2100,-13475)-(2100,-15175) 격벽 우면 → 레이어가 `-환기구`
|
||
(2100,-15175)-(2250,-15325) 집수조 좌하단 모따기 → `-환기구`
|
||
(1900,-15175)-(1750,-15325) 침사조 우하단 모따기 → `-환기구`
|
||
```
|
||
|
||
**대응 비용이 100배 차이 난다** — 레이어 오분류는 설정 한 줄, 선 누락은 도면
|
||
수정이다. 진단 도구가 둘을 갈라주지 않으면 없는 도면 작업을 하게 된다.
|
||
- **가구(furniture)를 구체 프로파일에 넣지 말 것**: 케이블 지지대·철근이 같은
|
||
레이어에 있으면 폐곡선이 그 윤곽을 따라 돈다. C-C 내부 공동 꼭짓점이 114개가
|
||
나왔다. 뷰 분리용 레이어(`struct`)와 프로파일용 레이어(`body`)를 따로 둔다.
|
||
- **기존 닫힌 폴리라인은 쓸 게 없다**: LWPOLYLINE 140개 중 closed 96개인데
|
||
전부 해치 패턴/심볼 조각. 실제 구조체 프로파일은 0개.
|
||
- **좌표 노이즈**: `-500.00000000001455` 같은 값이 흔하다. mm 정수 반올림으로 흡수.
|
||
- **파싱 캐시 키를 잘못 잡으면 다른 도면이 같은 내용으로 나온다.** 두 번 다르게 밟았다.
|
||
① 파일명 슬러그 `[^\w.-]→_` — `\w` 가 한글을 안 먹어 `거더배치`·`교대배치`·`교량폭원`
|
||
이 전부 `____` 로 뭉개졌다. ② 고쳐서 쓴 **목록 인덱스** — 폴더에 파일이 하나
|
||
늘자 전부 한 칸씩 밀려 `[6]` 이 옆 도면 캐시를 읽었다. 인덱스는 **순서**에,
|
||
슬러그는 **문자 집합**에 의존한다. **경로 해시**를 쓴다.
|
||
캐시는 틀린 답을 **조용히 재사용**하므로 「같은 결과가 나온다」를 우연으로 넘기지 말 것.
|
||
- **acadrust의 `DIMENSION_LINEAR.pt10`은 0으로 온다**. 치수 정의점은 레이어
|
||
`DEFPOINTS`의 POINT 또는 익명 `*D` 블록 안에 있다. `actualMeasurement`와
|
||
`textMidPt`는 정상.
|
||
|
||
### egm(EGModeler) 쪽
|
||
|
||
`Modeler_AI/docs/egm-usage.md`에 있는 내용 중 이 작업에서 특히 물리기 쉬운 것:
|
||
|
||
- **`Extrude C=True` 단독은 항상 +Z로 돌출한다.** 커브 평면의 법선을 따르지 않는다.
|
||
XZ/YZ 평면 커브에는 반드시 방향을 명시할 것 — `Extrude D Y C=True {dist}`.
|
||
- **`Extrude` 전에 `SelByUids` 필수.** 선택 없이 실행하면 조용히 `created:[]`.
|
||
- **닫힌 `Polyline`(시작점=끝점)에 `Enter`를 붙이지 말 것.** 자동 종료되므로
|
||
남은 Enter가 다음 명령으로 새어나가 오작동한다. 열린 커브는 반대로 `Enter` 필수.
|
||
- **`SelByUid(s)`는 `Esc`로 닫아야 한다.** 안 닫으면 이후 명령이 전부 무시된다.
|
||
- **뷰 조작 명령(`SetViewmode`)을 `~` 접두로 보내면 끝나지 않는다.** `egm status` 의
|
||
`command` 가 `"SetViewmode"` 로 물린 채 남고, 그 뒤 `~SelAll`·`~Delete` 가 전부
|
||
조용히 무시된다(씬은 그대로인데 아무 일도 안 일어난다). **접두 없는 `Esc`** 로
|
||
푼다. `run_egm.py` 가 `Esc`·`SelAll`·`Delete`·`Zoom E` 를 접두 없이 보내는 이유가
|
||
이것이다 — 흉내낼 때 `~` 를 붙이지 말 것.
|
||
- 명령은 비동기다. `egm status`의 `command`가 `null`이 될 때까지 대기할 것.
|
||
- **`BooleanDifference`/`BooleanUnion`은 두 개만 먹는다.** 셋 이상 선택하면 나머지는
|
||
조용히 무시되고 씬에 남는다. 둘씩 접을 것 → 교훈 #14.
|
||
- **뷰어 모드면 모델링 명령이 등록조차 안 된다.** `egm status`는 정상 응답하는데
|
||
생성만 안 된다. `egm guide` 개수로 먼저 확인할 것 (39 = 뷰어) → 교훈 #15.
|
||
- **`Loft` 는 기본이 서피스다.** 그대로 두면 boolean 이 죽는다. `C=True`(Cap)로
|
||
양 끝을 막아 솔리드로 받는다. `Enter` 는 한 번이면 된다.
|
||
- **`Loft` 의 `L`(DeleteOriginal)은 기본이 true** 라 단면 커브가 자동으로 지워진다.
|
||
끄면 커브가 씬에 남아 `modelCount` 대조가 어긋난다. 건드리지 말 것.
|
||
- **별도 `Cap` 명령은 loft 뒤처리에 못 쓴다.** `Cap` 은 대상을 **제자리에서 고치므로**
|
||
새 uid 가 안 나온다(`Originator_ChangeGeometry`). 파이프라인이 결과를 못 잡는다.
|
||
- **`Loft`·`Sweep2` 는 선택 순서가 결과를 바꾼다.** `SelByUids` 에 단면 순서 그대로
|
||
넘길 것.
|
||
- **씬 초기화(`SelAll`→`Delete`)가 파이프를 죽인 적이 있다.** 프로세스는 살아 있고
|
||
창도 응답하는데(`Responding=True`, CPU 유휴) 명령만 전부 "응답하지 않습니다"가
|
||
된다. CLI 로는 못 살린다 — 모델러를 재시작한다.
|
||
- **`ExportByPath` 의 경로에 `_` 가 있으면 큰따옴표 없이는 조용히 실패한다.**
|
||
`_` 는 egm 스크립트 특수문자다(`SetObjectName` 에서 이미 겪었다). 아래 두 줄의
|
||
차이가 "867KB OBJ" 와 "아무 일도 안 일어남" 이다.
|
||
|
||
```sh
|
||
egm '~ExportByPath D:/MYCLAUDE_PROJECT/dwg2model/out/a.obj Enter' # 파일 없음
|
||
egm '~ExportByPath "D:/MYCLAUDE_PROJECT/dwg2model/out/a.obj" Enter' # 867KB
|
||
```
|
||
|
||
**신호가 하나도 없다.** `egm status` 정상, `result` 는 `{"command":"ExportByPath",
|
||
"created":[],"deleted":[]}` 인데 이 명령은 원래 아무것도 생성하지 않으므로
|
||
`created:[]` 가 정상이다. 파일 존재를 직접 확인하는 수밖에 없다 —
|
||
`run_egm.py` 의 `export_obj()` 가 mtime 까지 대조한다.
|
||
|
||
같이 알아낸 것: 내보내기 전에 `SelAll` 이 필요하고, **`.obj` 만 나온다**
|
||
(`.stl` 은 확장자를 받아들이지만 파일을 안 만든다). OBJ 는 `~SetObjectName`
|
||
이름이 `g` 그룹으로 나오고 `.mtl` 이 같이 떨어진다.
|
||
|
||
---
|
||
|
||
## 6. 작업 목록 — 무엇을 하고 있고, 무엇이 자동화 가능한가
|
||
|
||
파이프라인은 [`src/`](../src/) 에 있다. 각 단계가 무슨
|
||
일을 하고, 사람 판단이 필요한지 아닌지를 아래에 정리한다. **판단이 필요 없는
|
||
단계는 전부 자동화 대상**이다.
|
||
|
||
| # | 단계 | 하는 일 | 입력 → 출력 | 사람 판단 |
|
||
|---|---|---|---|---|
|
||
| 1 | **파싱** `src/core/parse.mjs` | DWG를 엔티티로 읽고 LINE·LWPOLYLINE을 선분으로 편다 | DWG → 엔티티 JSON | 없음 |
|
||
| 2 | **기준점 검출** `src/core/datum.mjs` | 여러 뷰에 반복되는 수평선에서 표고 기준을 찾고, 도면 치수와 교차검증 | 엔티티 → datum + 검증표 | 검증 항목 지정 |
|
||
| 3 | **뷰 분리** `src/core/segment.mjs` | 기하를 잘게 쪼갠 뒤 표제로 다시 묶어 뷰를 만든다 | 엔티티 → 뷰 목록 | 뷰 제목 ↔ 평면 매핑 |
|
||
| 4 | **끊김 진단** `src/gaps.mjs` | 프로파일이 안 닫히는 지점을 찾고 원인을 **레이어 오분류 / 선 없음**으로 가른다 | 뷰 → 끊긴 좌표 목록 | 없음 |
|
||
| 5 | **원인 규명** `src/inspect.mjs` | 뷰 영역의 모든 엔티티를 레이어 무관하게 훑는다. 왜 안 잡히는지 볼 때 | 뷰 → 레이어별 실태 | 판정 |
|
||
| 6 | **프로파일 추출** `src/core/loops.mjs` | 선분 뭉치를 평면 분할해 면(=재료 영역)을 전부 뽑는다 | 선분 → 폐곡선 목록 | 없음 |
|
||
| 7 | **결과 확인** `src/probe.mjs` | 뽑힌 면의 크기·계층·레이어를 표로 본다 | 뷰 → 검증표 | 도면 치수 대조 |
|
||
| 8 | **조립 스펙 작성** `src/ir.mjs` | 어느 면을 어느 축으로 밀고 무엇을 뺄지 기술 | 폐곡선 → IR JSON | **필요** |
|
||
| 9 | **3D 생성** `src/emit/egm.mjs` | IR을 모델러 명령으로 번역해 실행 | IR → 솔리드 | 없음 |
|
||
|
||
### 자동화 관점
|
||
|
||
**완전 자동 (1·4·5·6·9)** — 판단이 안 들어간다. 도면이 바뀌어도 그대로 돈다.
|
||
|
||
**설정만 주면 자동 (2·3·7)** — 레이어 이름, 뷰 제목, 검증 항목만 도면별로 준다.
|
||
이게 `src/drawings/*.mjs` 한 파일에 모여 있다. 코드는 안 건드린다.
|
||
|
||
**사람이 판단해야 하는 곳 (8)** — 딱 한 군데다. "이 면이 구체인가 공동인가",
|
||
"이 두 단면을 로프트로 이을 것인가 각각 압출할 것인가". 나머지는 기계가 한다.
|
||
|
||
즉 **새 도면을 받으면 설정 한 파일 + 조립 스펙 한 번**이면 끝나는 구조다.
|
||
반복 실행·검증·회귀 확인은 전부 스크립트가 맡을 수 있다.
|
||
|
||
### 반복 실행 훅으로 걸어두면 좋은 것
|
||
|
||
```
|
||
node src/run.mjs <도면> 검증 실패 시 exit 1 → CI 게이트로 바로 쓸 수 있다
|
||
node src/gaps.mjs <도면> 도면이 갱신될 때마다 끊김이 늘었는지 확인
|
||
node src/probe.mjs <도면> "<뷰>" 프로파일 치수 회귀 확인
|
||
```
|
||
|
||
`src/run.mjs` 는 검증이 깨지면 종료 코드 1을 낸다. 도면이 수정될 때마다 자동으로
|
||
돌려두면, 기준점이나 뷰 분리가 깨지는 순간 바로 잡힌다.
|
||
|
||
---
|
||
|
||
## 7. 검증 체크리스트
|
||
|
||
- [ ] **시트 전체를 먼저 떴는가** — 도형이 몇 벌인지, 날짜 다른 작업본이 겹쳐 있지 않은지
|
||
- [ ] 뷰 상자를 잡기 전에 **그 도형만 떠서 눈으로 확인**했는가
|
||
- [ ] 같은 뷰에 **확대 상세**가 섞여 있지 않은가 (축척이 다르다)
|
||
- [ ] **표기 치수가 숫자인가** (기호 `L·β·θ·VAR` 뿐이면 그 도면 안에는 대조 상대가 없다.
|
||
다만 **다른 도면·표준 부재 치수**는 여전히 상대가 된다 — 바로 버리지 말 것)
|
||
- [ ] **DWG 모델단위**를 `DIMENSION.actualMeasurement` ↔ 치수 글자로 확정했는가
|
||
- [ ] **도형 축척**이 1:1 인가. 아니면 표기 치수 2개로 역산해 교차검증했는가
|
||
- [ ] 시트 영역 밖 유령 기하를 버렸는가
|
||
- [ ] 표제 **중복률**을 세봤는가 (높으면 제목으로 뷰를 못 가른다)
|
||
- [ ] 레이어가 구조선과 치수선을 가르는가. 아니면 색·선종으로 걸렀는가
|
||
- [ ] 표고 기준을 **선**에서 잡았는가 (텍스트 아님)
|
||
- [ ] 기준선이 **없는** 도면이면 그렇다고 명시했는가 (아무 선이나 자동 선택 금지)
|
||
- [ ] 기준선 후보가 둘일 때 라벨 근접도로 갈랐는가
|
||
- [ ] 기준점이 도면 치수 최소 2개로 교차 검증됐는가
|
||
- [ ] 뷰 폭·높이가 도면 표기값과 맞는가
|
||
- [ ] 서로 다른 뷰의 심도가 일치하는가 (교차 검증)
|
||
- [ ] 끊긴 지점의 원인을 레이어/누락으로 갈랐는가
|
||
- [ ] 프로파일 치수가 기하·치수 양쪽에서 같은 값인가
|
||
- [ ] `Extrude`에 방향(`D X`/`D Y`)을 명시했는가
|
||
- [ ] loft 구간이 **꺾이는 자리마다** 끊겨 있는가 (한 loft 로 두 번 꺾을 수 없다)
|
||
- [ ] loft 두 단면의 꼭짓점 수가 같은가 (다르면 위상을 맞춰서 넣어야 한다)
|
||
- [ ] 실행 전 `egm guide` 개수가 39가 아닌가 (뷰어 모드 확인)
|
||
- [ ] 실행 후 `modelCount` 가 최종 파트 수와 같은가
|
||
- [ ] `node src/measure.mjs <도면>` 이 기준선과 맞는가 (비틀림·누락 컷을 잡는 가장 빠른 신호)
|
||
- [ ] 새 형상이면 기대값을 한 번 조립해 확인하고 `--save` 로 얼렸는가
|
||
|
||
---
|
||
|
||
## 변경 이력
|
||
|
||
| 날짜 | 내용 |
|
||
|---|---|
|
||
| 2026-08-05 | 최초 작성. 교훈 #1~#3, 함정 모음, 작업 순서 정리 |
|
||
| 2026-08-05 | 교훈 #4(DWG 분리하지 말 것), #5(캡처 이미지 좌표 사이드카) 추가 |
|
||
| 2026-08-05 | 교훈 #2 전면 개정 — 뷰 분리를 "교대 분할 + 제목 귀속"으로 교체. 11개 뷰 전부 도면 치수와 일치 |
|
||
| 2026-08-05 | 파이프라인을 `src/` 로 일반화 (설정/범용코드/IR 3계층). 기준점 자동 검출 + 자동 교차검증 |
|
||
| 2026-08-05 | 교훈 #5 보강 — 캡처는 역산 말고 "뷰 박스로 줌 + 축척에서 캔버스 역산" |
|
||
| 2026-08-05 | 프로파일 추출(평면 분할) 구현. 끊김 50곳 진단 → 실제 결함은 5곳, 그중 도면 수정 필요는 1mm 스냅 오차 1건 |
|
||
| 2026-08-05 | 교훈 #6 추가 — 레이어가 재질을 인코딩. 사양(THK·fck)은 위치별로 다르므로 뷰별로 읽어야 함. IFC 매핑 초안 |
|
||
| 2026-08-05 | **첫 3D 생성 성공** — 전력구 본체(구체·기초·잡석) 15.4m. DWG→IR→egm 경로 전 구간 관통 |
|
||
| 2026-08-05 | 단면 오배정 발견·수정 — 본체는 C-C(3,800×4,600), 스터브만 B-B. 스터브 2기·사다리 샤프트 2기 추가(7파트) |
|
||
| 2026-08-05 | 맨홀 #2 · 환기구 추가. 벽 누락 3건 수정 → 교훈 #8(절단기호↔평면), #9(봉투 union), #10(구간 경계), #11(수평단면 축) 추가 |
|
||
| 2026-08-05 | 전이부 구멍·환기구 다리 폭·상부 슬래브 개구부 2개 처리. 최종 11파트 |
|
||
| 2026-08-05 | 제작 절차를 [`2d-3d-process.md`](2d-3d-process.md) 로 분리. 이 문서는 노하우만 |
|
||
| 2026-08-05 | 독립 저장소로 이관 후 재현 실행. 교훈 #14(boolean 은 이항 — 커터 8개가 조용히 무시됐다), #15(뷰어 모드면 명령이 등록조차 안 된다) 추가 |
|
||
| 2026-08-06 | Loft 백엔드 구현 + 링 정규화. 전이부(B-B→C-C)를 챔퍼·단차·종방향 헌치까지 세움. 교훈 #16(loft 는 꺾인 자리를 못 만든다) 추가. 부피 대조로 검증 — 오차 0.0002% |
|
||
| 2026-08-06 | 부피 대조를 `src/measure.mjs` 로 자동화(손계산 → 회귀 검사). 기준선을 `baselines/` 에 커밋. `run_egm.py --export`. 함정 추가 — `ExportByPath` 경로의 `_` 는 따옴표 없이는 조용히 실패 |
|
||
| 2026-08-06 | CIRCLE 지원 (`collectCircles` → `circles()` → `cylinder`). 손으로 적은 `BORE_R=450` 이 도면 대조로 바뀜. 평면도 `v0` 앵커 필요성 발견. 교훈 #17 추가 |
|
||
| 2026-08-06 | 구조물만 남기고(11→5파트) 전이부를 `assembleDuct()` 로 공유. **맨홀 #2 부피가 2배로 나오는 기존 결함 발견** — `mh.outer` 자기교차. 자기교차 검사 + `report.mjs` 뷰어 추가. 교훈 #18 |
|
||
| 2026-08-07 | 교량시종점 삽도로 **2지간 교량 한 기** 관통 — 상부공·거더 10본·교대·날개벽·교각·받침·말뚝, 95파트 820.10 m³, 의도 대비 전부 0.0000. 교훈 #29~#33 추가 (날짜별 작업본 겹침 · 표기치수 0 ≠ 사용불가 · 확대 상세 혼입 · 진단도구 불일치 · 중심선) |
|
||
| 2026-08-07 | **두 번째 도면군(교량 설계지침 삽도 7장) 실증.** 1장 관통(중앙분리대 방호벽 4.8058 m³ — 모델러·해석 일치), 6장은 막힌 게이트와 이유를 기록. 코어 가정 7개가 깨져 전부 수정 — 모델단위·뷰별 축척·명시 뷰 영역·datum 없음·레이어 아닌 필터·겹치는 선분 폐곡선 유실·규칙 적용범위. 교훈 #20~#25 추가 |
|