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

10 KiB
Raw Permalink Blame History

개선 결과 보고 — SubDMesh + 선가중치 (2026-08-04)

항목 내용
작성 목적 같은 날 들어간 두 개선(SubDMesh 렌더링, 선가중치 렌더링)의 측정 결과와, 개별 문서에는 없는 두 변경의 상호작용을 남긴다
개별 문서 subdmesh-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 에 있다. 여기에는 결과 수치만 남긴다.

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(068)· TEXT(054)도 같은 대역에 있어서 메쉬만 위로 뜨지 않는다

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 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 를 공유한다. 선가중치 작업이 pushSegdash|lineweight 버킷으로 쪼갰으므로, 메쉬 선분도 그 버킷 규칙을 탄다. 개별 문서 어느 쪽에도 이 얘기가 없어서 실측했다.

5-1. 지금은 안전하다

MESH 9개 전부:

own lineWeight = -3 (Default),  layer lineWeight = -3

_lwPx-3LW_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 절차로.

9. 측정 재현

# 네이티브 메쉬 진단 (면 크기가 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 으로 감싸는 방식이었다.