Files
dwg-dxf-viewer-sample/apps/viewer-3d/docs/architecture.html
T
lectom bedb357259 chore(monorepo): import hmwebviewer history at a717900
git-subtree-dir: apps/viewer-3d
git-subtree-mainline: 5e10bb1be4
git-subtree-split: a71790070d
2026-07-29 09:04:52 +09:00

231 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>hmwebviewer — 아키텍처 / 구조 / 계획</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0; padding: 32px; max-width: 1000px; margin: 0 auto;
font: 15px/1.6 system-ui, "Segoe UI", "Malgun Gothic", sans-serif;
background: #0f1115; color: #e6e9ef;
}
h1 { font-size: 26px; border-bottom: 2px solid #4aa3ff; padding-bottom: 10px; }
h2 { font-size: 19px; margin-top: 36px; color: #4aa3ff; border-left: 4px solid #4aa3ff; padding-left: 10px; }
h3 { font-size: 16px; margin-top: 24px; color: #cfe3ff; }
code, pre { font-family: "Cascadia Code", Consolas, monospace; }
code { background: #1c2230; padding: 1px 5px; border-radius: 4px; color: #ffd479; font-size: 13px; }
pre { background: #161a24; border: 1px solid #262c3a; border-radius: 8px; padding: 14px; overflow-x: auto; font-size: 13px; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 13.5px; }
th, td { border: 1px solid #2a3142; padding: 7px 10px; text-align: left; vertical-align: top; }
th { background: #1a2030; color: #9fc4ff; }
tr:nth-child(even) td { background: #141821; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.card { background: #161a24; border: 1px solid #262c3a; border-radius: 8px; padding: 14px 16px; }
.badge { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; }
.ok { background: #143a23; color: #57d98a; }
.wip { background: #3a3014; color: #e4b35a; }
.blk { background: #3a1414; color: #e48080; }
.meta { color: #8b93a7; font-size: 13px; }
.arrow { color: #4aa3ff; font-weight: 700; }
.sep { border: 0; border-top: 1px solid #262c3a; margin: 28px 0; }
.yes { color: #57d98a; font-weight: 700; }
.no { color: #e48080; font-weight: 700; }
</style>
</head>
<body>
<h1>hmwebviewer — 3D 뷰어</h1>
<p class="meta">Three.js 기반 하이브리드(SSR + CSR) 웹 3D 모델 뷰어. 설계 원본: <code>3d_viewer_architecture_spec.pdf</code></p>
<p class="meta"><strong>지원 포맷:</strong> GLB · GLTF · OBJ · FBX · DAE(Collada) · IFC(BIM) &nbsp;|&nbsp; <strong>로드 경로:</strong> SSR + CSR &nbsp;|&nbsp; <strong>성능:</strong> 화면 FPS 표시 + 60fps 미만 시 적응형 품질 강등</p>
<p class="meta"><strong>문서 갱신:</strong> <span id="updated">2026-06-19</span> — 매 작업 프롬프트 종료 시 이 파일을 함께 업데이트합니다.</p>
<h2>1. 개요 / 두 가지 로드 경로</h2>
<div class="grid">
<div class="card">
<h3>Path A — 서버 에셋 (SSR + CSR + Hydration)</h3>
<p>서버가 미리 렌더링한 360° Animated WebP 자리표시자 + HTML/CSS 골조를 먼저 보냄(빠른 체감 로드). 뒤에서 <code>modelLoader.loadModel(url)</code>이 확장자로 디스패치하여 실제 모델을 비동기 로드/디코드. WebGL 준비 <strong>and</strong> 모델 로드 완료 둘 다 충족 시 WebP <span class="arrow"></span> 캔버스로 opacity 0.5s 페이드.</p>
<p class="meta">트리거: <code>?model=&lt;url&gt;</code> · 프리뷰: <code>/previews/&lt;base&gt;.webp</code> (포맷 무관)</p>
</div>
<div class="card">
<h3>Path B — 로컬 파일 (Drag &amp; Drop, CSR)</h3>
<p>사용자가 <code>.glb .gltf .obj .fbx .dae .ifc</code> 드롭. <code>URL.createObjectURL(file)</code> <span class="arrow"></span> <code>loadModel</code> <span class="arrow"></span> 디코드 <span class="arrow"></span> 씬 추가 <span class="arrow"></span> <code>URL.revokeObjectURL</code>(성공/에러 <code>.finally()</code> 양쪽). 검증: <code>extOf()</code> 확장자.</p>
<p class="meta">트리거: 파일 드롭 / 클릭-탐색</p>
</div>
</div>
<p class="meta">두 경로 모두 단일 디스패처 <code>src/viewer/modelLoader.ts</code><code>loadModel(url, renderer, onProgress)</code>를 거쳐 <code>THREE.Object3D</code>로 정규화 → <code>ThreeDViewer.onLoaded()</code>는 원본 포맷을 알 필요 없음.</p>
<h2>2. 지원 파일 포맷</h2>
<table>
<tr><th>포맷</th><th>확장자</th><th>로더</th><th>비고</th><th>SSR</th><th>CSR</th><th>검증(체감 로드)</th></tr>
<tr>
<td>glTF (Binary/JSON)</td><td><code>.glb .gltf</code></td>
<td>GLTFLoader<br>(+DRACO +KTX2 싱글톤)</td>
<td>Draco 지오메트리 + KTX2/Basis·WebP 텍스처. 기준 경로.</td>
<td class="yes"></td><td class="yes"></td><td><span class="badge ok">PASS 229ms</span></td>
</tr>
<tr>
<td>Wavefront OBJ</td><td><code>.obj</code></td>
<td>OBJLoader (지연 청크 8.8KB)</td>
<td>지오메트리 전용. <code>blob:</code> URL에선 외부 <code>.mtl</code>/텍스처 미해결 → 기본 머티리얼.</td>
<td class="yes"></td><td class="yes"></td><td><span class="badge ok">PASS 173ms</span></td>
</tr>
<tr>
<td>Autodesk FBX</td><td><code>.fbx</code></td>
<td>FBXLoader (지연 청크 47.8KB)</td>
<td>바이너리(자체 포함). 애니메이션 가능(<code>.animations</code>) — 현재 정적 표시.</td>
<td class="yes"></td><td class="yes"></td><td><span class="badge ok">PASS 158ms</span></td>
</tr>
<tr>
<td>COLLADA</td><td><code>.dae</code></td>
<td>ColladaLoader (지연 청크 41KB)</td>
<td>XML(DOMParser 필요). 외부 텍스처는 OBJ와 동일 제약.</td>
<td class="yes"></td><td class="yes"></td><td><span class="badge ok">PASS 178ms</span></td>
</tr>
<tr>
<td>IFC (BIM)</td><td><code>.ifc</code></td>
<td>web-ifc <code>IfcAPI</code><br>(단일스레드 wasm, 지연 청크 3.5MB)</td>
<td><code>StreamAllMeshes</code> <span class="arrow"></span> <code>BufferGeometry</code>(인터리브 stride-6 pos+normal). 단일스레드라 <strong>COOP/COEP 헤더 불필요</strong>. <code>geom.delete()</code>+<code>CloseModel</code>로 wasm 메모리 해제.</td>
<td class="yes"></td><td class="yes"></td><td><span class="badge ok">PASS 463ms</span><br><span class="meta">wasm Init 포함</span></td>
</tr>
</table>
<p class="meta">검증: 빌드 <code>dist</code> + <code>vite preview</code>에 대해 헤드리스 Chrome으로 포맷별 1개 샘플 로드, 네비게이션→<code>hmw:ready</code> 측정. 전체 &lt;3000ms PASS (<code>tools/perf-smoke.mjs</code>).</p>
<h2>3. 기술 스택 (확정)</h2>
<table>
<tr><th>영역</th><th>선택</th><th>비고</th></tr>
<tr><td>렌더러</td><td>three.js WebGLRenderer (r0.169)</td><td>WebGPU는 차후 옵션</td></tr>
<tr><td>로더(기준)</td><td>GLTFLoader + DRACOLoader + KTX2Loader</td><td><strong>각 1개 싱글톤</strong> — 다중 인스턴스 크래시(#22445)</td></tr>
<tr><td>로더(확장)</td><td>OBJ / FBX / Collada — three example loaders</td><td>지연 <code>import()</code> → 별도 Vite 청크(초기 번들 미증가)</td></tr>
<tr><td>로더(IFC)</td><td>web-ifc <code>IfcAPI</code> (v0.0.77)</td><td>단일스레드 <code>web-ifc.wasm</code> 자가호스팅(<code>/web-ifc/</code>), <code>SetWasmPath(...,true)</code></td></tr>
<tr><td>적응형 품질</td><td>FPS 측정 + pixelRatio 사다리</td><td>외부 의존성 0 — <code>ui/fps.ts</code> + <code>viewer/adaptiveQuality.ts</code></td></tr>
<tr><td>지오메트리 압축</td><td>Draco</td><td>~73% 축소 검증됨</td></tr>
<tr><td>텍스처 압축</td><td>WebP(런타임) / KTX2·Basis(고급)</td><td>KTX2는 별도 CLI 단계(<code>toktx</code>)</td></tr>
<tr><td>빌드</td><td>Vite + TypeScript (strict)</td><td>three vendor + 포맷별 로더 청크 분리</td></tr>
<tr><td>디코더 호스팅</td><td><code>public/draco</code> + <code>public/basis</code> + <code>public/web-ifc</code></td><td>postinstall로 자동 복사</td></tr>
<tr><td>에셋 파이프라인</td><td>gltf-transform + draco3d</td><td><code>tools/preprocess.mjs</code> (GLB 전용)</td></tr>
</table>
<h2>4. 런타임 파일 구조</h2>
<pre>src/
main.ts # 진입점 — ThreeDViewer + Dropzone 와이어링, ?model= Path A
viewer/
loaders.ts # getLoaders() 싱글톤 (GLTF+DRACO+KTX2)
modelLoader.ts # ★ loadModel() 포맷 디스패치 (glb/gltf/obj/fbx/dae/ifc) → Object3D
ThreeDViewer.ts # 핵심 클래스: 렌더러/씬/카메라/OrbitControls/로드/dispose + FPS·적응형 와이어링
adaptiveQuality.ts # ★ pixelRatio 사다리 + 히스테리시스 (60fps↓ 강등)
hydration.ts # createHydrationGate() — 양쪽 마크 시 페이드
dnd/dropzone.ts # HTML5 DnD + 클릭탐색 + extOf 검증 → loadLocalFile
ui/
progress.ts # showProgress/hideProgress/setProgress/setStatus
fps.ts # ★ 화면 FPS 오버레이 (EMA, 색상코딩)
public/
draco/ basis/ # 디코더 WASM (자동 복사)
web-ifc/ # ★ web-ifc.wasm (단일스레드, 자동 복사)
samples/ # Box/Duck/Avocado/Duck.optimized + Cube.obj/dae/fbx/ifc
previews/ # Duck.webp + Cube.{obj,dae,fbx,ifc}.webp (24프레임 360°)
tools/
preprocess.mjs # gltf-transform Draco+WebP 압축 (GLB 전용)
copy-decoders.mjs # postinstall: draco+basis+web-ifc 복사
prerender.mjs # 360° 턴테이블 → Animated WebP (포맷 무관)
perf-smoke.mjs # 헤드리스 체감 로드 측정 (&lt;3s)
adaptive-smoke.mjs # ★ CPU 스로틀로 60fps↓ 강등 실증
</pre>
<h2>5. 멀티에이전트 + 다이나믹 워크플로우</h2>
<p>에이전트가 <code>PLAN.md</code> + <code>PROGRESS.md</code>를 매 시작 시 읽어 다음 작업을 결정. 병렬 안전(파일 스코프 분리).</p>
<p class="card"><strong>Phase 6/7은 다이나믹 Workflow <code>multiformat-viewer</code>로 구현</strong> (7 에이전트, 296k 토큰): Research(3 병렬·읽기전용: web-ifc API / 적응형 성능 / three 로더 형태) <span class="arrow"></span> Core(viewer-core) <span class="arrow"></span> Enhance(FPS ‖ 에셋) <span class="arrow"></span> Verify(빌드 green). git 미사용 환경이라 worktree 격리 대신 병렬 웨이브별 파일 스코프 분리로 충돌 방지.</p>
<div class="grid">
<div>
<h3>에이전트</h3>
<table>
<tr><th>이름</th><th>역할</th></tr>
<tr><td>task-lead</td><td>PLAN/PROGRESS 읽고 다음 작업 분배</td></tr>
<tr><td>viewer-core</td><td>src/viewer/* 씬·로더·하이드레이션·FPS</td></tr>
<tr><td>dnd-handler</td><td>src/dnd/* 드래그드롭·검증</td></tr>
<tr><td>asset-pipeline</td><td>tools/* 압축·프리렌더·샘플</td></tr>
<tr><td>hydration</td><td>SSR 자리표시자→캔버스 페이드</td></tr>
<tr><td>reviewer</td><td>누수/에러/성능 감사(읽기 위주)</td></tr>
</table>
</div>
<div>
<h3>명령 / 스킬 / 훅</h3>
<p><strong>명령:</strong> <code>/bootstrap</code> <code>/next-task</code> <code>/report</code> <code>/optimize-asset</code> <code>/verify-viewer</code></p>
<p><strong>스킬:</strong> <code>threejs-viewer</code>(로드 패턴), <code>task-graph</code>(조정 프로토콜)</p>
<p><strong>훅:</strong> <code>session-start.sh</code>(PLAN/PROGRESS 읽기 알림), <code>progress-nudge.sh</code>(src 편집 시 알림)</p>
<p class="meta">CLAUDE.md가 두 파일 선독을 강제 + 에이전트/스킬/명령 매핑표 + 세션 런북 포함.</p>
</div>
</div>
<h2>6. 계획 진행 상태 (PLAN.md)</h2>
<table>
<tr><th>Phase</th><th>범위</th><th>상태</th></tr>
<tr><td>P0 Bootstrap</td><td>Vite/TS 셋업, 디코더, 기본 씬</td><td><span class="badge ok">done · 빌드 검증</span></td></tr>
<tr><td>P1 Core 뷰어</td><td>싱글톤 로더, loadServer/Local, OrbitControls, 프레이밍</td><td><span class="badge ok">done · 빌드 검증</span></td></tr>
<tr><td>P2 Drag &amp; Drop</td><td>드롭존 UI, 검증, loadLocalFile 와이어링</td><td><span class="badge ok">done · 빌드 검증</span></td></tr>
<tr><td>P3 Hydration</td><td>WebP 자리표시자, 게이트, UI 토글</td><td><span class="badge ok">done</span></td></tr>
<tr><td>P4 에셋 파이프라인</td><td>preprocess.mjs, 샘플셋, 프리렌더</td><td><span class="badge ok">P4-1/2/3 done</span></td></tr>
<tr><td>P5 Hardening</td><td>누수/에러 감사, 퍼포먼스, 리뷰</td><td><span class="badge ok">P5-1/2/3/4 done</span> 0 critical</td></tr>
<tr><td>P6 멀티포맷 로더</td><td>OBJ/FBX/DAE/IFC, SSR+CSR, modelLoader 디스패치, 샘플+프리뷰</td><td><span class="badge ok">P6-1..5 done</span> 6포맷 검증</td></tr>
<tr><td>P7 FPS + 적응형 품질</td><td>화면 FPS, pixelRatio 사다리, animate 와이어링</td><td><span class="badge ok">P7-1/2/3 done</span> 강등 실증</td></tr>
</table>
<h2>7. FPS 표시 + 적응형 품질 (&lt;60fps → 최적화)</h2>
<div class="card">
<ul>
<li><strong>FPS 오버레이</strong> (<code>ui/fps.ts</code>): 좌상단, 프레임 델타 EMA 평활, ~250ms 스로틀 DOM 갱신. 색상 — <span class="yes">≥60 녹색</span> / 3059 주황 / <span class="no">&lt;30 빨강</span>.</li>
<li><strong>적응형 사다리</strong> (<code>viewer/adaptiveQuality.ts</code>): pixelRatio 5단계 <code>[min(DPR,2) · 1.5 · 1 · 0.75 · 0.5]</code>. 가장 고효율·저위험 런타임 노브(드로잉 버퍼 재할당, CSS 리플로우 없음).</li>
<li><strong>히스테리시스</strong>(진동 방지): 평균 &lt;60fps가 <strong>60프레임 지속</strong> 시 1단계 강등 / 평균 &gt;72fps가 <strong>180프레임 지속</strong> 시 1단계 복귀 / 60–72 데드밴드는 카운터 리셋.</li>
<li><strong>실증</strong>(<code>tools/adaptive-smoke.mjs</code>): 헤드리스 swiftshader + CDP CPU 스로틀 6×로 ABeautifulGame(11.5MB) 로드 → 실측 FPS <strong>19→4</strong>, 적응형 <strong>tier 0→1→2→3</strong> (pr 1.5→1→0.75) 로그 캡처. 로직 미패치, 실제 프레임 측정으로 발화.</li>
</ul>
<p class="meta">참고: CPU 스로틀 하에선 병목이 JS/지오메트리(필레이트 아님)라 pixelRatio 강등의 FPS 회복폭이 작음(4→7) — 메커니즘은 정상 발화하나, 이 노브는 GPU 필레이트 병목 씬에서 효과가 큼.</p>
</div>
<h2>8. 현재 구현 상태</h2>
<div class="card">
<ul>
<li><strong>빌드:</strong> <code>tsc --noEmit</code> 클린, <code>npm run build</code> 통과 (vite 5.4.21, 11.92s). 청크: 앱 138KB(gz 47) · three vendor 533KB(gz 136) · web-ifc 3.5MB(gz 398, <em>지연 로드</em>) · OBJ 8.8 / Collada 41 / FBX 48KB.</li>
<li><strong>포맷별 인브라우저 로드(헤드리스 Chrome):</strong> GLB 229ms · OBJ 173ms · FBX 158ms · DAE 178ms · <strong>IFC 463ms</strong> · Duck.optimized 182ms — 전체 &lt;3000ms PASS.</li>
<li><strong>적응형 품질:</strong> 60fps 미만 지속 시 pixelRatio 3단계 강등 실측 확인(위 7절).</li>
<li><strong>샘플:</strong> Box/Duck/Avocado/Duck.optimized(GLB) + Cube.obj(793B)/Cube.dae(2.1K)/Cube.fbx(16.2K)/Cube.ifc(2.3K). 각 24프레임 360° WebP 프리뷰.</li>
<li><strong>KTX2 런타임 경로:</strong> KTX2Loader+DRACOLoader 동시 디코드 확인(ABeautifulGame, 626ms). 인코딩은 toktx 필요(미설치).</li>
<li><strong>하드닝:</strong> WebGL 미지원 가드, 로드 실패 메시지(<code>#status</code>), DnD 검증 피드백, IFC wasm 메모리 해제 — reviewer 0 critical.</li>
</ul>
</div>
<h2>9. 실행 명령</h2>
<pre># 개발 서버 (HMR)
npm run dev # → http://127.0.0.1:3333
# 프로덕션 빌드 + 미리보기
npm run build # tsc --noEmit && vite build → dist/
npm run preview # → http://127.0.0.1:4173
# 사용
# CSR(Path B): .glb .gltf .obj .fbx .dae .ifc 를 드롭존에 드롭 / 클릭 선택
# SSR(Path A): http://127.0.0.1:4173/?model=/samples/Cube.ifc
# 검증/도구 (Git-Bash는 MSYS_NO_PATHCONV=1 접두 필요)
node tools/perf-smoke.mjs # 포맷별 체감 로드 <3s
node tools/adaptive-smoke.mjs --throttle 6 --seconds 30 # 60fps 강등 실증
node tools/preprocess.mjs &lt;file.glb&gt; # Draco+WebP 압축(GLB)
node tools/prerender.mjs # 360° WebP 프리뷰 생성
</pre>
<h2>10. 다음 단계 (선택)</h2>
<ul>
<li>✅ 전체 PLAN 완료 (P0P7). 6포맷 로드 · SSR+CSR · 화면 FPS · 적응형 품질 전부 인브라우저 실증.</li>
<li>OBJ/DAE 외부 텍스처: 다중 파일 드롭(.obj+.mtl+이미지) 또는 zip 수용 — 현재는 <code>blob:</code> 제약으로 지오메트리 전용.</li>
<li>FBX/Collada 애니메이션: <code>AnimationMixer</code> 재생(현재 정적). <code>document.hidden</code> 배경탭 FPS 가드.</li>
<li>KTX2 인코딩 파이프라인: KTX-Software(toktx) 설치 후 <code>gltf-transform etc1s|uastc</code>.</li>
<li>실서버 SSR 연결 + 실기기 체감 로드 측정(현재 수치는 헤드리스).</li>
</ul>
<hr class="sep" />
<p class="meta">작업 로그: <code>PROGRESS.md</code> · 작업 그래프: <code>PLAN.md</code> · 지침: <code>CLAUDE.md</code></p>
</body>
</html>