An 18-part gaming PC you pull apart in 3D, and a game where you build it. X-ray, a heat overlay from TDP and airflow, a power-on boot, and a five-mode build game.
Live: pc.badcodes.dev
interactive-pc.mp4
The machine comes apart on a slider. Pick any part, in 3D or from the list, to read what it does. A second page, /build, turns the same PC into a build game with compatibility rules, a spec sheet and share links. The 18 parts are generated in code, so there is no model file to download.
At / the machine pulls apart in three stages: the panels, then the card, drives, power and fans, then the cooler, memory and CPU off the board.
| Exploded view | Drag the slider to take the PC apart. Pick a part in 3D or in the list to focus it and read its copy. |
| X-ray | Fades the chassis so the parts inside show. |
| Heat overlay | Tints each part from its TDP and the case airflow (src/data/thermal.ts). The numbers are invented, and the legend says so. |
| Power on | Spins the fans up in order, fades the RGB up and prints a boot screen over the stage. |
/build is the build game: pick each part from the tray and fit it in the right order, from the power supply to the first boot. Everything works from the keyboard with no canvas; the 3D scene is a lazy enhancement.
- Five modes (
src/data/modes.ts): Guided (step by step, the next slot glows), Free (a sandbox with no clock), Brief (a client's job and budget, shop for the parts, then build), Won't boot (a finished PC with up to three faults to find and fix before you press power) and Speedrun (fixed parts and a clock). - Four tiers, Easy, Normal, Hard and Expert, each mode offering some of them. Tiers change the slot highlights, whether a rejection explains itself, how mistakes are punished (time, stars or both) and whether one rule stays hidden until power on.
- Eleven compatibility rules (
src/data/rules.ts): board and case, CPU socket, BIOS support, memory, cooler fit and heat, tall memory under a tower cooler, graphics card size, PSU headroom, access order and case fan orientation. - A spec sheet lists every pick with its status and an estimate of what it will run, by game genre and resolution (also invented). Parts can be swapped from it in Free and Brief; the 3D model always stays the shipped build.
- Share links: the picks are one short
?b=parameter, read on the server, so a shared build opens in Free with its sheet already filled in. A bad value falls back to the shipped build. - Best times: stars first, then time, kept per mode and tier in the browser's localStorage. Nothing is sent anywhere.
The site is built on a shared exploded-view engine, src/engine/explode/: a data-driven Next.js 16 setup where a feature is three files plus a route.
| File | Holds |
|---|---|
public/models/<feature>.<hash>.glb |
Geometry: Meshopt-compressed, one named node per part, a content hash in the name. The PC has none; its sidecar names procedural:pc and src/models/pc/ builds it in code. |
src/data/<feature>.sidecar.json |
Schema 1: parts keyed by node name, labels, groups and stages, optional explode vectors and camera views, copy keys. |
src/data/<feature>.copy.ts |
A typed copy book: one entry per sidecar copy key. |
The scene is React Three Fiber on three.js. The route page is a Server Component, so the headline, text and part list render into the HTML and the page works with JavaScript off. Only the canvas is lazy, loaded through next/dynamic with ssr: false and gated on WebGL 2.
src/app/ Routes: / (explode), /build (game), stage, overlays, boot screen
src/engine/explode/ The reusable engine: sidecar schema and loader, explode plan, camera, stage, AR export
src/models/pc/ The 18 procedural parts (chassis, board, cooler, gpu, psu, cables, fans)
src/data/ Sidecars, copy books, catalogue, compatibility rules, modes, scoring, thermal model
src/game/ The build game: store, state machine, faults, layout, 3D scene, audio
src/lib/ Share-link codec, stage effects store, heat ramp, site metadata
scripts/ Fixture builder, sidecar check, Chromium stage check
Each part moves by its authored explode vector if it has one; otherwise radially from the assembly centre by R * (0.35 + 0.65 * min(d / R, 1)); a part at the centre moves along its thinnest axis. Groups carry a stage: stages split the slider evenly, so structure can come away before the mechanism, and a group's own explode vector adds to each of its parts. assembly.axis switches to a single-axis blueprint layout. src/engine/explode/plan.test.ts holds worked examples.
- Import the stage only through
next/dynamicwithssr: false, from a Client Component (stage-client.tsx). The@/engine/explodebarrel deliberately omitsstage,planandcamera: importing any of them statically puts three into the route's initial JS.test:stagefails ifWebGLRendererappears in an initial chunk. - The sibling
import("three")instage-client.tsxis load-bearing. It gives three its own chunk, fetched in parallel. Without it the bundler folds the engine into three's chunk and the stage budget (120 KB gzipped, three excluded) cannot be measured. <Canvas flat>, never<Canvas shadows>. Withoutflat, R3F's ACES tone mapping recolours every material;shadowsselectsPCFSoftShadowMap, which three 0.186 replaces with a warning. Shadows are switched on inonCreatedwithPCFShadowMap.- WebGL 2 only. The probe in
gate.tsxaccepts only awebgl2context, so browsers with WebGL 1 never download three. - Per-frame values never go through React. The store publishes
k(the explode amount) only when a tween settles; the canvas reads the live value withstore.frameK(). - Camera focus uses
fitToSphere, notfitToBox. camera-controls'fitToBoxsnaps to the nearest axis-aligned angle, which shows flat parts edge-on. - An authored
viewis for the exploded pose. Write itspositionandtargetagainst where the part sits at k = 1. - Sidecar keys are node names after three's sanitiser. three turns whitespace into
_and drops[ ] . : /; the engine further requires[A-Za-z0-9_-]. A multi-primitive mesh's children take<mesh>_1-style names from the same pool, so a node namedgear_1can collide.npm run check:sidecarloads each GLB through three's real GLTFLoader to catch this. - Never run
gltf-transform optimizewith defaults. Its flatten, join, instance and palette steps merge named parts into one node. Pass--flatten false --join false --instance false --palette false --simplify false --compress meshopt, then runnpm run check:sidecar. - Models are cached as immutable (
next.config.ts), so a changed GLB must be a new filename.npm run fixtureshows the pattern: hash the bytes, write<name>.<hash>.glb, update the sidecar. - Any route that sets metadata goes through
pageMetadata()insrc/lib/site.ts. A route that declares its ownopenGraphortwitterreplaces the layout's wholesale and loses its picture; one that sets onlytitleunfurls with the home page's title and URL.SITE_URLis the literal production domain, neverVERCEL_URLor localhost, because a relative or deployment URL makes Slack and Facebook drop the image.src/lib/site.test.tschecks every route. - On
/buildthe page owns the X-ray.build-game.tsxsets it in one effect: heat first (fully faded), then the mode (Won't boot opens the case), otherwise 0.StageOverlaysandThermalOverlaytakeownsXray;/buildpassesfalseso the overlay never writes it. - The stage-fx store (
src/lib/stage-fx.ts) carries two switches,heatandpower. It sits outside React.power-scene.tsxre-applies only whenpoweritself changed; reacting to every notification would restart the fan spin-up and the RGB fade each time the heat switch moves. - The share codec (
src/lib/share-build.ts).?b=is two characters per slot: a slot letter and the base 36 index into that kind's list. Decoding is an allow-list (lowercase letters and digits, at most 64 characters, known slot letters, in-range indexes) and the result is built from catalogue ids, never from the input. The order of every kind insrc/data/catalogue.tsis the code and is pinned byshare-build.test.ts: append new parts, never insert or reorder. - Procedural parts carry their animation in
userData. Thergbandspinvalues on a part's node are whatpower-scene.tsxanimates. - Embedding.
next.config.tssendsframe-ancestors 'self'plushttps://badcodes.devandhttps://www.badcodes.dev, so only those origins can frame the pages. Change it if you host the site elsewhere. - Console output.
test:stagefails on any console error and on anyTHREE.warning except theTHREE.Clockdeprecation that R3F 9.8 triggers. It launches Chromium with SwiftShader, which prints no shader notes; a real Windows GPU prints harmless ANGLE D3D11 notes (X4122, X3595) throughTHREE.WebGLProgram.
Node 22 or 24.
git clone https://github.com/BadCodesGG/interactive-pc.git
cd interactive-pc
npm install
npm run dev # http://localhost:3000Checks:
npm run lint # ESLint (Next core-web-vitals + TypeScript)
npm run typecheck # tsc --noEmit
npm test # Vitest: engine maths, sidecar schema, store, loader, game rules
npm run build # production build
npm run check:sidecar # every src/data/*.sidecar.json against its model and copy book
npm run fixture # rebuilds public/models/fixture.<hash>.glb and points its sidecar at ittest:stage needs a build first and Playwright's Chromium:
npx playwright install chromium
npm run build
npm run test:stageIt boots next start and drives both routes in Chromium: the explode checks on /, and on /build the server-rendered tray, a part placed from the keyboard, a wrong order refused with its reason, and a full Guided build ending on the boot screen. Both routes also check for zero console errors and hold the payload budgets. Screenshots go to .stage-shots/ (override with STAGE_SHOTS_DIR); set STAGE_URL to test a server that is already running. Add every new feature route to ROUTES in scripts/check-stage.mjs.
The test fixture (public/models/fixture.<hash>.glb and src/data/fixture.sidecar.json) is a small nine-part model that the loader tests and the /models/ cache-header check use.
The code is MIT, see LICENSE.
The fan geometry is adapted from pc-anatomy by Yoosseph, Copyright (c) 2026 Yoseph, MIT License. The licence text is kept in src/models/pc/LICENSE-pc-anatomy.txt, and the same credit appears in every page's footer (src/data/credits.ts).
The parts catalogue and its specs are invented; no real brand or product is implied.
Files that are not covered by the MIT licence, and the terms for the BadCodes name and logo, are listed in NOTICE.
Built by BadCodes.