Files
dwg-dxf-viewer-sample/docs/improvement-results-2026-08-04.md
minsungandClaude Opus 5 c67a803e4d feat(viewer2d): render DWG lineweight in screen pixels
Every line was drawn 1px regardless of its CAD lineweight. Two causes:
the dwg-wasm parseResult never carried the field (acadrust reads
EntityCommon::line_weight but it was dropped in serialization), and the
renderer merged all segments into one LineSegments whose
LineBasicMaterial.linewidth WebGL ignores.

Resolve ByLayer/ByBlock/Default per entity and bucket segments by
dash|lineweight. Weighted buckets get their own LineSegments2 +
LineMaterial with worldUnits:false, so width stays constant in pixels
while zooming - AutoCAD LWDISPLAY semantics - at 8 px/mm (0.25mm = 2px).
Anything at or below the 0.25mm default stays in the hairline batch so
drawings that never assigned a weight render exactly as before.

Fat batches are instanced, so layer masking compacts the instance buffer
and lowers instanceCount instead of rewriting an index; slotOf tracks
where each segment moved so the selection highlight still finds its
vertices. Per-entity color spans (meta.spans) let the highlight repaint
across the hairline batch and every bucket it touched. Buckets past
400k segments fall back to hairline to bound GPU/heap cost.

Display follows the drawing's LWDISPLAY and can be forced from the
toolbar; the property panel now shows the entity lineweight.

Verified headless at a fixed camera on the 65k-entity road drawing:
cyan road edge 2px -> 5px, layer hide/restore returns the exact baseline
pixel counts, and a 0.35mm LWPOLYLINE selects and highlights.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:25:11 +09:00

228 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 개선 결과 보고 — SubDMesh + 선가중치 (2026-08-04)
| 항목 | 내용 |
|------|------|
| 작성 목적 | 같은 날 들어간 두 개선(SubDMesh 렌더링, 선가중치 렌더링)의 **측정 결과**와, 개별 문서에는 없는 **두 변경의 상호작용**을 남긴다 |
| 개별 문서 | [`subdmesh-rendering.md`](./subdmesh-rendering.md) · [`lineweight-rendering.md`](./lineweight-rendering.md) |
| 기준 도면 | `dist2/samples/대산당진 2공구_노면_20260804_143854.dwg` (37.1 MB, AC1032 / AutoCAD 2018) |
| 측정 방식 | `Viewer2D.js` 에서 `_meshSegs` · `_lwPx` 를 원본 그대로 들어내 실제 parseResult 에 돌린 값. 브라우저 픽셀 측정이 아니라 **세그먼트 회계** |
---
## 1. 요약
| | 개선 전 | 개선 후 |
|--|--------|--------|
| MESH 엔티티 파싱 | 0개 | 9개 |
| 도로면 메쉬 | 정점 100,000 / 면 14 (**깨진 값**) | 정점 105,497 / 면 185,444 / 모서리 290,931 |
| MESH 화면 출력 | 없음 | 315,633 선분 (면 wireframe) |
| parseResult 크기 | 116 MB | 130 MB (+12%) |
| 파싱 시간 | ~3.2 s | ~3.2 s (변화 없음) |
| 선가중치 | 전부 1px 헤어라인 | 3개 굵기 버킷이 fat line 으로 분리 (아래 3절) |
---
## 2. 기준 도면 프로필
파싱 결과 65,495 엔티티.
```
LWPOLYLINE 47,849 MTEXT 13,974 ATTRIB 1,504 TEXT 1,389
INSERT 752 ATTDEF 15 MESH 9 LINE 2 CIRCLE 1
```
MESH 9개:
| handle | layer | 정점 | 면 | 모서리 | 세분레벨 |
|--------|-------|------|-----|--------|---------|
| 4188 | 도로면 | 105,497 | 185,444 | 290,931 | 0 |
| 4189 | 부체도로면 | 11,712 | 12,568 | 24,226 | 0 |
| 296564 | 일반도로부체도로연결부 | 48 | 44 | 90 | 0 |
| 296766 · 296927 · 297111 · 297259 · 297433 · 297665 | 부체도로(부체도로)연결부 | 28~53 | 25~47 | 51~96 | 0 |
전부 `subdivisionLevel = 0` 삼각 TIN → Catmull-Clark 세분 미구현이 이 도면에서는
화면 차이를 만들지 않는다.
---
## 3. 개선 1 — SubDMesh
원인 분석과 수정 내용은 [`subdmesh-rendering.md`](./subdmesh-rendering.md) 에 있다.
여기에는 **결과 수치**만 남긴다.
### 3-1. 스트림 어긋남 해소
`read_mesh` 의 배열 상한을 `MAX_ARRAY_COUNT`(100,000) 에서 "남은 스트림 비트 ÷
항목 최소 인코딩 길이" 로 바꾼 결과:
| | 정점 | 면 | 모서리 | 앞쪽 면 크기 |
|--|------|-----|--------|-------------|
| 전 | 100,000 | 14 | 3 | `[64, 0, 64, 23, 0, 0, 0, 15]` |
| 후 | 105,497 | 185,444 | 290,931 | `[3, 3, 3, 3, 3, 3, 3, 3]` |
오일러 표수 V E + F = 105,497 290,931 + 185,444 = **10**. 경계를 가진
다중 컴포넌트 곡면으로 타당한 값이다 (닫힌 단일 구면이면 2).
### 3-2. 선분 생성
| 경로 | 선분 수 | 비고 |
|------|---------|------|
| `edges` (중복 제거된 모서리) | **315,633** | 실제 사용 |
| `faceList` 폴백 (면 루프 순회) | 741,072 | 2.35배. 두 경로의 그림 범위는 동일 |
- 비유한(NaN/Infinity) 좌표 **0건**
- 길이 0 선분 6건 (원본 데이터의 축퇴 모서리, 무해)
- 선분 bbox 가 메쉬 정점 bbox 와 일치 — x[151,892 … 161,087] y[477,930 … 482,146]
- z 는 선분 양 끝 표고의 중간값, 범위 1.5 ~ 68. 이 도면은 LWPOLYLINE(0~68)·
TEXT(0~54)도 같은 대역에 있어서 **메쉬만 위로 뜨지 않는다**
### 3-3. 손상 입력 방어
| 입력 | 결과 |
|------|------|
| 빈 객체 / 정점 배열 길이 2 (3의 배수 아님) | 0 선분, 예외 없음 |
| 범위 밖 모서리 인덱스 (`[0, 99, -3, 1]`) | 0 선분 (경계 검사에서 탈락) |
| 면 개수가 남은 배열보다 큼 (`[9, 0, 1]`) | 0 선분, 루프 탈출 |
| 면 개수 0 반복 (`[0, 0, 0]`) | 0 선분, **무한루프 없음** |
| 정상 삼각형 (`[3, 0, 1, 2]`) | 3 선분 |
---
## 4. 개선 2 — 선가중치
측정·판정은 [`lineweight-rendering.md`](./lineweight-rendering.md) 3절(헤드리스
Chrome `readPixels` 비교)에 있다. 이 문서는 그 결과를 다시 주장하지 않고,
아래 5절에서 **세그먼트 부하** 관점만 더한다.
이 도면의 엔티티 선가중치 원시값 분포 (i16, 1/100 mm):
```
-3 (Default) ×15,176 -1 (ByLayer) ×2,613
13 (0.13mm) × 8,794 35 (0.35mm) ×37,944 40 ×119 50 ×849
vars.lwdisplay = true vars.celweight = 29
```
---
## 5. 새로 확인된 상호작용 — 메쉬 315,633 선분이 어느 버킷에 들어가는가
두 개선이 같은 `pushSeg` 를 공유한다. 선가중치 작업이 `pushSeg`
`dash|lineweight` 버킷으로 쪼갰으므로, **메쉬 선분도 그 버킷 규칙을 탄다.**
개별 문서 어느 쪽에도 이 얘기가 없어서 실측했다.
### 5-1. 지금은 안전하다
MESH 9개 전부:
```
own lineWeight = -3 (Default), layer lineWeight = -3
```
`_lwPx``-3``LW_DEFAULT_MM`(0.25mm) → `!(0.25 > LW_HAIRLINE_MM)`**0 반환**.
즉 315,633 선분이 전부 기존 병합 `LineSegments` 헤어라인 배치에 남고,
`LineSegments2` 인스턴싱에는 **한 개도 들어가지 않는다.**
"0.25mm 이하는 헤어라인 유지" 규칙이 — 원래는 굵기 미지정 도면의 모양 보존이
목적이었는데 — 결과적으로 대형 TIN 메쉬가 fat-line 경로로 쏟아지는 것을 막고 있다.
### 5-2. 현재 버킷 부하
LINE · LWPOLYLINE · MESH 기준 (ARC/CIRCLE/HATCH 는 미포함이라 실제 값은 이보다 조금 큼):
| 굵기 px | 선분 수 | 경로 | `LW_MAX_FAT_SEGS`(400,000) 대비 |
|--------|---------|------|-------------------------------|
| 0 (헤어라인) | 1,760,253 | 병합 `LineSegments` | — |
| 2.8 (0.35mm) | 182,539 | fat line | 45.6% |
| 3.2 (0.40mm) | 4,254 | fat line | 1.1% |
| 4.0 (0.50mm) | 92,130 | fat line | 23.0% |
헤어라인 1,760,253 중 메쉬가 **315,633 (17.9%)** 을 차지한다.
### 5-3. 위험 — 메쉬 레이어에 굵기가 지정되면 버킷이 터진다
버킷 키는 `dash|lwPx` 이고 **도면 전역**이다. 도로면/부체도로면 레이어에
0.35mm 를 지정하면 메쉬 선분이 이미 182,539 선분이 있는 2.8px 버킷에 합류한다.
```
182,539 + 315,633 = 498,172 선분 = LW_MAX_FAT_SEGS 의 124.5%
```
상한을 넘으면 `bk.fat = false` 가 되어 **그 버킷 전체가 헤어라인으로 되돌아간다.**
메쉬만 헤어라인이 되는 게 아니라, 지금 정상적으로 굵게 나오는 도로 가장자리
182,539 선분까지 같이 얇아진다. `console.warn` 한 줄만 남고 화면상으로는
"선가중치가 갑자기 안 먹는" 것처럼 보인다.
**대응 후보** (지금 당장 필요하진 않다 — 이 도면은 5-1 때문에 해당 없음):
1. MESH 를 선가중치 대상에서 제외 — 메쉬 wireframe 을 굵게 그릴 실익이 없다.
`_lwPx` 호출부에서 `type === 'MESH'` 면 0 을 쓰는 게 가장 싸다.
2. 상한 초과 시 버킷 전체를 버리지 말고 **큰 기여자만** 헤어라인으로 강등.
3. `LW_MAX_FAT_SEGS` 를 GPU 능력 기준으로 잡기 — 현재 40만은 고정 상수다.
---
## 6. 회귀 검증
새 wasm 과 직전 wasm(`git show HEAD:` 로 추출)으로 나머지 샘플 6개를 파싱해
엔티티 히스토그램을 비교했다.
| 도면 | 버전 | 엔티티 | 히스토그램 |
|------|------|--------|-----------|
| BasicSample.dwg | R2018 | 7,309 | 동일 |
| bb.dwg | R2018 | 438 | 동일 |
| civil.dwg | R2000 | 17,810 | 동일 |
| C0060202-001-평면및종단면도(1) | R2000 | 17,810 | 동일 |
| C0060203-001-표준단면도(2차로) | R2007 | 2,068 | 동일 |
| dwg_18.3mb.dwg | R2018 | 82,393 | 동일 |
6개 전부 **완전 일치**. 노면 도면만 65,486 → 65,495 (+9 MESH). `read_mesh` 상한
변경이 MESH 이외 엔티티에 영향을 주지 않음을 확인한 것이다.
추가로 `tsc --noEmit`, `vite build`, 편집한 JS 3개 `node --check` 통과.
---
## 7. 반영 상태
| 대상 | 상태 |
|------|------|
| hmwebviewer `feat/subdmesh-render` `35f5b1c` | 파서 + 렌더러 + 속성 패널. **푸시 안 됨** |
| 이 저장소 `feat/subdmesh-render` `eec3382` | wasm + 속성 패널 + 문서. **푸시 안 됨** |
| 이 저장소 `src/viewer2d/Viewer2D.js` | **미커밋.** `_meshSegs` + MESH 디스패치 3군데가 선가중치/fat-line 리팩터와 hunk 가 겹쳐 분리 불가. 같은 코드가 hmwebviewer `35f5b1c` 에는 들어가 있다 |
| `dist2/` | **미갱신.** 현재 빌드에 반쯤 배선된 선가중치 UI 가 섞여 있다 |
| hmwebviewer wasm 산출물 | 미커밋. 선가중치 소스가 커밋될 때 `npm run build:dwg-wasm` 로 함께 |
커밋된 wasm 바이너리에는 선가중치 파서 필드(`lwdisplay`, `celweight`,
layer/entity `lineWeight`)도 들어 있다. 빌드 시점의 소스에 이미 있었기 때문이며,
소비 측 없는 추가 JSON 키라 무해하다.
---
## 8. 남은 과제
1. **5-3 버킷 상한** — 메쉬 레이어에 굵기가 지정된 도면이 들어오면 재현된다.
샘플을 만들어 재현시켜 두는 편이 좋다.
2. **Catmull-Clark 세분**`subdivisionLevel > 0` 메쉬는 AutoCAD 보다 각져 보인다.
기준 도면에는 해당 없어 미구현.
3. **POLYFACE MESH / POLYGON MESH** — 구형 POLYLINE flag 16/64 계열은 여전히
미지원. SubDMesh 와 별개 엔티티다.
4. **메쉬 채움(shaded) 패스**`faceList` 를 이미 내보내고 있으므로 삼각화만
하면 된다. 2D 와이어프레임 뷰어라 우선순위는 낮다.
5. **`dist2` 배포** — 선가중치 작업이 끝난 뒤
[`deploy-dist2-static-build.md`](./deploy-dist2-static-build.md) 절차로.
---
## 9. 측정 재현
```bash
# 네이티브 메쉬 진단 (면 크기가 3/4 로 균일하지 않으면 스트림 어긋남 의심)
cd D:\MYCLAUDE_PROJECT\hmwebviewer\rust\dwg-wasm
cargo run --release --bin meshprobe -- <file.dwg>
```
5절의 버킷 표는 `Viewer2D.js` 에서 `_meshSegs` · `_lwPx` 를 들어내 parseResult 에
직접 돌려 만들었다 (`LINE` / `LWPOLYLINE` / `MESH` 만 계수). `Viewer2D.js` 는 three
와 확장자 없는 TS 모듈을 import 하므로 node 에서 그대로 import 되지 않는다 —
함수 본문만 뽑아 `new Function` 으로 감싸는 방식이었다.