# 개선 결과 보고 — 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 -- ``` 5절의 버킷 표는 `Viewer2D.js` 에서 `_meshSegs` · `_lwPx` 를 들어내 parseResult 에 직접 돌려 만들었다 (`LINE` / `LWPOLYLINE` / `MESH` 만 계수). `Viewer2D.js` 는 three 와 확장자 없는 TS 모듈을 import 하므로 node 에서 그대로 import 되지 않는다 — 함수 본문만 뽑아 `new Function` 으로 감싸는 방식이었다.