Compare commits
2
Commits
6f200ef692
...
c67a803e4d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c67a803e4d | ||
|
|
eec33820a6 |
@@ -110,6 +110,9 @@ Vite `?url` import 로 로드됩니다.
|
|||||||
| 구형 한글 DWG TEXT 깨짐 (CP949→Latin-1 mojibake) | [`docs/korean-dwg-text-encoding.md`](./docs/korean-dwg-text-encoding.md) |
|
| 구형 한글 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) |
|
| 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) |
|
| `dist2` 정적 빌드 배포 절차 (공식 배포 스크립트 없음) | [`docs/deploy-dist2-static-build.md`](./docs/deploy-dist2-static-build.md) |
|
||||||
|
| SubDMesh(MESH) 미표시 — parseResult 누락 + 큰 메쉬 잘림 | [`docs/subdmesh-rendering.md`](./docs/subdmesh-rendering.md) |
|
||||||
|
| 선가중치(Lineweight) 미적용 — parseResult 누락 + WebGL 1px 한계 | [`docs/lineweight-rendering.md`](./docs/lineweight-rendering.md) |
|
||||||
|
| ↳ 위 둘의 측정 결과 + 상호작용(메쉬 선분이 굵기 버킷 상한을 넘길 위험) | [`docs/improvement-results-2026-08-04.md`](./docs/improvement-results-2026-08-04.md) |
|
||||||
|
|
||||||
샘플 패치: `fixDwgKoreanText`, `cadSpaces` + Viewer2D 공간 필터, Model/Layout 탭.
|
샘플 패치: `fixDwgKoreanText`, `cadSpaces` + Viewer2D 공간 필터, Model/Layout 탭.
|
||||||
원본 `hmwebviewer` 반영 절차는 각 문서를 따른다.
|
원본 `hmwebviewer` 반영 절차는 각 문서를 따른다.
|
||||||
@@ -121,6 +124,6 @@ Vite `?url` import 로 로드됩니다.
|
|||||||
| 조각 | 라이선스 |
|
| 조각 | 라이선스 |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| Viewer2D 포트 · 샘플 글루 | MIT (hmwebviewer 계열) |
|
| Viewer2D 포트 · 샘플 글루 | MIT (hmwebviewer 계열) |
|
||||||
| acadrust (WASM 내부) | MPL-2.0 |
|
| acadrust (WASM 내부, `read_mesh` 로컬 패치 포함) | MPL-2.0 |
|
||||||
| dxf-parser · three | MIT |
|
| dxf-parser · three | MIT |
|
||||||
| NanumGothic | OFL |
|
| NanumGothic | OFL |
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
function h(t){let e,n;try{const f=x(t,i.__wbindgen_malloc),c=w,o=i.parse_dwg_json(f,c);var r=o[0],s=o[1];if(o[3])throw r=0,s=0,T(o[2]);return e=r,n=s,l(r,s)}finally{i.__wbindgen_free(e,n,1)}}function A(){return{__proto__:null,"./acadrust_dwg_bg.js":{__proto__:null,__wbg_Error_92b29b0548f8b746:function(e,n){return Error(l(e,n))},__wbg_String_8564e559799eccda:function(e,n){const r=String(n),s=E(r,i.__wbindgen_malloc,i.__wbindgen_realloc),f=w;m().setInt32(e+4,f,!0),m().setInt32(e+0,s,!0)},__wbg___wbindgen_is_string_ea5e6cc2e4141dfe:function(e){return typeof e=="string"},__wbg___wbindgen_throw_344f42d3211c4765:function(e,n){throw new Error(l(e,n))},__wbg_new_32b398fb48b6d94a:function(){return new Array},__wbg_new_7796ffc7ed656783:function(){return new Map},__wbg_new_da52cf8fe3429cb2:function(){return new Object},__wbg_set_575dd786d51585f8:function(e,n,r){return e.set(n,r)},__wbg_set_6be42768c690e380:function(e,n,r){e[n]=r},__wbg_set_8a16b38e4805b298:function(e,n,r){e[n>>>0]=r},__wbindgen_cast_0000000000000001:function(e){return e},__wbindgen_cast_0000000000000002:function(e){return e},__wbindgen_cast_0000000000000003:function(e,n){return l(e,n)},__wbindgen_cast_0000000000000004:function(e){return BigInt.asUintN(64,e)},__wbindgen_init_externref_table:function(){const e=i.__wbindgen_externrefs,n=e.grow(4);e.set(0,void 0),e.set(n+0,void 0),e.set(n+1,null),e.set(n+2,!0),e.set(n+3,!1)}}}}let a=null;function m(){return(a===null||a.buffer.detached===!0||a.buffer.detached===void 0&&a.buffer!==i.memory.buffer)&&(a=new DataView(i.memory.buffer)),a}function l(t,e){return R(t>>>0,e)}let u=null;function b(){return(u===null||u.byteLength===0)&&(u=new Uint8Array(i.memory.buffer)),u}function x(t,e){const n=e(t.length*1,1)>>>0;return b().set(t,n/1),w=t.length,n}function E(t,e,n){if(n===void 0){const o=d.encode(t),_=e(o.length,1)>>>0;return b().subarray(_,_+o.length).set(o),w=o.length,_}let r=t.length,s=e(r,1)>>>0;const f=b();let c=0;for(;c<r;c++){const o=t.charCodeAt(c);if(o>127)break;f[s+c]=o}if(c!==r){c!==0&&(t=t.slice(c)),s=n(s,r,r=c+t.length*3,1)>>>0;const o=b().subarray(s+c,s+r),_=d.encodeInto(t,o);c+=_.written,s=n(s,r,c,1)>>>0}return w=c,s}function T(t){const e=i.__wbindgen_externrefs.get(t);return i.__externref_table_dealloc(t),e}let g=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});g.decode();const D=2146435072;let y=0;function R(t,e){return y+=e,y>=D&&(g=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),g.decode(),y=e),g.decode(b().subarray(t,t+e))}const d=new TextEncoder;"encodeInto"in d||(d.encodeInto=function(t,e){const n=d.encode(t);return e.set(n),{read:t.length,written:n.length}});let w=0,i;function W(t,e){return i=t.exports,a=null,u=null,i.__wbindgen_start(),i}async function M(t,e){if(typeof Response=="function"&&t instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(t,e)}catch(s){if(t.ok&&n(t.type)&&t.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",s);else throw s}const r=await t.arrayBuffer();return await WebAssembly.instantiate(r,e)}else{const r=await WebAssembly.instantiate(t,e);return r instanceof WebAssembly.Instance?{instance:r,module:t}:r}function n(r){switch(r){case"basic":case"cors":case"default":return!0}return!1}}async function S(t){if(i!==void 0)return i;t!==void 0&&(Object.getPrototypeOf(t)===Object.prototype?{module_or_path:t}=t:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),t===void 0&&(t=new URL("/assets/acadrust_dwg_bg-DmCEpFBu.wasm",import.meta.url));const e=A();(typeof t=="string"||typeof Request=="function"&&t instanceof Request||typeof URL=="function"&&t instanceof URL)&&(t=fetch(t));const{instance:n,module:r}=await M(await t,e);return W(n)}const O="/assets/acadrust_dwg_bg-DmCEpFBu.wasm";let p=null;function v(){return p||(p=S({module_or_path:O}).then(()=>{})),p}function I(t){return JSON.parse(h(t))}export{v as initAcadrustParser,I as parseDwgAcadrust};
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
+41
-1
@@ -14,6 +14,45 @@
|
|||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink); font-family: var(--sans); }
|
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink); font-family: var(--sans); }
|
||||||
#app { position: fixed; inset: 0; }
|
#app { position: fixed; inset: 0; }
|
||||||
|
|
||||||
|
/* ── 상단 로딩 프로그레스 바 (src/topProgress.ts 가 구동) ───────────────
|
||||||
|
.bar : 실제 진행률. width 트랜지션 = 메인 스레드 의존.
|
||||||
|
.sweep : WASM 파싱 / Viewer2D.load() 같은 동기 블로킹 구간용. transform
|
||||||
|
키프레임은 컴포지터 스레드에서 돌기 때문에 메인 스레드가 멈춰
|
||||||
|
있어도 계속 흐른다 — 그 구간에 줄 수 있는 유일한 피드백. */
|
||||||
|
#progress {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 100;
|
||||||
|
pointer-events: none; overflow: hidden;
|
||||||
|
opacity: 0; transition: opacity .3s ease .15s;
|
||||||
|
}
|
||||||
|
#progress.on { opacity: 1; transition-delay: 0s; }
|
||||||
|
#progress .bar {
|
||||||
|
position: absolute; top: 0; bottom: 0; left: 0; width: 0;
|
||||||
|
background: linear-gradient(90deg, var(--accent), #58a6ff 55%, var(--wasm));
|
||||||
|
box-shadow: 0 0 10px rgba(63,185,80,.65), 0 0 4px rgba(88,166,255,.5);
|
||||||
|
transition: width .28s cubic-bezier(.25,.8,.25,1);
|
||||||
|
}
|
||||||
|
#progress.err .bar {
|
||||||
|
background: var(--danger);
|
||||||
|
box-shadow: 0 0 10px rgba(248,81,73,.7);
|
||||||
|
}
|
||||||
|
#progress .sweep {
|
||||||
|
position: absolute; top: 0; bottom: 0; left: 0; width: 34%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(230,237,243,.9), transparent);
|
||||||
|
transform: translateX(-105%); will-change: transform; opacity: 0;
|
||||||
|
}
|
||||||
|
#progress.busy .sweep {
|
||||||
|
opacity: 1;
|
||||||
|
animation: pr-sweep 1.15s cubic-bezier(.4,0,.2,1) infinite;
|
||||||
|
}
|
||||||
|
@keyframes pr-sweep {
|
||||||
|
from { transform: translateX(-105%); }
|
||||||
|
to { transform: translateX(400%); }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#progress .sweep { animation: none; }
|
||||||
|
#progress .bar { transition: none; }
|
||||||
|
}
|
||||||
#toolbar {
|
#toolbar {
|
||||||
position: fixed; top: 12px; left: 12px; right: 12px; z-index: 10;
|
position: fixed; top: 12px; left: 12px; right: 12px; z-index: 10;
|
||||||
display: flex; flex-wrap: wrap; gap: 8px; align-items: center; pointer-events: none;
|
display: flex; flex-wrap: wrap; gap: 8px; align-items: center; pointer-events: none;
|
||||||
@@ -159,7 +198,7 @@
|
|||||||
}
|
}
|
||||||
.layer-sub-btn:hover { color: var(--ink); border-color: var(--accent); background: rgba(63,185,80,.1); }
|
.layer-sub-btn:hover { color: var(--ink); border-color: var(--accent); background: rgba(63,185,80,.1); }
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-KG0HO38y.js"></script>
|
<script type="module" crossorigin src="/assets/index-D3FqDGwP.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
@@ -175,6 +214,7 @@
|
|||||||
<div id="spaces" title="Model Space / Paper Space (Layout)"></div>
|
<div id="spaces" title="Model Space / Paper Space (Layout)"></div>
|
||||||
<button id="fit" type="button">Fit (F)</button>
|
<button id="fit" type="button">Fit (F)</button>
|
||||||
<button id="theme" type="button">Theme</button>
|
<button id="theme" type="button">Theme</button>
|
||||||
|
<button id="lwt" type="button" title="선가중치 표시 (AutoCAD LWDISPLAY)">선가중치</button>
|
||||||
<button id="layersBtn" type="button">Layers</button>
|
<button id="layersBtn" type="button">Layers</button>
|
||||||
<button id="propsBtn" type="button">Props (속성)</button>
|
<button id="propsBtn" type="button">Props (속성)</button>
|
||||||
<div id="status">ready</div>
|
<div id="status">ready</div>
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
# 개선 결과 보고 — 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 -- <file.dwg>
|
||||||
|
```
|
||||||
|
|
||||||
|
5절의 버킷 표는 `Viewer2D.js` 에서 `_meshSegs` · `_lwPx` 를 들어내 parseResult 에
|
||||||
|
직접 돌려 만들었다 (`LINE` / `LWPOLYLINE` / `MESH` 만 계수). `Viewer2D.js` 는 three
|
||||||
|
와 확장자 없는 TS 모듈을 import 하므로 node 에서 그대로 import 되지 않는다 —
|
||||||
|
함수 본문만 뽑아 `new Function` 으로 감싸는 방식이었다.
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
# 선가중치(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 × 8 (LW_PX_PER_MM, 최대 24px)
|
||||||
|
0.25mm ≈ 2px · 0.50mm ≈ 4px · 2.11mm ≈ 17px ← AutoCAD 기본 표시 배율과 동일 눈금
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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-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
|
||||||
|
|
||||||
|
- `index.html` · `src/main.ts` — 툴바 **선가중치** 버튼 (상태에 따라 강조, 툴팁에 ON/OFF)
|
||||||
|
- `src/propertyInspector.ts` — `선가중치 (Lineweight)` 행 추가.
|
||||||
|
`-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. 회귀 없음
|
||||||
|
|
||||||
|
- `npx tsc --noEmit` 통과
|
||||||
|
- `npm run build` 통과
|
||||||
|
- 대조 도면 4종: 굵기 전부 Default → **화면 변화 없음**(설계 3-2 의도대로)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 남은 과제
|
||||||
|
|
||||||
|
1. **표시 배율 고정** — `LW_PX_PER_MM = 8` 이 상수다. AutoCAD 는 "선가중치 설정 → 표시 배율"
|
||||||
|
슬라이더로 조절 가능하므로, 필요하면 `setLineweightScale()` 로 노출할 것.
|
||||||
|
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)
|
||||||
@@ -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 -- <file.dwg>
|
||||||
|
```
|
||||||
|
|
||||||
|
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`
|
||||||
+40
@@ -14,6 +14,45 @@
|
|||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink); font-family: var(--sans); }
|
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink); font-family: var(--sans); }
|
||||||
#app { position: fixed; inset: 0; }
|
#app { position: fixed; inset: 0; }
|
||||||
|
|
||||||
|
/* ── 상단 로딩 프로그레스 바 (src/topProgress.ts 가 구동) ───────────────
|
||||||
|
.bar : 실제 진행률. width 트랜지션 = 메인 스레드 의존.
|
||||||
|
.sweep : WASM 파싱 / Viewer2D.load() 같은 동기 블로킹 구간용. transform
|
||||||
|
키프레임은 컴포지터 스레드에서 돌기 때문에 메인 스레드가 멈춰
|
||||||
|
있어도 계속 흐른다 — 그 구간에 줄 수 있는 유일한 피드백. */
|
||||||
|
#progress {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 100;
|
||||||
|
pointer-events: none; overflow: hidden;
|
||||||
|
opacity: 0; transition: opacity .3s ease .15s;
|
||||||
|
}
|
||||||
|
#progress.on { opacity: 1; transition-delay: 0s; }
|
||||||
|
#progress .bar {
|
||||||
|
position: absolute; top: 0; bottom: 0; left: 0; width: 0;
|
||||||
|
background: linear-gradient(90deg, var(--accent), #58a6ff 55%, var(--wasm));
|
||||||
|
box-shadow: 0 0 10px rgba(63,185,80,.65), 0 0 4px rgba(88,166,255,.5);
|
||||||
|
transition: width .28s cubic-bezier(.25,.8,.25,1);
|
||||||
|
}
|
||||||
|
#progress.err .bar {
|
||||||
|
background: var(--danger);
|
||||||
|
box-shadow: 0 0 10px rgba(248,81,73,.7);
|
||||||
|
}
|
||||||
|
#progress .sweep {
|
||||||
|
position: absolute; top: 0; bottom: 0; left: 0; width: 34%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(230,237,243,.9), transparent);
|
||||||
|
transform: translateX(-105%); will-change: transform; opacity: 0;
|
||||||
|
}
|
||||||
|
#progress.busy .sweep {
|
||||||
|
opacity: 1;
|
||||||
|
animation: pr-sweep 1.15s cubic-bezier(.4,0,.2,1) infinite;
|
||||||
|
}
|
||||||
|
@keyframes pr-sweep {
|
||||||
|
from { transform: translateX(-105%); }
|
||||||
|
to { transform: translateX(400%); }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#progress .sweep { animation: none; }
|
||||||
|
#progress .bar { transition: none; }
|
||||||
|
}
|
||||||
#toolbar {
|
#toolbar {
|
||||||
position: fixed; top: 12px; left: 12px; right: 12px; z-index: 10;
|
position: fixed; top: 12px; left: 12px; right: 12px; z-index: 10;
|
||||||
display: flex; flex-wrap: wrap; gap: 8px; align-items: center; pointer-events: none;
|
display: flex; flex-wrap: wrap; gap: 8px; align-items: center; pointer-events: none;
|
||||||
@@ -174,6 +213,7 @@
|
|||||||
<div id="spaces" title="Model Space / Paper Space (Layout)"></div>
|
<div id="spaces" title="Model Space / Paper Space (Layout)"></div>
|
||||||
<button id="fit" type="button">Fit (F)</button>
|
<button id="fit" type="button">Fit (F)</button>
|
||||||
<button id="theme" type="button">Theme</button>
|
<button id="theme" type="button">Theme</button>
|
||||||
|
<button id="lwt" type="button" title="선가중치 표시 (AutoCAD LWDISPLAY)">선가중치</button>
|
||||||
<button id="layersBtn" type="button">Layers</button>
|
<button id="layersBtn" type="button">Layers</button>
|
||||||
<button id="propsBtn" type="button">Props (속성)</button>
|
<button id="propsBtn" type="button">Props (속성)</button>
|
||||||
<div id="status">ready</div>
|
<div id="status">ready</div>
|
||||||
|
|||||||
+12
@@ -162,6 +162,7 @@ async function loadAndRender(buf: ArrayBuffer, name: string): Promise<CadParseRe
|
|||||||
const def = pickDefaultSpace(spaces);
|
const def = pickDefaultSpace(spaces);
|
||||||
viewer.load(result, { spaceHandle: def?.handleHex ?? null });
|
viewer.load(result, { spaceHandle: def?.handleHex ?? null });
|
||||||
viewer.resize();
|
viewer.resize();
|
||||||
|
syncLwtBtn();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +230,17 @@ document.getElementById('theme')!.addEventListener('click', () => {
|
|||||||
viewer.setTheme(dark);
|
viewer.setTheme(dark);
|
||||||
fillLayers();
|
fillLayers();
|
||||||
});
|
});
|
||||||
|
// 선가중치(LWT): 처음에는 도면의 LWDISPLAY 값을 따르고, 누르면 강제 on/off.
|
||||||
|
const lwtBtn = document.getElementById('lwt') as HTMLButtonElement;
|
||||||
|
const syncLwtBtn = () => {
|
||||||
|
const on = viewer.getLineweightEnabled();
|
||||||
|
lwtBtn.classList.toggle('primary', on);
|
||||||
|
lwtBtn.title = `선가중치 표시 ${on ? 'ON' : 'OFF'} (AutoCAD LWDISPLAY)`;
|
||||||
|
};
|
||||||
|
lwtBtn.addEventListener('click', () => {
|
||||||
|
viewer.setLineweightEnabled(!viewer.getLineweightEnabled());
|
||||||
|
syncLwtBtn();
|
||||||
|
});
|
||||||
document.getElementById('layersBtn')!.addEventListener('click', () => {
|
document.getElementById('layersBtn')!.addEventListener('click', () => {
|
||||||
layersPanel.style.display = layersPanel.style.display === 'flex' ? 'none' : 'flex';
|
layersPanel.style.display = layersPanel.style.display === 'flex' ? 'none' : 'flex';
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ function formatColor(colorVal: any): string {
|
|||||||
return String(colorVal);
|
return String(colorVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** DWG lineweight: i16 in 1/100 mm, with -1/-2/-3 reserved for the inherit modes. */
|
||||||
|
function formatLineweight(raw: unknown): string {
|
||||||
|
if (typeof raw !== 'number') return 'BYLAYER';
|
||||||
|
if (raw === -1) return 'BYLAYER';
|
||||||
|
if (raw === -2) return 'BYBLOCK';
|
||||||
|
if (raw === -3) return 'DEFAULT (0.25mm)';
|
||||||
|
if (raw < 0) return String(raw);
|
||||||
|
return `${(raw / 100).toFixed(2)}mm`;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatEntityProperties(entity: any): FormattedProperties {
|
export function formatEntityProperties(entity: any): FormattedProperties {
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return { id: '', type: '', handle: '', layer: '', general: [], geometry: [] };
|
return { id: '', type: '', handle: '', layer: '', general: [], geometry: [] };
|
||||||
@@ -51,6 +61,9 @@ export function formatEntityProperties(entity: any): FormattedProperties {
|
|||||||
const layer = entity.layer ?? entity.layerName ?? '0';
|
const layer = entity.layer ?? entity.layerName ?? '0';
|
||||||
const color = formatColor(entity.color ?? d.color);
|
const color = formatColor(entity.color ?? d.color);
|
||||||
const linetype = entity.lineType ?? entity.linetype ?? d.lineType ?? 'BYLAYER';
|
const linetype = entity.lineType ?? entity.linetype ?? d.lineType ?? 'BYLAYER';
|
||||||
|
const lineweight = formatLineweight(
|
||||||
|
entity.entityHeader?.lineWeight ?? entity.lineWeight ?? entity.lineweight ?? d.lineweight,
|
||||||
|
);
|
||||||
|
|
||||||
const general: [string, string][] = [
|
const general: [string, string][] = [
|
||||||
['유형 (Type)', type],
|
['유형 (Type)', type],
|
||||||
@@ -58,6 +71,7 @@ export function formatEntityProperties(entity: any): FormattedProperties {
|
|||||||
['레이어 (Layer)', String(layer)],
|
['레이어 (Layer)', String(layer)],
|
||||||
['색상 (Color)', color],
|
['색상 (Color)', color],
|
||||||
['선종류 (Linetype)', String(linetype)],
|
['선종류 (Linetype)', String(linetype)],
|
||||||
|
['선가중치 (Lineweight)', lineweight],
|
||||||
];
|
];
|
||||||
|
|
||||||
const geometry: [string, string][] = [];
|
const geometry: [string, string][] = [];
|
||||||
@@ -155,6 +169,37 @@ export function formatEntityProperties(entity: any): FormattedProperties {
|
|||||||
break;
|
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 'TEXT':
|
||||||
case 'MTEXT':
|
case 'MTEXT':
|
||||||
case 'ATTRIB': {
|
case 'ATTRIB': {
|
||||||
|
|||||||
Vendored
+3
@@ -16,6 +16,9 @@ export class Viewer2D {
|
|||||||
setAccent(hex: string): void;
|
setAccent(hex: string): void;
|
||||||
setTheme(dark: boolean): void;
|
setTheme(dark: boolean): void;
|
||||||
setGrid(visible: boolean): void;
|
setGrid(visible: boolean): void;
|
||||||
|
/** Lineweight display (AutoCAD LWDISPLAY); null follows the drawing's own flag. */
|
||||||
|
setLineweightEnabled(on: boolean | null): void;
|
||||||
|
getLineweightEnabled(): boolean;
|
||||||
getLayerInfo(): { name: string; colorHex: string; count: number; visible: boolean }[];
|
getLayerInfo(): { name: string; colorHex: string; count: number; visible: boolean }[];
|
||||||
setHiddenLayers(nameSet: Set<string>): void;
|
setHiddenLayers(nameSet: Set<string>): void;
|
||||||
/** Model / Paper (Layout) spaces for the loaded drawing. */
|
/** Model / Paper (Layout) spaces for the loaded drawing. */
|
||||||
|
|||||||
+332
-24
@@ -4,10 +4,14 @@
|
|||||||
* 2D+3D merge. Renders CAD entities as LineSegments / Mesh / CanvasTexture sprites.
|
* 2D+3D merge. Renders CAD entities as LineSegments / Mesh / CanvasTexture sprites.
|
||||||
* Supported: LINE, CIRCLE, ARC, LWPOLYLINE, POLYLINE, POINT, ELLIPSE, SOLID,
|
* Supported: LINE, CIRCLE, ARC, LWPOLYLINE, POLYLINE, POINT, ELLIPSE, SOLID,
|
||||||
* TEXT, MTEXT, INSERT (ownerHandle block expansion), HATCH (solid+outline+bulge),
|
* TEXT, MTEXT, INSERT (ownerHandle block expansion), HATCH (solid+outline+bulge),
|
||||||
|
* MESH (AcDbSubDMesh, base-mesh wireframe),
|
||||||
* DIMENSION_LINEAR/ALIGNED/RADIUS/DIAMETER/ANG_3PT/ANG_2LN/ORDINATE
|
* DIMENSION_LINEAR/ALIGNED/RADIUS/DIAMETER/ANG_3PT/ANG_2LN/ORDINATE
|
||||||
*/
|
*/
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js';
|
||||||
|
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js';
|
||||||
|
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
|
||||||
import { aciToHex } from './aciColors.js';
|
import { aciToHex } from './aciColors.js';
|
||||||
import { buildAllowedOwnerHexes, listCadSpaces } from './cadSpaces';
|
import { buildAllowedOwnerHexes, listCadSpaces } from './cadSpaces';
|
||||||
import { SlugTextEngine, SlugTextBatch } from './slugText';
|
import { SlugTextEngine, SlugTextBatch } from './slugText';
|
||||||
@@ -18,6 +22,23 @@ const ARC_SEGS = 64;
|
|||||||
const ELLIPSE_SEGS = 72;
|
const ELLIPSE_SEGS = 72;
|
||||||
const CLICK_THRESHOLD_PX = 14;
|
const CLICK_THRESHOLD_PX = 14;
|
||||||
|
|
||||||
|
// ── Lineweight (AutoCAD LWDISPLAY) ─────────────────────────────────────────
|
||||||
|
// DWG stores lineweight as an i16 in 1/100 mm (-1 ByLayer, -2 ByBlock,
|
||||||
|
// -3 Default). AutoCAD shows it in *screen* pixels — the width does not grow
|
||||||
|
// when you zoom in — at roughly 8 px per mm on the default display-scale
|
||||||
|
// slider (0.25 mm ≈ 2 px, 2.11 mm ≈ 17 px), so that is the mapping used here.
|
||||||
|
const LW_PX_PER_MM = 8;
|
||||||
|
// LWDEFAULT: what -3 (and a layer that never set one) resolves to.
|
||||||
|
const LW_DEFAULT_MM = 0.25;
|
||||||
|
// At or below the CAD default the line stays in the 1-px hairline batch: a
|
||||||
|
// drawing that never assigned weights must look exactly as it did before.
|
||||||
|
const LW_HAIRLINE_MM = 0.25;
|
||||||
|
const LW_MAX_PX = 24;
|
||||||
|
// Fat lines cost 12 floats + 8 verts per segment (instanced quads). Past this
|
||||||
|
// many segments in one weight bucket, fall back to hairline instead of risking
|
||||||
|
// a GPU/heap stall on huge survey drawings.
|
||||||
|
const LW_MAX_FAT_SEGS = 400000;
|
||||||
|
|
||||||
/** Bucket pending texts by their owning CAD layer (insertion order preserved). */
|
/** Bucket pending texts by their owning CAD layer (insertion order preserved). */
|
||||||
function groupByLayer(list) {
|
function groupByLayer(list) {
|
||||||
const m = new Map();
|
const m = new Map();
|
||||||
@@ -72,9 +93,18 @@ export class Viewer2D {
|
|||||||
this._layerBoxes = new Map();
|
this._layerBoxes = new Map();
|
||||||
this._layerSamples = new Map();
|
this._layerSamples = new Map();
|
||||||
this._indexedBufs = [];
|
this._indexedBufs = [];
|
||||||
|
// · _fatBufs LineSegments2 batches (one per lineweight/dash bucket).
|
||||||
|
// Instanced, so layer masking compacts the instance buffer
|
||||||
|
// instead of rewriting an index (see _applyLayerVisibility).
|
||||||
|
this._fatBufs = [];
|
||||||
this._metaHidden = null;
|
this._metaHidden = null;
|
||||||
this._fullContentBox = null;
|
this._fullContentBox = null;
|
||||||
this._hiddenLayers = new Set();
|
this._hiddenLayers = new Set();
|
||||||
|
// Lineweight display: null = follow the drawing's LWDISPLAY header var,
|
||||||
|
// true/false = forced by the host UI (setLineweightEnabled).
|
||||||
|
this._lwForced = null;
|
||||||
|
this._lwDisplay = false;
|
||||||
|
this._curLwPx = 0;
|
||||||
SlugTextEngine.shared().catch(() => {}); // warm the font load; sprite fallback covers failure
|
SlugTextEngine.shared().catch(() => {}); // warm the font load; sprite fallback covers failure
|
||||||
|
|
||||||
this._scene = new THREE.Scene();
|
this._scene = new THREE.Scene();
|
||||||
@@ -302,6 +332,11 @@ export class Viewer2D {
|
|||||||
this._buildLayerMap(result?.tables?.layers);
|
this._buildLayerMap(result?.tables?.layers);
|
||||||
this._buildStyleMap(result?.tables?.styles || result?.tables?.textStyles);
|
this._buildStyleMap(result?.tables?.styles || result?.tables?.textStyles);
|
||||||
this._buildLinetypeMap(result);
|
this._buildLinetypeMap(result);
|
||||||
|
// LWDISPLAY decides whether weights show at all — same switch as AutoCAD's
|
||||||
|
// status-bar "Show/Hide Lineweight". The host UI can override it.
|
||||||
|
// DXF spells the header var $LWDISPLAY; dwg-wasm emits vars.lwdisplay.
|
||||||
|
this._lwDisplay = this._lwForced
|
||||||
|
?? !!(result?.vars?.lwdisplay ?? result?.vars?.$LWDISPLAY ?? result?.vars?.LWDISPLAY);
|
||||||
// Sheet orientation: the active model-space viewport's VIEWTWIST rotates the
|
// Sheet orientation: the active model-space viewport's VIEWTWIST rotates the
|
||||||
// view so a drawing stored tilted in WCS (rotated survey sheets — header UCS
|
// view so a drawing stored tilted in WCS (rotated survey sheets — header UCS
|
||||||
// stays identity) displays with its title border upright. Applied as a
|
// stays identity) displays with its title border upright. Applied as a
|
||||||
@@ -330,11 +365,14 @@ export class Viewer2D {
|
|||||||
if (id === undefined) { id = layerNames.length; layerNames.push(name); layerIds.set(name, id); }
|
if (id === undefined) { id = layerNames.length; layerNames.push(name); layerIds.set(name, id); }
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
// Dashed-linetype segments go into separate buckets keyed by dash|gap size;
|
// Segments that can't join the one merged hairline batch go into buckets
|
||||||
// each becomes its own LineDashedMaterial LineSegments at assembly. Solid
|
// keyed by dash|gap size and lineweight; each becomes its own mesh at
|
||||||
// (Continuous / ByLayer→Continuous) segments stay in lineVerts/lineColors.
|
// assembly (LineDashedMaterial for thin dashes, LineSegments2 for anything
|
||||||
const dashBuckets = new Map();
|
// with a real lineweight). Solid hairlines stay in lineVerts/lineColors.
|
||||||
|
const segBuckets = new Map();
|
||||||
let curDash = null; // {key,dash,gap} for the entity currently being emitted
|
let curDash = null; // {key,dash,gap} for the entity currently being emitted
|
||||||
|
let entIdx = -1; // _entityMeta index of that entity (-1 before the loop)
|
||||||
|
let curSpans = []; // buckets it has written to so far
|
||||||
const box = new THREE.Box3();
|
const box = new THREE.Box3();
|
||||||
const _tmp = new THREE.Vector3();
|
const _tmp = new THREE.Vector3();
|
||||||
const pendingTexts = [];
|
const pendingTexts = [];
|
||||||
@@ -423,9 +461,18 @@ export class Viewer2D {
|
|||||||
const r = ((color >> 16) & 0xFF) / 255;
|
const r = ((color >> 16) & 0xFF) / 255;
|
||||||
const g = ((color >> 8) & 0xFF) / 255;
|
const g = ((color >> 8) & 0xFF) / 255;
|
||||||
const b = (color & 0xFF) / 255;
|
const b = (color & 0xFF) / 255;
|
||||||
if (curDash) {
|
const lwPx = this._curLwPx;
|
||||||
let bk = dashBuckets.get(curDash.key);
|
if (curDash || lwPx > 0) {
|
||||||
if (!bk) { bk = { dash: curDash.dash, gap: curDash.gap, verts: [], colors: [], segLayer: [] }; dashBuckets.set(curDash.key, bk); }
|
const key = `${curDash ? curDash.key : ''}#${lwPx}`;
|
||||||
|
let bk = segBuckets.get(key);
|
||||||
|
if (!bk) {
|
||||||
|
bk = { dash: curDash?.dash ?? 0, gap: curDash?.gap ?? 0, lwPx, verts: [], colors: [], segLayer: [], entIdx: -2 };
|
||||||
|
segBuckets.set(key, bk);
|
||||||
|
}
|
||||||
|
// Selection highlight recolors an entity's own vertices. Its colors no
|
||||||
|
// longer live in one array, so remember where this entity's run starts
|
||||||
|
// in every bucket it touches (see meta.spans).
|
||||||
|
if (bk.entIdx !== entIdx) { bk.entIdx = entIdx; bk.entStart = bk.colors.length; curSpans.push(bk); }
|
||||||
bk.verts.push(ax, ay, z, bx, by, z);
|
bk.verts.push(ax, ay, z, bx, by, z);
|
||||||
bk.colors.push(r, g, b, r, g, b);
|
bk.colors.push(r, g, b, r, g, b);
|
||||||
bk.segLayer.push(this._curLayerId);
|
bk.segLayer.push(this._curLayerId);
|
||||||
@@ -502,7 +549,10 @@ export class Viewer2D {
|
|||||||
const complexLt = this._resolveComplexLinetype(e);
|
const complexLt = this._resolveComplexLinetype(e);
|
||||||
if (complexLt) curDash = null;
|
if (complexLt) curDash = null;
|
||||||
else curDash = this._resolveDash(e);
|
else curDash = this._resolveDash(e);
|
||||||
|
this._curLwPx = this._lwPx(e);
|
||||||
const meta = { entity: e, type, bounds: null, layer: this._curLayer, colStart: lineColors.length };
|
const meta = { entity: e, type, bounds: null, layer: this._curLayer, colStart: lineColors.length };
|
||||||
|
entIdx = this._entityMeta.length;
|
||||||
|
curSpans = [];
|
||||||
this._entityMeta.push(meta);
|
this._entityMeta.push(meta);
|
||||||
this._pickCurIdx = this._entityMeta.length - 1; // owner for pick geometry emitted below
|
this._pickCurIdx = this._entityMeta.length - 1; // owner for pick geometry emitted below
|
||||||
const textStart = pendingTexts.length;
|
const textStart = pendingTexts.length;
|
||||||
@@ -709,6 +759,16 @@ export class Viewer2D {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── MESH (AcDbSubDMesh) ───────────────────────────────────────────
|
||||||
|
// Drawn as its face wireframe, matching AutoCAD's 2D wireframe view.
|
||||||
|
case 'MESH': {
|
||||||
|
if (d.vertices?.length >= 3) {
|
||||||
|
this._meshSegs(d, color, pushSeg);
|
||||||
|
meta.bounds = { type:'point', cx:d.vertices[0], cy:d.vertices[1] };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Text ──────────────────────────────────────────────────────────
|
// ── Text ──────────────────────────────────────────────────────────
|
||||||
case 'TEXT':
|
case 'TEXT':
|
||||||
case 'MTEXT': {
|
case 'MTEXT': {
|
||||||
@@ -1045,6 +1105,10 @@ export class Viewer2D {
|
|||||||
}
|
}
|
||||||
} catch { /* skip malformed entity */ }
|
} catch { /* skip malformed entity */ }
|
||||||
meta.colEnd = lineColors.length;
|
meta.colEnd = lineColors.length;
|
||||||
|
if (curSpans.length) {
|
||||||
|
meta.spans = curSpans.map((bk) => ({ bk, start: bk.entStart, end: bk.colors.length }));
|
||||||
|
curSpans = [];
|
||||||
|
}
|
||||||
// Deferred texts are flushed after the loop (async) — stamp the owning
|
// Deferred texts are flushed after the loop (async) — stamp the owning
|
||||||
// layer now so _drawTexts can batch per layer.
|
// layer now so _drawTexts can batch per layer.
|
||||||
for (let ti = textStart; ti < pendingTexts.length; ti++) {
|
for (let ti = textStart; ti < pendingTexts.length; ti++) {
|
||||||
@@ -1088,7 +1152,26 @@ export class Viewer2D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._layerNames = layerNames;
|
this._layerNames = layerNames;
|
||||||
this._lineColorAttr = null; this._origColors = null;
|
this._lineColorAttr = null; this._origColors = null; this._hairRt = null;
|
||||||
|
|
||||||
|
// A weighted bucket only earns its own fat-line mesh while it stays inside
|
||||||
|
// the segment cap; past that it is folded back into the hairline batch
|
||||||
|
// (solid) or drawn as a thin dash bucket, which is what the viewer did
|
||||||
|
// before lineweight existed.
|
||||||
|
for (const bk of segBuckets.values()) {
|
||||||
|
bk.fat = bk.lwPx > 0 && bk.verts.length / 6 <= LW_MAX_FAT_SEGS;
|
||||||
|
if (bk.lwPx > 0 && !bk.fat) {
|
||||||
|
console.warn(`선가중치 ${bk.lwPx}px 세그먼트 ${bk.verts.length / 6}개 → 헤어라인으로 대체`);
|
||||||
|
}
|
||||||
|
if (!bk.fat && bk.dash <= 0) {
|
||||||
|
bk.mergeOffset = lineColors.length;
|
||||||
|
for (let i = 0; i < bk.verts.length; i++) lineVerts.push(bk.verts[i]);
|
||||||
|
for (let i = 0; i < bk.colors.length; i++) lineColors.push(bk.colors[i]);
|
||||||
|
for (let i = 0; i < bk.segLayer.length; i++) lineSegLayer.push(bk.segLayer[i]);
|
||||||
|
bk.merged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (lineVerts.length) {
|
if (lineVerts.length) {
|
||||||
const geom = new THREE.BufferGeometry();
|
const geom = new THREE.BufferGeometry();
|
||||||
geom.setAttribute('position', new THREE.Float32BufferAttribute(lineVerts, 3));
|
geom.setAttribute('position', new THREE.Float32BufferAttribute(lineVerts, 3));
|
||||||
@@ -1099,24 +1182,46 @@ export class Viewer2D {
|
|||||||
this._group.add(lmesh);
|
this._group.add(lmesh);
|
||||||
this._lineColorAttr = colorAttr;
|
this._lineColorAttr = colorAttr;
|
||||||
this._origColors = Float32Array.from(colorAttr.array);
|
this._origColors = Float32Array.from(colorAttr.array);
|
||||||
|
this._hairRt = { attr: colorAttr, orig: this._origColors };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dashed linetypes (HIDDEN / CENTER / …) — one LineSegments per dash|gap
|
// One mesh per remaining bucket.
|
||||||
// bucket with a LineDashedMaterial. computeLineDistances() is REQUIRED for
|
// · lineweight > 0 → LineSegments2: screen-space quads, so the width stays
|
||||||
// the dash pattern to appear; on LineSegments each 2-vertex pair dashes
|
// constant in pixels while zooming (AutoCAD LWDISPLAY semantics).
|
||||||
|
// · otherwise → LineSegments + LineDashedMaterial for the dash
|
||||||
|
// linetypes (HIDDEN / CENTER / …). computeLineDistances() is REQUIRED
|
||||||
|
// for the pattern to appear; on LineSegments each 2-vertex pair dashes
|
||||||
// independently from its own start (correct for CAD segments).
|
// independently from its own start (correct for CAD segments).
|
||||||
for (const bk of dashBuckets.values()) {
|
for (const bk of segBuckets.values()) {
|
||||||
if (!bk.verts.length) continue;
|
if (!bk.verts.length || bk.merged) continue;
|
||||||
|
if (bk.fat) {
|
||||||
|
const fgeom = new LineSegmentsGeometry();
|
||||||
|
fgeom.setPositions(bk.verts);
|
||||||
|
fgeom.setColors(bk.colors);
|
||||||
|
const fmat = new LineMaterial({
|
||||||
|
vertexColors: true, linewidth: bk.lwPx, worldUnits: false,
|
||||||
|
dashed: bk.dash > 0, dashSize: bk.dash, gapSize: bk.gap,
|
||||||
|
});
|
||||||
|
this._sizeLineMaterial(fmat);
|
||||||
|
const fline = new LineSegments2(fgeom, fmat);
|
||||||
|
if (bk.dash > 0) fline.computeLineDistances();
|
||||||
|
this._registerFat(fline, bk.segLayer);
|
||||||
|
bk.rt = this._fatBufs[this._fatBufs.length - 1];
|
||||||
|
this._group.add(fline);
|
||||||
|
} else {
|
||||||
const dgeom = new THREE.BufferGeometry();
|
const dgeom = new THREE.BufferGeometry();
|
||||||
dgeom.setAttribute('position', new THREE.Float32BufferAttribute(bk.verts, 3));
|
dgeom.setAttribute('position', new THREE.Float32BufferAttribute(bk.verts, 3));
|
||||||
dgeom.setAttribute('color', new THREE.Float32BufferAttribute(bk.colors, 3));
|
const dcol = new THREE.Float32BufferAttribute(bk.colors, 3);
|
||||||
|
dgeom.setAttribute('color', dcol);
|
||||||
const dline = new THREE.LineSegments(dgeom, new THREE.LineDashedMaterial({
|
const dline = new THREE.LineSegments(dgeom, new THREE.LineDashedMaterial({
|
||||||
vertexColors: true, dashSize: bk.dash, gapSize: bk.gap,
|
vertexColors: true, dashSize: bk.dash, gapSize: bk.gap,
|
||||||
}));
|
}));
|
||||||
dline.computeLineDistances(); // must run BEFORE setIndex (three skips indexed geometry)
|
dline.computeLineDistances(); // must run BEFORE setIndex (three skips indexed geometry)
|
||||||
this._registerIndexed(dline, bk.segLayer);
|
this._registerIndexed(dline, bk.segLayer);
|
||||||
|
bk.rt = { attr: dcol, orig: Float32Array.from(dcol.array) };
|
||||||
this._group.add(dline);
|
this._group.add(dline);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Coordinate readout frame (Model UCS vs paper identity).
|
// Coordinate readout frame (Model UCS vs paper identity).
|
||||||
this._setCoordFrame(result);
|
this._setCoordFrame(result);
|
||||||
@@ -1228,6 +1333,50 @@ export class Viewer2D {
|
|||||||
this._indexedBufs.push({ mesh, segLayer: Int32Array.from(segLayerArr), index, attr });
|
this._indexedBufs.push({ mesh, segLayer: Int32Array.from(segLayerArr), index, attr });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track a LineSegments2 batch for layer masking. Its geometry is instanced
|
||||||
|
* (one instance per segment, positions/colors interleaved 6 floats each), so
|
||||||
|
* there is no index to rewrite: hiding a layer compacts the visible segments
|
||||||
|
* into the front of the instance buffers and drops `instanceCount`.
|
||||||
|
* The untouched originals are kept so any later mask starts from clean data.
|
||||||
|
*/
|
||||||
|
_registerFat(mesh, segLayerArr) {
|
||||||
|
const geo = mesh.geometry;
|
||||||
|
const posBuf = geo.attributes.instanceStart?.data;
|
||||||
|
const colBuf = geo.attributes.instanceColorStart?.data;
|
||||||
|
if (!posBuf) return;
|
||||||
|
posBuf.setUsage(THREE.DynamicDrawUsage);
|
||||||
|
colBuf?.setUsage(THREE.DynamicDrawUsage);
|
||||||
|
this._fatBufs.push({
|
||||||
|
mesh,
|
||||||
|
segLayer: Int32Array.from(segLayerArr),
|
||||||
|
posBuf, colBuf,
|
||||||
|
posSrc: Float32Array.from(posBuf.array),
|
||||||
|
colSrc: colBuf ? Float32Array.from(colBuf.array) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fat-line materials need the canvas size in px to convert linewidth. */
|
||||||
|
_sizeLineMaterial(mat) {
|
||||||
|
const w = this._container?.clientWidth || this._renderer?.domElement?.clientWidth || 1;
|
||||||
|
const h = this._container?.clientHeight || this._renderer?.domElement?.clientHeight || 1;
|
||||||
|
mat.resolution.set(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lineweight display switch (AutoCAD LWDISPLAY).
|
||||||
|
* @param {boolean|null} on true/false forces it, null follows the drawing.
|
||||||
|
*/
|
||||||
|
setLineweightEnabled(on) {
|
||||||
|
this._lwForced = (on == null) ? null : !!on;
|
||||||
|
if (this._lastResult && !this._isLoading) {
|
||||||
|
this.load(this._lastResult, { keepView: true, keepTheme: true, spaceHandle: this._activeSpaceHex });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether lineweights are currently being drawn. */
|
||||||
|
getLineweightEnabled() { return this._lwDisplay; }
|
||||||
|
|
||||||
/** Track an entity-owned object so layer toggles can flip its visibility. */
|
/** Track an entity-owned object so layer toggles can flip its visibility. */
|
||||||
_addObj(obj, layer) {
|
_addObj(obj, layer) {
|
||||||
const key = layer ?? this._curLayer;
|
const key = layer ?? this._curLayer;
|
||||||
@@ -1272,6 +1421,28 @@ export class Viewer2D {
|
|||||||
buf.mesh.geometry.setDrawRange(0, w);
|
buf.mesh.geometry.setDrawRange(0, w);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2b) Fat-line batches → compact the instance buffers, then cap instanceCount.
|
||||||
|
// slotOf records where each source segment ended up so the selection
|
||||||
|
// highlight can still find its vertices (-1 = this segment is hidden).
|
||||||
|
for (const buf of this._fatBufs) {
|
||||||
|
const seg = buf.segLayer;
|
||||||
|
if (!buf.slotOf) buf.slotOf = new Int32Array(seg.length);
|
||||||
|
const slotOf = buf.slotOf;
|
||||||
|
let w = 0;
|
||||||
|
for (let s = 0; s < seg.length; s++) {
|
||||||
|
const id = seg[s];
|
||||||
|
if (id >= 0 && hiddenId[id]) { slotOf[s] = -1; continue; }
|
||||||
|
buf.posBuf.array.set(buf.posSrc.subarray(s * 6, s * 6 + 6), w * 6);
|
||||||
|
if (buf.colBuf) buf.colBuf.array.set(buf.colSrc.subarray(s * 6, s * 6 + 6), w * 6);
|
||||||
|
slotOf[s] = w;
|
||||||
|
w++;
|
||||||
|
}
|
||||||
|
buf.posBuf.needsUpdate = true;
|
||||||
|
if (buf.colBuf) buf.colBuf.needsUpdate = true;
|
||||||
|
buf.mesh.geometry.instanceCount = w;
|
||||||
|
buf.mesh.visible = w > 0;
|
||||||
|
}
|
||||||
|
|
||||||
// 3) Picking mask — _onClick skips segments/fills owned by hidden layers.
|
// 3) Picking mask — _onClick skips segments/fills owned by hidden layers.
|
||||||
const metas = this._entityMeta;
|
const metas = this._entityMeta;
|
||||||
const mh = new Uint8Array(metas.length);
|
const mh = new Uint8Array(metas.length);
|
||||||
@@ -1282,6 +1453,10 @@ export class Viewer2D {
|
|||||||
if (this._selMeta && hidden.has(this._selMeta.layer)) {
|
if (this._selMeta && hidden.has(this._selMeta.layer)) {
|
||||||
this._highlight(null);
|
this._highlight(null);
|
||||||
this._onSelectCb?.(null);
|
this._onSelectCb?.(null);
|
||||||
|
} else if (this._selMeta && this._fatBufs.length) {
|
||||||
|
// Compaction above rewrote the fat instance colors from the pristine
|
||||||
|
// source — repaint the selection on its new slots.
|
||||||
|
this._paintMeta(this._selMeta, this._selColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Fit box for the visible subset.
|
// 4) Fit box for the visible subset.
|
||||||
@@ -1586,6 +1761,10 @@ export class Viewer2D {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'MESH':
|
||||||
|
// pushSeg already maps model → paper and clips, so no xf here.
|
||||||
|
if (d.vertices?.length >= 3) this._meshSegs(d, color, pushSeg);
|
||||||
|
break;
|
||||||
case 'HATCH': {
|
case 'HATCH': {
|
||||||
const paths = d.paths || e.paths || [];
|
const paths = d.paths || e.paths || [];
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
@@ -2111,6 +2290,8 @@ export class Viewer2D {
|
|||||||
this._layerByHandle.clear();
|
this._layerByHandle.clear();
|
||||||
this._layerByName.clear();
|
this._layerByName.clear();
|
||||||
this._layerNameByHandle = new Map();
|
this._layerNameByHandle = new Map();
|
||||||
|
this._layerLwByHandle = new Map();
|
||||||
|
this._layerLwByName = new Map();
|
||||||
this._layer0Handle = null;
|
this._layer0Handle = null;
|
||||||
if (!layers) return;
|
if (!layers) return;
|
||||||
for (const l of layers) {
|
for (const l of layers) {
|
||||||
@@ -2122,8 +2303,39 @@ export class Viewer2D {
|
|||||||
if (name) this._layerByName.set(name, hex);
|
if (name) this._layerByName.set(name, hex);
|
||||||
if (handle != null && name) this._layerNameByHandle.set(String(handle), name);
|
if (handle != null && name) this._layerNameByHandle.set(String(handle), name);
|
||||||
if (name === '0' && handle != null) this._layer0Handle = String(handle);
|
if (name === '0' && handle != null) this._layer0Handle = String(handle);
|
||||||
|
// Raw i16 lineweight (1/100 mm, or -1/-2/-3). DXF spells it 370.
|
||||||
|
const lw = l.lineWeight ?? l.lineweight ?? l.lineWeightRaw;
|
||||||
|
if (typeof lw === 'number') {
|
||||||
|
if (handle != null) this._layerLwByHandle.set(String(handle), lw);
|
||||||
|
if (name) this._layerLwByName.set(name, lw); // DXF layers carry no handle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effective lineweight of an entity, in screen pixels (0 = hairline).
|
||||||
|
*
|
||||||
|
* ByLayer (-1) resolves against the LAYER table, ByBlock (-2) against the
|
||||||
|
* weight inherited from the owning INSERT, Default (-3) against LWDEFAULT.
|
||||||
|
* Anything at or below the CAD default stays hairline so drawings that never
|
||||||
|
* assigned a weight render exactly as before.
|
||||||
|
*/
|
||||||
|
_lwPx(entity, inheritedPx = 0) {
|
||||||
|
if (!this._lwDisplay) return 0;
|
||||||
|
let raw = entity?.entityHeader?.lineWeight ?? entity?.lineWeight ?? entity?.lineweight;
|
||||||
|
if (typeof raw !== 'number') return 0;
|
||||||
|
if (raw === -2) return inheritedPx; // ByBlock
|
||||||
|
if (raw === -1) { // ByLayer
|
||||||
|
const lh = entity.layerHandle?.value ?? entity.layerHandle;
|
||||||
|
const byHandle = lh != null ? this._layerLwByHandle.get(String(lh)) : undefined;
|
||||||
|
const ln = entity.layer ?? entity.layerName;
|
||||||
|
raw = byHandle ?? (ln != null ? this._layerLwByName.get(ln) : undefined) ?? -3;
|
||||||
|
if (raw === -1 || raw === -2) raw = -3; // layer can't be ByLayer/ByBlock
|
||||||
|
}
|
||||||
|
const mm = raw === -3 ? LW_DEFAULT_MM : raw / 100;
|
||||||
|
if (!(mm > LW_HAIRLINE_MM)) return 0;
|
||||||
|
return Math.min(LW_MAX_PX, Math.round(mm * LW_PX_PER_MM * 10) / 10);
|
||||||
|
}
|
||||||
|
|
||||||
// Active-viewport VIEWTWIST (radians). The sheet is un-twisted by rotating the
|
// Active-viewport VIEWTWIST (radians). The sheet is un-twisted by rotating the
|
||||||
// camera up by -viewTwist (verified against samples/11.dwg: title border upright).
|
// camera up by -viewTwist (verified against samples/11.dwg: title border upright).
|
||||||
@@ -2370,6 +2582,58 @@ export class Viewer2D {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MESH (AcDbSubDMesh) → wireframe segments.
|
||||||
|
*
|
||||||
|
* dwg-wasm emits the mesh as flat number arrays (see its `EntityType::Mesh`
|
||||||
|
* arm) because a civil road-surface mesh runs to ~10^5 vertices:
|
||||||
|
* vertices [x, y, z, …]
|
||||||
|
* edges deduplicated vertex-index pairs [a, b, …]
|
||||||
|
* faceList DXF group-93 layout — [n, i0 … i(n-1)] repeated, n-gons included
|
||||||
|
*
|
||||||
|
* The edge list is preferred: it is already deduplicated, so an edge shared by
|
||||||
|
* two triangles is drawn once instead of twice (~2× fewer segments on a TIN).
|
||||||
|
* faceList is the fallback for a mesh whose edge list is empty, and it is what
|
||||||
|
* a future filled pass would triangulate.
|
||||||
|
*
|
||||||
|
* Only the base (control) mesh is drawn. `subdivisionLevel > 0` means AutoCAD
|
||||||
|
* displays a Catmull-Clark refinement of these vertices; refining is not
|
||||||
|
* implemented, so such a mesh renders slightly more angular than in AutoCAD.
|
||||||
|
*
|
||||||
|
* `xf(x, y) -> [x, y]` optionally maps the coordinates (block-local INSERT,
|
||||||
|
* or model space seen through a paper-space viewport).
|
||||||
|
*/
|
||||||
|
_meshSegs(d, color, pushSeg, xf = null) {
|
||||||
|
const v = d.vertices;
|
||||||
|
if (!v?.length) return;
|
||||||
|
const n = (v.length / 3) | 0;
|
||||||
|
const seg = (a, b) => {
|
||||||
|
if (a === b || !(a >= 0 && a < n) || !(b >= 0 && b < n)) return;
|
||||||
|
let ax = v[a*3], ay = v[a*3+1];
|
||||||
|
let bx = v[b*3], by = v[b*3+1];
|
||||||
|
// pushSeg carries one z per segment; a sloped edge uses its midpoint
|
||||||
|
// elevation, which is what a top view needs for depth ordering.
|
||||||
|
const z = ((v[a*3+2] || 0) + (v[b*3+2] || 0)) / 2;
|
||||||
|
if (xf) { [ax, ay] = xf(ax, ay); [bx, by] = xf(bx, by); }
|
||||||
|
pushSeg(ax, ay, bx, by, z, color);
|
||||||
|
};
|
||||||
|
|
||||||
|
const edges = d.edges;
|
||||||
|
if (edges?.length >= 2) {
|
||||||
|
for (let i = 0; i + 1 < edges.length; i += 2) seg(edges[i], edges[i+1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fl = d.faceList;
|
||||||
|
if (!fl?.length) return;
|
||||||
|
for (let i = 0; i < fl.length; ) {
|
||||||
|
const cnt = fl[i++];
|
||||||
|
// A corrupt count would run the cursor off the end — stop instead.
|
||||||
|
if (!(cnt >= 3) || i + cnt > fl.length) break;
|
||||||
|
for (let k = 0; k < cnt; k++) seg(fl[i + k], fl[i + ((k + 1) % cnt)]);
|
||||||
|
i += cnt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve start/end width for segment i→i+1 (DXF 40/41, fallback 43 constant).
|
// Resolve start/end width for segment i→i+1 (DXF 40/41, fallback 43 constant).
|
||||||
_segWidths(ent, i, scale = 1) {
|
_segWidths(ent, i, scale = 1) {
|
||||||
const d = ent?.data || ent || {};
|
const d = ent?.data || ent || {};
|
||||||
@@ -3295,17 +3559,23 @@ export class Viewer2D {
|
|||||||
const scaleAcc = parentScale * Math.max(Math.abs(sx), Math.abs(sy));
|
const scaleAcc = parentScale * Math.max(Math.abs(sx), Math.abs(sy));
|
||||||
const rotAcc = parentRot + rot;
|
const rotAcc = parentRot + rot;
|
||||||
|
|
||||||
|
// Weight the INSERT itself carries — what a ByBlock child resolves to.
|
||||||
|
const insertLwPx = this._curLwPx;
|
||||||
for (const be of entities) {
|
for (const be of entities) {
|
||||||
const bd = be.data || be;
|
const bd = be.data || be;
|
||||||
const bt = (be.type || be.typeName || '').toUpperCase();
|
const bt = (be.type || be.typeName || '').toUpperCase();
|
||||||
if (bt === 'ATTDEF') continue; // skip attribute definitions
|
if (bt === 'ATTDEF') continue; // skip attribute definitions
|
||||||
const ecol = this._resolveBlockColor(be, color);
|
const ecol = this._resolveBlockColor(be, color);
|
||||||
|
this._curLwPx = this._lwPx(be, insertLwPx);
|
||||||
try {
|
try {
|
||||||
if (bt === 'LINE' && bd.start && bd.end) {
|
if (bt === 'LINE' && bd.start && bd.end) {
|
||||||
const [ax, ay] = xf(bd.start.x, bd.start.y);
|
const [ax, ay] = xf(bd.start.x, bd.start.y);
|
||||||
const [bx, by] = xf(bd.end.x, bd.end.y);
|
const [bx, by] = xf(bd.end.x, bd.end.y);
|
||||||
pushSeg(ax, ay, bx, by, 0, ecol);
|
pushSeg(ax, ay, bx, by, 0, ecol);
|
||||||
|
|
||||||
|
} else if (bt === 'MESH' && bd.vertices?.length >= 3) {
|
||||||
|
this._meshSegs(bd, ecol, pushSeg, xf);
|
||||||
|
|
||||||
} else if ((bt === 'CIRCLE' || bt === 'ARC') && bd.center && bd.radius != null) {
|
} else if ((bt === 'CIRCLE' || bt === 'ARC') && bd.center && bd.radius != null) {
|
||||||
const [cx, cy] = xf(bd.center.x, bd.center.y);
|
const [cx, cy] = xf(bd.center.x, bd.center.y);
|
||||||
const r = bd.radius * scaleAcc;
|
const r = bd.radius * scaleAcc;
|
||||||
@@ -3484,6 +3754,7 @@ export class Viewer2D {
|
|||||||
}
|
}
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
}
|
}
|
||||||
|
this._curLwPx = insertLwPx; // hand the INSERT's own weight back to the caller
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback dimension rendering from entity properties when no block geometry available
|
// Fallback dimension rendering from entity properties when no block geometry available
|
||||||
@@ -3710,19 +3981,53 @@ export class Viewer2D {
|
|||||||
|
|
||||||
/** Recolor the selected entity's line geometry to the accent color (deselect previous). */
|
/** Recolor the selected entity's line geometry to the accent color (deselect previous). */
|
||||||
_highlight(meta) {
|
_highlight(meta) {
|
||||||
const attr = this._lineColorAttr, orig = this._origColors;
|
if (this._selMeta) this._paintMeta(this._selMeta, null);
|
||||||
if (this._selMeta && attr && orig) {
|
|
||||||
const a = this._selMeta;
|
|
||||||
for (let i = a.colStart; i < a.colEnd && i < orig.length; i++) attr.array[i] = orig[i];
|
|
||||||
}
|
|
||||||
this._selMeta = meta || null;
|
this._selMeta = meta || null;
|
||||||
if (meta && attr && meta.colEnd > meta.colStart) {
|
if (meta) this._paintMeta(meta, this._selColor);
|
||||||
const c = this._selColor;
|
}
|
||||||
for (let i = meta.colStart; i < meta.colEnd; i += 3) {
|
|
||||||
attr.array[i] = c.r; attr.array[i + 1] = c.g; attr.array[i + 2] = c.b;
|
/**
|
||||||
|
* Paint every vertex an entity owns. Its segments are spread over the
|
||||||
|
* hairline batch plus one run per dash/lineweight bucket it touched
|
||||||
|
* (meta.spans), so all of them get recolored — or restored when rgb is null.
|
||||||
|
*/
|
||||||
|
_paintMeta(meta, rgb) {
|
||||||
|
const hair = this._hairRt;
|
||||||
|
if (hair && meta.colEnd > meta.colStart) this._paintSpan(hair, meta.colStart, meta.colEnd, rgb);
|
||||||
|
if (!meta.spans) return;
|
||||||
|
for (const s of meta.spans) {
|
||||||
|
const bk = s.bk;
|
||||||
|
if (bk.merged) {
|
||||||
|
if (hair) this._paintSpan(hair, s.start + bk.mergeOffset, s.end + bk.mergeOffset, rgb);
|
||||||
|
} else if (bk.rt) {
|
||||||
|
this._paintSpan(bk.rt, s.start, s.end, rgb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (attr) attr.needsUpdate = true;
|
}
|
||||||
|
|
||||||
|
/** Write one color run: `rgb` to select, null to restore the original color. */
|
||||||
|
_paintSpan(rt, start, end, rgb) {
|
||||||
|
if (rt.attr) { // plain vertex-color batch (hairline / thin dashes)
|
||||||
|
const arr = rt.attr.array, orig = rt.orig;
|
||||||
|
for (let i = start; i < end && i < arr.length; i += 3) {
|
||||||
|
if (rgb) { arr[i] = rgb.r; arr[i + 1] = rgb.g; arr[i + 2] = rgb.b; }
|
||||||
|
else { arr[i] = orig[i]; arr[i + 1] = orig[i + 1]; arr[i + 2] = orig[i + 2]; }
|
||||||
|
}
|
||||||
|
rt.attr.needsUpdate = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fat batch: hiding a layer compacts the instance buffer, so segment s of
|
||||||
|
// the source colors may now live at a different slot (slotOf, -1 = hidden).
|
||||||
|
const arr = rt.colBuf?.array, orig = rt.colSrc, slotOf = rt.slotOf;
|
||||||
|
if (!arr || !orig) return;
|
||||||
|
for (let i = start; i < end && i < orig.length; i += 3) {
|
||||||
|
const slot = slotOf ? slotOf[(i / 6) | 0] : (i / 6) | 0;
|
||||||
|
if (slot < 0) continue;
|
||||||
|
const t = slot * 6 + (i % 6);
|
||||||
|
if (rgb) { arr[t] = rgb.r; arr[t + 1] = rgb.g; arr[t + 2] = rgb.b; }
|
||||||
|
else { arr[t] = orig[i]; arr[t + 1] = orig[i + 1]; arr[t + 2] = orig[i + 2]; }
|
||||||
|
}
|
||||||
|
rt.colBuf.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selection highlight color (keeps in sync with the UI accent). */
|
/** Selection highlight color (keeps in sync with the UI accent). */
|
||||||
@@ -3910,6 +4215,7 @@ export class Viewer2D {
|
|||||||
this._layerBoxes.clear();
|
this._layerBoxes.clear();
|
||||||
this._layerSamples.clear();
|
this._layerSamples.clear();
|
||||||
this._indexedBufs = [];
|
this._indexedBufs = [];
|
||||||
|
this._fatBufs = [];
|
||||||
this._layerNames = null;
|
this._layerNames = null;
|
||||||
this._metaHidden = null;
|
this._metaHidden = null;
|
||||||
this._curLayer = null;
|
this._curLayer = null;
|
||||||
@@ -3928,6 +4234,8 @@ export class Viewer2D {
|
|||||||
this._camera.left = -halfW; this._camera.right = halfW;
|
this._camera.left = -halfW; this._camera.right = halfW;
|
||||||
this._camera.top = halfH; this._camera.bottom = -halfH;
|
this._camera.top = halfH; this._camera.bottom = -halfH;
|
||||||
this._camera.updateProjectionMatrix();
|
this._camera.updateProjectionMatrix();
|
||||||
|
// Fat lines size themselves against the canvas resolution — restate it.
|
||||||
|
for (const buf of this._fatBufs) this._sizeLineMaterial(buf.mesh.material);
|
||||||
}
|
}
|
||||||
|
|
||||||
_animate() {
|
_animate() {
|
||||||
|
|||||||
Binary file not shown.
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* DWG parser — rust/dwg-wasm wrapper around the acadrust crate (MPL-2.0,
|
* DWG parser — rust/dwg-wasm wrapper around the acadrust crate (MPL-2.0,
|
||||||
* consumed unmodified; see rust/dwg-wasm). Emits the parseResult shape
|
* vendored with a local read_mesh patch; see hmwebviewer rust/dwg-wasm). Emits
|
||||||
* Viewer2D.load() consumes. Lazy-imported so the wasm is only fetched when a
|
* the parseResult shape Viewer2D.load() consumes. Lazy-imported so the wasm is
|
||||||
* DWG is actually opened.
|
* only fetched when a DWG is actually opened.
|
||||||
*
|
*
|
||||||
* History: until 2026-07-10 this module selected between acadrust and
|
* History: until 2026-07-10 this module selected between acadrust and
|
||||||
* @horu2day/pure-cad-parser (?dwgparser= toggle). The old parser was removed
|
* @horu2day/pure-cad-parser (?dwgparser= toggle). The old parser was removed
|
||||||
|
|||||||
Reference in New Issue
Block a user