Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f80442f6c | ||
|
|
a924b1ec34 | ||
|
|
cf1298749b | ||
|
|
ce3859c487 | ||
|
|
c67a803e4d | ||
|
|
eec33820a6 | ||
|
|
6f200ef692 | ||
|
|
3480945b5a |
@@ -109,6 +109,11 @@ 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) |
|
||||||
|
| SubDMesh(MESH) 미표시 — parseResult 누락 + 큰 메쉬 잘림 | [`docs/subdmesh-rendering.md`](./docs/subdmesh-rendering.md) |
|
||||||
|
| 선가중치(Lineweight) 미적용 — parseResult 누락 + WebGL 1px 한계 | [`docs/lineweight-rendering.md`](./docs/lineweight-rendering.md) |
|
||||||
|
| acadrust MPL-2.0 고지 누락 (수정본 wasm 공개 배포) | [`docs/license-mpl2-acadrust.md`](./docs/license-mpl2-acadrust.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` 반영 절차는 각 문서를 따른다.
|
||||||
@@ -120,6 +125,11 @@ 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 · opentype.js | MIT |
|
||||||
| NanumGothic | OFL |
|
| NanumGothic | OFL |
|
||||||
|
|
||||||
|
acadrust 는 파일 단위 카피레프트라 **패치한 파일의 소스를 배포 대상자에게 제공할 의무**가
|
||||||
|
있다. 고지·MPL 원문·패치는 [`THIRD-PARTY-NOTICES.md`](./THIRD-PARTY-NOTICES.md) 와
|
||||||
|
`public/licenses/`(배포 시 `/licenses/`)에 동봉한다.
|
||||||
|
의무 범위와 조치 근거: [`docs/license-mpl2-acadrust.md`](./docs/license-mpl2-acadrust.md)
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Third-party notices
|
||||||
|
|
||||||
|
This viewer bundles third-party code. The notices below apply to the source
|
||||||
|
tree **and** to the built site (`dist2/`, deployed at
|
||||||
|
`https://dwg-dxf-viewer-sample.pages.dev/`), because the build embeds the
|
||||||
|
components listed here.
|
||||||
|
|
||||||
|
Served copies of the license texts live under
|
||||||
|
[`public/licenses/`](./public/licenses/) → `/licenses/…` on the deployed site.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## acadrust 0.4.1 — Mozilla Public License 2.0 — **MODIFIED**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Component | `acadrust` (DWG/DXF parser, Rust) |
|
||||||
|
| Version | 0.4.1 |
|
||||||
|
| License | MPL-2.0 — full text: [`public/licenses/acadrust-MPL-2.0.txt`](./public/licenses/acadrust-MPL-2.0.txt) (`/licenses/acadrust-MPL-2.0.txt`) |
|
||||||
|
| Upstream | https://github.com/hakanaktt/acadrust · https://crates.io/crates/acadrust/0.4.1 |
|
||||||
|
| Upstream commit | `f249c2f816acf36ee51cd5533716bdd443c2517e` (from the crate's `.cargo_vcs_info.json`) |
|
||||||
|
| Author | Hakan AK |
|
||||||
|
| Shipped as | compiled into `assets/acadrust_dwg_bg-*.wasm` (Executable Form) |
|
||||||
|
|
||||||
|
**This copy is modified.** One file differs from the published crate:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/io/dwg/dwg_stream_readers/object_reader/entities.rs (read_mesh array-count bounds)
|
||||||
|
```
|
||||||
|
|
||||||
|
The modification bounds `read_mesh`'s array counts by the bits remaining in the
|
||||||
|
object stream instead of the blanket 100 000-item guard, which truncated and
|
||||||
|
thereby corrupted large SubD meshes.
|
||||||
|
|
||||||
|
### How to obtain the Source Code Form (MPL-2.0 §3.2)
|
||||||
|
|
||||||
|
1. Get the unmodified crate source — `cargo vendor`, or
|
||||||
|
`https://crates.io/api/v1/crates/acadrust/0.4.1/download`, or the upstream
|
||||||
|
commit above.
|
||||||
|
2. Apply [`public/licenses/acadrust-0.4.1-read_mesh.patch`](./public/licenses/acadrust-0.4.1-read_mesh.patch)
|
||||||
|
(`/licenses/acadrust-0.4.1-read_mesh.patch` on the deployed site):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xf acadrust-0.4.1.crate && cd acadrust-0.4.1
|
||||||
|
patch -p1 < acadrust-0.4.1-read_mesh.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
The result is the exact Source Code Form of the acadrust code compiled into the
|
||||||
|
shipped `.wasm`. Both the original and the modified file remain under MPL-2.0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## dwg-wasm — MIT
|
||||||
|
|
||||||
|
The wasm-bindgen wrapper that turns acadrust output into this viewer's
|
||||||
|
`parseResult` (`hmwebviewer/rust/dwg-wasm/`). Our own code, MIT. MPL-2.0 §3.3
|
||||||
|
permits distributing this Larger Work under different terms as long as the
|
||||||
|
MPL-covered files above keep their license — which they do.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other components
|
||||||
|
|
||||||
|
| Component | License |
|
||||||
|
|---|---|
|
||||||
|
| three.js | MIT |
|
||||||
|
| dxf-parser | MIT |
|
||||||
|
| opentype.js | MIT |
|
||||||
|
| NanumGothic (`public/fonts/`) | SIL Open Font License 1.1 |
|
||||||
|
| Viewer2D / sample glue | MIT |
|
||||||
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+199
-88
@@ -1,9 +1,9 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>DWG/DXF · 모듈 조합 샘플</title>
|
<title>DWG/DXF · 모듈 조합 샘플</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #0d1117; --panel: #161b22; --ink: #e6edf3; --muted: #8b949e;
|
--bg: #0d1117; --panel: #161b22; --ink: #e6edf3; --muted: #8b949e;
|
||||||
@@ -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;
|
||||||
@@ -27,6 +66,25 @@
|
|||||||
button.primary { background: #238636; border-color: #2ea043; }
|
button.primary { background: #238636; border-color: #2ea043; }
|
||||||
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
||||||
label.btn input { display: none; }
|
label.btn input { display: none; }
|
||||||
|
/* 옵션 패널 — 값은 IndexedDB(hmw-viewer/options)에 저장된다 */
|
||||||
|
#optBody { padding: 12px 14px; overflow: auto; font-size: 12px; color: var(--ink); }
|
||||||
|
.opt-sec { margin-bottom: 16px; }
|
||||||
|
.opt-sec > h4 {
|
||||||
|
margin: 0 0 8px; font-size: 11px; font-weight: 700; letter-spacing: .05em;
|
||||||
|
text-transform: uppercase; color: var(--muted);
|
||||||
|
}
|
||||||
|
.opt-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||||
|
.opt-row > label { flex: 1; color: var(--ink); }
|
||||||
|
.opt-row input[type=range] { flex: 1; min-width: 90px; accent-color: var(--accent); }
|
||||||
|
.opt-row input[type=number] {
|
||||||
|
width: 58px; background: #0d1117; color: var(--ink);
|
||||||
|
border: 1px solid var(--line); border-radius: 6px; padding: 3px 6px;
|
||||||
|
font: 12px var(--mono); text-align: right;
|
||||||
|
}
|
||||||
|
.opt-row input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--accent); }
|
||||||
|
.opt-unit { font: 11px var(--mono); color: var(--muted); }
|
||||||
|
.opt-note { font: 11px var(--mono); color: var(--accent); line-height: 1.6; word-break: keep-all; }
|
||||||
|
.opt-help { font-size: 11px; color: var(--muted); line-height: 1.5; margin: 4px 0 0; }
|
||||||
#status {
|
#status {
|
||||||
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
||||||
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
||||||
@@ -130,6 +188,7 @@
|
|||||||
|
|
||||||
#properties { right: 12px; }
|
#properties { right: 12px; }
|
||||||
#layers { right: 344px; display: none; }
|
#layers { right: 344px; display: none; }
|
||||||
|
#options { right: 676px; width: 340px; height: auto; max-height: 70vh; display: none; }
|
||||||
|
|
||||||
#propBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
#propBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
||||||
#layerBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
#layerBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
||||||
@@ -158,85 +217,137 @@
|
|||||||
background: transparent; color: var(--muted); cursor: pointer; transition: all .15s;
|
background: transparent; color: var(--muted); cursor: pointer; transition: all .15s;
|
||||||
}
|
}
|
||||||
.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-CmUgC68r.js"></script>
|
<script type="module" crossorigin src="/assets/index-VgpJrp_J.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
<label class="btn">
|
<label class="btn">
|
||||||
Open file
|
Open file
|
||||||
<input id="file" type="file" accept=".dwg,.dxf" />
|
<input id="file" type="file" accept=".dwg,.dxf" />
|
||||||
</label>
|
</label>
|
||||||
<button id="sample" class="primary wasm" type="button" title="acadrust-dwg WASM">Sample DWG</button>
|
<button id="sample" class="primary wasm" type="button" title="acadrust-dwg WASM">Sample DWG</button>
|
||||||
<button id="sampleCivil" class="primary wasm" type="button" title="Civil Plan & Profile DWG">평면및종단면도.dwg</button>
|
<button id="sampleCivil" class="primary wasm" type="button" title="Civil Plan & Profile DWG">평면및종단면도.dwg</button>
|
||||||
<button id="sampleDxf" class="primary" type="button" title="dxf-parser">Sample DXF</button>
|
<button id="sampleDxf" class="primary" type="button" title="dxf-parser">Sample DXF</button>
|
||||||
<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="layersBtn" type="button">Layers</button>
|
<button id="optionsBtn" type="button" title="표시 옵션 (선가중치 · 그리드 · 줌)">옵션</button>
|
||||||
<button id="propsBtn" type="button">Props (속성)</button>
|
<button id="layersBtn" type="button">Layers</button>
|
||||||
<div id="status">ready</div>
|
<button id="propsBtn" type="button">Props (속성)</button>
|
||||||
</div>
|
<div id="status">ready</div>
|
||||||
|
</div>
|
||||||
<div id="coords" title="마우스 위치 CAD 좌표 (Model=UCS, Layout=Paper 도면단위)">
|
|
||||||
<span class="lbl">X</span><span id="coordX">—</span>
|
<div id="coords" title="마우스 위치 CAD 좌표 (Model=UCS, Layout=Paper 도면단위)">
|
||||||
<span class="sep">|</span>
|
<span class="lbl">X</span><span id="coordX">—</span>
|
||||||
<span class="lbl">Y</span><span id="coordY">—</span>
|
<span class="sep">|</span>
|
||||||
</div>
|
<span class="lbl">Y</span><span id="coordY">—</span>
|
||||||
|
</div>
|
||||||
<!-- 레이어 (Layers) 관리 패널 -->
|
|
||||||
<div id="layers" class="cad-panel">
|
<!-- 레이어 (Layers) 관리 패널 -->
|
||||||
<div class="hdr">
|
<div id="layers" class="cad-panel">
|
||||||
<h3>
|
<div class="hdr">
|
||||||
<span>🎨</span> 레이어 (Layers)
|
<h3>
|
||||||
<span id="layerCount" style="font-family:var(--mono);font-size:11px;color:var(--muted);background:rgba(255,255,255,.08);padding:1px 6px;border-radius:4px;font-weight:400"></span>
|
<span>🎨</span> 레이어 (Layers)
|
||||||
</h3>
|
<span id="layerCount" style="font-family:var(--mono);font-size:11px;color:var(--muted);background:rgba(255,255,255,.08);padding:1px 6px;border-radius:4px;font-weight:400"></span>
|
||||||
<div class="hdr-actions">
|
</h3>
|
||||||
<button id="collapseLayersBtn" class="hdr-btn collapse-btn" type="button" title="접기">▾</button>
|
<div class="hdr-actions">
|
||||||
<button id="closeLayersBtn" class="hdr-btn" type="button" title="닫기">✕</button>
|
<button id="collapseLayersBtn" class="hdr-btn collapse-btn" type="button" title="접기">▾</button>
|
||||||
</div>
|
<button id="closeLayersBtn" class="hdr-btn" type="button" title="닫기">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="layerBody">
|
</div>
|
||||||
<div class="layer-search-wrap">
|
<div id="layerBody">
|
||||||
<input id="layerSearch" class="layer-search-input" type="text" placeholder="🔍 레이어 검색..." />
|
<div class="layer-search-wrap">
|
||||||
</div>
|
<input id="layerSearch" class="layer-search-input" type="text" placeholder="🔍 레이어 검색..." />
|
||||||
<div class="layer-sub-actions">
|
</div>
|
||||||
<button id="layerShowAll" class="layer-sub-btn" type="button">전체 켜기</button>
|
<div class="layer-sub-actions">
|
||||||
<button id="layerHideAll" class="layer-sub-btn" type="button">전체 끄기</button>
|
<button id="layerShowAll" class="layer-sub-btn" type="button">전체 켜기</button>
|
||||||
</div>
|
<button id="layerHideAll" class="layer-sub-btn" type="button">전체 끄기</button>
|
||||||
<div id="layerList"></div>
|
</div>
|
||||||
</div>
|
<div id="layerList"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- 속성 (Properties) 패널 -->
|
|
||||||
<div id="properties" class="cad-panel">
|
<!-- 옵션 (Options) 패널 — 설정은 IndexedDB 에 저장 -->
|
||||||
<div class="hdr">
|
<div id="options" class="cad-panel">
|
||||||
<h3><span>📋</span> 속성 (Properties)</h3>
|
<div class="hdr">
|
||||||
<div class="hdr-actions">
|
<h3><span>⚙️</span> 옵션 (Options)</h3>
|
||||||
<button id="zoomSelBtn" type="button" title="선택 요소 위치로 확대">🔍 선택 확대</button>
|
<div class="hdr-actions">
|
||||||
<button id="collapsePropsBtn" class="hdr-btn collapse-btn" type="button" title="접기">▾</button>
|
<button id="optReset" type="button" title="기본값으로">↺ 기본값</button>
|
||||||
<button id="closePropsBtn" class="hdr-btn" type="button" title="닫기">✕</button>
|
<button id="collapseOptBtn" class="hdr-btn collapse-btn" type="button" title="접기">▾</button>
|
||||||
</div>
|
<button id="closeOptBtn" class="hdr-btn" type="button" title="닫기">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="propBody"></div>
|
</div>
|
||||||
</div>
|
<div id="optBody">
|
||||||
|
<div class="opt-sec">
|
||||||
<aside id="recipe">
|
<h4>선가중치 (Lineweight)</h4>
|
||||||
<h3>Module recipe</h3>
|
<div class="opt-row">
|
||||||
<ol>
|
<label for="optLwOn">표시 (LWDISPLAY)</label>
|
||||||
<li><code class="mod-j">ext2d</code> — .dwg / .dxf 분기</li>
|
<input id="optLwOn" type="checkbox" />
|
||||||
<li><code class="mod-w">acadrust-dwg</code> 또는 <code class="mod-j">dxf-parser</code></li>
|
</div>
|
||||||
<li><code class="mod-j">dxfAdapter</code> — DXF → 공통 스키마</li>
|
<div class="opt-row">
|
||||||
<li><code class="mod-j">Viewer2D</code> + <code class="mod-j">three</code> 렌더</li>
|
<label for="optLwScale">표시 배율</label>
|
||||||
<li><code class="mod-j">slugText</code> + TTF 폰트</li>
|
<input id="optLwScale" type="range" min="2" max="60" step="0.5" value="11" />
|
||||||
</ol>
|
<input id="optLwScaleNum" type="number" min="1" max="80" step="0.5" value="11" />
|
||||||
<p style="margin:10px 0 0">
|
<span class="opt-unit">px/mm</span>
|
||||||
코드: <code>src/main.ts</code> ·
|
</div>
|
||||||
지도: <a href="/docs/modules-dwg-dxf.html" target="_blank">docs/modules-dwg-dxf.html</a>
|
<div id="optLwOut" class="opt-note">—</div>
|
||||||
</p>
|
<p class="opt-help">
|
||||||
</aside>
|
AutoCAD "선가중치 설정 → 표시 배율"과 같다. 모델 공간에서 선가중치는 줌과
|
||||||
|
무관한 화면 픽셀로 그려지므로, 화면 DPI에 맞춰 이 값을 조정한다.
|
||||||
</body>
|
</p>
|
||||||
</html>
|
</div>
|
||||||
|
|
||||||
|
<div class="opt-sec">
|
||||||
|
<h4>화면 (Display)</h4>
|
||||||
|
<div class="opt-row">
|
||||||
|
<label for="optGrid">그리드 표시</label>
|
||||||
|
<input id="optGrid" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="opt-row">
|
||||||
|
<label for="optZoomSpeed">줌 속도</label>
|
||||||
|
<input id="optZoomSpeed" type="range" min="0.2" max="3" step="0.1" value="1" />
|
||||||
|
<input id="optZoomSpeedNum" type="number" min="0.1" max="5" step="0.1" value="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 속성 (Properties) 패널 -->
|
||||||
|
<div id="properties" class="cad-panel">
|
||||||
|
<div class="hdr">
|
||||||
|
<h3><span>📋</span> 속성 (Properties)</h3>
|
||||||
|
<div class="hdr-actions">
|
||||||
|
<button id="zoomSelBtn" type="button" title="선택 요소 위치로 확대">🔍 선택 확대</button>
|
||||||
|
<button id="collapsePropsBtn" class="hdr-btn collapse-btn" type="button" title="접기">▾</button>
|
||||||
|
<button id="closePropsBtn" class="hdr-btn" type="button" title="닫기">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="propBody"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside id="recipe">
|
||||||
|
<h3>Module recipe</h3>
|
||||||
|
<ol>
|
||||||
|
<li><code class="mod-j">ext2d</code> — .dwg / .dxf 분기</li>
|
||||||
|
<li><code class="mod-w">acadrust-dwg</code> 또는 <code class="mod-j">dxf-parser</code></li>
|
||||||
|
<li><code class="mod-j">dxfAdapter</code> — DXF → 공통 스키마</li>
|
||||||
|
<li><code class="mod-j">Viewer2D</code> + <code class="mod-j">three</code> 렌더</li>
|
||||||
|
<li><code class="mod-j">slugText</code> + TTF 폰트</li>
|
||||||
|
</ol>
|
||||||
|
<p style="margin:10px 0 0">
|
||||||
|
코드: <code>src/main.ts</code> ·
|
||||||
|
지도: <a href="/docs/modules-dwg-dxf.html" target="_blank">docs/modules-dwg-dxf.html</a>
|
||||||
|
</p>
|
||||||
|
<!-- MPL-2.0 §3.2(b): recipients of the wasm must be told where the
|
||||||
|
acadrust source (with our local patch) can be obtained. -->
|
||||||
|
<p style="margin:6px 0 0">
|
||||||
|
오픈소스 고지: <a href="/licenses/THIRD-PARTY-NOTICES.md" target="_blank">THIRD-PARTY-NOTICES</a>
|
||||||
|
(acadrust · MPL-2.0 · <a href="/licenses/acadrust-0.4.1-read_mesh.patch" target="_blank">local patch</a>)
|
||||||
|
</p>
|
||||||
|
</aside>
|
||||||
|
␍
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Third-party notices
|
||||||
|
|
||||||
|
This viewer bundles third-party code. The notices below apply to the source
|
||||||
|
tree **and** to the built site (`dist2/`, deployed at
|
||||||
|
`https://dwg-dxf-viewer-sample.pages.dev/`), because the build embeds the
|
||||||
|
components listed here.
|
||||||
|
|
||||||
|
Served copies of the license texts live under
|
||||||
|
[`public/licenses/`](./public/licenses/) → `/licenses/…` on the deployed site.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## acadrust 0.4.1 — Mozilla Public License 2.0 — **MODIFIED**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Component | `acadrust` (DWG/DXF parser, Rust) |
|
||||||
|
| Version | 0.4.1 |
|
||||||
|
| License | MPL-2.0 — full text: [`public/licenses/acadrust-MPL-2.0.txt`](./public/licenses/acadrust-MPL-2.0.txt) (`/licenses/acadrust-MPL-2.0.txt`) |
|
||||||
|
| Upstream | https://github.com/hakanaktt/acadrust · https://crates.io/crates/acadrust/0.4.1 |
|
||||||
|
| Upstream commit | `f249c2f816acf36ee51cd5533716bdd443c2517e` (from the crate's `.cargo_vcs_info.json`) |
|
||||||
|
| Author | Hakan AK |
|
||||||
|
| Shipped as | compiled into `assets/acadrust_dwg_bg-*.wasm` (Executable Form) |
|
||||||
|
|
||||||
|
**This copy is modified.** One file differs from the published crate:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/io/dwg/dwg_stream_readers/object_reader/entities.rs (read_mesh array-count bounds)
|
||||||
|
```
|
||||||
|
|
||||||
|
The modification bounds `read_mesh`'s array counts by the bits remaining in the
|
||||||
|
object stream instead of the blanket 100 000-item guard, which truncated and
|
||||||
|
thereby corrupted large SubD meshes.
|
||||||
|
|
||||||
|
### How to obtain the Source Code Form (MPL-2.0 §3.2)
|
||||||
|
|
||||||
|
1. Get the unmodified crate source — `cargo vendor`, or
|
||||||
|
`https://crates.io/api/v1/crates/acadrust/0.4.1/download`, or the upstream
|
||||||
|
commit above.
|
||||||
|
2. Apply [`public/licenses/acadrust-0.4.1-read_mesh.patch`](./public/licenses/acadrust-0.4.1-read_mesh.patch)
|
||||||
|
(`/licenses/acadrust-0.4.1-read_mesh.patch` on the deployed site):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xf acadrust-0.4.1.crate && cd acadrust-0.4.1
|
||||||
|
patch -p1 < acadrust-0.4.1-read_mesh.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
The result is the exact Source Code Form of the acadrust code compiled into the
|
||||||
|
shipped `.wasm`. Both the original and the modified file remain under MPL-2.0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## dwg-wasm — MIT
|
||||||
|
|
||||||
|
The wasm-bindgen wrapper that turns acadrust output into this viewer's
|
||||||
|
`parseResult` (`hmwebviewer/rust/dwg-wasm/`). Our own code, MIT. MPL-2.0 §3.3
|
||||||
|
permits distributing this Larger Work under different terms as long as the
|
||||||
|
MPL-covered files above keep their license — which they do.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other components
|
||||||
|
|
||||||
|
| Component | License |
|
||||||
|
|---|---|
|
||||||
|
| three.js | MIT |
|
||||||
|
| dxf-parser | MIT |
|
||||||
|
| opentype.js | MIT |
|
||||||
|
| NanumGothic (`public/fonts/`) | SIL Open Font License 1.1 |
|
||||||
|
| Viewer2D / sample glue | MIT |
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
--- a/src/io/dwg/dwg_stream_readers/object_reader/entities.rs
|
||||||
|
+++ b/src/io/dwg/dwg_stream_readers/object_reader/entities.rs
|
||||||
|
@@ -1682,20 +1682,49 @@
|
||||||
|
MLineData { scale_factor, justification, start_point, normal, openclosed, lines_in_style, vertex_count, vertices, style_handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
+/// Cap an array count against the bits actually left in the object's main
|
||||||
|
+/// stream instead of the blanket [`MAX_ARRAY_COUNT`].
|
||||||
|
+///
|
||||||
|
+/// MESH is the one entity whose arrays legitimately run past 100 000 items: a
|
||||||
|
+/// road-surface mesh in a civil drawing carries hundreds of thousands of
|
||||||
|
+/// vertices. Clamping those to the blanket cap does not merely truncate the
|
||||||
|
+/// mesh — the reader then continues from the wrong bit offset, so the face,
|
||||||
|
+/// edge and crease lists that follow decode as garbage (observed: a 100 000
|
||||||
|
+/// vertex clamp left a 14-face mesh with face sizes `[64, 0, 64, 23, 0, …]`).
|
||||||
|
+/// Bounding by the remaining bits keeps the corrupt-data protection — no
|
||||||
|
+/// unbounded allocation is possible — without mis-parsing a valid drawing.
|
||||||
|
+///
|
||||||
|
+/// `min_bits_per_item` is the shortest legal encoding of one element: a
|
||||||
|
+/// BitDouble is 2 bits at minimum (the 0.0 / 1.0 forms), so a 3BD vertex is 6
|
||||||
|
+/// and a BitLong is 2.
|
||||||
|
+fn stream_bounded_count(reader: &DwgMergedReader, raw: i32, min_bits_per_item: i64) -> i32 {
|
||||||
|
+ let raw = raw.max(0) as i64;
|
||||||
|
+ let total_bits = (reader.main().data_len() as i64).saturating_mul(8);
|
||||||
|
+ // The handle stream follows the main stream; anything past its start is not
|
||||||
|
+ // array data. handle_start() is 0 when the framing did not supply it.
|
||||||
|
+ let handle_start = reader.handle_start();
|
||||||
|
+ let end_bits = if handle_start > 0 { handle_start.min(total_bits) } else { total_bits };
|
||||||
|
+ let remaining = (end_bits - reader.position_in_bits()).max(0);
|
||||||
|
+ raw.min(remaining / min_bits_per_item.max(1)).min(i32::MAX as i64) as i32
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
pub fn read_mesh(reader: &mut DwgMergedReader) -> MeshData {
|
||||||
|
let version = reader.read_bit_short();
|
||||||
|
let blend_crease = reader.read_bit();
|
||||||
|
let subdivision_level = reader.read_bit_long();
|
||||||
|
|
||||||
|
- let num_verts = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_verts = reader.read_bit_long();
|
||||||
|
+ let num_verts = stream_bounded_count(reader, raw_verts, 6);
|
||||||
|
let mut vertices = Vec::with_capacity(num_verts as usize);
|
||||||
|
for _ in 0..num_verts { vertices.push(reader.read_3bit_double()); }
|
||||||
|
|
||||||
|
- let total_face_data = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_face_data = reader.read_bit_long();
|
||||||
|
+ let total_face_data = stream_bounded_count(reader, raw_face_data, 2);
|
||||||
|
let mut faces = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < total_face_data {
|
||||||
|
- let n = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_n = reader.read_bit_long();
|
||||||
|
+ let n = stream_bounded_count(reader, raw_n, 2);
|
||||||
|
i += 1;
|
||||||
|
let mut face = Vec::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
@@ -1705,7 +1734,8 @@
|
||||||
|
faces.push(face);
|
||||||
|
}
|
||||||
|
|
||||||
|
- let num_edges = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_edges = reader.read_bit_long();
|
||||||
|
+ let num_edges = stream_bounded_count(reader, raw_edges, 4);
|
||||||
|
let mut edges = Vec::with_capacity(num_edges as usize);
|
||||||
|
for _ in 0..num_edges {
|
||||||
|
let s = reader.read_bit_long();
|
||||||
|
@@ -1713,7 +1743,8 @@
|
||||||
|
edges.push((s, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
- let num_creases = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_creases = reader.read_bit_long();
|
||||||
|
+ let num_creases = stream_bounded_count(reader, raw_creases, 2);
|
||||||
|
let mut crease_values = Vec::with_capacity(num_creases as usize);
|
||||||
|
for _ in 0..num_creases { crease_values.push(reader.read_bit_double()); }
|
||||||
|
|
||||||
|
@@ -2350,14 +2381,31 @@
|
||||||
|
pub fn read_ole2frame(reader: &mut DwgMergedReader, version: DwgVersion) -> Ole2FrameData {
|
||||||
|
let ver = reader.read_bit_short();
|
||||||
|
let mode = if version.r2000_plus() { reader.read_bit_short() } else { 0 };
|
||||||
|
- // OLE binary data can be very large (embedded images/documents),
|
||||||
|
- // so don't use safe_count (100 KB cap). Use a generous 10 MB cap instead.
|
||||||
|
let data_len = (reader.read_bit_long().max(0) as usize).min(10_000_000);
|
||||||
|
let data = reader.read_bytes(data_len);
|
||||||
|
let trailing_byte = if version.r2000_plus() { reader.read_byte() } else { 3 };
|
||||||
|
- let upper_left = reader.read_3bit_double();
|
||||||
|
- let lower_right = reader.read_3bit_double();
|
||||||
|
+
|
||||||
|
+ let mut upper_left = Vector3::new(0.0, 0.0, 0.0);
|
||||||
|
+ let mut lower_right = Vector3::new(0.0, 0.0, 0.0);
|
||||||
|
+
|
||||||
|
+ // Extract 4-corner coordinates stored in the OLE binary data header starting at offset 2
|
||||||
|
+ if data.len() >= 66 {
|
||||||
|
+ let ul_x = f64::from_le_bytes(data[2..10].try_into().unwrap());
|
||||||
|
+ let ul_y = f64::from_le_bytes(data[10..18].try_into().unwrap());
|
||||||
|
+ let ul_z = f64::from_le_bytes(data[18..26].try_into().unwrap());
|
||||||
|
+ let lr_x = f64::from_le_bytes(data[50..58].try_into().unwrap());
|
||||||
|
+ let lr_y = f64::from_le_bytes(data[58..66].try_into().unwrap());
|
||||||
|
+ let lr_z = if data.len() >= 74 { f64::from_le_bytes(data[66..74].try_into().unwrap()) } else { 0.0 };
|
||||||
|
+
|
||||||
|
+ if ul_x.is_finite() && ul_y.is_finite() && lr_x.is_finite() && lr_y.is_finite()
|
||||||
|
+ && (ul_x != 0.0 || ul_y != 0.0 || lr_x != 0.0 || lr_y != 0.0) {
|
||||||
|
+ upper_left = Vector3::new(ul_x, ul_y, if ul_z.is_finite() { ul_z } else { 0.0 });
|
||||||
|
+ lower_right = Vector3::new(lr_x, lr_y, if lr_z.is_finite() { lr_z } else { 0.0 });
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
let ole_type = if version.r2000_plus() { reader.read_byte() } else { 2 };
|
||||||
|
+
|
||||||
|
Ole2FrameData { version: ver, mode, data, trailing_byte, upper_left, lower_right, ole_type }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under the name "LEGAL", and included in all Covered Software
|
||||||
|
distributed in Source Code Form. Except to the extent prohibited by
|
||||||
|
statute or regulation, such description must be sufficiently detailed
|
||||||
|
for a recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
|
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# `dist2` 정적 빌드 배포 절차
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| 작성 목적 | Slug GPU 텍스트 cap-height 수정 배포 작업을 기록하고, 공식 배포 스크립트가 없는 이 저장소에서 재사용 가능한 절차로 남김 |
|
||||||
|
| 배포 대상 | `dist2/` (저장소에 커밋되는 정적 빌드 산출물, 실제 서빙 디렉터리) |
|
||||||
|
| 계기 커밋 | `fb8f7ca` fix(viewer2d): correct Slug GPU text scale to match DWG cap-height |
|
||||||
|
| 배포일 | 2026-08-04 |
|
||||||
|
| 상태 | 완료 (origin/master push 완료) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. `dist2`란 무엇인가
|
||||||
|
|
||||||
|
이 저장소는 **공식 배포 스크립트가 없다.** `package.json`에는 `dev` / `build` (→ `dist/`, `.gitignore` 대상) / `preview` / `typecheck` 뿐이고, `dist2`를 만드는 스크립트나 CI는 존재하지 않는다.
|
||||||
|
|
||||||
|
그런데도 `dist2/`는 저장소에 **커밋되어 있고** (`git ls-files dist2` 로 확인 가능), 과거 커밋들(`3d3e052` 등)에서 반복적으로 갱신되어 온 정황으로 볼 때 **`vite build`를 수동으로 `--outDir dist2` 옵션과 함께 실행해 만든 산출물을 그대로 커밋 → push 하는 방식**이 이 프로젝트의 사실상 배포 방식이다.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/ ──(vite build)──▶ dist/ (gitignore, 로컬 전용)
|
||||||
|
src/ ──(vite build --outDir dist2)──▶ dist2/ (git 커밋, 실제 배포물)
|
||||||
|
```
|
||||||
|
|
||||||
|
`dist2/samples/`, `dist2/fonts/`, `dist2/docs/`는 `public/`의 사본 + 빌드시 자동 복사분이다.
|
||||||
|
|
||||||
|
### 함정: `dist2`에 소스가 아닌 파일이 섞여 있다
|
||||||
|
|
||||||
|
배포 작업 시작 시 `git status`에 다음 **untracked** 파일이 있었다:
|
||||||
|
|
||||||
|
```
|
||||||
|
dist2/samples/configBak/ (cuiBak.cui, regBak.json — AutoCAD 설정 백업, 테스트용)
|
||||||
|
dist2/samples/dwg_18.3mb.dwg (대용량 테스트 도면)
|
||||||
|
```
|
||||||
|
|
||||||
|
이 파일들은 `public/samples/`에 없다 → 빌드가 만든 게 아니라 **사람이 `dist2`에 직접 넣어둔 테스트 자료**다. Vite의 기본 `emptyOutDir`는 `outDir`가 프로젝트 루트 안에 있으면 `true`(빌드 전 디렉터리 전체 삭제)이므로, 아무 옵션 없이 `vite build --outDir dist2`를 돌리면 **이 파일들이 삭제된다.**
|
||||||
|
|
||||||
|
→ 배포 시 반드시 `--emptyOutDir false`를 붙인다 (아래 3절).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 사전 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd D:\MYCLAUDE_PROJECT\dwg-dxf-viewer-sample
|
||||||
|
git status --short # untracked/미커밋 상태 파악 (dist2 안 이물질 포함)
|
||||||
|
npx tsc --noEmit -p . # 타입체크 먼저 (build 스크립트가 어차피 포함하지만 분리 확인)
|
||||||
|
```
|
||||||
|
|
||||||
|
`npx vite ...`가 이 환경에서 `npm error Missing script: "vite"`로 실패하는 경우가 있었다 (원인 미상 — npx가 로컬 bin 대신 npm script를 찾으려 한 것으로 보임). 재현되면 로컬 바이너리를 직접 호출:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./node_modules/.bin/vite build --outDir dist2 --emptyOutDir false
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 빌드
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd D:\MYCLAUDE_PROJECT\dwg-dxf-viewer-sample
|
||||||
|
./node_modules/.bin/vite build --outDir dist2 --emptyOutDir false
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--outDir dist2`: 기본 `dist/`(gitignore) 대신 커밋 대상 디렉터리로 출력
|
||||||
|
- `--emptyOutDir false`: **필수.** `dist2/samples/configBak/`, `dwg_18.3mb.dwg` 등 빌드가 모르는 파일을 보존
|
||||||
|
- 결과: 새 해시가 붙은 `assets/index-*.js`, `assets/acadrust_dwg_bg-*.wasm` 등이 **추가로** 생기고, 예전 해시 파일은 자동 삭제되지 않음 (이 저장소의 기존 관행과 동일 — `dist2/assets`에 여러 세대의 `index-*.js`가 누적돼 있음). 이번 배포에서도 정리하지 않았다.
|
||||||
|
- `dist2/index.html`이 새 해시를 가리키도록 갱신됨
|
||||||
|
|
||||||
|
이번 실행 결과:
|
||||||
|
|
||||||
|
```
|
||||||
|
dist2/index.html 10.88 kB
|
||||||
|
dist2/assets/acadrust_dwg_bg-Dt2ifJSA.wasm 879.93 kB
|
||||||
|
dist2/assets/acadrustParser-CJeZuNZe.js 4.18 kB
|
||||||
|
dist2/assets/index-CmUgC68r.js 1000.36 kB
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 변경분 검토 (커밋 전 필수)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short dist2 src
|
||||||
|
git diff dist2/index.html # 새 해시 참조로만 바뀌었는지 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
빌드 직전 `dist2/index.html`이 커밋 `199317d`(Entity 선택/Property/Layer 패널) 이전 상태를 가리키고 있었다 — 즉 **이번 배포 전까지 `dist2`가 여러 커밋만큼 stale**했다. `git diff` 결과 UI 패널 CSS/마크업 100줄+ 이 새로 반영된 것으로 확인, 이번 빌드가 그 gap을 포함해 최신화했다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 스테이징 — 빌드 산출물만, 이물질 제외
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/viewer2d/slugText.ts \
|
||||||
|
dist2/index.html \
|
||||||
|
dist2/assets/acadrustParser-CJeZuNZe.js \
|
||||||
|
dist2/assets/acadrust_dwg_bg-Dt2ifJSA.wasm \
|
||||||
|
dist2/assets/index-CmUgC68r.js
|
||||||
|
git status --short # dist2/samples/configBak/, dwg_18.3mb.dwg 는 untracked 로 남아있어야 함
|
||||||
|
```
|
||||||
|
|
||||||
|
`git add dist2` 나 `git add -A`를 쓰지 않는다 — `configBak/`, 대용량 테스트 `.dwg`가 함께 커밋되는 것을 막기 위함.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 커밋 & 푸시
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "fix(viewer2d): correct Slug GPU text scale to match DWG cap-height
|
||||||
|
|
||||||
|
..."
|
||||||
|
git push origin master
|
||||||
|
```
|
||||||
|
|
||||||
|
- push는 공유 원격(gitea `origin`)에 영향을 주는 되돌리기 어려운 작업 → **사용자 확인 후 실행**했다.
|
||||||
|
- 결과: `fb8f7ca` 커밋으로 push 완료.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 체크리스트 (다음 배포 시 재사용)
|
||||||
|
|
||||||
|
- [ ] `git status --short` — `dist2` 안 untracked 이물질 있는지 먼저 확인
|
||||||
|
- [ ] `tsc --noEmit` 통과
|
||||||
|
- [ ] `./node_modules/.bin/vite build --outDir dist2 --emptyOutDir false` (npx가 실패하면 이 경로로)
|
||||||
|
- [ ] `git diff dist2/index.html` 로 해시/마크업 변경분 확인
|
||||||
|
- [ ] `git status --short dist2` 로 새로 생긴 `assets/*` 해시 파일 목록 확인
|
||||||
|
- [ ] 필요한 파일만 개별 `git add` (샘플/백업 이물질 제외)
|
||||||
|
- [ ] 커밋 메시지에 무엇/왜 남김
|
||||||
|
- [ ] push 전 사용자 확인 (원격 공유 저장소)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 후속 과제
|
||||||
|
|
||||||
|
1. **공식 배포 스크립트 부재** — `package.json`에 `"build:dist2": "vite build --outDir dist2 --emptyOutDir false"` 같은 스크립트를 추가하면 `--outDir`/`--emptyOutDir` 플래그를 매번 손으로 맞출 필요가 없어진다.
|
||||||
|
2. **누적된 구세대 해시 파일** — `dist2/assets/index-*.js`가 세대별로 계속 쌓여만 있다 (정리 안 됨). 정리하려면 배포 직후 `dist2/index.html`이 참조하지 않는 해시 파일을 찾아 지우는 스텝이 필요하나, 이번 배포에서는 기존 관행을 바꾸지 않기 위해 손대지 않았다.
|
||||||
|
3. **`dist2` 안 테스트 이물질**(`configBak/`, `dwg_18.3mb.dwg`) — 배포 산출물 디렉터리에 테스트 자료가 섞여 있는 구조 자체가 위험(다음 사람이 무심코 `emptyOutDir` 기본값으로 밀어버릴 수 있음). `public/samples/` 같은 소스 디렉터리로 옮기거나 별도 경로 분리를 검토할 것.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 참고
|
||||||
|
|
||||||
|
- 빌드 산출물: [`../dist2/`](../dist2/)
|
||||||
|
- Vite 설정: [`../vite.config.ts`](../vite.config.ts)
|
||||||
|
- 이번 배포로 반영된 수정: [`../src/viewer2d/slugText.ts`](../src/viewer2d/slugText.ts)
|
||||||
|
- 관련 배포(원인 무관, 이후 별도 push로 `dist2` 추가 갱신됨): `3480945` perf(viewer2d): toggle layers by visibility instead of reloading the drawing
|
||||||
@@ -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,137 @@
|
|||||||
|
# acadrust MPL-2.0 — 의무 사항과 조치 내역
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| 작성 목적 | 파서(acadrust, MPL-2.0)를 담은 wasm 을 공개 배포하면서 지켜야 할 의무를 확정하고 조치 내역 기록 |
|
||||||
|
| 대상 배포물 | `https://dwg-dxf-viewer-sample.pages.dev/` (Cloudflare Pages, 공개) |
|
||||||
|
| 검토일 | 2026-08-04 |
|
||||||
|
| 상태 | **조치 완료** — 고지 파일·MPL 원문·패치 동봉, 사이트에서 링크 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 결론 (세 줄)
|
||||||
|
|
||||||
|
1. **이번 lineweight 작업은 MPL 파일을 건드리지 않았다.** 수정한 `dwg-wasm/src/lib.rs` 는
|
||||||
|
우리 MIT 코드다. → MPL 의무가 *새로* 생기지 않았다.
|
||||||
|
2. 그러나 **이전부터** acadrust 를 한 파일 패치해서 wasm 에 컴파일해 공개 배포 중이었고,
|
||||||
|
그 상태가 **MPL-2.0 §3.2 위반**이었다. 라이선스 원문도 고지도 소스 입수 경로도 없었다.
|
||||||
|
3. **upstream(hakanaktt/acadrust)에 PR 을 올릴 의무는 없다.** MPL 은 기여 강제가 아니다.
|
||||||
|
**배포물에 고지 + 소스 입수 경로만 넣으면 된다.** ← 이번에 그렇게 조치했다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 무엇이 MPL 이고 무엇이 우리 것인가
|
||||||
|
|
||||||
|
| 코드 | 위치 | 라이선스 | 이번 세션에서 수정? |
|
||||||
|
|------|------|----------|---------------------|
|
||||||
|
| **acadrust 0.4.1** (DWG 파서 본체) | `hmwebviewer/rust/acadrust/` | **MPL-2.0** | **아니오** |
|
||||||
|
| dwg-wasm (wasm-bindgen 래퍼 + JSON 직렬화) | `hmwebviewer/rust/dwg-wasm/src/lib.rs` | MIT (우리 코드) | 예 (lineWeight 필드 추가) |
|
||||||
|
| Viewer2D 외 프론트엔드 | `src/` | MIT | 예 |
|
||||||
|
|
||||||
|
MPL-2.0 은 **파일 단위(file-level) 카피레프트**다. GPL 처럼 프로젝트 전체로 번지지 않는다.
|
||||||
|
MPL 파일을 고쳤으면 **그 파일만** MPL 로 유지하고 소스를 공개하면 되고,
|
||||||
|
MPL 파일을 호출·링크하는 우리 코드(§3.3 의 "Larger Work")는 우리가 원하는 라이선스로 배포할 수 있다.
|
||||||
|
|
||||||
|
→ **`lib.rs` 에 필드 세 개 추가한 것은 MPL 전염 사유가 아니다.**
|
||||||
|
|
||||||
|
## 2. 그럼에도 의무가 있는 이유 — 이미 한 파일을 고쳤다
|
||||||
|
|
||||||
|
vendored acadrust 를 crates.io 원본과 diff 하면 **정확히 한 파일**이 다르다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
diff -rq ~/.cargo/registry/src/index.crates.io-*/acadrust-0.4.1/src \
|
||||||
|
hmwebviewer/rust/acadrust/src
|
||||||
|
# → src/io/dwg/dwg_stream_readers/object_reader/entities.rs 만 differ
|
||||||
|
```
|
||||||
|
|
||||||
|
`read_mesh` 의 배열 길이를 남은 비트 수로 제한하는 패치다(대형 SubD 메쉬 손상 수정,
|
||||||
|
[`subdmesh-rendering.md`](./subdmesh-rendering.md)). **이 파일은 Modified Covered
|
||||||
|
Software** 이고, 그 소스는 MPL 하에 제공되어야 한다.
|
||||||
|
|
||||||
|
그리고 `.wasm` 은 acadrust 를 컴파일해 담은 **Executable Form** 이다. 공개 사이트에서
|
||||||
|
브라우저로 내려보내는 순간 MPL 상 "Distribution" 에 해당한다.
|
||||||
|
|
||||||
|
### 적용되는 조항
|
||||||
|
|
||||||
|
| 조항 | 요구 |
|
||||||
|
|------|------|
|
||||||
|
| §3.1 | Source Code Form 을 배포하면 MPL 하에 배포하고 라이선스 사본을 동봉 |
|
||||||
|
| **§3.2** | **Executable Form 배포 시**: (a) 해당 Covered Software 의 Source Code Form 을 MPL 하에 제공하고, (b) **수령자에게 그 입수 방법을 알릴 것** (배포 비용 이상 청구 금지) |
|
||||||
|
| §3.3 | Larger Work(우리 앱)는 다른 라이선스로 배포 가능 — 위 (a)(b)를 지키는 한 |
|
||||||
|
| §3.4 | 라이선스·저작권 고지를 제거하지 말 것 |
|
||||||
|
|
||||||
|
**MPL 에는 upstream 기여 의무 조항이 없다.** §3.2 는 "수령자에게 제공"이지
|
||||||
|
"원저자에게 반영"이 아니다.
|
||||||
|
|
||||||
|
## 3. 위반 상태였던 지점 (조치 전)
|
||||||
|
|
||||||
|
- 배포 사이트에 MPL-2.0 **원문 없음**
|
||||||
|
- acadrust 를 쓴다는 **고지 없음** (README 표에만 한 줄, 배포물에는 미포함)
|
||||||
|
- 패치된 `entities.rs` 소스를 **얻을 방법 없음**
|
||||||
|
- `hmwebviewer` GitHub 저장소는 비공개(API 404)
|
||||||
|
- 샘플 저장소는 사내 gitea
|
||||||
|
- 즉 공개 배포 대상자가 소스에 접근할 경로가 전무
|
||||||
|
|
||||||
|
## 4. 조치 사항 (완료)
|
||||||
|
|
||||||
|
### 4-1. 배포물에 동봉
|
||||||
|
|
||||||
|
| 파일 | 배포 URL | 내용 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `public/licenses/acadrust-MPL-2.0.txt` | `/licenses/acadrust-MPL-2.0.txt` | MPL-2.0 원문 (§3.1·§3.4) |
|
||||||
|
| `public/licenses/acadrust-0.4.1-read_mesh.patch` | `/licenses/acadrust-0.4.1-read_mesh.patch` | 우리 수정분 diff (§3.2(a)) |
|
||||||
|
| `public/licenses/THIRD-PARTY-NOTICES.md` | `/licenses/THIRD-PARTY-NOTICES.md` | 컴포넌트·버전·업스트림·수정 여부·복원 절차 (§3.2(b)) |
|
||||||
|
|
||||||
|
저장소 루트에도 같은 내용의 [`../THIRD-PARTY-NOTICES.md`](../THIRD-PARTY-NOTICES.md) 를 둔다.
|
||||||
|
|
||||||
|
### 4-2. 사이트에서 링크
|
||||||
|
|
||||||
|
`index.html` 우측 "Module recipe" 패널 하단에 고지 링크 추가.
|
||||||
|
§3.2(b) 의 "reasonable manner" 요건은 **수령자가 실제로 찾을 수 있는 위치**를 요구한다.
|
||||||
|
|
||||||
|
### 4-3. 소스 복원 절차 명시
|
||||||
|
|
||||||
|
원본 크레이트 + 패치 조합이면 컴파일된 소스를 바이트 단위로 재현할 수 있다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -O https://crates.io/api/v1/crates/acadrust/0.4.1/download
|
||||||
|
tar xf download && cd acadrust-0.4.1
|
||||||
|
patch -p1 < acadrust-0.4.1-read_mesh.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
업스트림 커밋 `f249c2f816acf36ee51cd5533716bdd443c2517e` 도 고지에 적어 뒀다
|
||||||
|
(크레이트의 `.cargo_vcs_info.json` 값).
|
||||||
|
|
||||||
|
## 5. 질문에 대한 답
|
||||||
|
|
||||||
|
> **"해당 레포(upstream)에 올려야 하나, 배포시 문서만 추가하면 되나?"**
|
||||||
|
|
||||||
|
**문서(고지) + 소스 입수 경로만 배포물에 넣으면 된다. upstream PR 은 의무가 아니다.**
|
||||||
|
|
||||||
|
| 선택지 | MPL 상 필요? | 판단 |
|
||||||
|
|--------|--------------|------|
|
||||||
|
| 배포물에 MPL 원문 + 고지 + 패치 동봉 | **필수** | ✅ 이번에 조치 |
|
||||||
|
| upstream `hakanaktt/acadrust` 에 PR | 아니오 | 선택 — read_mesh 수정은 일반 버그픽스라 올리면 유지보수 부담이 준다 (권장, 의무 아님) |
|
||||||
|
| `hmwebviewer` 저장소 공개 전환 | 아니오 | 불필요 — 패치 동봉으로 §3.2 충족 |
|
||||||
|
| 패치된 acadrust 전체 소스를 이 저장소에 vendoring | 아니오 | 더 엄격한 대안. 189 파일이 샘플 저장소에 들어와 무거워짐. 원본+패치 방식은 배포판(distro) 관행과 동일하고 §3.2 를 충족 |
|
||||||
|
|
||||||
|
> **"lineweight 작업으로 라이선스 의무가 생겼나?"**
|
||||||
|
|
||||||
|
**아니오.** 이번에 고친 `dwg-wasm/src/lib.rs` 는 MIT 인 우리 파일이고, MPL 파일은
|
||||||
|
건드리지 않았다(위 §2 diff 로 확인). 의무는 **이전 read_mesh 패치 + wasm 공개 배포**에서
|
||||||
|
이미 발생해 있었고, 이번에 뒤늦게 정리한 것이다.
|
||||||
|
|
||||||
|
## 6. 앞으로 지킬 것 (체크리스트)
|
||||||
|
|
||||||
|
- [ ] `rust/acadrust/` 아래 파일을 고치면 → 패치 파일을 다시 뽑아 `public/licenses/` 갱신
|
||||||
|
- [ ] acadrust 버전을 올리면 → 고지의 버전·커밋·패치를 함께 갱신
|
||||||
|
- [ ] 새 서드파티를 번들하면 → `THIRD-PARTY-NOTICES.md` 에 행 추가
|
||||||
|
- [ ] MPL 파일에서 라이선스 헤더 주석을 지우지 말 것 (§3.4)
|
||||||
|
- [ ] 배포 전 `/licenses/` 3개 파일이 실제로 서빙되는지 확인
|
||||||
|
|
||||||
|
## 7. 참고
|
||||||
|
|
||||||
|
- MPL-2.0 원문: [`../public/licenses/acadrust-MPL-2.0.txt`](../public/licenses/acadrust-MPL-2.0.txt)
|
||||||
|
- FAQ: https://www.mozilla.org/MPL/2.0/FAQ/ (Q: file-level copyleft, Q: 배포 의무 범위)
|
||||||
|
- 수정 경위: [`subdmesh-rendering.md`](./subdmesh-rendering.md)
|
||||||
|
- 이번 파서 변경(MIT 영역): [`lineweight-rendering.md`](./lineweight-rendering.md) §4-1
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
# 선가중치(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)
|
||||||
@@ -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`
|
||||||
+111
@@ -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;
|
||||||
@@ -27,6 +66,25 @@
|
|||||||
button.primary { background: #238636; border-color: #2ea043; }
|
button.primary { background: #238636; border-color: #2ea043; }
|
||||||
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
||||||
label.btn input { display: none; }
|
label.btn input { display: none; }
|
||||||
|
/* 옵션 패널 — 값은 IndexedDB(hmw-viewer/options)에 저장된다 */
|
||||||
|
#optBody { padding: 12px 14px; overflow: auto; font-size: 12px; color: var(--ink); }
|
||||||
|
.opt-sec { margin-bottom: 16px; }
|
||||||
|
.opt-sec > h4 {
|
||||||
|
margin: 0 0 8px; font-size: 11px; font-weight: 700; letter-spacing: .05em;
|
||||||
|
text-transform: uppercase; color: var(--muted);
|
||||||
|
}
|
||||||
|
.opt-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||||
|
.opt-row > label { flex: 1; color: var(--ink); }
|
||||||
|
.opt-row input[type=range] { flex: 1; min-width: 90px; accent-color: var(--accent); }
|
||||||
|
.opt-row input[type=number] {
|
||||||
|
width: 58px; background: #0d1117; color: var(--ink);
|
||||||
|
border: 1px solid var(--line); border-radius: 6px; padding: 3px 6px;
|
||||||
|
font: 12px var(--mono); text-align: right;
|
||||||
|
}
|
||||||
|
.opt-row input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--accent); }
|
||||||
|
.opt-unit { font: 11px var(--mono); color: var(--muted); }
|
||||||
|
.opt-note { font: 11px var(--mono); color: var(--accent); line-height: 1.6; word-break: keep-all; }
|
||||||
|
.opt-help { font-size: 11px; color: var(--muted); line-height: 1.5; margin: 4px 0 0; }
|
||||||
#status {
|
#status {
|
||||||
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
||||||
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
||||||
@@ -130,6 +188,7 @@
|
|||||||
|
|
||||||
#properties { right: 12px; }
|
#properties { right: 12px; }
|
||||||
#layers { right: 344px; display: none; }
|
#layers { right: 344px; display: none; }
|
||||||
|
#options { right: 676px; width: 340px; height: auto; max-height: 70vh; display: none; }
|
||||||
|
|
||||||
#propBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
#propBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
||||||
#layerBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
#layerBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
||||||
@@ -174,6 +233,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="optionsBtn" type="button" title="표시 옵션 (선가중치 · 그리드 · 줌)">옵션</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>
|
||||||
@@ -209,6 +269,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 옵션 (Options) 패널 — 설정은 IndexedDB 에 저장 -->
|
||||||
|
<div id="options" class="cad-panel">
|
||||||
|
<div class="hdr">
|
||||||
|
<h3><span>⚙️</span> 옵션 (Options)</h3>
|
||||||
|
<div class="hdr-actions">
|
||||||
|
<button id="optReset" type="button" title="기본값으로">↺ 기본값</button>
|
||||||
|
<button id="collapseOptBtn" class="hdr-btn collapse-btn" type="button" title="접기">▾</button>
|
||||||
|
<button id="closeOptBtn" class="hdr-btn" type="button" title="닫기">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="optBody">
|
||||||
|
<div class="opt-sec">
|
||||||
|
<h4>선가중치 (Lineweight)</h4>
|
||||||
|
<div class="opt-row">
|
||||||
|
<label for="optLwOn">표시 (LWDISPLAY)</label>
|
||||||
|
<input id="optLwOn" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="opt-row">
|
||||||
|
<label for="optLwScale">표시 배율</label>
|
||||||
|
<input id="optLwScale" type="range" min="2" max="60" step="0.5" value="11" />
|
||||||
|
<input id="optLwScaleNum" type="number" min="1" max="80" step="0.5" value="11" />
|
||||||
|
<span class="opt-unit">px/mm</span>
|
||||||
|
</div>
|
||||||
|
<div id="optLwOut" class="opt-note">—</div>
|
||||||
|
<p class="opt-help">
|
||||||
|
AutoCAD "선가중치 설정 → 표시 배율"과 같다. 모델 공간에서 선가중치는 줌과
|
||||||
|
무관한 화면 픽셀로 그려지므로, 화면 DPI에 맞춰 이 값을 조정한다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="opt-sec">
|
||||||
|
<h4>화면 (Display)</h4>
|
||||||
|
<div class="opt-row">
|
||||||
|
<label for="optGrid">그리드 표시</label>
|
||||||
|
<input id="optGrid" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="opt-row">
|
||||||
|
<label for="optZoomSpeed">줌 속도</label>
|
||||||
|
<input id="optZoomSpeed" type="range" min="0.2" max="3" step="0.1" value="1" />
|
||||||
|
<input id="optZoomSpeedNum" type="number" min="0.1" max="5" step="0.1" value="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 속성 (Properties) 패널 -->
|
<!-- 속성 (Properties) 패널 -->
|
||||||
<div id="properties" class="cad-panel">
|
<div id="properties" class="cad-panel">
|
||||||
<div class="hdr">
|
<div class="hdr">
|
||||||
@@ -235,6 +340,12 @@
|
|||||||
코드: <code>src/main.ts</code> ·
|
코드: <code>src/main.ts</code> ·
|
||||||
지도: <a href="/docs/modules-dwg-dxf.html" target="_blank">docs/modules-dwg-dxf.html</a>
|
지도: <a href="/docs/modules-dwg-dxf.html" target="_blank">docs/modules-dwg-dxf.html</a>
|
||||||
</p>
|
</p>
|
||||||
|
<!-- MPL-2.0 §3.2(b): recipients of the wasm must be told where the
|
||||||
|
acadrust source (with our local patch) can be obtained. -->
|
||||||
|
<p style="margin:6px 0 0">
|
||||||
|
오픈소스 고지: <a href="/licenses/THIRD-PARTY-NOTICES.md" target="_blank">THIRD-PARTY-NOTICES</a>
|
||||||
|
(acadrust · MPL-2.0 · <a href="/licenses/acadrust-0.4.1-read_mesh.patch" target="_blank">local patch</a>)
|
||||||
|
</p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Third-party notices
|
||||||
|
|
||||||
|
This viewer bundles third-party code. The notices below apply to the source
|
||||||
|
tree **and** to the built site (`dist2/`, deployed at
|
||||||
|
`https://dwg-dxf-viewer-sample.pages.dev/`), because the build embeds the
|
||||||
|
components listed here.
|
||||||
|
|
||||||
|
Served copies of the license texts live under
|
||||||
|
[`public/licenses/`](./public/licenses/) → `/licenses/…` on the deployed site.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## acadrust 0.4.1 — Mozilla Public License 2.0 — **MODIFIED**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Component | `acadrust` (DWG/DXF parser, Rust) |
|
||||||
|
| Version | 0.4.1 |
|
||||||
|
| License | MPL-2.0 — full text: [`public/licenses/acadrust-MPL-2.0.txt`](./public/licenses/acadrust-MPL-2.0.txt) (`/licenses/acadrust-MPL-2.0.txt`) |
|
||||||
|
| Upstream | https://github.com/hakanaktt/acadrust · https://crates.io/crates/acadrust/0.4.1 |
|
||||||
|
| Upstream commit | `f249c2f816acf36ee51cd5533716bdd443c2517e` (from the crate's `.cargo_vcs_info.json`) |
|
||||||
|
| Author | Hakan AK |
|
||||||
|
| Shipped as | compiled into `assets/acadrust_dwg_bg-*.wasm` (Executable Form) |
|
||||||
|
|
||||||
|
**This copy is modified.** One file differs from the published crate:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/io/dwg/dwg_stream_readers/object_reader/entities.rs (read_mesh array-count bounds)
|
||||||
|
```
|
||||||
|
|
||||||
|
The modification bounds `read_mesh`'s array counts by the bits remaining in the
|
||||||
|
object stream instead of the blanket 100 000-item guard, which truncated and
|
||||||
|
thereby corrupted large SubD meshes.
|
||||||
|
|
||||||
|
### How to obtain the Source Code Form (MPL-2.0 §3.2)
|
||||||
|
|
||||||
|
1. Get the unmodified crate source — `cargo vendor`, or
|
||||||
|
`https://crates.io/api/v1/crates/acadrust/0.4.1/download`, or the upstream
|
||||||
|
commit above.
|
||||||
|
2. Apply [`public/licenses/acadrust-0.4.1-read_mesh.patch`](./public/licenses/acadrust-0.4.1-read_mesh.patch)
|
||||||
|
(`/licenses/acadrust-0.4.1-read_mesh.patch` on the deployed site):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xf acadrust-0.4.1.crate && cd acadrust-0.4.1
|
||||||
|
patch -p1 < acadrust-0.4.1-read_mesh.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
The result is the exact Source Code Form of the acadrust code compiled into the
|
||||||
|
shipped `.wasm`. Both the original and the modified file remain under MPL-2.0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## dwg-wasm — MIT
|
||||||
|
|
||||||
|
The wasm-bindgen wrapper that turns acadrust output into this viewer's
|
||||||
|
`parseResult` (`hmwebviewer/rust/dwg-wasm/`). Our own code, MIT. MPL-2.0 §3.3
|
||||||
|
permits distributing this Larger Work under different terms as long as the
|
||||||
|
MPL-covered files above keep their license — which they do.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other components
|
||||||
|
|
||||||
|
| Component | License |
|
||||||
|
|---|---|
|
||||||
|
| three.js | MIT |
|
||||||
|
| dxf-parser | MIT |
|
||||||
|
| opentype.js | MIT |
|
||||||
|
| NanumGothic (`public/fonts/`) | SIL Open Font License 1.1 |
|
||||||
|
| Viewer2D / sample glue | MIT |
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
--- a/src/io/dwg/dwg_stream_readers/object_reader/entities.rs
|
||||||
|
+++ b/src/io/dwg/dwg_stream_readers/object_reader/entities.rs
|
||||||
|
@@ -1682,20 +1682,49 @@
|
||||||
|
MLineData { scale_factor, justification, start_point, normal, openclosed, lines_in_style, vertex_count, vertices, style_handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
+/// Cap an array count against the bits actually left in the object's main
|
||||||
|
+/// stream instead of the blanket [`MAX_ARRAY_COUNT`].
|
||||||
|
+///
|
||||||
|
+/// MESH is the one entity whose arrays legitimately run past 100 000 items: a
|
||||||
|
+/// road-surface mesh in a civil drawing carries hundreds of thousands of
|
||||||
|
+/// vertices. Clamping those to the blanket cap does not merely truncate the
|
||||||
|
+/// mesh — the reader then continues from the wrong bit offset, so the face,
|
||||||
|
+/// edge and crease lists that follow decode as garbage (observed: a 100 000
|
||||||
|
+/// vertex clamp left a 14-face mesh with face sizes `[64, 0, 64, 23, 0, …]`).
|
||||||
|
+/// Bounding by the remaining bits keeps the corrupt-data protection — no
|
||||||
|
+/// unbounded allocation is possible — without mis-parsing a valid drawing.
|
||||||
|
+///
|
||||||
|
+/// `min_bits_per_item` is the shortest legal encoding of one element: a
|
||||||
|
+/// BitDouble is 2 bits at minimum (the 0.0 / 1.0 forms), so a 3BD vertex is 6
|
||||||
|
+/// and a BitLong is 2.
|
||||||
|
+fn stream_bounded_count(reader: &DwgMergedReader, raw: i32, min_bits_per_item: i64) -> i32 {
|
||||||
|
+ let raw = raw.max(0) as i64;
|
||||||
|
+ let total_bits = (reader.main().data_len() as i64).saturating_mul(8);
|
||||||
|
+ // The handle stream follows the main stream; anything past its start is not
|
||||||
|
+ // array data. handle_start() is 0 when the framing did not supply it.
|
||||||
|
+ let handle_start = reader.handle_start();
|
||||||
|
+ let end_bits = if handle_start > 0 { handle_start.min(total_bits) } else { total_bits };
|
||||||
|
+ let remaining = (end_bits - reader.position_in_bits()).max(0);
|
||||||
|
+ raw.min(remaining / min_bits_per_item.max(1)).min(i32::MAX as i64) as i32
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
pub fn read_mesh(reader: &mut DwgMergedReader) -> MeshData {
|
||||||
|
let version = reader.read_bit_short();
|
||||||
|
let blend_crease = reader.read_bit();
|
||||||
|
let subdivision_level = reader.read_bit_long();
|
||||||
|
|
||||||
|
- let num_verts = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_verts = reader.read_bit_long();
|
||||||
|
+ let num_verts = stream_bounded_count(reader, raw_verts, 6);
|
||||||
|
let mut vertices = Vec::with_capacity(num_verts as usize);
|
||||||
|
for _ in 0..num_verts { vertices.push(reader.read_3bit_double()); }
|
||||||
|
|
||||||
|
- let total_face_data = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_face_data = reader.read_bit_long();
|
||||||
|
+ let total_face_data = stream_bounded_count(reader, raw_face_data, 2);
|
||||||
|
let mut faces = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < total_face_data {
|
||||||
|
- let n = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_n = reader.read_bit_long();
|
||||||
|
+ let n = stream_bounded_count(reader, raw_n, 2);
|
||||||
|
i += 1;
|
||||||
|
let mut face = Vec::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
@@ -1705,7 +1734,8 @@
|
||||||
|
faces.push(face);
|
||||||
|
}
|
||||||
|
|
||||||
|
- let num_edges = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_edges = reader.read_bit_long();
|
||||||
|
+ let num_edges = stream_bounded_count(reader, raw_edges, 4);
|
||||||
|
let mut edges = Vec::with_capacity(num_edges as usize);
|
||||||
|
for _ in 0..num_edges {
|
||||||
|
let s = reader.read_bit_long();
|
||||||
|
@@ -1713,7 +1743,8 @@
|
||||||
|
edges.push((s, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
- let num_creases = safe_count(reader.read_bit_long());
|
||||||
|
+ let raw_creases = reader.read_bit_long();
|
||||||
|
+ let num_creases = stream_bounded_count(reader, raw_creases, 2);
|
||||||
|
let mut crease_values = Vec::with_capacity(num_creases as usize);
|
||||||
|
for _ in 0..num_creases { crease_values.push(reader.read_bit_double()); }
|
||||||
|
|
||||||
|
@@ -2350,14 +2381,31 @@
|
||||||
|
pub fn read_ole2frame(reader: &mut DwgMergedReader, version: DwgVersion) -> Ole2FrameData {
|
||||||
|
let ver = reader.read_bit_short();
|
||||||
|
let mode = if version.r2000_plus() { reader.read_bit_short() } else { 0 };
|
||||||
|
- // OLE binary data can be very large (embedded images/documents),
|
||||||
|
- // so don't use safe_count (100 KB cap). Use a generous 10 MB cap instead.
|
||||||
|
let data_len = (reader.read_bit_long().max(0) as usize).min(10_000_000);
|
||||||
|
let data = reader.read_bytes(data_len);
|
||||||
|
let trailing_byte = if version.r2000_plus() { reader.read_byte() } else { 3 };
|
||||||
|
- let upper_left = reader.read_3bit_double();
|
||||||
|
- let lower_right = reader.read_3bit_double();
|
||||||
|
+
|
||||||
|
+ let mut upper_left = Vector3::new(0.0, 0.0, 0.0);
|
||||||
|
+ let mut lower_right = Vector3::new(0.0, 0.0, 0.0);
|
||||||
|
+
|
||||||
|
+ // Extract 4-corner coordinates stored in the OLE binary data header starting at offset 2
|
||||||
|
+ if data.len() >= 66 {
|
||||||
|
+ let ul_x = f64::from_le_bytes(data[2..10].try_into().unwrap());
|
||||||
|
+ let ul_y = f64::from_le_bytes(data[10..18].try_into().unwrap());
|
||||||
|
+ let ul_z = f64::from_le_bytes(data[18..26].try_into().unwrap());
|
||||||
|
+ let lr_x = f64::from_le_bytes(data[50..58].try_into().unwrap());
|
||||||
|
+ let lr_y = f64::from_le_bytes(data[58..66].try_into().unwrap());
|
||||||
|
+ let lr_z = if data.len() >= 74 { f64::from_le_bytes(data[66..74].try_into().unwrap()) } else { 0.0 };
|
||||||
|
+
|
||||||
|
+ if ul_x.is_finite() && ul_y.is_finite() && lr_x.is_finite() && lr_y.is_finite()
|
||||||
|
+ && (ul_x != 0.0 || ul_y != 0.0 || lr_x != 0.0 || lr_y != 0.0) {
|
||||||
|
+ upper_left = Vector3::new(ul_x, ul_y, if ul_z.is_finite() { ul_z } else { 0.0 });
|
||||||
|
+ lower_right = Vector3::new(lr_x, lr_y, if lr_z.is_finite() { lr_z } else { 0.0 });
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
let ole_type = if version.r2000_plus() { reader.read_byte() } else { 2 };
|
||||||
|
+
|
||||||
|
Ole2FrameData { version: ver, mode, data, trailing_byte, upper_left, lower_right, ole_type }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under the name "LEGAL", and included in all Covered Software
|
||||||
|
distributed in Source Code Form. Except to the extent prohibited by
|
||||||
|
statute or regulation, such description must be sufficiently detailed
|
||||||
|
for a recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
|
|
||||||
+45
-26
@@ -49,6 +49,15 @@ export class LayerPanelManager {
|
|||||||
if (options?.hideAllBtnEl) {
|
if (options?.hideAllBtnEl) {
|
||||||
options.hideAllBtnEl.addEventListener('click', () => this.hideAllLayers());
|
options.hideAllBtnEl.addEventListener('click', () => this.hideAllLayers());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delegated click handling: bound once, so render() never has to re-attach
|
||||||
|
// a listener per row (that was O(layers) DOM work on every single toggle).
|
||||||
|
this.listEl.addEventListener('click', (e) => {
|
||||||
|
const row = (e.target as HTMLElement)?.closest?.('.layer-row') as HTMLElement | null;
|
||||||
|
if (!row) return;
|
||||||
|
const layerName = row.dataset.name;
|
||||||
|
if (layerName) this.toggleLayer(layerName);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Refresh and re-render layer list from Viewer2D state */
|
/** Refresh and re-render layer list from Viewer2D state */
|
||||||
@@ -66,7 +75,7 @@ export class LayerPanelManager {
|
|||||||
public showAllLayers(): void {
|
public showAllLayers(): void {
|
||||||
this.hiddenLayers.clear();
|
this.hiddenLayers.clear();
|
||||||
this.viewer.setHiddenLayers(this.hiddenLayers);
|
this.viewer.setHiddenLayers(this.hiddenLayers);
|
||||||
this.render();
|
this.syncRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
public hideAllLayers(): void {
|
public hideAllLayers(): void {
|
||||||
@@ -75,7 +84,7 @@ export class LayerPanelManager {
|
|||||||
this.hiddenLayers.add(l.name);
|
this.hiddenLayers.add(l.name);
|
||||||
}
|
}
|
||||||
this.viewer.setHiddenLayers(this.hiddenLayers);
|
this.viewer.setHiddenLayers(this.hiddenLayers);
|
||||||
this.render();
|
this.syncRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleLayer(name: string): void {
|
public toggleLayer(name: string): void {
|
||||||
@@ -85,7 +94,34 @@ export class LayerPanelManager {
|
|||||||
this.hiddenLayers.add(name);
|
this.hiddenLayers.add(name);
|
||||||
}
|
}
|
||||||
this.viewer.setHiddenLayers(this.hiddenLayers);
|
this.viewer.setHiddenLayers(this.hiddenLayers);
|
||||||
this.render();
|
this.syncRow(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Patch one row in place — a full render() would rebuild the whole list. */
|
||||||
|
private syncRow(name: string): void {
|
||||||
|
const row = this.listEl?.querySelector<HTMLElement>(
|
||||||
|
`.layer-row[data-name="${cssEscape(name)}"]`
|
||||||
|
);
|
||||||
|
if (!row) return;
|
||||||
|
this.paintRow(row, !this.hiddenLayers.has(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Patch every row (show-all / hide-all) without touching the DOM structure. */
|
||||||
|
private syncRows(): void {
|
||||||
|
if (!this.listEl) return;
|
||||||
|
this.listEl.querySelectorAll<HTMLElement>('.layer-row').forEach((row) => {
|
||||||
|
const name = row.dataset.name;
|
||||||
|
if (name != null) this.paintRow(row, !this.hiddenLayers.has(name));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private paintRow(row: HTMLElement, visible: boolean): void {
|
||||||
|
row.style.opacity = visible ? '1' : '0.4';
|
||||||
|
const btn = row.querySelector<HTMLElement>('.eye-btn');
|
||||||
|
if (btn) {
|
||||||
|
btn.textContent = visible ? '👁️' : '🕶️';
|
||||||
|
btn.title = visible ? '숨기기' : '표시하기';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): void {
|
public render(): void {
|
||||||
@@ -141,32 +177,15 @@ export class LayerPanelManager {
|
|||||||
</div>`;
|
</div>`;
|
||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
// Clicks are handled by the delegated listener bound in the constructor.
|
||||||
// Bind eye button click events
|
|
||||||
this.listEl.querySelectorAll('.eye-btn').forEach((btn) => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const layerName = (btn as HTMLElement).dataset.toggle;
|
|
||||||
if (layerName) {
|
|
||||||
this.toggleLayer(layerName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also toggle when clicking row
|
|
||||||
this.listEl.querySelectorAll('.layer-row').forEach((row) => {
|
|
||||||
row.addEventListener('click', (e) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (target.closest('.eye-btn')) return;
|
|
||||||
const layerName = (row as HTMLElement).dataset.name;
|
|
||||||
if (layerName) {
|
|
||||||
this.toggleLayer(layerName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Escape a layer name for use inside a double-quoted CSS attribute selector. */
|
||||||
|
function cssEscape(str: string): string {
|
||||||
|
return str.replace(/["\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(str: string): string {
|
function escapeHtml(str: string): string {
|
||||||
return str
|
return str
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
|
|||||||
+87
@@ -29,6 +29,7 @@ import { setDefaultFontUrl } from './viewer2d/slugText';
|
|||||||
import { renderPropertyInspector } from './propertyInspector';
|
import { renderPropertyInspector } from './propertyInspector';
|
||||||
import { LayerPanelManager } from './layerPanel';
|
import { LayerPanelManager } from './layerPanel';
|
||||||
import { makePanelDraggableAndCollapsible } from './panelUtils';
|
import { makePanelDraggableAndCollapsible } from './panelUtils';
|
||||||
|
import { getAllOptions, setOption } from './optionsStore';
|
||||||
|
|
||||||
// parseDxfBuffer 내부:
|
// parseDxfBuffer 내부:
|
||||||
// dxf-parser → HatchHandler → dxfAdapter
|
// dxf-parser → HatchHandler → dxfAdapter
|
||||||
@@ -162,6 +163,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();
|
||||||
|
syncOptions();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +231,91 @@ document.getElementById('theme')!.addEventListener('click', () => {
|
|||||||
viewer.setTheme(dark);
|
viewer.setTheme(dark);
|
||||||
fillLayers();
|
fillLayers();
|
||||||
});
|
});
|
||||||
|
// ── 옵션 패널 ────────────────────────────────────────────────────
|
||||||
|
// 값은 IndexedDB(hmw-viewer/options)에 남는다. 선가중치 배율은 재파싱 없이
|
||||||
|
// 즉시 반영되므로 드래그하면서 화면을 보고 맞출 수 있다.
|
||||||
|
const optionsPanel = document.getElementById('options')!;
|
||||||
|
const optLwOn = document.getElementById('optLwOn') as HTMLInputElement;
|
||||||
|
const optLwScale = document.getElementById('optLwScale') as HTMLInputElement;
|
||||||
|
const optLwScaleNum = document.getElementById('optLwScaleNum') as HTMLInputElement;
|
||||||
|
const optLwOut = document.getElementById('optLwOut')!;
|
||||||
|
const optGrid = document.getElementById('optGrid') as HTMLInputElement;
|
||||||
|
const optZoomSpeed = document.getElementById('optZoomSpeed') as HTMLInputElement;
|
||||||
|
const optZoomSpeedNum = document.getElementById('optZoomSpeedNum') as HTMLInputElement;
|
||||||
|
|
||||||
|
makePanelDraggableAndCollapsible(optionsPanel, {
|
||||||
|
collapseBtnEl: document.getElementById('collapseOptBtn')!,
|
||||||
|
});
|
||||||
|
document.getElementById('optionsBtn')!.addEventListener('click', () => {
|
||||||
|
optionsPanel.style.display = optionsPanel.style.display === 'flex' ? 'none' : 'flex';
|
||||||
|
});
|
||||||
|
document.getElementById('closeOptBtn')!.addEventListener('click', () => {
|
||||||
|
optionsPanel.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 컨트롤 값을 뷰어의 현재 상태로 되돌린다(도면을 새로 열었을 때도 호출). */
|
||||||
|
function syncOptions() {
|
||||||
|
const s = viewer.getLineweightScale();
|
||||||
|
optLwOn.checked = viewer.getLineweightEnabled();
|
||||||
|
optLwScale.value = String(s);
|
||||||
|
optLwScaleNum.value = String(s);
|
||||||
|
const classes = viewer.getLineweightClasses();
|
||||||
|
optLwOut.textContent = classes.length
|
||||||
|
? classes.map((mm) => `${mm.toFixed(2)}mm → ${Math.round(mm * s * 10) / 10}px`).join(' ')
|
||||||
|
: `이 도면에는 굵은 선이 없다 (0.50mm 였다면 ${Math.round(0.5 * s * 10) / 10}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLwScale(v: number, persist = true) {
|
||||||
|
if (!Number.isFinite(v) || v <= 0) return;
|
||||||
|
viewer.setLineweightScale(v);
|
||||||
|
if (persist) void setOption('lineweightScale', v);
|
||||||
|
syncOptions();
|
||||||
|
}
|
||||||
|
optLwScale.addEventListener('input', () => applyLwScale(parseFloat(optLwScale.value)));
|
||||||
|
optLwScaleNum.addEventListener('input', () => applyLwScale(parseFloat(optLwScaleNum.value)));
|
||||||
|
|
||||||
|
optLwOn.addEventListener('change', () => {
|
||||||
|
viewer.setLineweightEnabled(optLwOn.checked);
|
||||||
|
void setOption('lineweightOn', optLwOn.checked);
|
||||||
|
syncOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
optGrid.addEventListener('change', () => {
|
||||||
|
viewer.setGrid(optGrid.checked);
|
||||||
|
void setOption('grid', optGrid.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
function applyZoomSpeed(v: number, persist = true) {
|
||||||
|
if (!Number.isFinite(v) || v <= 0) return;
|
||||||
|
viewer.setZoomSpeed(v);
|
||||||
|
optZoomSpeed.value = String(v);
|
||||||
|
optZoomSpeedNum.value = String(v);
|
||||||
|
if (persist) void setOption('zoomSpeed', v);
|
||||||
|
}
|
||||||
|
optZoomSpeed.addEventListener('input', () => applyZoomSpeed(parseFloat(optZoomSpeed.value)));
|
||||||
|
optZoomSpeedNum.addEventListener('input', () => applyZoomSpeed(parseFloat(optZoomSpeedNum.value)));
|
||||||
|
|
||||||
|
document.getElementById('optReset')!.addEventListener('click', () => {
|
||||||
|
viewer.setLineweightScale(null); // 뷰어 기본 배율
|
||||||
|
viewer.setLineweightEnabled(null); // 도면의 LWDISPLAY 를 다시 따름
|
||||||
|
viewer.setGrid(false);
|
||||||
|
applyZoomSpeed(1);
|
||||||
|
optGrid.checked = false;
|
||||||
|
void setOption('lineweightScale', null);
|
||||||
|
void setOption('lineweightOn', null);
|
||||||
|
void setOption('grid', false);
|
||||||
|
syncOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 저장된 옵션 복원 (IndexedDB 는 비동기라 첫 도면 로드와 경쟁하지 않도록 즉시 적용)
|
||||||
|
void getAllOptions().then((o) => {
|
||||||
|
if (typeof o.lineweightScale === 'number') applyLwScale(o.lineweightScale, false);
|
||||||
|
if (typeof o.lineweightOn === 'boolean') viewer.setLineweightEnabled(o.lineweightOn);
|
||||||
|
if (o.grid === true) { viewer.setGrid(true); optGrid.checked = true; }
|
||||||
|
if (typeof o.zoomSpeed === 'number') applyZoomSpeed(o.zoomSpeed, false);
|
||||||
|
syncOptions();
|
||||||
|
});
|
||||||
|
syncOptions();
|
||||||
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';
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 뷰어 옵션 영구 저장소 (IndexedDB).
|
||||||
|
*
|
||||||
|
* localStorage 대신 IndexedDB 를 쓰는 이유: 값이 문자열로 강제되지 않고(숫자/불린/
|
||||||
|
* 객체 그대로), 용량 제한이 사실상 없으며, 나중에 도면별 설정처럼 구조가 커져도
|
||||||
|
* 같은 저장소를 그대로 쓸 수 있다.
|
||||||
|
*
|
||||||
|
* DB 하나(`hmw-viewer`) 안에 key-value 스토어(`options`) 하나만 둔다.
|
||||||
|
* 실패(사생활 보호 모드, 스토리지 차단 등)해도 앱이 죽지 않도록 모든 API 는
|
||||||
|
* 조용히 기본값으로 떨어진다.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DB_NAME = 'hmw-viewer';
|
||||||
|
const DB_VERSION = 1;
|
||||||
|
const STORE = 'options';
|
||||||
|
|
||||||
|
let dbPromise: Promise<IDBDatabase | null> | null = null;
|
||||||
|
|
||||||
|
function openDb(): Promise<IDBDatabase | null> {
|
||||||
|
if (dbPromise) return dbPromise;
|
||||||
|
dbPromise = new Promise((resolve) => {
|
||||||
|
if (typeof indexedDB === 'undefined') return resolve(null);
|
||||||
|
let req: IDBOpenDBRequest;
|
||||||
|
try {
|
||||||
|
req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||||
|
} catch {
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
req.onupgradeneeded = () => {
|
||||||
|
const db = req.result;
|
||||||
|
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
|
||||||
|
};
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => resolve(null);
|
||||||
|
req.onblocked = () => resolve(null);
|
||||||
|
});
|
||||||
|
return dbPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tx<T>(mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest): Promise<T | null> {
|
||||||
|
return openDb().then(
|
||||||
|
(db) =>
|
||||||
|
new Promise<T | null>((resolve) => {
|
||||||
|
if (!db) return resolve(null);
|
||||||
|
try {
|
||||||
|
const t = db.transaction(STORE, mode);
|
||||||
|
const req = run(t.objectStore(STORE));
|
||||||
|
req.onsuccess = () => resolve(req.result as T);
|
||||||
|
req.onerror = () => resolve(null);
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 저장된 옵션 하나. 없으면 `fallback`. */
|
||||||
|
export async function getOption<T>(key: string, fallback: T): Promise<T> {
|
||||||
|
const v = await tx<T>('readonly', (s) => s.get(key));
|
||||||
|
return v === null || v === undefined ? fallback : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 옵션 하나 저장. 저장이 막혀 있어도 예외를 던지지 않는다. */
|
||||||
|
export async function setOption(key: string, value: unknown): Promise<void> {
|
||||||
|
await tx('readwrite', (s) => s.put(value, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 전체 옵션을 한 번에 읽는다 (기동 시 1회 호출용). */
|
||||||
|
export async function getAllOptions(): Promise<Record<string, unknown>> {
|
||||||
|
const keys = (await tx<IDBValidKey[]>('readonly', (s) => s.getAllKeys())) ?? [];
|
||||||
|
const vals = (await tx<unknown[]>('readonly', (s) => s.getAll())) ?? [];
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
keys.forEach((k, i) => { out[String(k)] = vals[i]; });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -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
+10
@@ -16,6 +16,16 @@ 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;
|
||||||
|
setZoomSpeed(speed: number): void;
|
||||||
|
getZoomSpeed(): number;
|
||||||
|
/** Lineweight display (AutoCAD LWDISPLAY); null follows the drawing's own flag. */
|
||||||
|
setLineweightEnabled(on: boolean | null): void;
|
||||||
|
getLineweightEnabled(): boolean;
|
||||||
|
/** Lineweight display scale in px per mm (AutoCAD's display-scale slider). Default 20. */
|
||||||
|
setLineweightScale(pxPerMm: number | null): void;
|
||||||
|
getLineweightScale(): number;
|
||||||
|
/** Distinct lineweights (mm) present in the loaded drawing, ascending. */
|
||||||
|
getLineweightClasses(): number[];
|
||||||
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. */
|
||||||
|
|||||||
+651
-91
File diff suppressed because it is too large
Load Diff
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