Three.js viewer supporting glb/gltf/obj/fbx/dae/ifc/ply via both server (SSR) and drag&drop (CSR) paths. - Streaming OBJ parser (src/viewer/objStream.ts) for files past the V8 max string length (>~1GB text) that OBJLoader can't handle; indexed geometry, per-vertex color from MTL Kd, float64 recenter baked in. - In-viewer float64 recenter (objRecenter.ts) for huge CAD/survey coordinates (~1e8) so float32 vertex buffers keep precision (no cracked faces). - Z-up right-handed world; Y-up formats rotated on load. - OBJ+MTL+texture drag&drop (LoadingManager URL-modifier maps dropped images). - OrbitControls ground-plane panning (road/rail alignment workflow). - UI: Zoom Fit, perspective/orthographic toggle, feature-edge outline. - DoubleSide for CAD OBJ; PLY mesh + point-cloud support. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.1 KiB
7.1 KiB
PLAN.md — hmwebviewer task breakdown
Agents: read this + PROGRESS.md on start. Pick the next
todotask whosedepends_onare alldone. Set it toin_progresswith your name before working. Move todoneonly 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; thengltf-transform etc1s|uastc. Not on this box. Documented intools/preprocess.mjsheader + user memory.