Files
dwg-dxf-viewer-sample/apps/viewer-3d/.claude/skills/threejs-viewer/SKILL.md
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

3.7 KiB

name, description
name description
threejs-viewer Domain knowledge for building the hmwebviewer Three.js 3D viewer. Load when working on src/viewer, src/dnd, src/ui, the asset pipeline, or hydration. Covers the locked technical decisions, loader setup, Draco/KTX2, Blob URL lifecycle, and SSR hydration patterns specific to this project.

hmwebviewer — Three.js 3D viewer domain guide

Source spec: 3d_viewer_architecture_spec.pdf. Always also read CLAUDE.md + PLAN.md.

Locked stack

  • Three.js + Vite + TypeScript.
  • Loaders: GLTFLoader + DRACOLoader + KTX2Loader.
  • Compression: Draco (geometry) + KTX2/Basis Universal (textures).

Critical patterns (do not deviate without approval)

Single shared loader instances

Multiple simultaneous DRACOLoader instances crash (three.js #22445). KTX2 same risk. Build a singleton:

// src/viewer/loaders.ts
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';

let _gltf: GLTFLoader | null = null;
export function getLoaders(renderer: THREE.WebGLRenderer) {
  if (!_gltf) {
    const draco = new DRACOLoader().setDecoderPath('/draco/');
    const ktx2  = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
    _gltf = new GLTFLoader().setDRACOLoader(draco).setKTX2Loader(ktx2);
  }
  return _gltf;
}

Pin decoder WASM version to the installed three.js version. Mismatch → silent decode failures.

Local file load (Blob URL lifecycle)

const url = URL.createObjectURL(file);
loader.load(url, (gltf) => {
  scene.add(gltf.scene);
  URL.revokeObjectURL(url);          // success → revoke
}, undefined, (err) => {
  URL.revokeObjectURL(url);          // error → also revoke
  throw err;
});

For repeat/cached loads: FileReaderArrayBufferGLTFLoader.parse() enables IndexedDB caching and avoids URL overhead.

Server asset (SSR + CSR + hydration)

  1. SSR ships HTML/CSS skeleton + pre-rendered 360° animated WebP placeholder.
  2. CSR background: GLTFLoader.load(serverUrl, onLoad, onProgress).
  3. Hydration: gate the fade on BOTH webglReady AND modelLoaded (Promise.all). Fade CSS opacity 0.5s easedisplay:none after 500ms. Race → empty canvas flash.

Drag & drop

dropzone.addEventListener('dragover', (e) => { e.preventDefault(); });
dropzone.addEventListener('drop', (e) => {
  e.preventDefault();
  const file = e.dataTransfer?.files?.[0];
  if (!file) return;
  if (!/\.gl[bt]f$/i.test(file.name)) return showError('GLB/GLTF only');
  loadLocalFile(file);   // viewer-core, handles createObjectURL/revoke
});

Also wire click→<input type=file"> for accessibility.

Disposal

On teardown / model swap: geometry.dispose(), material.dispose() (and material maps), texture.dispose(). Revoke any lingering object URLs.

Offline asset pipeline

  • Compress: gltf-transform CLI or programmatic functions — Draco + KTX2 in one pass.
  • KTX2 encoder binary toktx must be on PATH (gltf-transform fetches via @ktx2/basis-transcoder/platform binaries — confirm available).
  • Pre-render 360°: Blender headless blender -b scene.blend -o //frame_### -f 1..N -F PNG → ffmpeg → animated WebP. Fallback: Puppeteer + headless three.js screenshot per rotation.

Common pitfalls

  • setDecoderPath wrong → worker fetch 404 in console. Verify /draco/ resolves under public/.
  • Forgetting e.preventDefault() on dragover → browser opens the file.
  • Decoder version mismatch → model fails to decode with no obvious error.
  • Creating loaders per load → intermittent crashes + memory growth.

References in user memory reference-threejs-resources.md.