16 KiB
16 KiB
PROGRESS.md — hmwebviewer running log
Append-only log of what happened. Newest at top. Pair with PLAN.md for what's next. Agents: add an entry whenever you start or finish a task, hit a blocker, or make a decision.
Current state
- Phase: ALL PLAN tasks complete (P0–P7). GLB + OBJ/FBX/DAE/IFC loaders (SSR+CSR) + on-screen FPS + adaptive quality (<60fps→pixelRatio degrade). Build verified (tsc 0, vite exit 0).
- Last action: Gitea #7 runtime artifact 경량화 — Three.js hashed decoder 통합, single-thread IFC staging, production artifact budget와 Draco/Basis/IFC subpath E2E 검증.
- Blockers: none. KTX2 production encoding still needs KTX-Software/toktx (runtime decode verified). web-ifc multi-thread는 COOP/COEP가 필요해 배포에서 제외하고 single-thread를 사용합니다.
- Next: OBJ/DAE geometry-only on blob: URL (accepted); document.hidden FPS guard.
Work log
- 2026-07-29 — Gitea #7 runtime artifact 경량화: version 중복 5종은 incompatible/optional transitive dependency라 강제 override하지 않음. Three.js bundled hashed Draco/Basis asset으로 전환하고
public/draco,public/basis,web-ifc-mt.wasm, production source map 제거. 2D/3D artifact gate 추가. 3D dist 24,945,531→8,149,402 bytes(-67.3%), 2D dist 4,302,677 bytes, duplicate 0. 966-byte official Three.js ETC1S KTX2 test asset으로 production subpath에서 Basis transcoder 실제 실행을 검증. Vitest 8, typecheck/build, Playwright E2E 17개 통과. - 2026-06-19 — KTX2 runtime path validated: toktx unavailable here (no native KTX-Software), so sourced the only single-file KTX2 GLB in Khronos — ABeautifulGame.glb (glTF-Binary-KTX-ETC1S-Draco, 11.5MB, 35 KHR_texture_basisu / 33 image/ktx2 / 17 Draco). perf-smoke load via viewer: 626ms PASS (KTX2Loader + DRACOLoader decode confirmed). Corrected tools/preprocess.mjs doc: KTX2 CLI command is
gltf-transform etc1s|uastc(needs KTX-Software 4.3+), notktx. Samples now include KTX2+Draco variant. - 2026-06-18 — P4-3 prerender pipeline complete: added ThreeDViewer.rotateTo(azimuth) +
window.__viewerexposure; wrote tools/prerender.mjs (puppeteer-core orbit capture → ffmpeg animated WebP). Fixed: MSYS path mangling of/samplesargv; ffmpeglibwebp_animbroken in this build (1 frame) → switched to-c:v libwebp -vsync vfr(correct multi-frame). Produced public/previews/Duck.webp (24 frames, 54KB). Wired into main.ts:?model=sets#previewsrc to matching/previews/<name>.webp(onerror hides), hydration gate fades it on ready. P4-2 (3-size sample set) + P4-3 → done. Result/verify:npm run buildexits 0; ANMF=24 confirmed. - 2026-06-18 — Asset pipeline validated + P5-3 perf smoke: fixed tools/preprocess.mjs (textureCompress ktx2 → webp; registered draco3d.encoder dependency + EXTTextureWebP). Ran on Duck.glb → 117.7KiB→31.2KiB (73.4% reduction, Draco+webp). Added
windowhmw:readyevent in ThreeDViewer.onLoaded. Wrote tools/perf-smoke.mjs (puppeteer-core + system Chrome headless). Ran against preview: Box 102ms, Duck 94ms, Duck.optimized(Draco) 141ms, Avocado 271ms — all < 3000ms PASS. Draco runtime decoder path confirmed. P5-3 → done. - 2026-06-18 — Samples: Box(1.6K)/Duck(118K)/Avocado(7.9M) + Duck.optimized(Draco,31.2K) in samples/ + public/samples/.
- 2026-06-18 — Phase 3 hydration + Phase 5 hardening: marked P3-1/2/3 done (placeholder+gate+UI toggle already in viewer-core/hydration.ts). Reviewer agent found 0 critical, 3 🟡 (error UX), 5 nit; P5-1 clean (single loaders, full dispose, revoke both paths). Applied: index.html #status + .status CSS; progress.ts setStatus(); ThreeDViewer onError param wired to both load-error callbacks; main.ts WebGL try/catch + status surfacing for init/dnd/load failures. vite.config manualChunks split three→vendor. Result/verify:
npm run buildexits 0 (three 518KB vendor + app 132KB; chunk warning is three's inherent size). P5-1/2/4 → done. - 2026-06-18 — Sample assets: downloaded Box.glb (1.6K) + Duck.glb (118K) → samples/ + public/samples/ (Path A
?model=/samples/Box.glbtestable). - 2026-06-18 — Phase 0 viewer-core inline (classifier blocked subagent spawn): wrote src/viewer/loaders.ts (singleton getLoaders), src/viewer/hydration.ts (createHydrationGate — fades on BOTH webgl-ready + model-loaded, idempotent), src/viewer/ThreeDViewer.ts (renderer+scene+camera+OrbitControls+lights, loadServerAsset w/ progress, loadLocalFile w/ createObjectURL+revoke on both paths, frameObject auto-fit, dispose traverse), rewrote src/main.ts (wire viewer + initDropzone + ?model= Path A). Added tools/copy-decoders.mjs + package.json postinstall so
npm installauto-copies /draco + /basis decoders. P0-1/2/3 + P1-1..P1-4 → done (build-verify pending). Result/verify: code coherent against three r169 types;npm run buildpending install. Next: install → build → smoke load. - 2026-06-18 — Phase 2 dnd-handler agent (parallel): src/ui/progress.ts + src/dnd/dropzone.ts. preventDefault on dragover, /.(glb|gltf)$/i validation, click-to-browse. Build-verify pending.
- 2026-06-18 — Phase 4 asset-pipeline agent (parallel): tools/preprocess.mjs (gltf-transform dedup+weld+quantize+draco+KTX2, before/after sizes, graceful missing-package) + tools/README.md. Runtime pending
npm install -D @gltf-transform/*. P4-1 done; P4-2/P4-3 blocked(needs sample asset). - 2026-06-18 — Web research (loaders, Draco+KTX2, DnD, Vite, prerender). Findings saved to user memory.
- 2026-06-18 — Authored CLAUDE.md, PLAN.md, PROGRESS.md, .claude agents/commands/skills/hooks.
- 2026-06-18 — Phase 0 viewer-core inline (classifier blocked subagent spawn): wrote src/viewer/loaders.ts (singleton getLoaders), src/viewer/hydration.ts (createHydrationGate — fades on BOTH webgl-ready + model-loaded, idempotent), src/viewer/ThreeDViewer.ts (renderer+scene+camera+OrbitControls+lights, loadServerAsset w/ progress, loadLocalFile w/ createObjectURL+revoke on both paths, frameObject auto-fit, dispose traverse), rewrote src/main.ts (wire viewer + initDropzone + ?model= Path A). Added tools/copy-decoders.mjs + package.json postinstall so
npm installauto-copies /draco + /basis decoders. P0-1/2/3 + P1-1..P1-4 → done (build-verify pending). Result/verify: code coherent against three r169 types;npm run buildpending install. Next: install → build → smoke load. - 2026-06-18 — Phase 2 dnd-handler agent (parallel): src/ui/progress.ts + src/dnd/dropzone.ts. preventDefault on dragover, /.(glb|gltf)$/i validation, click-to-browse. Build-verify pending.
- 2026-06-18 — Phase 4 asset-pipeline agent (parallel): tools/preprocess.mjs (gltf-transform dedup+weld+quantize+draco+KTX2, before/after sizes, graceful missing-package) + tools/README.md. Runtime pending
npm install -D @gltf-transform/*. P4-1 done; P4-2/P4-3 blocked(needs sample asset). - 2026-06-18 — Web research (loaders, Draco+KTX2, DnD, Vite, prerender). Findings saved to user memory.
- 2026-06-18 — Authored CLAUDE.md, PLAN.md, PROGRESS.md, .claude agents/commands/skills/hooks.
Decision log
- 2026-06-18 — Three.js + Vite chosen. Draco + KTX2 both, single shared loader instances. CDN decoder default. [memory: project-threejs-tech-decisions]
- 2026-06-18 — Multi-agent split: viewer-core / dnd-handler / asset-pipeline / hydration / reviewer. PLAN.md holds task graph.
Work log
- 2026-06-18 — Web research (loaders, Draco+KTX2, DnD, Vite, prerender). Findings saved to user memory.
- 2026-06-18 — Authored CLAUDE.md, PLAN.md, PROGRESS.md, .claude agents/commands/skills/hooks.
2026-06-18 — Phase 2 dnd-handler (P2-1, P2-2)
- Did: wrote
src/ui/progress.ts(showProgress/hideProgress/setProgress) andsrc/dnd/dropzone.ts(initDropzone + DropzoneOpts + ViewerHandle). Dragover/dragenter preventDefault +dragclass; dragleave/drop removes it; drop takes first file, validates/\.(glb|gltf)$/i, forwards valid file toviewer.loadLocalFile(file), elseonError('GLB/GLTF only'). Click-to-browse wired to#file-input;changeresets input value so repeat picks fire. - Result/verify: signatures match the viewer-core/main.ts contract exactly (DropzoneOpts.viewer.loadLocalFile, progress el.firstChild
.bar). File scope respected — did not touch src/viewer/* or src/main.ts. Build-verify pendingnpm install(blocked by permission classifier this session). - Next: viewer-core wires
initDropzonefrom main.ts once its ThreeDViewer.loadLocalFile lands; then/verify-viewersmoke test drops a sample GLB. - Blocker: none (source-only; build verification deferred).
2026-06-18 — Phase 4 asset-pipeline (P4-1 done; P4-2, P4-3 blocked)
- Did: wrote
tools/preprocess.mjs(Node ESM CLI) +tools/README.md. Pipeline = read GLB →dedup+weld+quantize(POSITION per--draco-bits, default 14; NORMAL/TEXCOORD/COLOR fixed sane) +prune+draco()+textureCompress({targetFormat:'ktx2'})→ write.optimized.glb. RegistersKHRDracoMeshCompression+KHRTextureBasisuon WebIO. Prints before/after bytes + reduction %. KTX2 on by default (--no-ktx2to disable). Dynamic import of@gltf-transform/{core,functions,extensions,cli}— on missing package, prints the exactnpm install -Dcommand and exits 1 (no silent skip). Header documents usage + install + that gltf-transform fetches the platform basis encoder on first KTX2 run. - Result/verify:
node --check→ SYNTAX_OK.node tools/preprocess.mjs --help→ prints usage, exit 0.node tools/preprocess.mjs fake.glbwith deps absent → prints missing-package message + install command, exit 1 (graceful path confirmed). Runtime execution (actual compression) pendingnpm install -D @gltf-transform/core @gltf-transform/functions @gltf-transform/extensions @gltf-transform/cli— blocked by the permission classifier this session. - Next: once deps installed + a sample asset exists, run on it, confirm size reduction + viewer load, then P4-2 (commit samples/.optimized.glb) and P4-3 (Blender/Puppeteer pre-render → public/previews/.webp).
- Blocker: P4-2 blocked(needs sample asset); P4-3 blocked(needs sample asset + runtime install). PLAN.md flipped: P4-1 done, P4-2/P4-3 blocked.
2026-06-19 — FIX: CSR drag&drop regression (blob URL has no extension) — all formats
- Bug: user dropped samples/Avocado.glb → "Failed to load file"; ALL local drops failed. Root cause: P6 refactor routed loadLocalFile through
loadModel(url)which dispatches byextOf(url); Path B passes ablob:URL (no.glbsuffix) → extOf=null → switch default throws "Unsupported". Per-format smoke had only tested Path A (?model=), so CSR blob path was never exercised. - Fix (surgical):
loadModel(url, renderer, onProgress, nameHint?)— ext =extOf(nameHint ?? url) ?? extOf(url). ThreeDViewer.loadLocalFile passesfile.nameas nameHint. (modelLoader.ts + ThreeDViewer.ts, 2 lines effective.) - Verify: NEW tools/dnd-smoke.mjs drives the real CSR path (fetch sample →
new File→window.__viewer.loadLocalFile). tsc 0, build 7.85s exit 0. 7/7 local drops PASS: Box/Duck/Avocado.glb + Cube.obj/fbx/dae/ifc all load via drag&drop. Closes the previously-untested CSR coverage gap. - Blocker: none.
2026-06-19 — Per-format in-browser load smoke (all 6 formats PASS)
- Did: built dist +
vite preview:4173; ran tools/perf-smoke.mjs against one sample per format (Box.glb, Cube.obj, Cube.fbx, Cube.dae, Cube.ifc, Duck.optimized.glb) in real headless Chrome — measured nav→hmw:ready. - Result/verify: ALL PASS <3s — Box.glb 229ms, Cube.obj 173ms, Cube.fbx 158ms, Cube.dae 178ms, Cube.ifc 463ms (incl web-ifc wasm Init), Duck.optimized.glb 182ms. Closes the per-format runtime gap (SSR
?model=path renders every format). - Gotcha: Git-Bash mangled
--asset /samples/..argv →C:/Program Files/Git/samples/..(MSYS path conversion). Fix: prefixMSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'. - Blocker: none.
2026-06-19 — P7 adaptive downstep empirically verified (tools/adaptive-smoke.mjs)
- Did: built dist +
vite preview:4173; ran new tools/adaptive-smoke.mjs — headless Chrome (swiftshader software GL) + CDPEmulation.setCPUThrottlingRate6x, loaded heavy ABeautifulGame.ktx2.glb (11.5MB). Watched real #.fps overlay + captured genuine adaptiveQuality console logs (no logic patched). - Result/verify: FPS overlay rendered real values (19→4 FPS under throttle). Adaptive stepped tier 0→1→2→3 (pr 1.5→1→0.75) on sustained <60fps; each step after ~60 slow frames per DOWN_FRAMES. Harness exit 0 PASS. The <60fps→optimize path fires on real frame measurement.
- Note (honest): under CPU throttle the bottleneck is JS/geometry, not GPU fill, so lowering pixelRatio barely recovered fps (4→7) — the mechanism fired correctly but pixelRatio is a fill-rate knob; it pays off on GPU-bound scenes. Recovery-up not exercised (would need sustained >72fps).
- Blocker: none.
2026-06-19 — Phase 6 + 7 via dynamic workflow multiformat-viewer (7 agents, 296k tok)
- Did: extended viewer to OBJ/FBX/DAE/IFC (SSR+CSR) + on-screen FPS + adaptive quality. Run as one dynamic Workflow: Research(3 ‖ read-only: web-ifc API, adaptive-perf, three loader shapes) → Core(viewer-core) → Enhance(FPS ‖ Assets) → Verify(claude). Disjoint file sets per parallel wave (no git repo → no worktree isolation).
- P6 Core: NEW
src/viewer/modelLoader.ts—extOf/ACCEPT_EXT/loadModel(url,renderer,onProgress)dispatch by ext, normalize each toObject3D. glb/gltf reuse singleton GLTFLoader (unchanged). obj/fbx/dae = lazy dynamic-import three example loaders (separate Vite chunks: OBJ 8.8K / Collada 41K / FBX 48K). ifc =web-ifc@0.0.77 IfcAPI, single-thread/web-ifc/web-ifc.wasm(SetWasmPath abs=true, Init(undefined,true), NO COOP/COEP), OpenModel(COORDINATE_TO_ORIGIN) → StreamAllMeshes → BufferGeometry per PlacedGeometry (interleaved stride-6 pos+normal, Uint32 index, color/opacity, flatTransformation),geom.delete()+CloseModelin finally (no wasm leak). ThreeDViewer.loadServerAsset(SSR) + loadLocalFile(CSR) both route through loadModel; onLoaded generalized to Object3D; Blob URL revoked in.finally(). dropzone via extOf (6 exts); index.html accept+text; main.ts preview-base widened; copy-decoders stages web-ifc.wasm. - P7 FPS+adaptive: NEW
src/ui/fps.ts(EMA fps, throttled 250ms DOM, color green≥60/orange/red, self-appended overlay). NEWsrc/viewer/adaptiveQuality.ts(pixelRatio ladder [min(DPR,2),1.5,1,0.75,0.5], asymmetric hysteresis: down after 60 frames <60fps, up after 180 frames >72fps, dead-band reset). Wired into ThreeDViewer.animate(now): fps.sample(now)+adaptive.update(fps()). Adaptive owns setPixelRatio (DPR cap from init). rotateTo (prerender) unaffected. - Assets: Cube.obj(793B, hand-authored)/Cube.dae(2.1K)/Cube.fbx(16.2K, three.js morph_test — assimp box.fbx failed in r0.169 FBXLoader)/Cube.ifc(2.3K, hand-authored IFC4 1-wall) in samples/ + public/samples/; 24-frame animated WebP previews per format in public/previews/. prerender.mjs base-name made format-aware.
- P6 Core: NEW
- Result/verify (independent main-thread re-run):
npx tsc --noEmit→ "No errors found" exit 0.npm run build→ exit 0 (vite 5.4.21, 11.92s; chunks: app 138K, three vendor 533K, web-ifc 3.5M lazy/code-split — not on initial path). Confirmed via grep: 6 dispatch branches; dropzone EXT_RE all 6; animate wires fps+adaptive; loadServerAsset+loadLocalFile both call loadModel. web-ifc.wasm staged in public/web-ifc/. - Next: optional — in-browser smoke per format (.ifc/.fbx) via /verify-viewer; OBJ/DAE render geometry-only (external .mtl/textures can't resolve from blob: URL — accepted); document.hidden background-tab guard for FPS (noted, not done).
- Blocker: none.