# Model / Layout (Paper) 뷰어 이슈 — 원인 분석 및 처리 이력

| 항목 | 내용 |
|------|------|
| 작성 목적 | 샘플에서 재현·수정한 이슈를 **원본 `hmwebviewer`** 에 이식하기 위한 기술 메모 |
| 샘플 | `dwg-dxf-viewer-sample` |
| 원본 | `hmwebviewer` (`src/viewer2d/` …) |
| 주 재현 도면 | `C0060202-001-평면및종단면도(1)(대산방향STA.18+492.08-19+411.50).dwg` |
| 관련 도면 | `C0060203-*`, `BasicSample.dwg` (회귀) |
| 최종 정리 | 2026-07-29 |
| 상태 | **샘플 패치 완료** / 원본 뷰어 UI·Viewer2D 이식은 확인 후 |

### 제약

| 대상 | 정책 |
|------|------|
| **오픈소스 파서 본체 (acadrust 등 crates.io)** | **수정 금지** (unmodified 사용) |
| SORTENTSTABLE / 헤더 확장 export | 파서 수정 없이 **뷰어 규칙으로 대체** |
| 한글 인코딩 (CP949 mojibake) | 별도 문서 [`korean-dwg-text-encoding.md`](./korean-dwg-text-encoding.md) — **본 문서와 독립, 둘 다 필요** |

---

## 0. 이슈 한눈에 보기

| # | 증상 | 원인 | 처리 (샘플) | 수정 위치 |
|---|------|------|-------------|-----------|
| 1 | 글자·선이 한곳에 뒤섞여 겹침 | Model + Paper 동시 렌더 | 공간 필터 + Model/Layout 탭 | `cadSpaces.ts`, `Viewer2D.js`, `main.ts`, `index.html` |
| 2 | Layout에 도곽만 있고 평면/종단 비어 있음 | VIEWPORT 미투영 | Layout 시 Model을 VP 창에 투영·클립 | `Viewer2D.js` |
| 3 | Fit / 좌표가 CAD와 다름, (0,0)에 없음 | WCS 표시 + 이상치 bbox | **Model UCS 좌표** + 백분위 Fit | `Viewer2D.js` |
| 4 | 선형·종단에서 글자만 거대 | `minTextH = diag×0.0002` ≈ 250u | `min(diag×0.0002, medianHeight)` | `Viewer2D.js` |
| 5 | Layout에서 VP 내용이 도곽·주석 위 | VP를 paper 뒤에 append | VP 먼저 + `z=-1` + underlay 텍스트 | `Viewer2D.js` |
| 6 | 커서 좌표 표시 | — | 우하단 X/Y (Model=UCS) | `#coords`, `screenToWorld` |

---

## 1. DWG 공간 개념 (배경)

| 공간 | 블록 이름 예 | 역할 |
|------|----------------|------|
| Model Space | `*Model_Space` | 실좌표(측량 등) 본도면 |
| Paper Space / Layout | `*Paper_Space`, `*Paper_Space0`… | 출력 시트, 도곽, 주석, VIEWPORT |

- 엔티티 `ownerHandle` → 소속 블록 핸들.
- Layout은 보통 **VIEWPORT** 로 Model 일부를 시트 좌표에 투영한다.
- Model에는 **원거리 이중 좌표**가 흔함 (예: 원점 근처 종단 + X≈-436k 근처 선형).

### 재현 도면 규모 (C0060202-001, 참고)

| Space | handle (dec / hex) | 엔티티 수 (대략) | 좌표 |
|-------|--------------------|------------------|------|
| `*Model_Space` | 31 / `1f` | ~17,377 | 측량·다중 군집 |
| `*Paper_Space` | 210 / `d2` | ~200+ | 시트 (~1.7e3–2.6e3) |
| VIEWPORT | paper 소유 | 3 | overall 1 + 종단·평면 창 2 |

---

## 2. 이슈 #1 — Model + Paper 동시 렌더

### 2.1 현상

다색 TEXT가 중앙에 뭉치고, 기하·주석이 읽기 불가.  
CAD에서는 Layout 활성 시 정상 시트.

### 2.2 원인

기존 `Viewer2D.load()`:

1. `*Model_Space` / `*Paper_Space` 를 space 로 인식
2. **사용자 블록 정의**만 메인 루프에서 스킵 (INSERT 전개용)
3. Model·Paper **둘 다** 메인 루프에서 그림

→ 좌표계·스케일이 다른 두 공간을 한 씬에 합치고 `fit()` → 파국.

### 2.3 처리

| 파일 | 내용 |
|------|------|
| `src/viewer2d/cadSpaces.ts` | `listCadSpaces`, `pickDefaultSpace`, `buildAllowedOwnerHexes` |
| `Viewer2D.js` | `allowedOwners` 필터, `setSpace` / `getSpaces` / `getActiveSpace` |
| `main.ts` + `index.html` | **Model / Layout** 탭, 로드 시 기본 공간 |

**기본 탭:** Paper에 엔티티 있으면 Layout, 없으면 Model.

```
C0060202-001 → Layout
BasicSample (paper 0) → Model
```

---

## 3. 이슈 #2 — Layout이 비어 보임 (VIEWPORT)

### 3.1 현상

공간 분리 후 Layout은 도곽·주석만 있고, CAD처럼 평면·종단이 없음.

### 3.2 원인

- Paper 엔티티만으로는 시트 장식/주석 수준.
- 본도면은 Model에 있고, CAD는 **VIEWPORT** 로 비춤.
- 뷰어가 Layout에서 Model을 VP에 투영하지 않으면 창이 비어 보임.

### 3.3 VIEWPORT 데이터

acadrust 타입에는 `Viewport` 가 있음.  
뷰어는 parseResult 의 `type: "VIEWPORT"` 필드를 사용한다.

재현 도면 VP 예:

| 역할 | size (대략) | twist | 비고 |
|------|-------------|-------|------|
| overall paper | 시트 전체 | 0 | 스킵 (모델 창 아님) |
| 종단 창 | ~840×302 | 0 | Model 투영 |
| 평면 창 | ~840×215 | ~1.28 rad | Model 투영 |

**overall 판별:** `id===1` 또는 (scale≈1 ∧ viewTarget≈0 ∧ viewCenter≈paper center).

### 3.4 처리 (`Viewer2D.js`)

Layout 활성 시:

1. (순서: 아래 이슈 #5) Model을 각 활성 VIEWPORT에 투영
2. 변환 (2D / top, ODA·acadrust writer 규약):

```
DCS   = R(+twist) * (model − viewTarget)
paper = center + (paperHeight / viewHeight) * (DCS − viewCenter)
```

3. paper 사각형 Cohen–Sutherland 클리핑  
4. `_isModelViewport` / `_viewportModelToPaper` / `_emitModelThroughViewport`

### 3.5 파서 정책

- **acadrust(오픈소스 본체)는 수정하지 않는다.**
- VIEWPORT JSON 방출이 래퍼/기존 빌드에 이미 있으면 뷰어만 사용.
- 추가 파서 기능(SORTENTS 등)은 **넣지 않음** → 뷰어 규칙으로 충분.

---

## 4. 이슈 #3 — Zoom Fit

### 4.1 확정: 가시 엔티티 AABB

| 항목 | 내용 |
|------|------|
| 범위 | 활성 Model/Layout + 숨긴 레이어 제외, 렌더 시 `expand` |
| 이상치 | 샘플 **0.5%–99.5%** 로 극단 점 제거 → `_contentBox` |
| `fit()` | **`_contentBox` 우선** (씬 재계산보다 로드 시점과 동일) |

### 4.2 가로로 매우 긴 Model (C0060202 / bb.dwg)

실측 기하(백분위 후) 대략:

| | WCS | CAD UCS |
|--|-----|---------|
| 폭 | ~438 000 | ~-437 000 … ~1 000 |
| 높이 | **~2 900** | ~-2 950 … 0 |
| 종횡비 | **~150 : 1** | |

화면 비율(~2:1)로 **classic contain** 하면:

- 세로 시야를 폭에 맞춰 키움 → 내용 높이가 화면의 **~1%**  
- **수 픽셀 두께의 가로 선**처럼 보임 (하늘색 박스도 상단에 얇은 선)

`bb.dwg`는 같은 종횡비의 단순 박스 2개라 식별 가능했고, 원본 1.7만 엔티티는 한 줄로 뭉개짐.

### 4.3 처리 (극단 스트립 Fit)

```
contentAspect = bw / bh
if contentAspect > screenAspect × 8:   // 매우 가로로 김
    vh = bh × pad          // Y를 화면에 채움
    vw = vh × screenAspect // 좌우는 잘라 보고 패닝
else if contentAspect < screenAspect / 8:
    // 매우 세로로 김 → 폭 채움
else:
    // 일반 contain
```

→ 선형/종단 구간이 **세로로 화면에 채워짐**. 전체 연장은 좌우 팬.

CAD status 참고 extents(Y ±10만)와 파서 실측(Y 약 3천)이 다를 수 있음 — Fit은 **파서가 그린 가시 점** 기준.

---

## 5. 이슈 #4 — 글자만 거대 (minTextH)

### 5.1 현상

선형(~-436802, -1949) 및 종단 근처에서 `MOT`/`FLOW`/수치가 등고선보다 훨씬 큼.

### 5.2 원인

| 항목 | C0060202-001 Model |
|------|---------------------|
| CAD TEXT height | ~0.3 ~ 3.5 (중앙값 ~3, FLOW≈0.3) |
| 전체 bbox 대각선 | ~1.25×10⁶ |
| 구 `minTextH = diag × 0.0002` | **~250** |

`_drawTexts` → `Math.max(t.height, minTextH)` 로 **모든 라벨이 높이 ~250** 강제.

작은 시트 도면용 “fit 시 가독성” floor가 측량 Model에서 폭주한 것.

### 5.3 처리

```js
minTextH = Math.min(diagSize * 0.0002, medianNativeHeight);
```

- 측량 Model: floor ≤ median → **사실상 CAD 높이 유지**
- 작은 용지 좌표: diag floor가 작으면 기존과 유사

---

## 6. 이슈 #5 — Layout draw order (VIEWPORT가 위)

### 6.1 현상

VIEWPORT로 올린 Model 기하/라벨이 **도곽·NOTE 위**에 그려짐.

### 6.2 원인 (뷰어)

기존: paper 패스 후 VP 투영을 **버퍼 뒤**에 append → VP가 위.

### 6.3 DWG draw-order 메타 (참고만)

| 소스 | 의미 | 정책 |
|------|------|------|
| `SORTENTSTABLE` / `ACAD_SORTENTS` | sort handle 낮을수록 아래 | acadrust에 타입 있음 |
| Header `SORTENTS` | 정렬 플래그 | — |
| parseResult 엔티티 | zOrder 등 **없음** | — |

**오픈소스 파서를 고치지 않으므로 SORTENTS export는 하지 않는다.**  
Layout 관례로 고정:

> **VIEWPORT 안 Model 내용 < Paper 주석·도곽**

### 6.4 처리

1. VP Model 투영을 **paper 엔티티 루프보다 먼저** 방출  
2. VP 선분 `z = -1`, paper `z = 0` (카메라 +Z 기준 뒤/앞)  
3. 텍스트: `underlay` (VP) / 일반 (paper) 분리, `renderOrder` 0 → 1  
4. slug/스프라이트 배치도 underlay 먼저 추가  

---

## 7. 커서 좌표 / Fit 좌표계 (UCS)

### 7.1 현상

- Model에서 우하단 좌표로 (0,0) 근처를 찾아도 기하가 없음  
- Fit 후 표시 좌표가 CAD status bar 와 다름  

### 7.2 원인

| 프레임 | 내용 |
|--------|------|
| 기하 저장 (파서 WCS) | 예: 선형 클러스터 ≈ (162065, 476725) |
| CAD Model UCS 원점 (`vars.ucsOrigin`) | ≈ (598948, 478643) |
| CAD에 보이는 좌표 | `UCS = WCS − ucsOrigin` → 선형 ≈ **(-436883, -1918)** |

뷰어가 **WCS를 그대로** 읽어 주면 CAD 사용자 좌표와 어긋난다.  
또한 Fit bbox에 극단 이상치(수 점)가 포함되면 중심이 빈 공간으로 갈 수 있다.

CAD 참고 extents (Model, 사용자 측정):

```
X: -439601.13 … 4412.05
Y: -100418.91 … 99422.03
```

### 7.3 처리

1. **좌표 표시 (Model):** WCS → **model UCS**  
   `cad = project(WCS − ucsOrigin)` (축 단위 직교 시 subtract origin)  
   Layout/Paper: identity (시트 좌표)  
2. **Pick/measure:** 내부 **WCS** (`_screenToWcs`)  
3. **Fit:** 가시 기하 **0.5%–99.5% 백분위** AABB (이상치 제거)  
4. 스크린→WCS: ortho frustum × camera right/up (view-twist 포함)

### 7.4 검증용 심플 도면 `bb.dwg`

Model 엔티티 5개 (사각형 2 + 연결 poly + hatch 2). `ucsOrigin ≈ (598948, 478643)`.

| 덩어리 | WCS (대략) | CAD UCS (대략) |
|--------|------------|----------------|
| 선형 쪽 박스 | (161969…162448, 475696…476795) | **(-436979…-436500, -2947…-1848)** |
| 원점 쪽 박스 | (598948…600030, 477765…478643) | **(0…1081, -878…0)** — **UCS (0,0)에 모서리** |
| Fit CAD 범위 | — | X ≈ -436979…1081, Y ≈ -2947…0 |

**확인 절차**

1. `bb.dwg` 열기 → **Model** 탭 (기본이 Layout이면 반드시 Model로)  
2. Fit → 우하단 좌표가 대략 위 UCS 범위 안  
3. 커서 오른쪽 박스 모서리 ≈ **X=0, Y=0**  
4. 왼쪽 박스 ≈ **X=-436900, Y=-2400** 부근  

CAD WCS (0,0) 에는 기하가 없음. CAD status의 (0,0)은 **UCS 원점**이다.

---

## 8. 검증 체크리스트

| 확인 | 기대 |
|------|------|
| C0060202-001 기본 탭 | Layout |
| Layout | 도곽 + 평면/종단 VP, 주석이 VP 위, 한글(인코딩 패치 병행) |
| Model | ~17k 엔티티, 공간 필터로 paper 제외 |
| Fit | **현재 가시** 전체 AABB (이중 좌표면 둘 다 포함) |
| Model 줌 인 선형/종단 | 글자 크기 ≈ CAD (거대 빌보드 없음) |
| BasicSample | Model 기본, 회귀 없음 |
| 우하단 Model | 커서 좌표 ≈ CAD UCS (선형 ~-436k 등) |
| Fit 후 중심 | 빈 (0,0) WCS 가 아닌 실제 기하 범위 |

---

## 9. 원본 `hmwebviewer` 이식 가이드

| 순서 | 작업 |
|------|------|
| 1 | `cadSpaces.ts` 신규 복사 |
| 2 | `Viewer2D.js` — 공간 필터, VP 투영, draw order, minTextH, fit/AABB, 좌표 API **diff 이식** (통째 덮어쓰기 비권장) |
| 3 | `Viewer2D.d.ts` 동기화 |
| 4 | 호스트 UI: Model/Layout 탭, `pickDefaultSpace` + `spaceHandle` |
| 5 | (선택) 우하단 좌표 표시 |
| 6 | `fixDwgKoreanText` 는 [한글 문서](./korean-dwg-text-encoding.md) 대로 별도 |
| 7 | C0060202-001 + BasicSample 회귀 |

**하지 말 것**

| 금지 | 이유 |
|------|------|
| acadrust 등 오픈소스 파서 수정 | 정책 |
| SORTENTS를 위해 파서 개조 | 뷰어 규칙으로 대체 |
| Model+Paper 동시 렌더 복귀 | 이슈 #1 재발 |
| 밀도 기반 Fit 재도입 | 이슈 #3 에서 철회 |
| `minTextH = diag×0.0002` 단독 | 이슈 #4 재발 |

---

## 10. 잔여 / 후속 (뷰어 측만)

1. VIEWPORT 안 솔리드 HATCH/SOLID — 윤곽 위주 근사  
2. 다중 named Layout 탭 캡션 (블록명 `*Paper_Space*` 이상)  
3. TILEMODE/CTAB 을 파서 없이 알 수 없으면 현재 휴리스틱 유지  
4. 비직교 VIEWPORT (3D viewDirection)  
5. (선택) Model 이중 군집 점프 UI  
6. (선택) Fit “전체/선택” 분리는 요구 시만  

---

## 11. 한 줄 요약

> **Model/Paper를 나눠 그리고, Layout은 VIEWPORT로 Model을 투영하되 paper 아래에 깐다.  
> Fit은 가시 엔티티 bbox. 글자 높이는 CAD median을 넘기지 않는다.  
> 오픈소스 파서는 수정하지 않으며, draw-order는 Layout 관례로 처리한다.**

---

## 12. 참고 파일 (샘플)

| 경로 | 역할 |
|------|------|
| [`../src/viewer2d/cadSpaces.ts`](../src/viewer2d/cadSpaces.ts) | 공간 목록·기본 탭·owner 클로저 |
| [`../src/viewer2d/Viewer2D.js`](../src/viewer2d/Viewer2D.js) | 필터, VP 투영, draw order, minTextH, fit, 좌표 |
| [`../src/viewer2d/Viewer2D.d.ts`](../src/viewer2d/Viewer2D.d.ts) | 타입 |
| [`../src/main.ts`](../src/main.ts) | 탭·좌표 UI 글루 |
| [`../index.html`](../index.html) | `#spaces`, `#coords` |
| [`korean-dwg-text-encoding.md`](./korean-dwg-text-encoding.md) | CP949 한글 깨짐 (병행 필수) |
