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>
10 KiB
개선 결과 보고 — 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(0
68)· 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 를 공유한다. 선가중치 작업이 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 때문에 해당 없음):
- MESH 를 선가중치 대상에서 제외 — 메쉬 wireframe 을 굵게 그릴 실익이 없다.
_lwPx호출부에서type === 'MESH'면 0 을 쓰는 게 가장 싸다. - 상한 초과 시 버킷 전체를 버리지 말고 큰 기여자만 헤어라인으로 강등.
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. 남은 과제
- 5-3 버킷 상한 — 메쉬 레이어에 굵기가 지정된 도면이 들어오면 재현된다. 샘플을 만들어 재현시켜 두는 편이 좋다.
- Catmull-Clark 세분 —
subdivisionLevel > 0메쉬는 AutoCAD 보다 각져 보인다. 기준 도면에는 해당 없어 미구현. - POLYFACE MESH / POLYGON MESH — 구형 POLYLINE flag 16/64 계열은 여전히 미지원. SubDMesh 와 별개 엔티티다.
- 메쉬 채움(shaded) 패스 —
faceList를 이미 내보내고 있으므로 삼각화만 하면 된다. 2D 와이어프레임 뷰어라 우선순위는 낮다. 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 으로 감싸는 방식이었다.