Interactive 3D platform for learning mechanical systems. Explore eight machines — V8, Inline-4, Diesel, Turbine, Wankel, Air Compressor, Differential and Planetary Gearbox — with orbit/zoom/pan, physically believable animations, exploded views, part selection and wireframe mode.
Built with React + TypeScript + Vite, Three.js / React Three Fiber / Drei, GSAP, Framer Motion, Tailwind CSS v4 and Zustand.
npm install
npm run dev # http://localhost:5173
npm run build # production build → dist/
npm run preview # serve the production build locally
npm run lint # ESLint
npm run smoke # headless test: builds every machine, runs every animationNode 18+ recommended.
The project is a static Vite SPA — zero configuration needed.
- Push the repository to GitHub/GitLab.
- In Vercel: New Project → Import. Vercel auto-detects Vite.
- Build command:
npm run build - Output directory:
dist
- Build command:
- Deploy. No environment variables are required.
CLI alternative: npm i -g vercel && vercel --prod.
Any static host works (Netlify, Cloudflare Pages, GitHub Pages): upload dist/.
src/
├── components/ UI: TopBar, Sidebar, ControlBar, ViewerOverlay,
│ InfoPanel, BrowseModal, icons
├── layouts/ AppLayout — composes the page exactly per the design
├── three/ Viewer (R3F canvas, lights, env, camera rig),
│ MachineRig (build/animate/explode/picking),
│ thumbnails (offscreen sidebar renders)
├── modules/
│ └── machines/ types (MachineDef / MachineBuild contract),
│ registry (data: names, stats, animations, cameras),
│ materials, engineCore (shared crank/piston kinematics),
│ builders/ (v8, inline, turbineWankel, drivetrain)
├── store/ Zustand store — single source of truth for UI state
├── hooks/ useFullscreen
├── utils/ geometry helpers (gears, bolts, pipes), viewerBus
├── animations/ pages/ shaders/ assets/ reserved per spec
Each machine is one entry in modules/machines/registry.ts plus one builder
returning the MachineBuild contract:
interface MachineBuild {
root: THREE.Group // added to the scene
parts: MachinePart[] // name + group + baked explode vector
update(t: number, mode: string): void // drive kinematics
dispose(): void
}Sidebar, thumbnails, viewer, explode, picking and the control bar all read from
the registry — nothing else changes. A GLB-backed machine can implement the same
contract via GLTFLoader (drei useGLTF), mapping named nodes to parts.
- The store holds
playing / speed / animationId;MachineRigaccumulates speed-scaled time each frame and callsbuild.update(t, mode). - Kinematics are physically derived, not keyframed:
- slider-crank piston position from crank angle, rod length, crank radius
- cross-plane V8 phasing + firing-order combustion flashes
- camshafts at half crank speed (four-stroke timing)
- Wankel rotor: eccentric orbit at shaft speed, rotor spin at 1/3 shaft speed inside a true epitrochoid housing
- differential: ring/pinion ratio, spider gears walking during "cornering" (±35 % axle speed split)
- planetary: exact epicyclic ratios (ring-fixed ωc = ωs·Zs/(Zs+Zr), etc.)
- GSAP drives camera transitions; explode is lerped per-frame from baked per-part vectors.
A single Zustand store (store/useAppStore.ts) holds machine selection,
animation, playback, explode/wireframe/auto-rotate flags, part hover/selection,
modal state and generated thumbnails. Components subscribe with narrow
selectors to avoid unnecessary re-renders; the render loop reads via
useAppStore.getState() so animation never re-renders React.
Models are procedural (no network 3D assets), generated synchronously in
< 50 ms each, so there is nothing to fetch or compress. Sidebar thumbnails are
rendered once at startup by an offscreen WebGL canvas (three/thumbnails.ts)
and stored as data URLs; skeleton shimmer placeholders prevent layout shift.
Fonts are self-hosted via @fontsource/jetbrains-mono. The architecture is
GLB-ready (see contract above) — DRACO/KTX2 compression would slot into a
loader-based builder without touching the viewer.
Bundle (gzip): app ≈ 140 kB, three vendor chunk ≈ 244 kB (split via
manualChunks so the app shell parses first), CSS ≈ 32 kB, fonts loaded
per-weight on demand.
Runtime:
- One
WebGLRenderer,dprcapped at 2, ACES tone mapping, PMREMRoomEnvironmentgenerated in-memory (no HDR download). - Single 2048² shadow map +
ContactShadowsat 512² — soft shadows without per-light cost. - Frustum culling is on by default for every mesh; geometry is low-poly primitives (whole V8 ≈ 60 k triangles).
- Explode/hover work by mutating existing transforms/materials — zero allocations in the frame loop; damping handled by OrbitControls.
- Machine switch disposes all geometries/materials of the previous build
(
disposeTree) — no GPU memory growth when browsing the library. - Thumbnails render once on a throwaway 192×144 context which is disposed immediately afterwards.
prefers-reduced-motioncollapses UI transitions; the canvas honours pause state.
Measured headless (npm run smoke): all 8 machines build, animate through
every program and dispose cleanly.
Keyboard reachable controls with visible focus rings, ARIA labels/roles on
nav, listbox, dialog and toggles (aria-pressed, aria-current,
aria-expanded), Escape closes menus/modals, reduced-motion support, and
high-contrast monochrome palette per the design.