diff --git a/README.md b/README.md index bc4c9a4..26fa263 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Vite `?url` import 로 로드됩니다. | 구형 한글 DWG TEXT 깨짐 (CP949→Latin-1 mojibake) | [`docs/korean-dwg-text-encoding.md`](./docs/korean-dwg-text-encoding.md) | | Model + Paper 동시 렌더로 글자 겹침 / Layout 탭 | [`docs/model-paper-space.md`](./docs/model-paper-space.md) | | `dist2` 정적 빌드 배포 절차 (공식 배포 스크립트 없음) | [`docs/deploy-dist2-static-build.md`](./docs/deploy-dist2-static-build.md) | +| SubDMesh(MESH) 미표시 — parseResult 누락 + 큰 메쉬 잘림 | [`docs/subdmesh-rendering.md`](./docs/subdmesh-rendering.md) | 샘플 패치: `fixDwgKoreanText`, `cadSpaces` + Viewer2D 공간 필터, Model/Layout 탭. 원본 `hmwebviewer` 반영 절차는 각 문서를 따른다. @@ -121,6 +122,6 @@ Vite `?url` import 로 로드됩니다. | 조각 | 라이선스 | |------|----------| | Viewer2D 포트 · 샘플 글루 | MIT (hmwebviewer 계열) | -| acadrust (WASM 내부) | MPL-2.0 | +| acadrust (WASM 내부, `read_mesh` 로컬 패치 포함) | MPL-2.0 | | dxf-parser · three | MIT | | NanumGothic | OFL | diff --git a/docs/subdmesh-rendering.md b/docs/subdmesh-rendering.md new file mode 100644 index 0000000..ee56654 --- /dev/null +++ b/docs/subdmesh-rendering.md @@ -0,0 +1,162 @@ +# SubDMesh(MESH) 렌더링 + +| 항목 | 내용 | +|------|------| +| 증상 | `대산당진 2공구_노면_*.dwg` 의 노면/부체도로면이 뷰어에 전혀 나오지 않음 | +| 원인 | ① dwg-wasm 이 MESH 엔티티를 parseResult 에 아예 내보내지 않음 ② acadrust 의 배열 개수 상한(100,000)이 큰 메쉬를 잘라내 뒤따르는 face/edge 목록까지 깨뜨림 | +| 대상 클래스 | `AcDbSubDMesh` (DXF 엔티티명 `MESH`, AutoCAD 2010+) | +| 수정일 | 2026-08-04 | +| 반영 상태 | 파서(wasm)·속성 패널·문서는 이 커밋에 포함. **`Viewer2D.js` 렌더러 hunk 는 아직 미커밋** — 같은 파일에서 진행 중인 lineweight/fat-line 리팩터와 hunk 가 겹쳐 분리가 안 된다. 렌더러 원본은 본가 hmwebviewer `35f5b1c` 에 들어가 있고, 이 저장소에는 작업 트리에만 있다. 그 리팩터가 커밋될 때 같이 들어간다. | + +--- + +## 1. 무엇이 안 나왔나 + +문제 도면(AC1032 / AutoCAD 2018)에는 MESH 엔티티가 9개 있다. + +| handle | layer | 정점 | 면 | 모서리 | +|--------|-------|------|----|--------| +| 4188 | 도로면 | 105,497 | 185,444 | 290,931 | +| 4189 | 부체도로면 | 11,712 | 12,568 | 24,226 | +| 296564 … 297665 | 연결부 (7개) | 28 ~ 53 | 25 ~ 47 | 51 ~ 96 | + +파싱 결과 엔티티 히스토그램에는 `MESH` 가 **0개** 였다. 즉 뷰어의 렌더 문제가 아니라 +**파서 출력에 데이터 자체가 없었다.** + +--- + +## 2. 원인 두 겹 + +### 2-1. dwg-wasm 이 MESH 를 버리고 있었다 + +`rust/dwg-wasm/src/lib.rs` 의 `entity_to_json()` 마지막 arm: + +```rust +// Not rendered by Viewer2D (leaders, rays, meshes, …) — skip. +_ => {} +``` + +acadrust 자체는 MESH 를 **완전히 읽고 있었다** (`OBJ_MESH` → `entities::read_mesh()` → +`EntityType::Mesh`). JSON 직렬화 단계에서만 빠져 있었다. + +### 2-2. acadrust 의 배열 상한이 큰 메쉬를 깨뜨렸다 + +`object_reader/mod.rs` 에는 손상 데이터 방어용 상한이 있다. + +```rust +const MAX_ARRAY_COUNT: i32 = 100_000; +fn safe_count(raw: i32) -> i32 { raw.max(0).min(MAX_ARRAY_COUNT) } +``` + +`read_mesh()` 가 이 상한을 정점 개수에 그대로 쓰고 있었다. 도로면 메쉬의 실제 정점은 +105,497개 → 100,000 으로 잘림 → **5,497개 분량의 비트를 안 읽고 다음 필드로 넘어감** → +그 뒤의 face/edge/crease 목록이 통째로 어긋난다. 진단 출력이 이렇게 나왔다. + +``` +MESH #1 verts=100000 faces=14 edges=3 + first face sizes: [64, 0, 64, 23, 0, 0, 0, 15] +``` + +정점 수가 그럴듯해 보여서 에러로 드러나지 않는 종류의 고장이다. 정상 TIN 이면 face 는 전부 +3정점이어야 한다. + +**수정**: `read_mesh()` 전용으로 “남은 스트림 비트 수” 기준 상한을 쓴다 +(`stream_bounded_count`). 항목 1개의 최소 인코딩 길이(3BD = 6비트, BitLong = 2비트)로 +남은 비트를 나눈 값이 상한이므로, 무제한 할당 방어는 그대로 유지되면서 정상 도면은 +잘리지 않는다. + +``` +MESH #1 verts=105497 faces=185444 edges=290931 + first face sizes: [3, 3, 3, 3, 3, 3, 3, 3] +``` + +> acadrust 는 MPL-2.0. 이 저장소 기준으로 **처음 생긴 로컬 패치**다. MPL 은 파일 단위 +> copyleft 이므로 해당 파일이 MPL 로 남고 소스가 저장소에 포함되면 요건을 만족한다. +> `rust/dwg-wasm/Cargo.toml` · `src/lib.rs` 의 "consumed unmodified" 문구를 갱신했다. + +--- + +## 3. parseResult 스키마 + +MESH 는 **평면 숫자 배열**로 나간다. `{x,y,z}` 객체와 중첩 face 배열을 쓰면 같은 숫자에 +대해 JSON 이 약 3배로 부푼다 (이 도면 기준 전체 JSON 116MB → 130MB 로 끝난 이유). + +```jsonc +{ + "type": "MESH", + "handle": { "value": 4188 }, "layer": "도로면", "entityHeader": { … }, + "vertices": [x, y, z, x, y, z, …], // 평면 + "vertexCount": 105497, + "faceList": [n, i0, …, i(n-1), n, …],// DXF group 93 배열과 같은 형태 (n각형 허용) + "faceCount": 185444, + "edges": [a, b, a, b, …], // acadrust 가 중복 제거해 준 모서리 목록 + "subdivisionLevel": 0, + "blendCrease": true, + "meshVersion": 2 +} +``` + +범위를 벗어난 인덱스는 `u32::MAX` 로 나가므로, 소비 측 경계 검사에서 그 모서리 하나만 +버려진다 (엉뚱한 정점으로 감기지 않는다). + +--- + +## 4. 렌더링 + +`Viewer2D._meshSegs()` 가 면 wireframe 을 선분으로 뱉는다. AutoCAD 의 2D 와이어프레임 +표시와 같은 그림이다. + +- **`edges` 우선.** 이미 중복이 제거돼 있어 삼각형이 공유하는 모서리를 한 번만 그린다. + 이 도면에서 315,633 선분 (faceList 로 그리면 741,072 → 약 1.8배). +- `edges` 가 비어 있으면 `faceList` 로 폴백. 두 경로의 그림 범위(extent)는 동일함을 확인. +- z 는 선분 양 끝 정점 표고의 중간값. 이 도면은 LWPOLYLINE·TEXT 도 이미 z 0~68 대역에 + 있으므로 메쉬만 위로 뜨지 않는다. +- 세 군데 디스패치 모두에 연결: 모델 공간 switch, 페이퍼 뷰포트 통과 switch, + `_insertEntities`(블록 안의 MESH, `xf` 로 좌표 변환). + +### 한계 + +`subdivisionLevel > 0` 인 메쉬는 **기본(control) 메쉬만** 그린다. AutoCAD 는 그 정점들을 +Catmull-Clark 로 세분한 결과를 보여주므로, 그런 메쉬는 AutoCAD 보다 각져 보인다. +문제 도면은 9개 전부 `subdivisionLevel = 0` 이라 차이가 없다. + +`POLYFACE MESH` / `POLYGON MESH`(구형 POLYLINE flag 16/64)는 별개 엔티티이며 이 작업 +범위 밖이다. + +--- + +## 5. 검증 도구 + +```bash +# 네이티브에서 메쉬가 제대로 읽히는지 (hmwebviewer 쪽) +cd D:\MYCLAUDE_PROJECT\hmwebviewer\rust\dwg-wasm +cargo run --release --bin meshprobe -- +``` + +face 크기가 전부 3(또는 4)이 아니라 `[64, 0, 64, 23, …]` 처럼 들쭉날쭉하면 위 2-2 의 +스트림 어긋남을 다시 의심한다. + +--- + +## 6. wasm 재빌드 + +MESH 지원은 wasm 안에 들어 있으므로 재빌드가 필요하다. 빌드 스크립트는 hmwebviewer 에만 +있고, 산출물을 이 저장소로 복사한다. + +```bash +cd D:\MYCLAUDE_PROJECT\hmwebviewer +npm run build:dwg-wasm # → hmwebviewer/src/viewer2d/acadrust-dwg/ +cp src/viewer2d/acadrust-dwg/acadrust_dwg* \ + ../dwg-dxf-viewer-sample/src/viewer2d/acadrust-dwg/ +``` + +이후 정적 배포는 [`deploy-dist2-static-build.md`](./deploy-dist2-static-build.md) 절차를 따른다. + +--- + +## 7. 참고 + +- 렌더러: [`../src/viewer2d/Viewer2D.js`](../src/viewer2d/Viewer2D.js) — `_meshSegs`, `case 'MESH'` +- 속성 패널: [`../src/propertyInspector.ts`](../src/propertyInspector.ts) — `case 'MESH'` +- 직렬화: `hmwebviewer/rust/dwg-wasm/src/lib.rs` — `EntityType::Mesh` +- 비트 파서: `hmwebviewer/rust/acadrust/src/io/dwg/dwg_stream_readers/object_reader/entities.rs` — `read_mesh`, `stream_bounded_count` diff --git a/src/propertyInspector.ts b/src/propertyInspector.ts index f7d80ae..5fb0cfd 100644 --- a/src/propertyInspector.ts +++ b/src/propertyInspector.ts @@ -155,6 +155,37 @@ export function formatEntityProperties(entity: any): FormattedProperties { break; } + // MESH (AcDbSubDMesh). dwg-wasm emits flat arrays: vertices [x,y,z,…], + // faceList [n, i0 … i(n-1)] repeated, edges [a,b,…]. + case 'MESH': { + const verts: number[] = d.vertices ?? []; + const vCount = d.vertexCount ?? Math.floor(verts.length / 3); + const fCount = d.faceCount ?? 0; + const eCount = Math.floor((d.edges?.length ?? 0) / 2); + const sub = d.subdivisionLevel ?? 0; + + geometry.push(['정점 개수 (Vertices)', String(vCount)]); + geometry.push(['면 개수 (Faces)', String(fCount)]); + geometry.push(['모서리 개수 (Edges)', String(eCount)]); + geometry.push(['세분 레벨 (Subdivision)', String(sub)]); + geometry.push(['크리스 혼합 (Blend Crease)', d.blendCrease ? '예 (Yes)' : '아니오 (No)']); + + if (vCount > 0) { + let minX = Infinity, minY = Infinity, minZ = Infinity; + let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; + for (let i = 0; i + 2 < verts.length; i += 3) { + const x = verts[i], y = verts[i + 1], z = verts[i + 2]; + if (x < minX) minX = x; if (x > maxX) maxX = x; + if (y < minY) minY = y; if (y > maxY) maxY = y; + if (z < minZ) minZ = z; if (z > maxZ) maxZ = z; + } + geometry.push(['최소점 (Min)', pt({ x: minX, y: minY, z: minZ })]); + geometry.push(['최대점 (Max)', pt({ x: maxX, y: maxY, z: maxZ })]); + geometry.push(['표고 범위 (Z Range)', `${f3(minZ)} ~ ${f3(maxZ)}`]); + } + break; + } + case 'TEXT': case 'MTEXT': case 'ATTRIB': { diff --git a/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm b/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm index 2115719..36bd496 100644 Binary files a/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm and b/src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm differ diff --git a/src/viewer2d/dwgParser.ts b/src/viewer2d/dwgParser.ts index f7ac766..f8307bd 100644 --- a/src/viewer2d/dwgParser.ts +++ b/src/viewer2d/dwgParser.ts @@ -1,8 +1,8 @@ /** * DWG parser — rust/dwg-wasm wrapper around the acadrust crate (MPL-2.0, - * consumed unmodified; see rust/dwg-wasm). Emits the parseResult shape - * Viewer2D.load() consumes. Lazy-imported so the wasm is only fetched when a - * DWG is actually opened. + * vendored with a local read_mesh patch; see hmwebviewer rust/dwg-wasm). Emits + * the parseResult shape Viewer2D.load() consumes. Lazy-imported so the wasm is + * only fetched when a DWG is actually opened. * * History: until 2026-07-10 this module selected between acadrust and * @horu2day/pure-cad-parser (?dwgparser= toggle). The old parser was removed