chore(monorepo): import hmwebviewer history at a717900
git-subtree-dir: apps/viewer-3d git-subtree-mainline:5e10bb1be4git-subtree-split:a71790070d
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# PLAN.md — hmwebviewer task breakdown
|
||||
|
||||
> **Agents: read this + PROGRESS.md on start.** Pick the next `todo` task whose `depends_on` are all `done`. Set it to `in_progress` with your name before working. Move to `done` only when acceptance criteria pass and you updated PROGRESS.md.
|
||||
|
||||
## Status legend
|
||||
`todo` · `in_progress(@agent)` · `blocked(reason)` · `done` · `skipped(reason)`
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Bootstrap
|
||||
| ID | Task | Agent | Status | Depends | Acceptance |
|
||||
|----|------|-------|--------|---------|------------|
|
||||
| P0-1 | Scaffold Vite + TS project, install three + types | setup | done (build-verify pending) | — | `npm run dev` serves blank page; `npm run build` exits 0 |
|
||||
| P0-2 | Copy Draco + KTX2 decoder WASM to `public/`, wire CDN fallback | setup | done (auto-copies via postinstall) | P0-1 | `DRACOLoader.setDecoderPath('/draco/')` resolves; console clean |
|
||||
| P0-3 | Basic WebGLRenderer scene (camera, lights, resize loop) | viewer-core | done (build-verify pending) | P0-1 | canvas renders a test cube; resizes on window resize |
|
||||
|
||||
## Phase 1 — Core viewer (parallelizable)
|
||||
| ID | Task | Agent | Status | Depends | Acceptance |
|
||||
|----|------|-------|--------|---------|------------|
|
||||
| P1-1 | Singleton loaders (GLTFLoader + DRACOLoader + KTX2Loader, ONE each) | viewer-core | done (build-verify pending) | P0-2 | exported factory; reused across loads; no double-instance |
|
||||
| P1-2 | `loadServerAsset(url)` with progress bar wiring | viewer-core | done (build-verify pending) | P1-1 | loads sample Draco GLB, progress % updates, scene populated |
|
||||
| P1-3 | `loadLocalFile(blob)` — createObjectURL + revoke after load | viewer-core | done (build-verify pending) | P1-1 | drop GLB loads; URL revoked post-load; memory stable across N drops |
|
||||
| P1-4 | OrbitControls + camera framing (fit model to view) | viewer-core | done (build-verify pending) | P1-2 | model auto-framed; rotate/zoom works |
|
||||
|
||||
## Phase 2 — Drag & Drop (parallelizable with Phase 1)
|
||||
| ID | Task | Agent | Status | Depends | Acceptance |
|
||||
|----|------|-------|--------|---------|------------|
|
||||
| P2-1 | Dropzone UI + HTML5 DnD event capture, file type/size validation | dnd-handler | done (build-verify pending) | P0-1 | invalid files rejected with message; valid files accepted |
|
||||
| P2-2 | Wire dropzone → `loadLocalFile` | dnd-handler | done (build-verify pending) | P1-3, P2-1 | dropped file appears in scene |
|
||||
|
||||
## Phase 3 — SSR hydration (serial, depends on core)
|
||||
| ID | Task | Agent | Status | Depends | Acceptance |
|
||||
|----|------|-------|--------|---------|------------|
|
||||
| P3-1 | WebP placeholder element + CSS opacity transition scaffolding | hydration | done | P1-2 | placeholder shows, fades on command |
|
||||
| P3-2 | `executeHydration()` — coordinate WebGL-ready AND model-loaded → fade | hydration | done | P3-1 | no empty-canvas flash; race handled; transition 0.5s |
|
||||
| P3-3 | Toggle UI states (progress bar show/hide, loading→ready) | hydration | done | P3-2 | UI reflects each load phase |
|
||||
|
||||
## Phase 4 — Asset optimization pipeline (offline, parallelizable)
|
||||
| ID | Task | Agent | Status | Depends | Acceptance |
|
||||
|----|------|-------|--------|---------|------------|
|
||||
| P4-1 | `tools/preprocess.mjs` — gltf-transform Draco+KTX2 wrapper | asset-pipeline | done | — | input GLB → output compressed GLB, size reduced, loads in viewer |
|
||||
| P4-2 | Sample asset set (small/medium/large GLB) for testing | asset-pipeline | done | — | Box(1.6K)/Duck(118K)/Avocado(7.9M) + Duck.optimized(Draco) under `samples/` + `public/samples/` |
|
||||
| P4-3 | Pre-render pipeline (Blender CLI OR Puppeteer) → animated WebP | asset-pipeline | done | P4-1 | Duck → 24-frame animated WebP (54KB) at `public/previews/Duck.webp`, wired into `#preview` hydration |
|
||||
|
||||
## Phase 5 — Hardening (serial)
|
||||
| ID | Task | Agent | Status | Depends | Acceptance |
|
||||
|----|------|-------|--------|---------|------------|
|
||||
| P5-1 | Dispose pattern: geometry/material/texture + revoke on teardown | reviewer | done | P1, P2 | no leaks after 10 load/unload cycles (DevTools heap) |
|
||||
| P5-2 | Error handling: bad file, decode fail, WebGL unsupported | reviewer | done | P1-3, P2-1 | graceful message, no uncaught exception |
|
||||
| P5-3 | Perf smoke: load each sample, record time, assert < 3s perceived | reviewer | done | P4-2 | timings logged in PROGRESS.md |
|
||||
| P5-4 | Full review pass (correctness + simplification) | reviewer | done | all | `/review` clean; no high-severity findings |
|
||||
|
||||
## Phase 6 — Multi-format loaders (OBJ/FBX/DAE/IFC), SSR+CSR — via dynamic workflow `multiformat-viewer`
|
||||
| ID | Task | Agent | Status | Depends | Acceptance |
|
||||
|----|------|-------|--------|---------|------------|
|
||||
| P6-1 | Unified `src/viewer/modelLoader.ts` — `loadModel(url)` dispatch by ext, normalize each loader to `Object3D` | viewer-core | done | P1-1 | glb/gltf/obj/fbx/dae/ifc all resolve to scene-addable Object3D; GLB path unchanged |
|
||||
| P6-2 | OBJ/FBX/DAE via three example loaders, lazy dynamic-import (separate Vite chunks) | viewer-core | done | P6-1 | OBJLoader/FBXLoader/ColladaLoader code-split; load samples |
|
||||
| P6-3 | IFC via `web-ifc` IfcAPI (single-thread wasm `/web-ifc/`, no COOP/COEP) — StreamAllMeshes → BufferGeometry | viewer-core | done | P6-1 | Cube.ifc loads; geom.delete + CloseModel (no wasm leak) |
|
||||
| P6-4 | CSR: dropzone accepts all 6 exts via `extOf`; SSR: `loadServerAsset` + main.ts preview-base route any ext | viewer-core+dnd | done | P6-1, P2-1 | drop any of 6 loads; `?model=` + `/previews/<base>.webp` works per format |
|
||||
| P6-5 | Samples + 360° WebP previews per format (Cube.obj/dae/fbx/ifc) | asset-pipeline | done | P6-1, P4-3 | 4 samples in samples/ + public/samples/; 4 animated WebP (24 frames) in public/previews/ |
|
||||
|
||||
## Phase 7 — On-screen FPS + adaptive quality (<60fps → optimize)
|
||||
| ID | Task | Agent | Status | Depends | Acceptance |
|
||||
|----|------|-------|--------|---------|------------|
|
||||
| P7-1 | `src/ui/fps.ts` — on-screen FPS overlay (EMA, throttled DOM, color-coded) | viewer-core | done | P0-3 | FPS readout visible; green≥60/orange/red |
|
||||
| P7-2 | `src/viewer/adaptiveQuality.ts` — pixelRatio tier ladder + hysteresis; step down when sustained <60, recover with headroom | viewer-core | done | P7-1 | tier steps on sustained <60fps; no oscillation; logs tier change |
|
||||
| P7-3 | Wire fps.sample + adaptive.update into `ThreeDViewer.animate` | viewer-core | done | P7-1, P7-2 | both fed each frame; rotateTo (prerender) unaffected |
|
||||
|
||||
---
|
||||
|
||||
## Parallelization map
|
||||
- **Wave 1 (after P0):** P1-1 (core) then forks → P1-2/P1-3/P1-4 + P2-1 + P4-1/P4-2 can proceed in parallel.
|
||||
- **Wave 2:** P3-* depends on core; P2-2 depends on P1-3+P2-1.
|
||||
- **Wave 3:** P5-* after functional work lands.
|
||||
|
||||
Independent agents that may run concurrently: `viewer-core`, `dnd-handler`, `asset-pipeline`. `hydration` waits on `viewer-core`. `reviewer` runs last + on-demand.
|
||||
|
||||
## Validated extras (post-PLAN)
|
||||
- **KTX2 runtime decode** ✅ — KTX2Loader + DRACOLoader together decode the Khronos ABeautifulGame KTX2+Draco GLB (11.5MB) in 626ms perceived (perf-smoke). Runtime path confirmed.
|
||||
- **KTX2 production encoding** ⏸ — needs KTX-Software (`toktx`) installed; then `gltf-transform etc1s|uastc`. Not on this box. Documented in `tools/preprocess.mjs` header + user memory.
|
||||
Reference in New Issue
Block a user