refactor: establish unified viewer workspaces
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# dwg-dxf-viewer-sample
|
||||
|
||||
**hmwebviewer 형제 폴더**에 분리된 DWG/DXF 2D 뷰어 샘플입니다.
|
||||
|
||||
`docs/modules-dwg-dxf.html` 모듈 지도의 카드
|
||||
(`acadrust-dwg`, `dxf-parser`, `Viewer2D` …)를 **직접 조합**해 동작하는 독립 프로젝트입니다.
|
||||
`hmwebviewer` 저장소 안에 의존하지 않습니다.
|
||||
|
||||
```
|
||||
D:\MYCLAUDE_PROJECT\
|
||||
hmwebviewer\ ← 본 제품
|
||||
dwg-dxf-viewer-sample\ ← 이 샘플 (형제)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 실행
|
||||
|
||||
```bash
|
||||
cd D:\MYCLAUDE_PROJECT\dwg-dxf-viewer-sample
|
||||
npm install
|
||||
npm run dev
|
||||
# → http://localhost:5173
|
||||
```
|
||||
|
||||
| UI | 모듈 경로 |
|
||||
|----|-----------|
|
||||
| **Sample DWG** | `ext2d` → **acadrust-dwg WASM** → `Viewer2D` |
|
||||
| **Sample DXF** | `ext2d` → **dxf-parser + hatch + adapter** → `Viewer2D` |
|
||||
| 파일 드롭 / Open | 확장자로 위 둘 중 하나 |
|
||||
|
||||
모듈 지도: 브라우저에서 [`docs/modules-dwg-dxf.html`](./docs/modules-dwg-dxf.html)
|
||||
조합 코드: [`src/main.ts`](./src/main.ts)
|
||||
|
||||
---
|
||||
|
||||
## 레이아웃
|
||||
|
||||
```
|
||||
dwg-dxf-viewer-sample/
|
||||
docs/modules-dwg-dxf.html # 모듈 조합 레시피 HTML
|
||||
public/
|
||||
fonts/NanumGothic-Regular.ttf
|
||||
samples/BasicSample.dwg
|
||||
samples/simple.dxf
|
||||
src/
|
||||
main.ts # 모듈을 import 해서 조립
|
||||
viewer2d/ # 재사용 모듈 본체
|
||||
acadrust-dwg/ # WASM
|
||||
Viewer2D.js
|
||||
dwgParser.ts
|
||||
parseDxf.ts
|
||||
dxfAdapter.js
|
||||
dxfHatchHandler.js
|
||||
ext2d.ts
|
||||
slugText.ts
|
||||
…
|
||||
package.json
|
||||
vite.config.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 조합 파이프라인
|
||||
|
||||
```
|
||||
File/URL
|
||||
→ ext2d (isDwg / isDxf)
|
||||
→ parseDwgBuffer | parseDxfBuffer
|
||||
│ │
|
||||
└ acadrust-dwg ├ decodeDxf
|
||||
├ dxf-parser + dxfHatchHandler
|
||||
└ dxfAdapter
|
||||
→ CadParseResult
|
||||
→ Viewer2D.load() (+ three, slugText/font)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 의존성
|
||||
|
||||
| 패키지 | 역할 |
|
||||
|--------|------|
|
||||
| `three` | WebGL 렌더 / OrbitControls |
|
||||
| `dxf-parser` | DXF 텍스트 파싱 |
|
||||
| `opentype.js` | TTF 글리프 (slugText) |
|
||||
|
||||
WASM(`acadrust_dwg_bg.wasm`)은 `src/viewer2d/acadrust-dwg/` 에 포함되어 있으며
|
||||
Vite `?url` import 로 로드됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 다른 앱에 이식
|
||||
|
||||
1. `src/viewer2d/` 폴더 복사
|
||||
2. `npm i three dxf-parser opentype.js`
|
||||
3. 폰트 `public/fonts/NanumGothic-Regular.ttf`
|
||||
4. Vite: `assetsInclude: ['**/*.wasm']`
|
||||
5. `src/main.ts` 의 ①~④ 블록을 호스트에 붙이고 ⑤ UI만 교체
|
||||
|
||||
소스 동기화(선택): 본가 `hmwebviewer/src/viewer2d` 가 업데이트되면 해당 파일을
|
||||
이 프로젝트 `src/viewer2d` 로 다시 복사하면 됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 라이선스 메모
|
||||
|
||||
| 조각 | 라이선스 |
|
||||
|------|----------|
|
||||
| Viewer2D 포트 · 샘플 글루 | MIT (hmwebviewer 계열) |
|
||||
| acadrust (WASM 내부) | MPL-2.0 |
|
||||
| dxf-parser · three | MIT |
|
||||
| NanumGothic | OFL |
|
||||
@@ -0,0 +1,99 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>DWG/DXF · 모듈 조합 샘플</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117; --panel: #161b22; --ink: #e6edf3; --muted: #8b949e;
|
||||
--accent: #3fb950; --wasm: #f0a03c; --line: #30363d; --danger: #f85149;
|
||||
--mono: ui-monospace, "Cascadia Code", Consolas, monospace;
|
||||
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Malgun Gothic", sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink); font-family: var(--sans); }
|
||||
#app { position: fixed; inset: 0; }
|
||||
#toolbar {
|
||||
position: fixed; top: 12px; left: 12px; right: 12px; z-index: 10;
|
||||
display: flex; flex-wrap: wrap; gap: 8px; align-items: center; pointer-events: none;
|
||||
}
|
||||
#toolbar > * { pointer-events: auto; }
|
||||
button, label.btn {
|
||||
background: var(--panel); color: var(--ink); border: 1px solid var(--line);
|
||||
border-radius: 8px; padding: 8px 12px; font: 13px var(--sans); cursor: pointer;
|
||||
}
|
||||
button:hover, label.btn:hover { border-color: var(--accent); }
|
||||
button.primary { background: #238636; border-color: #2ea043; }
|
||||
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
||||
label.btn input { display: none; }
|
||||
#status {
|
||||
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
||||
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
||||
border-radius: 8px; padding: 8px 12px; max-width: min(48vw, 480px);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
#status.err { color: var(--danger); }
|
||||
#recipe {
|
||||
position: fixed; bottom: 14px; left: 14px; z-index: 10; max-width: 340px;
|
||||
background: rgba(22,27,34,.94); border: 1px solid var(--line); border-radius: 10px;
|
||||
padding: 12px 14px; font-size: 12px; line-height: 1.45; color: var(--muted);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.35);
|
||||
}
|
||||
#recipe h3 {
|
||||
margin: 0 0 8px; font: 600 11px var(--mono); letter-spacing: .1em;
|
||||
text-transform: uppercase; color: var(--accent);
|
||||
}
|
||||
#recipe ol { margin: 0; padding-left: 18px; }
|
||||
#recipe li { margin-bottom: 4px; }
|
||||
#recipe code { font-family: var(--mono); font-size: 11px; color: var(--ink); }
|
||||
#recipe .mod-w { color: var(--wasm); }
|
||||
#recipe .mod-j { color: var(--accent); }
|
||||
#recipe a { color: var(--accent); font-family: var(--mono); font-size: 11px; }
|
||||
#layers {
|
||||
position: fixed; top: 56px; right: 12px; width: 200px; max-height: 50vh;
|
||||
overflow: auto; z-index: 10; background: rgba(22,27,34,.95);
|
||||
border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
|
||||
font-size: 12px; display: none;
|
||||
}
|
||||
#layers h3 { margin: 0 0 8px; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
||||
#layers label { display: flex; gap: 8px; align-items: center; margin: 4px 0; cursor: pointer; }
|
||||
#layers .swatch { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="toolbar">
|
||||
<label class="btn">
|
||||
Open file
|
||||
<input id="file" type="file" accept=".dwg,.dxf" />
|
||||
</label>
|
||||
<button id="sample" class="primary wasm" type="button" title="acadrust-dwg WASM">Sample DWG</button>
|
||||
<button id="sampleDxf" class="primary" type="button" title="dxf-parser">Sample DXF</button>
|
||||
<button id="fit" type="button">Fit (F)</button>
|
||||
<button id="theme" type="button">Theme</button>
|
||||
<button id="layersBtn" type="button">Layers</button>
|
||||
<div id="status">ready</div>
|
||||
</div>
|
||||
|
||||
<div id="layers"><h3>Layers</h3><div id="layerList"></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>
|
||||
</aside>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@hmwebviewer/viewer-2d-sample",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5173 --strictPort",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hmwebviewer/viewer2d": "*",
|
||||
"three": "0.185.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>DWG/DXF · 모듈 조합 레시피 & 샘플</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eef1f4; --panel: #fff; --ink: #1a2230; --ink-soft: #4a5568; --ink-faint: #8592a3;
|
||||
--line: #d3dae2; --line-strong: #b7c1cd; --accent: #0e7c86; --accent-soft: #d3ecef;
|
||||
--wasm: #b45309; --wasm-soft: #fbe8cf; --js: #0e7c86;
|
||||
--grid: rgba(14,124,134,.06);
|
||||
--shadow: 0 1px 2px rgba(26,34,48,.06), 0 8px 24px rgba(26,34,48,.06);
|
||||
--mono: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;
|
||||
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Malgun Gothic", sans-serif;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0d1219; --panel: #141b25; --ink: #e6ecf3; --ink-soft: #a3b0c0; --ink-faint: #6b7a8d;
|
||||
--line: #243040; --line-strong: #33445a; --accent: #3fd0dc; --accent-soft: #123038;
|
||||
--wasm: #f0a03c; --wasm-soft: #33260f; --js: #3fd0dc; --grid: rgba(63,208,220,.05);
|
||||
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 28px rgba(0,0,0,.35);
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background:
|
||||
linear-gradient(var(--grid) 1px, transparent 1px) 0 0 / 26px 26px,
|
||||
linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0 / 26px 26px,
|
||||
var(--bg);
|
||||
color: var(--ink); font-family: var(--sans); line-height: 1.6;
|
||||
}
|
||||
.wrap { max-width: 980px; margin: 0 auto; padding: 48px 24px 96px; }
|
||||
header { border-bottom: 1px solid var(--line); padding-bottom: 28px; margin-bottom: 36px; }
|
||||
.eyebrow {
|
||||
font-family: var(--mono); font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
|
||||
color: var(--accent); display: flex; align-items: center; gap: 10px; margin-bottom: 14px;
|
||||
}
|
||||
.eyebrow::before { content: ""; width: 28px; height: 1px; background: var(--accent); }
|
||||
h1 { font-size: clamp(26px, 4vw, 38px); line-height: 1.15; margin: 0 0 12px; letter-spacing: -.01em; }
|
||||
h1 .dim { color: var(--ink-faint); font-weight: 400; }
|
||||
.lede { font-size: 16px; color: var(--ink-soft); max-width: 64ch; margin: 0; }
|
||||
.cta {
|
||||
display: inline-flex; align-items: center; gap: 10px; margin-top: 20px;
|
||||
font-family: var(--mono); font-size: 13px; font-weight: 600; text-decoration: none;
|
||||
color: var(--accent); background: var(--panel); border: 1px solid var(--line);
|
||||
border-left: 3px solid var(--accent); border-radius: 8px; padding: 10px 14px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
h2 {
|
||||
font-size: 12px; font-family: var(--mono); letter-spacing: .12em; text-transform: uppercase;
|
||||
color: var(--ink-faint); margin: 48px 0 16px; display: flex; align-items: center; gap: 12px;
|
||||
}
|
||||
h2 .num { color: var(--accent); }
|
||||
h2::after { content: ""; flex: 1; height: 1px; background: var(--line); }
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 16px; font-size: 13px; color: var(--ink-soft); margin: -4px 0 18px; }
|
||||
.legend span { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.dot { width: 11px; height: 11px; border-radius: 3px; }
|
||||
.dot.wasm { background: var(--wasm); }
|
||||
.dot.js { background: var(--js); }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; }
|
||||
.card {
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
|
||||
padding: 14px; box-shadow: var(--shadow); position: relative; overflow: hidden;
|
||||
}
|
||||
.card::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--tag); }
|
||||
.card.w { --tag: var(--wasm); }
|
||||
.card.j { --tag: var(--js); }
|
||||
.card .k {
|
||||
font-family: var(--mono); font-size: 13px; font-weight: 600; margin-bottom: 6px;
|
||||
display: flex; justify-content: space-between; gap: 8px; align-items: baseline;
|
||||
}
|
||||
.badge {
|
||||
font-family: var(--mono); font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase;
|
||||
padding: 2px 6px; border-radius: 4px; font-weight: 600;
|
||||
}
|
||||
.badge.w { background: var(--wasm-soft); color: var(--wasm); }
|
||||
.badge.j { background: var(--accent-soft); color: var(--js); }
|
||||
.card .d { font-size: 13px; color: var(--ink-soft); line-height: 1.45; }
|
||||
.card .path { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); margin-top: 8px; word-break: break-all; }
|
||||
.flow {
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
|
||||
padding: 22px 16px; box-shadow: var(--shadow); overflow-x: auto;
|
||||
}
|
||||
.pipe { display: flex; align-items: stretch; min-width: 720px; }
|
||||
.node {
|
||||
flex: 1; background: var(--bg); border: 1px solid var(--line-strong); border-radius: 9px;
|
||||
padding: 12px; display: flex; flex-direction: column; gap: 4px; min-width: 0;
|
||||
}
|
||||
.node.hl { border-color: var(--accent); background: var(--accent-soft); }
|
||||
.node.w { border-color: var(--wasm); }
|
||||
.node .tier { font-family: var(--mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
|
||||
.node.hl .tier { color: var(--accent); }
|
||||
.node.w .tier { color: var(--wasm); }
|
||||
.node .name { font-family: var(--mono); font-size: 12.5px; font-weight: 600; word-break: break-all; }
|
||||
.node .sub { font-size: 12px; color: var(--ink-soft); }
|
||||
.arrow { flex: 0 0 32px; display: flex; align-items: center; justify-content: center; color: var(--accent); font-size: 18px; }
|
||||
.flow-note { font-size: 12.5px; color: var(--ink-faint); margin-top: 14px; font-family: var(--mono); }
|
||||
.flow-note b { color: var(--accent); }
|
||||
pre {
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 16px 18px;
|
||||
overflow-x: auto; font-family: var(--mono); font-size: 12.5px; color: var(--ink-soft); line-height: 1.55;
|
||||
box-shadow: var(--shadow); margin: 0;
|
||||
}
|
||||
pre .c { color: var(--accent); }
|
||||
pre .w { color: var(--wasm); }
|
||||
pre .cm { color: var(--ink-faint); }
|
||||
.steps { display: grid; gap: 12px; }
|
||||
.step {
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
|
||||
padding: 16px 18px; box-shadow: var(--shadow); display: grid; grid-template-columns: 36px 1fr; gap: 12px;
|
||||
}
|
||||
.step .n {
|
||||
font-family: var(--mono); font-weight: 700; font-size: 18px; color: var(--accent);
|
||||
width: 36px; height: 36px; border-radius: 8px; background: var(--accent-soft);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.step h3 { margin: 0 0 4px; font-size: 15px; }
|
||||
.step p { margin: 0; font-size: 13.5px; color: var(--ink-soft); }
|
||||
.step code { font-family: var(--mono); font-size: .9em; background: var(--bg); padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line); }
|
||||
code { font-family: var(--mono); font-size: .9em; background: var(--bg); padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line); }
|
||||
footer {
|
||||
margin-top: 56px; padding-top: 18px; border-top: 1px solid var(--line);
|
||||
font-family: var(--mono); font-size: 12px; color: var(--ink-faint);
|
||||
display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px;
|
||||
}
|
||||
a { color: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="eyebrow">compose · don’t invent</div>
|
||||
<h1>DWG · DXF 모듈 조합 <span class="dim">레시피</span></h1>
|
||||
<p class="lede">
|
||||
전체 제품이 아니라, 지도에 있는 <b>재사용 모듈만</b> 골라 조립하면 2D CAD 뷰어가 됩니다.
|
||||
아래 카드 → 파이프라인 → 샘플 코드 순으로 옮기면 다른 프로젝트에서도 동일하게 동작합니다.
|
||||
</p>
|
||||
<a class="cta" href="/">
|
||||
▶ 실행 샘플 · 이 프로젝트 루트 (npm run dev)
|
||||
</a>
|
||||
<p style="margin:10px 0 0; font-family:var(--mono); font-size:12px; color:var(--ink-faint)">
|
||||
cd dwg-dxf-viewer-sample && npm i && npm run dev → http://localhost:5173
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- 01 modules -->
|
||||
<h2><span class="num">01</span> 조합에 쓰는 모듈 (지도 카드)</h2>
|
||||
<div class="legend">
|
||||
<span><i class="dot wasm"></i> WASM 파서</span>
|
||||
<span><i class="dot js"></i> JS 모듈 · 렌더러</span>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="card w">
|
||||
<div class="k">acadrust-dwg <span class="badge w">wasm</span></div>
|
||||
<div class="d">DWG 바이너리 → JSON parseResult. 외부 네트워크 없음.</div>
|
||||
<div class="path">src/viewer2d/acadrust-dwg/*.wasm (이 프로젝트)</div>
|
||||
</div>
|
||||
<div class="card j">
|
||||
<div class="k">dwgParser / acadrustParser <span class="badge j">wrap</span></div>
|
||||
<div class="d">WASM 1회 init + <code>parseDwgBuffer(bytes)</code>.</div>
|
||||
<div class="path">dwgParser.ts · acadrustParser.ts</div>
|
||||
</div>
|
||||
<div class="card j">
|
||||
<div class="k">dxf-parser <span class="badge j">npm</span></div>
|
||||
<div class="d">DXF 텍스트 파싱 (MIT). HATCH 기본 미지원.</div>
|
||||
<div class="path">npm: dxf-parser</div>
|
||||
</div>
|
||||
<div class="card j">
|
||||
<div class="k">dxfHatchHandler <span class="badge j">patch</span></div>
|
||||
<div class="d"><code>registerEntityHandler</code>로 HATCH 보강.</div>
|
||||
<div class="path">dxfHatchHandler.js</div>
|
||||
</div>
|
||||
<div class="card j">
|
||||
<div class="k">dxfAdapter <span class="badge j">adapt</span></div>
|
||||
<div class="d">dxf-parser 출력 → Viewer2D 공통 스키마.</div>
|
||||
<div class="path">dxfAdapter.js</div>
|
||||
</div>
|
||||
<div class="card j">
|
||||
<div class="k">Viewer2D <span class="badge j">render</span></div>
|
||||
<div class="d">Orthographic three 씬. LINE/ARC/HATCH/TEXT/INSERT…</div>
|
||||
<div class="path">Viewer2D.js</div>
|
||||
</div>
|
||||
<div class="card j">
|
||||
<div class="k">three <span class="badge j">peer</span></div>
|
||||
<div class="d">WebGLRenderer + OrbitControls (pan/zoom).</div>
|
||||
<div class="path">npm: three ≥0.160</div>
|
||||
</div>
|
||||
<div class="card j">
|
||||
<div class="k">slugText + font <span class="badge j">text</span></div>
|
||||
<div class="d">opentype.js → GPU 텍스트. TTF를 호스트가 서빙.</div>
|
||||
<div class="path">slugText.ts · /fonts/*.ttf</div>
|
||||
</div>
|
||||
<div class="card j">
|
||||
<div class="k">ext2d <span class="badge j">util</span></div>
|
||||
<div class="d"><code>isDwg</code> / <code>isDxf</code> / <code>is2D</code> — 의존 zero.</div>
|
||||
<div class="path">ext2d.ts</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 02 pipeline -->
|
||||
<h2><span class="num">02</span> 조립 파이프라인</h2>
|
||||
<div class="flow">
|
||||
<div class="pipe">
|
||||
<div class="node">
|
||||
<div class="tier">Host</div>
|
||||
<div class="name">File · URL · Drop</div>
|
||||
<div class="sub">.dwg / .dxf</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="node hl">
|
||||
<div class="tier">ext2d</div>
|
||||
<div class="name">isDwg / isDxf</div>
|
||||
<div class="sub">분기</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="node w">
|
||||
<div class="tier">Parser A or B</div>
|
||||
<div class="name">acadrust · dxf-parser</div>
|
||||
<div class="sub">(+ hatch · adapter)</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="node hl">
|
||||
<div class="tier">Contract</div>
|
||||
<div class="name">CadParseResult</div>
|
||||
<div class="sub">공통 JSON</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="node">
|
||||
<div class="tier">Viewer2D</div>
|
||||
<div class="name">.load(result)</div>
|
||||
<div class="sub">three 씬</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-note">
|
||||
<b>핵심 계약</b>: 파서는 파일을 안 열고, 렌더러는 바이트를 안 연다.
|
||||
중간은 항상 <code>CadParseResult</code> 하나.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 03 recipe steps -->
|
||||
<h2><span class="num">03</span> 다른 프로젝트에 옮기는 순서</h2>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="n">1</div>
|
||||
<div>
|
||||
<h3>모듈 소스 복사</h3>
|
||||
<p>
|
||||
이 샘플의 <code>src/viewer2d/</code> 전체를 복사.
|
||||
npm: <code>three</code>, <code>dxf-parser</code>, <code>opentype.js</code>.
|
||||
(원본 소스는 형제 폴더 <code>hmwebviewer/src/viewer2d</code>.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="n">2</div>
|
||||
<div>
|
||||
<h3>정적 자산</h3>
|
||||
<p>
|
||||
<code>public/fonts/NanumGothic-Regular.ttf</code> (한글 TEXT).
|
||||
WASM은 소스가 <code>?url</code>로 import — Vite면 <code>assetsInclude: ['**/*.wasm']</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="n">3</div>
|
||||
<div>
|
||||
<h3>아래 스니펫처럼 조합</h3>
|
||||
<p>
|
||||
래퍼 클래스 없이 모듈을 직접 import 해도 됩니다.
|
||||
실행 가능한 전체 예: 이 프로젝트 <code>src/main.ts</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="n">4</div>
|
||||
<div>
|
||||
<h3>호스트 UI만 붙이기</h3>
|
||||
<p>
|
||||
드롭존 · 툴바 · 레이어 패널은 모듈이 아님.
|
||||
샘플의 ⑤ 영역만 교체하면 디자인 시스템에 맞게 붙일 수 있음.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 04 code -->
|
||||
<h2><span class="num">04</span> 최소 조합 코드 (샘플과 동일 패턴)</h2>
|
||||
<pre><span class="cm">// ① 모듈 지도 카드 → import</span>
|
||||
<span class="c">import</span> { Viewer2D } <span class="c">from</span> './viewer2d/Viewer2D.js';
|
||||
<span class="c">import</span> { parseDwgBuffer } <span class="c">from</span> './viewer2d/dwgParser'; <span class="cm">// acadrust-dwg</span>
|
||||
<span class="c">import</span> { parseDxfBuffer } <span class="c">from</span> './viewer2d/parseDxf'; <span class="cm">// dxf-parser+hatch+adapter</span>
|
||||
<span class="c">import</span> { isDwg, isDxf } <span class="c">from</span> './viewer2d/ext2d';
|
||||
<span class="c">import</span> { setDefaultFontUrl } <span class="c">from</span> './viewer2d/slugText';
|
||||
|
||||
setDefaultFontUrl('/fonts/NanumGothic-Regular.ttf');
|
||||
|
||||
<span class="cm">// ② 렌더러</span>
|
||||
<span class="c">const</span> viewer = <span class="c">new</span> Viewer2D(document.getElementById('app')!);
|
||||
|
||||
<span class="cm">// ③ 파서 조합</span>
|
||||
<span class="c">async function</span> parseCad(buf: ArrayBuffer, name: string) {
|
||||
<span class="c">if</span> (isDxf(name)) <span class="c">return</span> parseDxfBuffer(buf); <span class="cm">// dxf-parser 경로</span>
|
||||
<span class="c">if</span> (isDwg(name)) <span class="c">return</span> parseDwgBuffer(<span class="c">new</span> Uint8Array(buf)); <span class="cm">// WASM 경로</span>
|
||||
<span class="c">throw new</span> Error('need .dwg or .dxf name');
|
||||
}
|
||||
|
||||
<span class="cm">// ④ 계약: parseResult → load</span>
|
||||
<span class="c">async function</span> open(file: File) {
|
||||
<span class="c">const</span> result = <span class="c">await</span> parseCad(<span class="c">await</span> file.arrayBuffer(), file.name);
|
||||
viewer.load(result);
|
||||
viewer.resize();
|
||||
viewer.fit();
|
||||
}</pre>
|
||||
|
||||
<h2><span class="num">05</span> DWG 경로 vs DXF 경로</h2>
|
||||
<div class="grid">
|
||||
<div class="card w">
|
||||
<div class="k">.dwg 경로 <span class="badge w">wasm</span></div>
|
||||
<div class="d">
|
||||
<code>parseDwgBuffer</code><br>
|
||||
→ acadrustParser.init<br>
|
||||
→ <span style="color:var(--wasm)">parse_dwg(bytes)</span><br>
|
||||
→ CadParseResult
|
||||
</div>
|
||||
<div class="path">Sample DWG 버튼으로 검증</div>
|
||||
</div>
|
||||
<div class="card j">
|
||||
<div class="k">.dxf 경로 <span class="badge j">js</span></div>
|
||||
<div class="d">
|
||||
<code>parseDxfBuffer</code><br>
|
||||
→ decodeDxf (UTF-8 / CP949…)<br>
|
||||
→ DxfParser + HatchHandler<br>
|
||||
→ dxfToParseResult
|
||||
</div>
|
||||
<div class="path">Sample DXF 버튼으로 검증</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2><span class="num">06</span> 샘플 위치</h2>
|
||||
<pre style="white-space:pre-wrap"><span class="c">dwg-dxf-viewer-sample/</span> ← hmwebviewer 형제 폴더
|
||||
src/main.ts ← 모듈을 직접 import 해서 조립 (핵심)
|
||||
src/viewer2d/ ← acadrust-dwg · Viewer2D · dxf-*
|
||||
index.html ← 툴바 + Module recipe 패널
|
||||
public/fonts/ ← NanumGothic (slugText)
|
||||
public/samples/ ← BasicSample.dwg · simple.dxf
|
||||
docs/modules-dwg-dxf.html
|
||||
|
||||
<span class="c">실행</span>
|
||||
cd ../dwg-dxf-viewer-sample # (hmwebviewer 기준 형제)
|
||||
npm install
|
||||
npm run dev → http://localhost:5173</pre>
|
||||
|
||||
<footer>
|
||||
<span>DWG/DXF only · sibling of hmwebviewer</span>
|
||||
<span>no 3D / SHP / R2</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,146 @@
|
||||
0
|
||||
SECTION
|
||||
2
|
||||
HEADER
|
||||
9
|
||||
$ACADVER
|
||||
1
|
||||
AC1015
|
||||
9
|
||||
$INSUNITS
|
||||
70
|
||||
4
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
SECTION
|
||||
2
|
||||
TABLES
|
||||
0
|
||||
TABLE
|
||||
2
|
||||
LAYER
|
||||
0
|
||||
LAYER
|
||||
2
|
||||
0
|
||||
70
|
||||
0
|
||||
62
|
||||
7
|
||||
0
|
||||
LAYER
|
||||
2
|
||||
OUTLINE
|
||||
70
|
||||
0
|
||||
62
|
||||
1
|
||||
0
|
||||
ENDTAB
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
SECTION
|
||||
2
|
||||
BLOCKS
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
LINE
|
||||
8
|
||||
OUTLINE
|
||||
10
|
||||
0.0
|
||||
20
|
||||
0.0
|
||||
30
|
||||
0.0
|
||||
11
|
||||
100.0
|
||||
21
|
||||
0.0
|
||||
31
|
||||
0.0
|
||||
0
|
||||
LINE
|
||||
8
|
||||
OUTLINE
|
||||
10
|
||||
100.0
|
||||
20
|
||||
0.0
|
||||
30
|
||||
0.0
|
||||
11
|
||||
100.0
|
||||
21
|
||||
60.0
|
||||
31
|
||||
0.0
|
||||
0
|
||||
LINE
|
||||
8
|
||||
OUTLINE
|
||||
10
|
||||
100.0
|
||||
20
|
||||
60.0
|
||||
30
|
||||
0.0
|
||||
11
|
||||
0.0
|
||||
21
|
||||
60.0
|
||||
31
|
||||
0.0
|
||||
0
|
||||
LINE
|
||||
8
|
||||
OUTLINE
|
||||
10
|
||||
0.0
|
||||
20
|
||||
60.0
|
||||
30
|
||||
0.0
|
||||
11
|
||||
0.0
|
||||
21
|
||||
0.0
|
||||
31
|
||||
0.0
|
||||
0
|
||||
CIRCLE
|
||||
8
|
||||
0
|
||||
10
|
||||
50.0
|
||||
20
|
||||
30.0
|
||||
30
|
||||
0.0
|
||||
40
|
||||
15.0
|
||||
0
|
||||
TEXT
|
||||
8
|
||||
0
|
||||
10
|
||||
10.0
|
||||
20
|
||||
70.0
|
||||
30
|
||||
0.0
|
||||
40
|
||||
5.0
|
||||
1
|
||||
Hello DWG/DXF package
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════════════════════
|
||||
* DWG/DXF viewer sample — 모듈 조합 레시피
|
||||
* (docs/modules-dwg-dxf.html 에 정리된 모듈을 그대로 조립)
|
||||
*
|
||||
* 이 프로젝트는 hmwebviewer 와 형제 폴더로 분리된 독립 샘플입니다.
|
||||
* ═══════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* ┌─ 모듈 지도 카드 ──────────────┬─ 이 파일에서의 역할 ──────────┐
|
||||
* │ acadrust-dwg (WASM) │ DWG 바이트 → parseResult │
|
||||
* │ dxf-parser + dxfHatchHandler │ DXF 텍스트 → parseResult │
|
||||
* │ dxfAdapter │ dxf-parser 출력을 공통 스키마로 │
|
||||
* │ Viewer2D │ parseResult → three 씬 렌더 │
|
||||
* │ three (peer) │ WebGLRenderer / OrbitControls │
|
||||
* │ slugText + NanumGothic.ttf │ TEXT/MTEXT 한글·라틴 표시 │
|
||||
* │ ext2d │ .dwg / .dxf 확장자 판별 │
|
||||
* └───────────────────────────────┴───────────────────────────────┘
|
||||
*
|
||||
* 흐름: File/URL → (확장자 분기) → 파서 → CadParseResult → Viewer2D.load()
|
||||
*/
|
||||
|
||||
// ── ① 모듈 지도의 각 카드 (src/viewer2d/*) ────────────────────────
|
||||
import {
|
||||
createViewer2D,
|
||||
isCad2DFile,
|
||||
parseCad,
|
||||
type CadParseResult,
|
||||
} from '@hmwebviewer/viewer2d';
|
||||
|
||||
// parseDxfBuffer 내부:
|
||||
// dxf-parser → HatchHandler → dxfAdapter
|
||||
// parseDwgBuffer 내부:
|
||||
// acadrustParser → acadrust-dwg WASM
|
||||
|
||||
// ── ② 호스트 정적 자산 ──────────────────────────────────────────
|
||||
const appBase = new URL(import.meta.env.BASE_URL, window.location.origin);
|
||||
const publicUrl = (path: string) => new URL(path, appBase).href;
|
||||
|
||||
// ── ③ Viewer2D 마운트 ───────────────────────────────────────────
|
||||
const app = document.getElementById('app')!;
|
||||
const statusEl = document.getElementById('status')!;
|
||||
const layerList = document.getElementById('layerList')!;
|
||||
const layersPanel = document.getElementById('layers')!;
|
||||
|
||||
function setStatus(msg: string, err = false) {
|
||||
statusEl.textContent = msg;
|
||||
statusEl.classList.toggle('err', err);
|
||||
}
|
||||
|
||||
const viewer = createViewer2D(app, {
|
||||
fontUrl: publicUrl('fonts/NanumGothic-Regular.ttf'),
|
||||
});
|
||||
viewer.onSelect((entity: unknown) => {
|
||||
if (!entity) {
|
||||
setStatus('deselected');
|
||||
return;
|
||||
}
|
||||
const e = entity as { type?: string; layer?: string; handle?: { value?: string } };
|
||||
setStatus(`select ${e.type ?? '?'} · layer=${e.layer ?? '-'} · h=${e.handle?.value ?? '-'}`);
|
||||
});
|
||||
|
||||
// ── ④ 파서 조합 ─────────────────────────────────────────────────
|
||||
async function loadAndRender(buf: ArrayBuffer, name: string): Promise<CadParseResult> {
|
||||
const result = await parseCad({ data: buf, name });
|
||||
viewer.load(result);
|
||||
viewer.resize();
|
||||
return result;
|
||||
}
|
||||
|
||||
async function loadUrl(url: string, nameHint?: string) {
|
||||
const name = nameHint ?? url.split('?')[0]!.split('/').pop() ?? url;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return loadAndRender(await res.arrayBuffer(), name);
|
||||
}
|
||||
|
||||
async function loadFile(file: File) {
|
||||
if (!isCad2DFile(file.name)) throw new Error(`not .dwg/.dxf: ${file.name}`);
|
||||
return loadAndRender(await file.arrayBuffer(), file.name);
|
||||
}
|
||||
|
||||
// ── ⑤ 호스트 UI 글루 ────────────────────────────────────────────
|
||||
function fillLayers() {
|
||||
layerList.innerHTML = '';
|
||||
for (const L of viewer.getLayerInfo()) {
|
||||
const label = document.createElement('label');
|
||||
label.dataset.name = L.name;
|
||||
const sw = document.createElement('span');
|
||||
sw.className = 'swatch';
|
||||
sw.style.background = L.colorHex || '#888';
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.checked = L.visible;
|
||||
cb.addEventListener('change', () => {
|
||||
const next = new Set<string>();
|
||||
layerList.querySelectorAll('label').forEach((row) => {
|
||||
const input = row.querySelector('input') as HTMLInputElement;
|
||||
if (!input.checked) next.add(row.dataset.name!);
|
||||
});
|
||||
viewer.setHiddenLayers(next);
|
||||
});
|
||||
const text = document.createElement('span');
|
||||
text.textContent = `${L.name} (${L.count})`;
|
||||
label.append(sw, cb, text);
|
||||
layerList.append(label);
|
||||
}
|
||||
}
|
||||
|
||||
async function afterLoad(name: string, result: CadParseResult) {
|
||||
setStatus(`✓ ${name} · ${result.entities?.length ?? 0} entities`);
|
||||
fillLayers();
|
||||
viewer.fit();
|
||||
}
|
||||
|
||||
async function run(label: string, job: () => Promise<CadParseResult>) {
|
||||
setStatus(`loading ${label}…`);
|
||||
try {
|
||||
await afterLoad(label, await job());
|
||||
} catch (err) {
|
||||
setStatus(`${label}: ${(err as Error).message}`, true);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('sample')!.addEventListener('click', () => {
|
||||
void run('BasicSample.dwg', () =>
|
||||
loadUrl(publicUrl('samples/BasicSample.dwg'), 'BasicSample.dwg'),
|
||||
);
|
||||
});
|
||||
document.getElementById('sampleDxf')!.addEventListener('click', () => {
|
||||
void run('simple.dxf', () => loadUrl(publicUrl('samples/simple.dxf'), 'simple.dxf'));
|
||||
});
|
||||
document.getElementById('file')!.addEventListener('change', (ev) => {
|
||||
const file = (ev.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
void run(file.name, () => loadFile(file));
|
||||
(ev.target as HTMLInputElement).value = '';
|
||||
});
|
||||
document.getElementById('fit')!.addEventListener('click', () => viewer.fit());
|
||||
let dark = true;
|
||||
document.getElementById('theme')!.addEventListener('click', () => {
|
||||
dark = !dark;
|
||||
viewer.setTheme(dark);
|
||||
});
|
||||
document.getElementById('layersBtn')!.addEventListener('click', () => {
|
||||
layersPanel.style.display = layersPanel.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
|
||||
window.addEventListener('dragover', (e) => e.preventDefault());
|
||||
window.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer?.files?.[0];
|
||||
if (file) void run(file.name, () => loadFile(file));
|
||||
});
|
||||
|
||||
const model = new URLSearchParams(location.search).get('model');
|
||||
if (model) void run(model, () => loadUrl(model));
|
||||
else setStatus('ready · Sample DWG / DXF 또는 파일 드롭');
|
||||
|
||||
window.addEventListener('resize', () => viewer.resize());
|
||||
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src/**/*", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['three', 'dxf-parser', 'opentype.js'],
|
||||
},
|
||||
assetsInclude: ['**/*.wasm'],
|
||||
});
|
||||
Generated
-4304
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "hmwebviewer",
|
||||
"name": "@hmwebviewer/viewer-3d",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
@@ -8,21 +8,13 @@
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"postinstall": "node tools/copy-decoders.mjs"
|
||||
"stage:decoders": "node tools/copy-decoders.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"three": "^0.169.0",
|
||||
"web-ifc": "^0.0.77"
|
||||
"three": "0.185.1",
|
||||
"web-ifc": "0.0.77"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gltf-transform/cli": "^4.4.0",
|
||||
"@gltf-transform/core": "^4.4.0",
|
||||
"@gltf-transform/extensions": "^4.4.0",
|
||||
"@gltf-transform/functions": "^4.4.0",
|
||||
"@types/three": "^0.169.0",
|
||||
"draco3d": "^1.5.7",
|
||||
"puppeteer-core": "^25.1.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^5.4.0"
|
||||
"puppeteer-core": "^25.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ Both are dependencies of `KTX2Loader`:
|
||||
|
||||
```js
|
||||
const ktx2Loader = new KTX2Loader();
|
||||
ktx2Loader.setTranscoderPath( 'examples/jsm/libs/basis/' );
|
||||
ktx2Loader.detectSupport( renderer );
|
||||
ktx2Loader.load( 'diffuse.ktx2', function ( texture ) {
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
ply
|
||||
format ascii 1.0
|
||||
comment Three.js r185 Float64 normalization fixture
|
||||
element vertex 3
|
||||
property double x
|
||||
property double y
|
||||
property double z
|
||||
property uchar red
|
||||
property uchar green
|
||||
property uchar blue
|
||||
element face 1
|
||||
property list uchar int vertex_indices
|
||||
end_header
|
||||
0.0 0.0 0.0 255 0 0
|
||||
100.0 0.0 0.0 0 255 0
|
||||
0.0 100.0 0.0 0 0 255
|
||||
3 0 1 2
|
||||
Regular → Executable
Regular → Executable
@@ -62,6 +62,21 @@ async function getPLY(): Promise<PLYLoaderT> {
|
||||
return (_ply = new PLYLoader());
|
||||
}
|
||||
|
||||
function normalizePlyAttributes(geometry: THREE.BufferGeometry): void {
|
||||
for (const name of ['position', 'normal', 'uv', 'color']) {
|
||||
const attribute = geometry.getAttribute(name);
|
||||
if (!attribute || !(attribute.array instanceof Float64Array)) continue;
|
||||
geometry.setAttribute(
|
||||
name,
|
||||
new THREE.Float32BufferAttribute(
|
||||
new Float32Array(attribute.array),
|
||||
attribute.itemSize,
|
||||
attribute.normalized,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load any supported model URL and resolve to a scene-addable Object3D.
|
||||
* `nameHint` carries the real file name for ext detection when `url` is a
|
||||
@@ -165,7 +180,18 @@ export async function loadModel(
|
||||
case 'dae': {
|
||||
const loader = await getCollada();
|
||||
return new Promise<THREE.Object3D>((resolve, reject) => {
|
||||
loader.load(url, (result) => resolve(result.scene), onXhr, (err) => reject(err));
|
||||
loader.load(
|
||||
url,
|
||||
(result) => {
|
||||
if (!result) {
|
||||
reject(new Error('Collada loader returned no scene'));
|
||||
return;
|
||||
}
|
||||
resolve(result.scene);
|
||||
},
|
||||
onXhr,
|
||||
(err) => reject(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
case 'ply': {
|
||||
@@ -174,6 +200,7 @@ export async function loadModel(
|
||||
loader.load(
|
||||
url,
|
||||
(geometry) => {
|
||||
normalizePlyAttributes(geometry);
|
||||
const hasColor = geometry.hasAttribute('color');
|
||||
if (geometry.index) {
|
||||
// Triangle mesh
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
// runtime paths /draco/ and /basis/ resolve (version-matched to installed three).
|
||||
// Also copies the web-ifc single-thread WASM to public/web-ifc/ for the IFC path.
|
||||
// Runs automatically via `npm install` (postinstall). Safe to re-run.
|
||||
import { cpSync, copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { cpSync, copyFileSync, existsSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, '..');
|
||||
|
||||
const libs = resolve(root, 'node_modules/three/examples/jsm/libs');
|
||||
const threeEntry = fileURLToPath(import.meta.resolve('three'));
|
||||
const threeRoot = resolve(dirname(threeEntry), '..');
|
||||
const libs = resolve(threeRoot, 'examples/jsm/libs');
|
||||
const targets = [
|
||||
[resolve(libs, 'draco/gltf'), resolve(root, 'public/draco')],
|
||||
[resolve(libs, 'basis'), resolve(root, 'public/basis')],
|
||||
@@ -25,13 +27,15 @@ for (const [src, dest] of targets) {
|
||||
console.warn(`[copy-decoders] source missing: ${src}`);
|
||||
continue;
|
||||
}
|
||||
rmSync(dest, { force: true, recursive: true });
|
||||
mkdirSync(dest, { recursive: true });
|
||||
cpSync(src, dest, { recursive: true });
|
||||
console.log(`[copy-decoders] ${src} -> ${dest}`);
|
||||
}
|
||||
|
||||
// web-ifc single-thread WASM (+ MT build if present) for the IFC load path.
|
||||
const ifcSrcDir = resolve(root, 'node_modules/web-ifc');
|
||||
const ifcEntry = fileURLToPath(import.meta.resolve('web-ifc'));
|
||||
const ifcSrcDir = dirname(ifcEntry);
|
||||
const ifcDest = resolve(root, 'public/web-ifc');
|
||||
if (existsSync(ifcSrcDir)) {
|
||||
mkdirSync(ifcDest, { recursive: true });
|
||||
|
||||
@@ -7,7 +7,9 @@ export default defineConfig({
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: { three: ['three'] },
|
||||
manualChunks(id) {
|
||||
if (id.includes('/node_modules/three/')) return 'three';
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user