# Puzzle cube

The twisty cube as a flat SVG machine: orthographic projection, painter's algorithm and flat shading over the same permutation solver as the WebGL rig.

> For the complete index, see [llms.txt](https://robocn.dev/llms.txt). A Markdown version of any page is available by appending `.md` to its URL or by sending an `Accept: text/markdown` header.

## Install

```bash
bunx --bun shadcn@latest add https://robocn.dev/r/puzzle-cube.json
```

Registry item: `puzzle-cube` · [`https://robocn.dev/r/puzzle-cube.json`](https://robocn.dev/r/puzzle-cube.json)

## Notes

- One solver, two renderers: the state, every turn, the scramble, the drag geometry, the sticker colours and the solve all come from `cube-geometry`, exactly as they do in `rubiks-cube`. This component owns only the projection and the paint, so nothing is solved twice. Solved, not illustrated: everything the solver owns. Illustrated: the flat shading — one key light, no rays — the eased travel of a turn, the gap between stickers and the underglow when it comes home.
- The cube is modelled once in world units and pushed through `robotCamera`, so all four views are one projection, never four drawings. Faces pointing away from the camera are culled by their projected normal; what is left is painter-sorted by cubie centre, which is exact for convex boxes that never interpenetrate — including the middle of a turn, when the travelling slice is carried by the same partial rotation as a rigid body.
- The camera goes anywhere at all, not only to the four angles the set names: press the plastic and sweep, and the camera runs round the cube through `robotCameraAt` — over the top, under the bottom, wrapping all the way round — with the same projection, depth sort and drag geometry at every angle. A press on a sticker is the layer's; a press anywhere else is the camera's. The ground shadow steps out when the camera drops to its plane rather than degenerating into a line.
- `solve()` is a real solve: `cube-geometry` searches out a layer-by-layer line, replays it to check it, and the cube plays it one turn at a time so you can watch it. `hint()` is the first move of that line. On anything but a 3×3 there is no method, so both return `null` and nothing is queued — the cube says so rather than turning at random.
- Drag follows the hand, and the picking is the drawing: the sticker polygon you press is its own hit target. The camera is linear, so the two in-plane axes of the grabbed face project to a 2×2 basis and the screen drag solves back through it into an exact world direction — the same `grabFromDrag` the WebGL rig feeds. The press decides the layer on the first few pixels and then holds it; the release snaps to the quarter turn it is nearest, so letting go half way back snaps back rather than through.
- A move carries where it came from, so a game can time a person without timing the idle loop or the solver. That is what the demo's stopwatch runs on.
- Six face colours are the one place the four palette roles are not enough, so each face resolves prop → `--robot-cube-u` … `--robot-cube-l` → the standard white, yellow, green, blue, red, orange — the same names the WebGL rig reads. The plastic body and the solved glow still come from the theme.
- No canvas, no WebGL, no `three`: it is SVG all the way down, installs as source, and works anywhere a `<svg>` does — which is the whole reason it exists next to `rubiks-cube`. The `data-cube-*` hooks (`order`, `solved`, `turning`, `dragging`, `moves`) are set on the `<svg>` itself.
- Reduced motion lands each turn immediately rather than animating it, parks the behaviour loop and drops the solved swell — drag, keys and the solver all still work.

## Usage

```tsx
import { PuzzleCube } from "@/components/ui/puzzle-cube"

// Runs its own cycle.
<PuzzleCube behavior="cycle" />

// Or drive it, which stops the loop.
<PuzzleCube algorithm="R U R' U'" interactive />
```

## Props

| name | type | default | description |
| --- | --- | --- | --- |
| `order` | `number` | `3` | Cubies to a side, 2 … 7. 3 is the cube everyone means. |
| `algorithm` | `string | CubeMove[]` | — | Controlled: the cube is exactly this algorithm applied to a solved one, and the behaviour loop stops. Appending a move animates it; anything else rebuilds the state. |
| `behavior` | `"cycle" | "scramble" | "solve" | "static"` | `"cycle"` | What it does with nobody driving it. `solve` is the live one — it scrambles itself, solves itself with the real method, and starts again; `cycle` repeats R U R' U', which comes home every six repeats; `scramble` walks a seeded shuffle. |
| `interactive` | `boolean` | `false` | Press a sticker and the layer turns with the pointer, snapping to the nearest quarter turn when you let go; press the plastic or the background and the whole cube turns under the camera, any direction at all. Focus the cube and type at it: U D L R F B, shift for anticlockwise, arrow keys turn the cube, S to scramble, H for a hint, enter to solve it, backspace to undo, escape resets. |
| `azimuth` | `number` | `0` | Degrees the camera swings round the cube, on top of `view`. Any angle at all, and it wraps: the far side is 180 either way. Supplying it (or `elevation`) takes the camera away from the pointer. |
| `elevation` | `number` | `0` | Degrees the camera rises above the view's own elevation — all the way to straight overhead or straight underneath, where it stops: both poles are reachable, and none further. |
| `onOrbitChange` | `(orbit: PuzzleCubeOrbit) => void` | — | Where the camera has been turned to, as it is turned, so orbiting works in controlled mode too. |
| `controls` | `(api: PuzzleCubeApi) => void` | — | Handed a driver: `turn`, `scramble`, `solve`, `hint`, `undo`, `redo`, `reset`, `state`, `history`, `solved`. |
| `faces` | `Partial<Record<CubeFace, string>>` | — | Per-face colour overrides. Each face otherwise resolves prop → `--robot-cube-<face>` → the standard scheme — the same variables the WebGL rig reads, so one variable retints both renderers. |
| `scrambleOnMount` | `boolean | number` | `false` | Start shuffled rather than solved; a number says how many turns. |
| `seed` | `number` | `1` | Fixes the scramble, so two cubes on a page can be told to agree. |
| `onMove` | `(move: CubeMove, state: CubeState, source: PuzzleCubeSource) => void` | — | Every turn, once it has landed, and where it came from: `user`, `solver`, `scramble`, `loop`, `undo`, `redo`. A stopwatch that times a person reads that third argument. |
| `onSolved` | `() => void` | — | The moment it comes home. |
| `onSolvedChange` | `(solved: boolean) => void` | — | Both edges of solved — the one a stopwatch starts and stops on. |
| `onHistoryChange` | `(moves: CubeMove[]) => void` | — | The turns made so far, as they are made and unmade. |
| `speed` | `number` | `0.9` | Turns per second for the loop, and how fast a turn travels. |
| `showGround` | `boolean` | `true` | The contact shadow under the cube. |
| `label` | `string` | — | Optional technical caption under the drawing. |
| `view` | `"plan" | "front" | "profile" | "iso"` | `"iso"` | Where the camera stands. One the cube, showing three faces, four projections: straight down, straight on, side elevation, or three-quarter from above. |
| `animate` | `boolean` | `true` | Off parks the machine at phase and stops rendering. A reduced-motion preference does the same. |
| `paused` | `boolean` | `false` | Freeze where it stands. |
| `phase` | `number` | `0` | Seconds of offset, so a row of machines breaks step. |
| `variant` | `"solid" | "outline" | "blueprint" | "wire"` | `"solid"` | How the machine is painted. Geometry never changes between variants. |
| `size` | `"xs" | "sm" | "md" | "lg" | "xl" | number` | `"md"` | Rendered width in pixels, or a step on the scale. |
| `color` | `string` | `var(--robot-shell)` | Body panels — the colour the machine reads as. |
| `accent` | `string` | `var(--robot-accent)` | Status colour: tip light, live tool, readouts. |
| `metal` | `string` | `var(--robot-metal)` | Bare machined parts: collars, bolts, tool bodies. |
| `dark` | `string` | `var(--robot-dark)` | Cast joints, base, shadow side. |
| `palette` | `Partial<RobotPalette>` | — | Override any subset of roles at once, including glow and grid. |

## Source

- `src/components/ui/puzzle-cube.tsx`
