# 선가중치(Lineweight) 미적용 — 원인 분석 및 해결 가이드 | 항목 | 내용 | |------|------| | 작성 목적 | 모든 선이 1px 헤어라인으로만 그려지던 문제의 원인 규명과 수정·검증 내역 기록 | | 샘플 저장소 | `dwg-dxf-viewer-sample` (본 문서 위치) | | 원본 저장소 | `hmwebviewer` (`rust/dwg-wasm/`, `src/viewer2d/` 동기화 대상) | | 재현 도면 | `대산당진 2공구_노면_20260804_143854.dwg` (65,495 entities · LWDISPLAY=on · 0.13/0.35/0.40/0.50mm 혼재) | | 대조 도면 | `BasicSample.dwg`, `civil.dwg`, `bb.dwg`, `C0060202-001-평면및종단면도(1).dwg` (모두 굵기 미지정) | | 정답지 | AutoCAD 스크린샷 (`dist2/samples/1785826356.png`) | | 수정일 | 2026-08-04 | | 상태 | **해결** — 파서·렌더러 수정 완료, `dist2` 재빌드 완료, 커밋/푸시는 미실행 | > **폭(width)과 혼동 주의.** 도곽 외곽선이 예전부터 굵게 그려진 것은 lineweight 가 아니라 > LWPOLYLINE 의 **폭**(`constantWidth` / `startWidths` / `endWidths`)이다. 도면단위 실폭이라 > 줌하면 같이 커지고, `_widePolyMesh` 가 삼각형 메쉬로 처리한다. lineweight 는 화면 픽셀 > 단위(줌해도 폭 고정)이며 이번 문서의 대상이다. 둘은 별개 기능이다. --- ## 1. 현상 AutoCAD 에서는 도로 가장자리·교량라인이 뚜렷하게 굵고 비탈면 선은 가는데, 뷰어에서는 **모든 선이 같은 1px**로 그려진다. 색상·선종류(점선)는 정상. 정답지와 대조하면 굵기 비율이 완전히 소실된 상태였다. --- ## 2. 오류 원인 원인은 **두 겹**이었다. 한쪽만 고쳐서는 화면이 바뀌지 않는다. ### 2-1. 파서가 lineweight 를 내보내지 않았다 (데이터 부재) acadrust 는 lineweight 를 **이미 읽고 있었다.** ``` acadrust/src/entities/mod.rs:260 pub line_weight: LineWeight, // EntityCommon acadrust/src/tables/layer.rs:58 pub line_weight: LineWeight, // Layer acadrust/src/types/line_weight.rs DWG 5비트 테이블 인덱스 ↔ 값 변환까지 구현 ``` 그런데 `dwg-wasm` 의 JSON 직렬화(`common_json` / `tables_value` / `vars_value`)에서 해당 필드가 빠져 있어 parseResult 에 값이 존재하지 않았다. 확인 방법 — wasm 바이너리의 serde 필드명 문자열을 직접 조회: ```bash grep -a -c "linetypeScale" acadrust_dwg_bg.wasm # 1 (있음) grep -a -c "entityHeader" acadrust_dwg_bg.wasm # 1 (있음) grep -a -o -i "[a-z_]*weight[a-z_]*" acadrust_dwg_bg.wasm | sort -u # (없음) ``` parseResult 덤프로도 재확인: ``` layer[0] = { colorIndex, frozen, handle, lineType, locked, name, on, plotting } ← lineWeight 없음 entityHeader = { colorIndex, invisible, linetypeScale } ← lineWeight 없음 ``` ### 2-2. 렌더러가 굵기를 그릴 수단이 없었다 (표현 부재) Viewer2D 는 성능을 위해 도면의 모든 선분을 **하나의 병합 버퍼**로 그린다. ```js const lmesh = new THREE.LineSegments(geom, new THREE.LineBasicMaterial({ vertexColors: true })); ``` WebGL 코어 프로파일에서 `LineBasicMaterial.linewidth` 는 **무시되고 항상 1px** 이다 (three.js 문서 명시). 즉 2-1 이 해결되어 값이 들어오더라도 이 배치 구조로는 굵기를 표현할 수 없다. ### 2-3. 왜 지금까지 드러나지 않았나 굵기를 지정한 도면이 저장소 샘플에 사실상 없었다. 엔티티 lineweight 히스토그램: | 도면 | LWDISPLAY | 엔티티 lineweight | |------|-----------|-------------------| | `BasicSample.dwg` | true | `-2` ×62, `-1` ×7247 → 전부 ByLayer/ByBlock → 레이어가 전부 Default | | `civil.dwg` / `bb.dwg` / `C0060202` | **false** | `-1` ×17241, `0` ×569 | | **`대산당진 2공구_노면`** | true | `-3` ×15176, `-1` ×2613, **`13` ×8794, `35` ×37944, `40` ×119, `50` ×849** | 굵기 데이터가 실제로 있는 도면은 노면 도면 하나뿐이었고, 그 도면은 37MB 라 로딩 성능 이슈가 해결된 뒤에야 일상적으로 열리기 시작했다. --- ## 3. 개선 사항 (설계 결정) 수정 방향을 정할 때 확정한 규칙들. ### 3-1. 픽셀 환산 — AutoCAD LWDISPLAY 규격을 따른다 lineweight 는 **화면 픽셀 폭**이다. 줌해도 굵기가 변하지 않는다(모델 공간 기준). 따라서 월드 단위가 아니라 스크린 스페이스로 그려야 한다. ``` px = mm × 11 (LW_PX_PER_MM 기본값, 최대 48px) 0.35mm = 3.9px · 0.50mm = 5.5px · 2.11mm = 23px ``` **이 배율은 상수로 확정할 수 있는 값이 아니다.** AutoCAD 도 모델 공간에서는 선가중치를 줌과 무관한 픽셀로만 그리고, 그 환산은 "선가중치 설정 → 표시 배율" 슬라이더가 정한다. 즉 화면 DPI·모니터에 따라 맞는 값이 달라진다. - 코드 기본값: `LW_PX_PER_MM = 11` (사용자가 실제 화면에서 AutoCAD 와 나란히 놓고 맞춘 값) - 런타임 조정: **옵션 패널 → 선가중치 → 표시 배율**, 또는 `setLineweightScale(pxPerMm)` - 조정은 **재파싱 없이 즉시 반영**된다 (3-6 절) > 시행착오 기록: 처음 8px/mm(96dpi 어림) → 손테스트 "얇다" 반려 → 정답지 캡처에서 > 역산해 20px/mm → 실제 화면 대조 후 최종 11px/mm. 상수 추정이 두 번 빗나갔고, > 그래서 값을 코드에서 빼 옵션으로 노출했다. ### 3-2. 0.25mm 이하는 헤어라인 유지 — **회귀 방지 장치** `-3`(Default)·`-1`(ByLayer → 레이어도 Default)이 도면 대부분을 차지한다. 이것까지 2px 로 그리면 **굵기를 지정한 적 없는 기존 도면이 통째로 두꺼워진다.** → `LW_HAIRLINE_MM = 0.25` 이하는 기존 병합 배치에 그대로 남긴다. 그 결과 대조 도면 4종은 **픽셀 단위로 이전과 동일**하게 렌더링된다. ### 3-3. 굵은 선만 별도 배치 — 성능 보존 전체를 fat line 으로 바꾸면 세그먼트당 인스턴스 8정점이라 대형 도면에서 감당이 안 된다. `dash|lineweight` 조합으로 버킷을 나눠 **굵기가 있는 버킷만** 별도 메쉬로 뽑는다. | 버킷 | 메쉬 | 비고 | |------|------|------| | 실선 + 헤어라인 | 기존 병합 `LineSegments` | 변경 없음 | | 점선 + 헤어라인 | `LineSegments` + `LineDashedMaterial` | 변경 없음 | | 굵기 있음 | `LineSegments2` + `LineMaterial` | 신규 (스크린스페이스 쿼드) | 한 버킷이 `LW_MAX_FAT_SEGS`(40만) 세그먼트를 넘으면 헤어라인으로 폴백하고 `console.warn` 을 남긴다. 대형 측량 도면에서 GPU/힙이 터지는 것을 막는 상한. ### 3-4. 기존 기능을 깨지 않는다 새 메쉬 타입이 들어오면서 다음 두 기능이 조용히 망가질 수 있었다. 둘 다 대응했다. - **레이어 토글** — 인스턴스 지오메트리는 인덱스 재작성이 불가 - **선택 하이라이트** — 색상 배열이 여러 버퍼로 쪼개짐 ### 3-6. 배율 조정은 리빌드 없이 헤어라인 컷오프가 **mm 값**(0.25mm)이라 배율이 바뀌어도 어떤 세그먼트가 어느 버킷에 들어가는지는 변하지 않는다. 모든 굵기가 같은 배수로 움직일 뿐이다. → 각 fat 배치가 자기 굵기를 **mm 로 기억**(`_fatBufs[].lwMm`)하고, `setLineweightScale()` 은 `material.linewidth` 만 다시 쓴다. 37MB 도면에서도 슬라이더를 끄는 동안 지연이 없다. 반면 **표시 on/off** 는 굵기가 0이 되어 버킷 구성이 바뀌므로 재빌드가 필요하다. ### 3-5. 표시 on/off — AutoCAD LWDISPLAY 와 동일하게 굵기 표시는 CAD 에서 원래 켜고 끌 수 있는 옵션이다. 도면의 헤더 값을 기본으로 따르되 사용자가 강제할 수 있어야 한다. --- ## 4. 조치 사항 ### 4-1. 파서 — `hmwebviewer/rust/dwg-wasm/src/lib.rs` 원시 i16 값(1/100 mm, `-1` ByLayer · `-2` ByBlock · `-3` Default)을 세 군데에 추가. | 함수 | 추가 필드 | |------|-----------| | `common_json` → `entityHeader` | `lineWeight` | | `tables_value` → `layers[]` | `lineWeight` | | `vars_value` | `lwdisplay`, `celweight` | 재빌드 + 산출물 복사: ```bash cd D:\MYCLAUDE_PROJECT\hmwebviewer npm run build:dwg-wasm cp src/viewer2d/acadrust-dwg/acadrust_dwg* \ ../dwg-dxf-viewer-sample/src/viewer2d/acadrust-dwg/ ``` 검증(노면 도면): ``` vars.lwdisplay = true vars.celweight = 29 entity lineWeight = { -3:15176, -1:2613, 13:8794, 35:37944, 40:119, 50:849 } ``` ### 4-2. 렌더러 — `src/viewer2d/Viewer2D.js` | 추가/변경 | 내용 | |-----------|------| | `_lwPx(entity, inheritedPx)` | ByLayer→LAYER 테이블, ByBlock→INSERT 굵기, Default→0.25mm 해석 후 픽셀 환산. 0.25mm 이하는 `0`(헤어라인) 반환 | | `_buildLayerMap` | 레이어 lineweight 를 핸들·이름 두 키로 보관 (DXF 레이어는 핸들이 없음) | | `pushSeg` | 세그먼트를 `dash|lineweight` 버킷으로 분기 + 엔티티별 색상 구간(`meta.spans`) 기록 | | 조립부 | 굵은 버킷 → `LineSegments2` + `LineMaterial(worldUnits:false)`. 캡 초과 시 헤어라인 폴백 | | `_registerFat` / `_applyLayerVisibility` | 보이는 인스턴스만 버퍼 앞으로 압축 후 `geometry.instanceCount` 축소. 원본 배열 보존, 이동 위치는 `slotOf` 에 기록 | | `_paintMeta` / `_paintSpan` | 헤어라인 구간 + 모든 버킷 구간을 함께 칠함. 레이어 토글로 재배치되면 `slotOf` 경유로 다시 칠함 | | `_insertEntities` | 자식마다 `_lwPx(child, insertLwPx)` — ByBlock 이 INSERT 굵기를 상속. 루프 종료 시 호출자 값 복구 | | `_onResize` | `LineMaterial.resolution` 재설정 (픽셀 폭 기준이 캔버스 크기라서 필수) | | `setLineweightEnabled(on)` / `getLineweightEnabled()` | 표시 스위치. `null` = 도면의 LWDISPLAY 를 따름 | ### 4-3. UI 툴바 **옵션** 버튼 → `옵션 (Options)` 패널 (`index.html` · `src/main.ts`). | 항목 | 동작 | |------|------| | 표시 (LWDISPLAY) | on/off. 체크 해제 시 전부 헤어라인 (재빌드) | | 표시 배율 | 슬라이더 + 숫자 입력 (px/mm). 즉시 반영 | | 환산 표시 | 그 도면이 실제로 쓰는 굵기만 `0.35mm → 3.9px` 식으로 표기 | | ↺ 기본값 | 배율·표시·그리드·줌속도 초기화 | 설정은 **IndexedDB** (`hmw-viewer` DB → `options` 스토어)에 저장한다 ([`../src/optionsStore.ts`](../src/optionsStore.ts)). 키: `lineweightScale`, `lineweightOn`, `grid`, `zoomSpeed`. 스토리지가 막혀 있어도 조용히 기본값으로 동작한다. 속성 패널에는 `선가중치 (Lineweight)` 행이 있다 (`src/propertyInspector.ts`). `-1`→`BYLAYER`, `-2`→`BYBLOCK`, `-3`→`DEFAULT (0.25mm)`, 그 외 `0.35mm` 형식. ### 4-4. 배포 ```bash ./node_modules/.bin/vite build --outDir dist2 --emptyOutDir false ``` → `dist2/assets/index-D3FqDGwP.js`, `acadrust_dwg_bg-DmCEpFBu.wasm`. `--emptyOutDir false` 는 필수 ([`deploy-dist2-static-build.md`](./deploy-dist2-static-build.md) 참조). **커밋/푸시는 하지 않았다** — 공유 원격이라 사용자 확인 필요. --- ## 5. 검증 헤드리스 Chrome(SwiftShader) 1400×1000, 노면 도면, 동일 카메라(30단계 확대)에서 `gl.readPixels` 로 직접 계측. ### 5-1. 굵기 반영 | | 밝은 픽셀 수 | 중앙 스캔라인 최대 cyan 폭 | |--|--|--| | 선가중치 **ON** | 137,528 | **5 px** | | 선가중치 **OFF** | 122,245 | 2 px | 0.50mm × 8 = 4px + 안티에일리어싱 → 5px. 설계값과 일치. ### 5-2. 레이어 토글 무손실 `도로 가장자리` 레이어 끄기 → 켜기: ``` 기준 {"lit":137528,"cyan":12162,"accent":6741} 숨김 {"lit":136735,"cyan":12553,"accent":5776} 복원 {"lit":137528,"cyan":12162,"accent":6741} ← 기준과 완전 일치 ``` ### 5-3. 선택 하이라이트 굵은 선(0.35mm LWPOLYLINE, 레이어 `비탈면방향`) 클릭: ``` LWPOLYLINE · #151257 레이어 비탈면방향 · 색상 BYLAYER · 선가중치 (Lineweight) 0.35mm ``` cyan 픽셀 수가 감소(11502→11486) → fat 배치의 정점이 강조색으로 재도색됨을 확인. ### 5-4. 도면이 실제로 들고 있는 수치 굵기는 **레이어가 아니라 엔티티**에 들어 있다. 23개 레이어 lineweight 는 **전부 `-3` (Default)** 이라 ByLayer 로는 아무 정보도 나오지 않는다. | 레이어 | ACI | lineweight | 개수 | |---|---|---|---| | 도로 가장자리 | 1 (빨강) | **50 = 0.50mm** | 18 | | 도로마킹 | 255 / 7 / 2 / 4 | 50 = 0.50mm | 799 | | 부체도로 가장자리 | 20 | 50 = 0.50mm | 32 | | 비탈면방향 | 140 / 150 | 35 = 0.35mm | 34,758 | | 노면측구 | 4 (시안) | 35 = 0.35mm | 303 | | 시작·끝라인 / Drawing확폭 | 4 / 6 / 2 / 3 | 40 = 0.40mm | 105 | | SubContour · MainContour | 3 / 6 | 13 = 0.13mm | 8,794 | | **도로면 (MESH 삼각망)** | 7 | **-3 Default** | 1 (185,444 faces) | | Station (중심선·라벨) | 1 (빨강) | -1 → 레이어 -3 → Default | 1,082 | 주의: 화면의 **빨간 선이 두 종류**다. 굵은 쪽이 `도로 가장자리`(0.50mm), 도로 한가운데를 지나는 얇은 쪽이 `Station` 중심선(Default → 헤어라인). 정답지에서 굵게 나오는 것은 전자다. ### 5-5. 정답지 대조 (배율 확정 근거) lineweight 는 줌과 무관한 화면 픽셀이므로, 두 캡처의 **절대 픽셀 두께**를 그대로 비교한다. | 배율 | `도로 가장자리` 0.50mm | `노면측구` 0.35mm (헤드리스 실측) | |--|--|--| | 8 px/mm (최초, 반려) | 4 px | 2–3 px | | 20 px/mm (캡처 역산) | 10 px | 7 px | | **11 px/mm (현재 기본값)** | **5.5 px** | **3–4 px** | | 25 px/mm (참고) | 12.5 px | 8–9 px | 캡처 이미지에서 역산한 20 은 실제 화면에서 과했다 — 캡처 DPI 와 사용자 모니터가 달라서다. 최종값은 사용자가 뷰어와 AutoCAD 를 나란히 놓고 맞춘 **11**. 이래서 상수로 못 박지 않고 옵션으로 뺐다. ### 5-6. 회귀 없음 - `npx tsc --noEmit` 통과 - `npm run build` 통과 - 대조 도면 4종: 굵기 전부 Default → **화면 변화 없음**(설계 3-2 의도대로) --- ## 6. 남은 과제 1. **배율 기본값은 환경 의존** — 11 은 특정 모니터에서 맞춘 값이다. 다른 화면에서는 옵션 패널로 다시 맞춰야 한다. `devicePixelRatio` 로 자동 보정하는 것은 검토만 하고 넣지 않았다(캡처·원격 데스크톱 등에서 오히려 어긋난다). 2. **ByBlock 근사** — 블록 자식의 `-2` 는 INSERT 굵기를 상속하지만, 중첩 블록에서 중간 INSERT 가 `-2` 인 경우까지는 추적하지 않는다(점선 처리와 동일한 기존 정책). 3. **DXF 레이어 굵기 없음** — `dxf-parser` 의 LAYER 핸들러가 코드 370 을 무시한다. DXF 는 엔티티 자체 굵기(`entity.lineweight`)만 반영되고 ByLayer 는 Default 로 떨어진다. 4. **`celweight` 미사용** — 헤더 CELWEIGHT 를 값이 아니라 DWG 5비트 테이블 인덱스로 내보내고 있다(노면 도면에서 `29` = ByLayer 인덱스). 현재 렌더링에 쓰지 않으므로 방치했으나, 쓰게 되면 `LineWeight::from_dwg_index` 변환을 파서 쪽에 넣어야 한다. 5. **40만 세그먼트 캡** — 초과 시 조용히 얇아지는 대신 `console.warn` 만 남는다. 상태 표시줄 등 사용자에게 보이는 알림이 필요할 수 있다. --- ## 7. 참고 - 렌더러: [`../src/viewer2d/Viewer2D.js`](../src/viewer2d/Viewer2D.js) — `_lwPx`, `_registerFat`, `_applyLayerVisibility`, `_paintMeta`, `setLineweightEnabled` - 속성 패널: [`../src/propertyInspector.ts`](../src/propertyInspector.ts) — `formatLineweight` - 파서: `hmwebviewer/rust/dwg-wasm/src/lib.rs` — `common_json`, `tables_value`, `vars_value` - 폭(width)과의 차이: `_segWidths` / `_widePolyMesh` (도면단위 실폭) - 같은 유형의 선례(파서 필드 누락 → 렌더 불가): [`subdmesh-rendering.md`](./subdmesh-rendering.md) - 배포 절차: [`deploy-dist2-static-build.md`](./deploy-dist2-static-build.md)