---
name: scrapyard-rod
description: >
  Build a single-file Three.js scrapyard car from primitives: an extruded
  side-view body, typed sockets, parts you can pull off, and a drive gate
  that stays shut until the checklist is green. Use when adding or changing
  a procedural vehicle, attachable parts, or a The Long Drive style assembly
  loop in WebGPU TSL. The reference file is car.html beside this skill. If
  they disagree, the HTML wins.
---

# Scrapyard rod

One HTML file. No bundler, no npm, no glTF, no texture pack. Three.js 0.186.1 from a CDN via an import map (`three.webgpu.js`, `three.tsl.js`). `WebGPURenderer` only. Set `renderer._getFallback = null`. If `navigator.gpu` is missing, or the backend is not WebGPU after `init()`, stop. No WebGL fallback.

The body and the ground use `MeshStandardNodeMaterial`. Loose parts use a fresh `MeshStandardMaterial` per mesh, because hover writes `material.emissive`. A shared material lights every mesh that uses it.

Open it through a server. `file://` will not load the ES modules.

## Body

`makeBody()` draws a `Shape` in the side view. X is length, Y is height, rear at negative X, nose at positive X. Extrude depth 1.5, centred with `translate(0, 0, -0.75)`, bevel on. Wheel arches are `absarc` of radius 0.56. The engine tray and the cabin are gaps in that outline. Leave them open. A closed bay hides the missing parts.

The mesh sits at `y = 0.42`, so the rocker line is at wheel-centre height. Wheel radius is 0.42.

`paintMaterial(baseHex, rustAmount)` bakes the rust amount on the CPU (`float(0.92 - rustAmount * 0.45)`). The body passes 0.55.

- Broad noise at `positionLocal * 3.2`, detail at `positionLocal * 16`.
- `lowPanels` is `smoothstep(1.1, -0.3, positionLocal.y) * 0.30`, rustier toward the rocker.
- The mask drives `colorNode` (base to rust `0x6e3b20`), `roughnessNode` (0.38 to 0.95) and `metalnessNode` (0.55 to 0.08).

Ground is a disc, radius 140. `colorNode` mixes two sand colours from `mx_noise_float` on `positionWorld` at 0.12 and 1.4.

## Parts and sockets

Part types: `engine`, `battery`, `wheel`, `seat`, `exhaust`.

A socket is an `Object3D` on the chassis. `userData.type` is the type. `userData.part` is the installed part, or null.

Makers build in socket-local space, origin at the mount. `makeWheel` keeps a `userData.spin` group. `makeEngine` keeps `userData.pulley`. Do not flatten those into the root group.

`rest` is `{ y, rot }` for the loose pose. Wheels use `rot: [π/2, 0, 0]` so they lie down in the yard. Exhaust uses a roll. That is intentional.

States: `loose`, `carried`, `installed`.

`install(part, sock)` sets `sock.userData.part`, parents `part.group` to the socket, and zeroes local position and rotation.

`detachTo(part, pos, yaw)` clears the socket, parents the group to the scene, sets Y from `rest.y`, and adds `yaw` to `rest.rot[1]`.

Click an installed part and it is popped about 2.6 units out from the chassis, on the ground.

## Carrying

The ghost is `group.clone(true)` with one shared `MeshBasicMaterial` (green, transparent, `depthWrite: false`). It starts hidden.

While carrying, parent the ghost to the nearest free socket of the same type. Nearest means smallest `ray.distanceToPoint` from the pointer ray to the socket's world position. Opacity pulses.

Click installs when the ray hits the ghost, or passes within 0.55 of the socket world position. A ground click inside radius 60 drops the part. `freeSock` prefers the ghost socket, then any free socket of that type.

Carried parts float about 2.4 units along the camera direction and spin slowly. They are excluded from the hover raycast.

## Drive gate

```javascript
function operational() {
  return installedOfType('engine') === 1
      && installedOfType('battery') === 1
      && installedOfType('wheel') === 4
      && installedOfType('seat') >= 1;
}
```

Exhaust is optional. Do not add it to `operational()`. Headlamp `emissiveIntensity` is 2.2 when the gate is open and 0 when it is shut. When the gate is shut, `drive.speed` is forced to 0.

WASD. Acceleration 5 forward, 4 reverse, drag `speed * 1.1 * dt`, clamp -3.5 to 6.5. Steer only `frontSockets` (the two wheels at +X). Heading integrates `steer * speed`. `chassis.translateX` moves along the car's X, which is the nose. Clamp the chassis to radius 55.

Installed wheels spin `userData.spin` by `speed * dt / 0.42`. The pulley spins only while the engine is installed.

The camera target lerps toward the chassis. The sun follows the chassis so the shadow stays on the car.

## Start

Installed: three wheels (sockets 2, 3, 4) and one seat (socket 7). Loose in the yard: engine, battery, the fourth wheel, the second seat, side pipes.

Call `install` and `detachTo` only after `refreshStatus` exists. They call it.

## If you add a part

1. A maker that builds in socket space, and a `rest` pose for the yard.
2. A socket with that type, placed on the chassis.
3. A row in the checklist.
4. A decision: required in `operational()`, or optional like the side pipes. Most parts are optional. Requiring every part removes the point of the checklist.

Keep the engine bay and the cabin as holes in the profile. Axle stubs stay on the chassis so a missing wheel still shows a mount.
