Files
dwg-dxf-viewer-sample/apps/viewer-3d/PROGRESS.md
T

16 KiB
Raw Blame History

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 (P0P7). 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+), not ktx. Samples now include KTX2+Draco variant.
  • 2026-06-18 — P4-3 prerender pipeline complete: added ThreeDViewer.rotateTo(azimuth) + window.__viewer exposure; wrote tools/prerender.mjs (puppeteer-core orbit capture → ffmpeg animated WebP). Fixed: MSYS path mangling of /samples argv; ffmpeg libwebp_anim broken 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 #preview src 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 build exits 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 window hmw:ready event 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 build exits 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.glb testable).
  • 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 install auto-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 build pending 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 install auto-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 build pending 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) and src/dnd/dropzone.ts (initDropzone + DropzoneOpts + ViewerHandle). Dragover/dragenter preventDefault + drag class; dragleave/drop removes it; drop takes first file, validates /\.(glb|gltf)$/i, forwards valid file to viewer.loadLocalFile(file), else onError('GLB/GLTF only'). Click-to-browse wired to #file-input; change resets 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 pending npm install (blocked by permission classifier this session).
  • Next: viewer-core wires initDropzone from main.ts once its ThreeDViewer.loadLocalFile lands; then /verify-viewer smoke 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. Registers KHRDracoMeshCompression + KHRTextureBasisu on WebIO. Prints before/after bytes + reduction %. KTX2 on by default (--no-ktx2 to disable). Dynamic import of @gltf-transform/{core,functions,extensions,cli} — on missing package, prints the exact npm install -D command 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.glb with deps absent → prints missing-package message + install command, exit 1 (graceful path confirmed). Runtime execution (actual compression) pending npm 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 by extOf(url); Path B passes a blob: URL (no .glb suffix) → 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 passes file.name as nameHint. (modelLoader.ts + ThreeDViewer.ts, 2 lines effective.)
  • Verify: NEW tools/dnd-smoke.mjs drives the real CSR path (fetch sample → new Filewindow.__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: prefix MSYS_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) + CDP Emulation.setCPUThrottlingRate 6x, 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.tsextOf/ACCEPT_EXT/loadModel(url,renderer,onProgress) dispatch by ext, normalize each to Object3D. 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() + CloseModel in 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). NEW src/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.
  • 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.