feat: implement OLE2Frame embedded image parsing and exact coordinate rendering
This commit is contained in:
@@ -103,6 +103,18 @@ Vite `?url` import 로 로드됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 알려진 이슈 / 원본 반영 메모
|
||||
|
||||
| 이슈 | 문서 |
|
||||
|------|------|
|
||||
| 구형 한글 DWG TEXT 깨짐 (CP949→Latin-1 mojibake) | [`docs/korean-dwg-text-encoding.md`](./docs/korean-dwg-text-encoding.md) |
|
||||
| Model + Paper 동시 렌더로 글자 겹침 / Layout 탭 | [`docs/model-paper-space.md`](./docs/model-paper-space.md) |
|
||||
|
||||
샘플 패치: `fixDwgKoreanText`, `cadSpaces` + Viewer2D 공간 필터, Model/Layout 탭.
|
||||
원본 `hmwebviewer` 반영 절차는 각 문서를 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 라이선스 메모
|
||||
|
||||
| 조각 | 라이선스 |
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
# 한글 DWG 텍스트 깨짐 — 원인 분석 및 원본 반영 가이드
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 작성 목적 | 샘플에서 재현·수정한 이슈를 **원본(`hmwebviewer`)** 에 반영하기 위한 기술 메모 |
|
||||
| 샘플 저장소 | `dwg-dxf-viewer-sample` (본 문서 위치) |
|
||||
| 원본 저장소 | `hmwebviewer` (`src/viewer2d/` 동기화 대상) |
|
||||
| 재현 도면 | `C0060203-002-굴착및시공순서도(2차로,자연환기300m이상).dwg` 등 구형 한글 CAD 도면 |
|
||||
| 대조 도면 | `BasicSample.dwg` (이미 정상 유니코드 한글) |
|
||||
| 샘플 수정일 | 2026-07-29 |
|
||||
| 상태 | 샘플 패치 완료 / 원본 미반영 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 현상
|
||||
|
||||
웹 2D 뷰어에서 일부 DWG를 열면 **도형·치수 기하, 영문 TEXT는 정상**인데 **한글 TEXT/MTEXT·타이틀블록 문구·NOTE** 등이 다음과 같이 깨져 보인다.
|
||||
|
||||
| 화면/파서 출력 (깨짐) | 정상 의미 |
|
||||
|----------------------|-----------|
|
||||
| `½Ã°ø½Ã` | 시공시 |
|
||||
| `·Ïº¼Æ®` | 록볼트 |
|
||||
| `¼ôÅ©¸®Æ®` | 숏크리트 |
|
||||
| `±¼Âø ¹× ½Ã°ø¼ø¼µµ` | 굴착 및 시공순서도 |
|
||||
| `Á߽ɼ±(ÀϹÝ)` (라인타입 설명) | 중심선(일반) |
|
||||
|
||||
외형상 “폰트가 없다 / 글리프가 없다”와 혼동하기 쉽지만, **글리프 부재(tofu □)** 가 아니라 **잘못된 유니코드 코드포인트가 그대로 그려진 것(mojibake)** 이다.
|
||||
|
||||
### 자주 하는 오진
|
||||
|
||||
| 가설 | 실제 |
|
||||
|------|------|
|
||||
| `dist/fonts` / `public/fonts` 에 돋움이 없어서 | 폰트 폴더에는 `NanumGothic-Regular.ttf`(한글 포함)가 있음 |
|
||||
| Style `Standard` + 폰트 `돋움` 미매핑 | 뷰어는 스타일→폰트 매핑 없이 **단일 TTF** 로 그림. 돋움 없어도 코드포인트만 맞으면 한글 표시 가능 |
|
||||
| slugText / opentype 버그 | 동일 파이프라인에서 `BasicSample.dwg` 한글은 정상 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 파이프라인 위치
|
||||
|
||||
```
|
||||
DWG bytes
|
||||
→ acadrust WASM (parse_dwg) ← 여기서 문자열이 이미 깨지거나 정상으로 나옴
|
||||
→ CadParseResult (entities/tables/…)
|
||||
→ [샘플 패치] fixDwgKoreanText ← 복원 계층 (원본에는 아직 없음)
|
||||
→ Viewer2D.load()
|
||||
→ slugText (NanumGothic TTF)
|
||||
→ WebGL
|
||||
```
|
||||
|
||||
- **DXF 경로**는 텍스트 파일 디코딩 단계에서 코드페이지를 고려한다.
|
||||
- 샘플: `src/viewer2d/decodeDxf.ts` (`$DWGCODEPAGE` / 949 → `euc-kr` 등)
|
||||
- 원본: `hmwebviewer/src/viewerController.ts` 내 유사 `decodeDxf`
|
||||
- **DWG 경로**는 바이너리 → WASM → JSON 문자열이며, 파싱 직후 한글 코드페이지 보정 계층이 없었다.
|
||||
|
||||
관련 모듈:
|
||||
|
||||
| 역할 | 샘플 경로 | 원본 경로 |
|
||||
|------|-----------|-----------|
|
||||
| DWG 파서 진입점 | `src/viewer2d/dwgParser.ts` | `hmwebviewer/src/viewer2d/dwgParser.ts` |
|
||||
| WASM 래퍼 | `src/viewer2d/acadrustParser.ts` | 동일 |
|
||||
| 텍스트 렌더 | `src/viewer2d/slugText.ts` | 동일 |
|
||||
| 한글 복원 (샘플만) | `src/viewer2d/fixDwgKoreanText.ts` | **없음 → 이식 대상** |
|
||||
|
||||
---
|
||||
|
||||
## 3. 원인 분석
|
||||
|
||||
### 3.1 근본 원인 (Root cause)
|
||||
|
||||
**구형/한글 CAD DWG에 저장된 CP949(Windows-949) 계열 바이트 시퀀스를, acadrust 파서(또는 그 문자열 경계)가 Latin-1/바이트-단위 유니코드로 승격해 버린 것.**
|
||||
|
||||
즉:
|
||||
|
||||
```
|
||||
원본 바이트 (CP949) --잘못된 해석--> U+00xx Latin-1 문자들 --렌더--> 깨진 화면
|
||||
예: EC 8B 9C ... (시) 가 ½ Ã ° ø … 같은 문자로 보임
|
||||
```
|
||||
|
||||
복원 공식 (검증됨):
|
||||
|
||||
```text
|
||||
broken.encode('latin1') → bytes
|
||||
bytes.decode('cp949' | 'euc-kr' | 'windows-949') → 정상 한글
|
||||
```
|
||||
|
||||
브라우저/Node `TextDecoder` 라벨 우선순위 (샘플 구현):
|
||||
|
||||
1. `cp949`
|
||||
2. `windows-949`
|
||||
3. `euc-kr`
|
||||
|
||||
### 3.2 왜 도면마다 다른가
|
||||
|
||||
| 도면 | 파서 직후 TEXT | 원인 해석 |
|
||||
|------|----------------|-----------|
|
||||
| `BasicSample.dwg` | `설계사`, `시공사` 등 **정상 한글 음절** | 이미 유니코드/UTF 계열로 저장·추출됨 |
|
||||
| `C0060203-002-….dwg` | 한글 음절 **0건**, Latin-1 고위 문자 다수 | CP949 레거시 저장 + 오해독 |
|
||||
|
||||
동일 뷰어·동일 폰트에서 결과가 갈리므로 **렌더러/폰트 문제가 아님**이 확정된다.
|
||||
|
||||
### 3.3 영향 범위 (문자열 필드 전반)
|
||||
|
||||
TEXT/MTEXT 본문뿐 아니라 같은 인코딩으로 들어간 메타 문자열도 깨진다.
|
||||
|
||||
- `entities[].text` (TEXT / MTEXT / ATTRIB 등)
|
||||
- `tables.lineTypes[].description` (예: 중심선, 파선 한글 설명)
|
||||
- `tables.blocks[].name` (예: `CXGLOGO-EX흑백` → `CXGLOGO-EXÈæ¹é`)
|
||||
- 기타 파서가 문자열로 넘기는 테이블/속성 필드
|
||||
|
||||
→ **필드 단위 패치보다 parseResult 트리 전체 문자열 walk** 가 안전하다.
|
||||
|
||||
### 3.4 부수 관찰 (이번 깨짐의 주원인은 아님)
|
||||
|
||||
| 관찰 | 의미 |
|
||||
|------|------|
|
||||
| `tables.styles` 가 빈 배열 | 스타일/폰트명(돋움) 정보를 뷰어가 쓰지 못함. 장기 개선 항목 |
|
||||
| 뷰어 고정 폰트 `NanumGothic-Regular.ttf` | 한글 글리프 보유. 코드포인트만 맞으면 표시됨 |
|
||||
| UI 파일명·OS 경로 한글 | OS 유니코드 경로라 정상일 수 있음. DWG 내부 텍스트와 무관 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 증거 (재현 절차)
|
||||
|
||||
### 4.1 파서만 돌려 TEXT 통계
|
||||
|
||||
WASM 초기화 후 `parse_dwg` 결과에서:
|
||||
|
||||
```js
|
||||
// TEXT/MTEXT 중 한글 음절 포함 여부
|
||||
/[\uAC00-\uD7A3]/.test(entity.text)
|
||||
```
|
||||
|
||||
문제 도면 예 (패치 전):
|
||||
|
||||
- hangul: **0**
|
||||
- 깨진 Latin: 다수 (`½Ã°ø…` 패턴)
|
||||
|
||||
패치 후:
|
||||
|
||||
- hangul: 다수 (예: 130)
|
||||
- NOTE 샘플: `2. 시공시 막장관찰, 계측분석결과, …`
|
||||
- 라인타입: `중심선(일반) Center …`
|
||||
- 블록: `CXGLOGO-EX흑백`
|
||||
|
||||
### 4.2 단위 변환 검증
|
||||
|
||||
| 입력 | `latin1` 바이트 재해석 → CP949 |
|
||||
|------|-------------------------------|
|
||||
| `½Ã°ø½Ã` | `시공시` |
|
||||
| `·Ïº¼Æ®` | `록볼트` |
|
||||
| `NOTE` | `NOTE` (변경 없음) |
|
||||
| `설계사` | 휴리스틱상 **스킵** (이미 한글) |
|
||||
|
||||
### 4.3 음성 대조
|
||||
|
||||
`BasicSample.dwg` 에 동일 복원기를 적용해도 `설계사` / `시 공 사` 등이 **변하지 않아야** 한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 샘플에 적용한 처리 방법
|
||||
|
||||
### 5.1 신규 모듈
|
||||
|
||||
`src/viewer2d/fixDwgKoreanText.ts`
|
||||
|
||||
- `fixCp949Mojibake(s: string): string`
|
||||
단일 문자열 복원
|
||||
- `fixDwgKoreanText<T>(root: T): T`
|
||||
parseResult 딥 워크, in-place 수정 후 동일 참조 반환
|
||||
|
||||
### 5.2 휴리스틱 (중요 — 정상 도면 보호)
|
||||
|
||||
다음이면 **원문 유지**:
|
||||
|
||||
1. 빈 문자열
|
||||
2. 이미 한글 음절(`U+AC00–U+D7A3`) 또는 자모 포함
|
||||
3. 순수 ASCII (모든 코드포인트 ≤ 127)
|
||||
4. Latin-1 범위를 넘는 코드포인트 포함 (진짜 유니코드)
|
||||
5. 한국 코드페이지 디코더 없음 / 디코드 실패
|
||||
6. 디코드 결과에 한글이 **없거나** `U+FFFD` 포함
|
||||
|
||||
다음일 때만 **교체**:
|
||||
|
||||
- 고위 Latin-1(>127) 포함 **그리고**
|
||||
- CP949/EUC-KR 디코드 성공 **그리고**
|
||||
- 결과에 한글 음절/자모 포함 **그리고**
|
||||
- 치환 문자 없음
|
||||
|
||||
### 5.3 삽입 지점
|
||||
|
||||
`src/viewer2d/dwgParser.ts`:
|
||||
|
||||
```ts
|
||||
import { fixDwgKoreanText } from './fixDwgKoreanText';
|
||||
|
||||
export async function parseDwgBuffer(bytes: Uint8Array): Promise<CadParseResult> {
|
||||
const { initAcadrustParser, parseDwgAcadrust } = await import('./acadrustParser');
|
||||
await initAcadrustParser();
|
||||
return fixDwgKoreanText(parseDwgAcadrust(bytes));
|
||||
}
|
||||
```
|
||||
|
||||
- Viewer2D / slugText / 폰트 설정 변경 **불필요**
|
||||
- DXF 경로 변경 **불필요** (이미 별도 디코드)
|
||||
|
||||
### 5.4 왜 WASM 내부 수정이 아닌가 (1차 선택 이유)
|
||||
|
||||
| 접근 | 장점 | 단점 |
|
||||
|------|------|------|
|
||||
| **JS 후처리 (채택)** | 배포 단순, acadrust 크레이트 재빌드 없음, 샘플↔원본 이식 쉬움 | 근본 수정은 아님. 휴리스틱 오탐 가능성(낮음) |
|
||||
| acadrust/코드페이지 수정 | 파서 정합 | MPL·Rust/WASM 빌드 체인, 검증 범위 큼 |
|
||||
| 스타일→돋움 TTF 매핑 | WYSIWYG 개선 | **이번 mojibake를 해결하지 못함** |
|
||||
|
||||
중장기적으로는 WASM/파서가 DWG 코드페이지(또는 문자열 유니코드 플래그)를 올바르게 반영하는 것이 이상적이다.
|
||||
그 전 단계의 **제품 적용 가능한 완화책**이 JS 후처리다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 원본(`hmwebviewer`) 반영 절차
|
||||
|
||||
원본 `src/viewer2d/dwgParser.ts` 는 샘플 패치 전과 동일하게 `parseDwgAcadrust` 결과를 그대로 반환한다.
|
||||
|
||||
### 6.1 최소 이식 (권장 1차)
|
||||
|
||||
1. 샘플에서 복사
|
||||
- `dwg-dxf-viewer-sample/src/viewer2d/fixDwgKoreanText.ts`
|
||||
- → `hmwebviewer/src/viewer2d/fixDwgKoreanText.ts`
|
||||
2. `hmwebviewer/src/viewer2d/dwgParser.ts` 에 샘플과 동일하게
|
||||
`fixDwgKoreanText(parseDwgAcadrust(bytes))` 연결
|
||||
3. `parseDwgBuffer` 를 거치지 않는 다른 진입점이 있으면 **동일 후처리** 적용
|
||||
- 예: 직접 `parseDwgAcadrust` / `parse_dwg` 호출부 검색
|
||||
4. 검증
|
||||
- 문제 DWG: NOTE·공정 문구 한글
|
||||
- `BasicSample` 또는 기존 유니코드 한글 DWG: 회귀 없음
|
||||
- 영문·숫자·`Sealing` 등 ASCII 유지
|
||||
5. (선택) 단위 테스트
|
||||
- `fixCp949Mojibake('½Ã°ø½Ã') === '시공시'`
|
||||
- `fixCp949Mojibake('설계사') === '설계사'`
|
||||
- `fixCp949Mojibake('NOTE') === 'NOTE'`
|
||||
|
||||
### 6.2 동기화 정책
|
||||
|
||||
README 기준: `hmwebviewer/src/viewer2d` 가 본가이고 샘플은 형제 복사본이다.
|
||||
이번 이슈는 **샘플에서 먼저 수정**했으므로, 원본 반영 후 필요 시 샘플과 다시 맞춘다.
|
||||
|
||||
### 6.3 반영 체크리스트
|
||||
|
||||
- [ ] `fixDwgKoreanText.ts` 원본 트리에 추가
|
||||
- [ ] `dwgParser.ts` 후처리 연결
|
||||
- [ ] 직접 WASM 호출 경로 없음 확인
|
||||
- [ ] 문제 한글 DWG 수동 확인
|
||||
- [ ] 정상 유니코드 한글 DWG 회귀 확인
|
||||
- [ ] (선택) DXF ANSI_949 도면도 기존 `decodeDxf` 동작 유지 확인
|
||||
- [ ] (선택) `tables.styles` / 폰트 매핑은 **별 이슈**로 분리
|
||||
|
||||
---
|
||||
|
||||
## 7. 하지 말아야 할 것
|
||||
|
||||
| 비권장 | 이유 |
|
||||
|--------|------|
|
||||
| 돋움 TTF만 추가하고 끝 | 문자열이 깨진 상태면 돋움으로도 동일 mojibake |
|
||||
| 모든 문자열을 무조건 CP949 디코드 | 정상 유니코드·라틴 확장 도면 손상 |
|
||||
| 렌더러에서 글리프 폴백만 강화 | 코드포인트 자체가 틀림 |
|
||||
| `dist` 만 수동 패치 | 소스는 `src/`; 빌드 산출물에만 고치면 재빌드 시 소실 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 잔여 이슈 / 후속 과제
|
||||
|
||||
1. **파서 본가 수정**
|
||||
acadrust(또는 문자열 추출 경로)에서 DWG 코드페이지·유니코드 플래그 반영 → JS 휴리스틱 제거 가능 여부 검토
|
||||
2. **STYLE 테이블**
|
||||
현재 `tables.styles` 가 비어 스타일/폰트명 미노출. 돋움 등 CAD 지정 폰트 매핑은 별 과제
|
||||
3. **다른 코드페이지**
|
||||
일본(932)/중국(936)/대만(950) 도면에 동일 패턴이 있으면 라벨 확장 + 문자 체계 휴리스틱 필요 (현 구현은 **한글 음절 출현** 조건으로 한국 도면에 한정)
|
||||
4. **ATTRIB / 치수 상주 텍스트 등**
|
||||
딥 워크로 대부분 커버되나, 바이너리/비문자열 필드는 대상 외
|
||||
|
||||
---
|
||||
|
||||
## 9. 한 줄 요약
|
||||
|
||||
> **폰트 폴더 문제가 아니라, 구형 한글 DWG의 CP949 바이트가 Latin-1 문자열로 올라온 인코딩 오해독이다.
|
||||
> `parse_dwg` 직후 문자열 트리를 CP949로 재해석(이미 한글인 값은 스킵)하면 해결되며,
|
||||
> 원본 `hmwebviewer` 에는 샘플의 `fixDwgKoreanText.ts` + `dwgParser.ts` 연결을 그대로 이식하면 된다.**
|
||||
|
||||
---
|
||||
|
||||
## 10. 참고 파일 (샘플)
|
||||
|
||||
- 복원 구현: [`../src/viewer2d/fixDwgKoreanText.ts`](../src/viewer2d/fixDwgKoreanText.ts)
|
||||
- 연결부: [`../src/viewer2d/dwgParser.ts`](../src/viewer2d/dwgParser.ts)
|
||||
- DXF 쪽 코드페이지 처리(대조): [`../src/viewer2d/decodeDxf.ts`](../src/viewer2d/decodeDxf.ts)
|
||||
- 모듈 지도: [`modules-dwg-dxf.html`](./modules-dwg-dxf.html)
|
||||
@@ -0,0 +1,370 @@
|
||||
# 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 한글 깨짐 (병행 필수) |
|
||||
+44
@@ -59,6 +59,43 @@
|
||||
#layers h3 { margin: 0 0 8px; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
||||
#layers label { display: flex; gap: 8px; align-items: center; margin: 4px 0; cursor: pointer; }
|
||||
#layers .swatch { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
||||
#spaces {
|
||||
display: none; align-items: center; gap: 0;
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#spaces button {
|
||||
border: none; border-radius: 0; border-right: 1px solid var(--line);
|
||||
background: transparent; padding: 8px 12px; margin: 0;
|
||||
}
|
||||
#spaces button:last-child { border-right: none; }
|
||||
#spaces button.active {
|
||||
background: #238636; border-color: #2ea043; color: #fff;
|
||||
}
|
||||
#spaces button:hover:not(.active) { border-color: transparent; color: var(--accent); }
|
||||
/* CAD 좌표 — 항상 우측 하단 (툴바 status 와 별개) */
|
||||
#coords {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
left: auto;
|
||||
top: auto;
|
||||
z-index: 20;
|
||||
font: 13px/1.35 var(--mono);
|
||||
color: var(--ink);
|
||||
background: rgba(13,17,23,.95);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
min-width: 240px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
letter-spacing: 0.02em;
|
||||
box-shadow: 0 8px 28px rgba(0,0,0,.45);
|
||||
user-select: none;
|
||||
}
|
||||
#coords .lbl { color: var(--accent); margin-right: 6px; font-weight: 600; }
|
||||
#coords .sep { color: var(--line); margin: 0 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -71,12 +108,19 @@
|
||||
</label>
|
||||
<button id="sample" class="primary wasm" type="button" title="acadrust-dwg WASM">Sample DWG</button>
|
||||
<button id="sampleDxf" class="primary" type="button" title="dxf-parser">Sample DXF</button>
|
||||
<div id="spaces" title="Model Space / Paper Space (Layout)"></div>
|
||||
<button id="fit" type="button">Fit (F)</button>
|
||||
<button id="theme" type="button">Theme</button>
|
||||
<button id="layersBtn" type="button">Layers</button>
|
||||
<div id="status">ready</div>
|
||||
</div>
|
||||
|
||||
<div id="coords" title="마우스 위치 CAD 좌표 (Model=UCS, Layout=Paper 도면단위)">
|
||||
<span class="lbl">X</span><span id="coordX">—</span>
|
||||
<span class="sep">|</span>
|
||||
<span class="lbl">Y</span><span id="coordY">—</span>
|
||||
</div>
|
||||
|
||||
<div id="layers"><h3>Layers</h3><div id="layerList"></div></div>
|
||||
|
||||
<aside id="recipe">
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+59
-2
@@ -24,6 +24,7 @@ import { Viewer2D } from './viewer2d/Viewer2D.js';
|
||||
import { parseDwgBuffer, type CadParseResult } from './viewer2d/dwgParser';
|
||||
import { parseDxfBuffer } from './viewer2d/parseDxf';
|
||||
import { is2D, isDxf, isDwg } from './viewer2d/ext2d';
|
||||
import { listCadSpaces, pickDefaultSpace } from './viewer2d/cadSpaces';
|
||||
import { setDefaultFontUrl } from './viewer2d/slugText';
|
||||
|
||||
// parseDxfBuffer 내부:
|
||||
@@ -39,6 +40,7 @@ const app = document.getElementById('app')!;
|
||||
const statusEl = document.getElementById('status')!;
|
||||
const layerList = document.getElementById('layerList')!;
|
||||
const layersPanel = document.getElementById('layers')!;
|
||||
const spacesBar = document.getElementById('spaces')!;
|
||||
|
||||
function setStatus(msg: string, err = false) {
|
||||
statusEl.textContent = msg;
|
||||
@@ -55,6 +57,54 @@ viewer.onSelect((entity: unknown) => {
|
||||
setStatus(`select ${e.type ?? '?'} · layer=${e.layer ?? '-'} · h=${e.handle?.value ?? '-'}`);
|
||||
});
|
||||
|
||||
// CAD coordinates under the cursor (Model: UCS like AutoCAD; Layout: paper units)
|
||||
const coordXEl = document.getElementById('coordX')!;
|
||||
const coordYEl = document.getElementById('coordY')!;
|
||||
function formatCadCoord(v: number): string {
|
||||
const a = Math.abs(v);
|
||||
if (a >= 1e6) return v.toFixed(1);
|
||||
if (a >= 1e3) return v.toFixed(2);
|
||||
if (a >= 1) return v.toFixed(3);
|
||||
return v.toFixed(4);
|
||||
}
|
||||
viewer.onPointerWorld((p) => {
|
||||
if (!p) {
|
||||
coordXEl.textContent = '—';
|
||||
coordYEl.textContent = '—';
|
||||
return;
|
||||
}
|
||||
coordXEl.textContent = formatCadCoord(p.x);
|
||||
coordYEl.textContent = formatCadCoord(p.y);
|
||||
});
|
||||
|
||||
/** Model / Layout tab strip after each load or space switch. */
|
||||
function fillSpaces() {
|
||||
const spaces = viewer.getSpaces();
|
||||
const active = viewer.getActiveSpace();
|
||||
spacesBar.innerHTML = '';
|
||||
if (spaces.length < 2 && !(spaces[0]?.entityCount)) {
|
||||
spacesBar.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
spacesBar.style.display = 'flex';
|
||||
for (const s of spaces) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = `${s.label}${s.entityCount ? ` (${s.entityCount})` : ''}`;
|
||||
btn.title = `${s.name} · handle ${s.handleHex}`;
|
||||
if (s.handleHex === active) btn.classList.add('active');
|
||||
btn.addEventListener('click', () => {
|
||||
if (s.handleHex === viewer.getActiveSpace()) return;
|
||||
viewer.setSpace(s.handleHex);
|
||||
fillSpaces();
|
||||
fillLayers();
|
||||
viewer.fit();
|
||||
setStatus(`space ${s.label} · ${s.entityCount} entities`);
|
||||
});
|
||||
spacesBar.append(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ④ 파서 조합 ─────────────────────────────────────────────────
|
||||
async function parseCad(buf: ArrayBuffer, name: string): Promise<CadParseResult> {
|
||||
if (isDxf(name)) return parseDxfBuffer(buf);
|
||||
@@ -68,7 +118,9 @@ async function parseCad(buf: ArrayBuffer, name: string): Promise<CadParseResult>
|
||||
|
||||
async function loadAndRender(buf: ArrayBuffer, name: string): Promise<CadParseResult> {
|
||||
const result = await parseCad(buf, name);
|
||||
viewer.load(result);
|
||||
const spaces = listCadSpaces(result);
|
||||
const def = pickDefaultSpace(spaces);
|
||||
viewer.load(result, { spaceHandle: def?.handleHex ?? null });
|
||||
viewer.resize();
|
||||
return result;
|
||||
}
|
||||
@@ -113,7 +165,12 @@ function fillLayers() {
|
||||
}
|
||||
|
||||
async function afterLoad(name: string, result: CadParseResult) {
|
||||
setStatus(`✓ ${name} · ${result.entities?.length ?? 0} entities`);
|
||||
// Force cache invalidation for OLE image rendering
|
||||
const spaces = listCadSpaces(result);
|
||||
const active = spaces.find((s) => s.handleHex === viewer.getActiveSpace());
|
||||
const spaceNote = active ? ` · ${active.label}` : '';
|
||||
setStatus(`✓ ${name} · ${result.entities?.length ?? 0} entities${spaceNote}`);
|
||||
fillSpaces();
|
||||
fillLayers();
|
||||
viewer.fit();
|
||||
}
|
||||
|
||||
Vendored
+12
-1
@@ -1,9 +1,11 @@
|
||||
// Types for the ported JS 2D engine (Viewer2D.js). Colocated so tsc resolves
|
||||
// `./Viewer2D.js` imports without allowJs. Only the methods the app uses are typed.
|
||||
import type { CadSpace } from './cadSpaces';
|
||||
|
||||
export class Viewer2D {
|
||||
constructor(container: HTMLElement);
|
||||
/** Render a DWG/DXF parseResult. `keepView` preserves pan/zoom on re-render. */
|
||||
load(result: unknown, opts?: { keepView?: boolean }): void;
|
||||
load(result: unknown, opts?: { keepView?: boolean; spaceHandle?: string | number | null }): void;
|
||||
/** Zoom-extents to the loaded drawing. */
|
||||
fit(): void;
|
||||
/** Resync renderer/camera to the container size (call after un-hiding). */
|
||||
@@ -13,8 +15,17 @@ export class Viewer2D {
|
||||
setGrid(visible: boolean): void;
|
||||
getLayerInfo(): { name: string; colorHex: string; count: number; visible: boolean }[];
|
||||
setHiddenLayers(nameSet: Set<string>): void;
|
||||
/** Model / Paper (Layout) spaces for the loaded drawing. */
|
||||
getSpaces(): CadSpace[];
|
||||
getActiveSpace(): string | null;
|
||||
/** Switch Model ↔ Layout; re-renders (fit unless keepView). */
|
||||
setSpace(spaceHandle: string | number | null, opts?: { keepView?: boolean }): void;
|
||||
getZoomPercent(): number;
|
||||
onViewChange(cb: () => void): void;
|
||||
/** Screen client coords → drawing world (Z=0 plane). */
|
||||
screenToWorld(clientX: number, clientY: number): { x: number; y: number; z: number } | null;
|
||||
/** Live CAD coords under the cursor; null when pointer leaves the canvas. */
|
||||
onPointerWorld(cb: ((p: { x: number; y: number; z: number } | null) => void) | null): void;
|
||||
startMeasure(cb: (r: { phase: string; ax: number; ay: number; az: number; bx: number; by: number; bz: number; d: number }) => void): void;
|
||||
stopMeasure(): void;
|
||||
}
|
||||
|
||||
+841
-35
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Model Space / Paper Space (Layout) discovery for CadParseResult.
|
||||
*
|
||||
* DWG entities are owned by a block header handle. System blocks
|
||||
* *Model_Space / *Paper_Space (and *Paper_Space0…) are the two spaces
|
||||
* AutoCAD switches between. Viewer2D must render only one at a time —
|
||||
* drawing both stacks survey-model geometry on top of layout sheet
|
||||
* geometry and produces the "all text piled in the center" glitch.
|
||||
*/
|
||||
|
||||
export type CadSpaceKind = 'model' | 'paper' | 'other';
|
||||
|
||||
export type CadSpace = {
|
||||
/** Stable id for UI (hex handle string, same as ownerHandle.value.toString(16)). */
|
||||
id: string;
|
||||
/** Display label, e.g. "Model" / "Layout" / raw block name. */
|
||||
label: string;
|
||||
/** Raw block name from tables.blocks. */
|
||||
name: string;
|
||||
/** Block handle as number when available. */
|
||||
handle: number | null;
|
||||
/** Hex string matching entity ownerHandle.value.toString(16). */
|
||||
handleHex: string;
|
||||
kind: CadSpaceKind;
|
||||
entityCount: number;
|
||||
};
|
||||
|
||||
function handleToHex(handle: unknown): string | null {
|
||||
if (handle == null) return null;
|
||||
if (typeof handle === 'object' && handle !== null && 'value' in handle) {
|
||||
const v = (handle as { value?: unknown }).value;
|
||||
if (typeof v === 'number' && Number.isFinite(v)) return v.toString(16);
|
||||
if (typeof v === 'string' && v.length) {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n.toString(16) : v.toLowerCase();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (typeof handle === 'number' && Number.isFinite(handle)) return handle.toString(16);
|
||||
if (typeof handle === 'string' && handle.length) {
|
||||
const n = Number(handle);
|
||||
return Number.isFinite(n) ? n.toString(16) : handle.toLowerCase();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ownerHex(entity: { ownerHandle?: unknown }): string | null {
|
||||
return handleToHex(entity.ownerHandle);
|
||||
}
|
||||
|
||||
function classifySpaceName(raw: string): CadSpaceKind | null {
|
||||
const name = raw.toLowerCase().replace(/\*/g, '').trim();
|
||||
if (!name) return null;
|
||||
if (name === 'model_space' || name === 'ms' || name.startsWith('model')) return 'model';
|
||||
if (name === 'paper_space' || name === 'ps' || name.startsWith('paper')) return 'paper';
|
||||
return null;
|
||||
}
|
||||
|
||||
function spaceLabel(kind: CadSpaceKind, name: string, paperIndex: number): string {
|
||||
if (kind === 'model') return 'Model';
|
||||
if (kind === 'paper') {
|
||||
// First paper space → "Layout"; further ones keep a disambiguator.
|
||||
if (paperIndex <= 1) return 'Layout';
|
||||
return `Layout (${name.replace(/^\*/, '')})`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* List model/paper space blocks that own at least zero entities.
|
||||
* Always includes Model/Paper headers found in tables.blocks.
|
||||
*/
|
||||
export function listCadSpaces(result: {
|
||||
entities?: unknown[];
|
||||
tables?: { blocks?: Array<{ name?: string; handle?: unknown }> };
|
||||
} | null | undefined): CadSpace[] {
|
||||
const blocks = result?.tables?.blocks ?? [];
|
||||
const entities = (result?.entities ?? []) as Array<{ ownerHandle?: unknown }>;
|
||||
|
||||
const counts = new Map<string, number>();
|
||||
for (const e of entities) {
|
||||
const hex = ownerHex(e);
|
||||
if (!hex) continue;
|
||||
counts.set(hex, (counts.get(hex) || 0) + 1);
|
||||
}
|
||||
|
||||
const spaces: CadSpace[] = [];
|
||||
let paperIndex = 0;
|
||||
for (const b of blocks) {
|
||||
const kind = classifySpaceName(b.name ?? '');
|
||||
if (!kind) continue;
|
||||
const hex = handleToHex(b.handle);
|
||||
if (!hex) continue;
|
||||
if (kind === 'paper') paperIndex += 1;
|
||||
const handleNum =
|
||||
typeof b.handle === 'number'
|
||||
? b.handle
|
||||
: typeof b.handle === 'object' && b.handle && 'value' in b.handle
|
||||
? Number((b.handle as { value: unknown }).value)
|
||||
: null;
|
||||
spaces.push({
|
||||
id: hex,
|
||||
label: spaceLabel(kind, b.name ?? '', paperIndex),
|
||||
name: b.name ?? '',
|
||||
handle: Number.isFinite(handleNum as number) ? (handleNum as number) : null,
|
||||
handleHex: hex,
|
||||
kind,
|
||||
entityCount: counts.get(hex) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Stable order: Model first, then papers in table order.
|
||||
spaces.sort((a, b) => {
|
||||
if (a.kind !== b.kind) {
|
||||
if (a.kind === 'model') return -1;
|
||||
if (b.kind === 'model') return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return spaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default tab when a file is opened:
|
||||
* - Paper/Layout with entities if any (civil sheet drawings often leave Layout active)
|
||||
* - else Model
|
||||
* - else first listed space
|
||||
*/
|
||||
export function pickDefaultSpace(spaces: CadSpace[]): CadSpace | null {
|
||||
if (!spaces.length) return null;
|
||||
const paperWithContent = spaces.find((s) => s.kind === 'paper' && s.entityCount > 0);
|
||||
if (paperWithContent) return paperWithContent;
|
||||
const model = spaces.find((s) => s.kind === 'model');
|
||||
if (model) return model;
|
||||
return spaces[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owners allowed when filtering to one space: the space handle itself plus
|
||||
* handles of entities that live under it (e.g. INSERT → ATTRIB children).
|
||||
*/
|
||||
export function buildAllowedOwnerHexes(
|
||||
entities: Array<{ handle?: unknown; ownerHandle?: unknown }>,
|
||||
activeSpaceHex: string,
|
||||
rounds = 3,
|
||||
): Set<string> {
|
||||
const allowed = new Set<string>([activeSpaceHex]);
|
||||
for (let pass = 0; pass < rounds; pass++) {
|
||||
let grew = false;
|
||||
for (const ent of entities) {
|
||||
const oh = ownerHex(ent);
|
||||
if (!oh || !allowed.has(oh)) continue;
|
||||
const h = handleToHex(ent.handle);
|
||||
if (h && !allowed.has(h)) {
|
||||
allowed.add(h);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
if (!grew) break;
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
|
||||
export function isSystemSpaceBlockName(name: string): boolean {
|
||||
return classifySpaceName(name) != null;
|
||||
}
|
||||
@@ -7,8 +7,14 @@
|
||||
* History: until 2026-07-10 this module selected between acadrust and
|
||||
* @horu2day/pure-cad-parser (?dwgparser= toggle). The old parser was removed
|
||||
* after side-by-side validation (PROGRESS.md Phase 15).
|
||||
*
|
||||
* Post-process: fixDwgKoreanText repairs CP949→Latin-1 mojibake common in
|
||||
* older Korean DWGs (see fixDwgKoreanText.ts). Unicode-correct drawings
|
||||
* (e.g. BasicSample) are left unchanged.
|
||||
*/
|
||||
|
||||
import { fixDwgKoreanText } from './fixDwgKoreanText';
|
||||
|
||||
export type CadParseResult = {
|
||||
version?: string;
|
||||
vars?: Record<string, unknown>;
|
||||
@@ -21,5 +27,5 @@ export type CadParseResult = {
|
||||
export async function parseDwgBuffer(bytes: Uint8Array): Promise<CadParseResult> {
|
||||
const { initAcadrustParser, parseDwgAcadrust } = await import('./acadrustParser');
|
||||
await initAcadrustParser();
|
||||
return parseDwgAcadrust(bytes);
|
||||
return fixDwgKoreanText(parseDwgAcadrust(bytes));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Repair Korean text that acadrust emits as Latin-1 mojibake of CP949/EUC-KR.
|
||||
*
|
||||
* Older Korean DWGs store strings in code page 949. When those bytes are
|
||||
* decoded as ISO-8859-1/Windows-1252, Hangul becomes garbled Latin
|
||||
* (e.g. "½Ã°ø½Ã" instead of "시공시"). Unicode-correct drawings
|
||||
* (already containing Hangul syllables) are left unchanged.
|
||||
*
|
||||
* Apply after parse_dwg, before Viewer2D.load().
|
||||
*/
|
||||
|
||||
const HANGUL_SYLLABLE = /[\uAC00-\uD7A3]/;
|
||||
const HANGUL_JAMO = /[\u1100-\u11FF\u3130-\u318F]/;
|
||||
|
||||
/** Labels tried in order; first supported decoder wins. */
|
||||
const KOREAN_LABELS = ['cp949', 'windows-949', 'euc-kr'] as const;
|
||||
|
||||
let koreanDecoder: TextDecoder | null | undefined;
|
||||
|
||||
function getKoreanDecoder(): TextDecoder | null {
|
||||
if (koreanDecoder !== undefined) return koreanDecoder;
|
||||
for (const label of KOREAN_LABELS) {
|
||||
try {
|
||||
koreanDecoder = new TextDecoder(label);
|
||||
return koreanDecoder;
|
||||
} catch {
|
||||
/* label not available in this runtime */
|
||||
}
|
||||
}
|
||||
koreanDecoder = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `s` looks like CP949 bytes misread as Latin-1, return the Hangul
|
||||
* form; otherwise return `s` unchanged.
|
||||
*/
|
||||
export function fixCp949Mojibake(s: string): string {
|
||||
if (!s) return s;
|
||||
// Already real Hangul → leave alone (e.g. BasicSample.dwg).
|
||||
if (HANGUL_SYLLABLE.test(s) || HANGUL_JAMO.test(s)) return s;
|
||||
|
||||
let hasHigh = false;
|
||||
const bytes = new Uint8Array(s.length);
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const code = s.charCodeAt(i);
|
||||
if (code > 255) return s; // true Unicode beyond Latin-1
|
||||
if (code > 127) hasHigh = true;
|
||||
bytes[i] = code;
|
||||
}
|
||||
if (!hasHigh) return s; // pure ASCII
|
||||
|
||||
const decoder = getKoreanDecoder();
|
||||
if (!decoder) return s;
|
||||
|
||||
let fixed: string;
|
||||
try {
|
||||
fixed = decoder.decode(bytes);
|
||||
} catch {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Accept only when we gain Hangul and the decode is clean.
|
||||
if (
|
||||
(HANGUL_SYLLABLE.test(fixed) || HANGUL_JAMO.test(fixed)) &&
|
||||
!fixed.includes('\uFFFD')
|
||||
) {
|
||||
return fixed;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep-walk a CadParseResult (or any JSON-like tree) and fix string fields
|
||||
* in place. Returns the same object for chaining.
|
||||
*/
|
||||
export function fixDwgKoreanText<T>(root: T): T {
|
||||
if (root == null || typeof root !== 'object') return root;
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
const walk = (value: unknown): void => {
|
||||
if (value == null || typeof value !== 'object') return;
|
||||
if (seen.has(value as object)) return;
|
||||
seen.add(value as object);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const item = value[i];
|
||||
if (typeof item === 'string') value[i] = fixCp949Mojibake(item);
|
||||
else walk(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const obj = value as Record<string, unknown>;
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (key === 'imageDataUrl') continue;
|
||||
const item = obj[key];
|
||||
if (typeof item === 'string') obj[key] = fixCp949Mojibake(item);
|
||||
else walk(item);
|
||||
}
|
||||
};
|
||||
|
||||
walk(root);
|
||||
return root;
|
||||
}
|
||||
Reference in New Issue
Block a user