# Runek docs (full)
Every page of https://runek.nullorder.org/docs as one Markdown file: guides first, then every component. The index is https://runek.nullorder.org/llms.txt; the changelog is https://runek.nullorder.org/docs/changelog.md.
# What is Runek?
Source: https://runek.nullorder.org/docs/what-is-runek
> A source registry of procedural 3D components for React Three Fiber — shadcn for 3D worlds.
Runek is a **source registry** of procedural 3D components for [React Three Fiber](https://r3f.docs.pmnd.rs/). You pull a component's source into your project with one command and own it — no black box, no version lock.
Every component — a bookshelf, a lake, a whole house — generates its own geometry from **props and a `seed`**. There are no `.glb` files, no textures, no CDN. A whole world is just data: diffable, forkable, and version-controlled like any repo.
## The five principles (the moat)
1. **Procedural-first** — geometry from props + `seed`; no binary assets.
2. **A world is data** — every component is a pure, deterministic function of its props.
3. **Seeded determinism** — same seed → same result, everywhere.
4. **Parametric LOD** — detail scales with props and distance.
5. **Local-first** — no backend; a world deploys as a static site.
This very documentation is a Runek world: a walkable library built from the components it documents. Read the [getting started guide](https://runek.nullorder.org/docs/getting-started.md) to compose your first scene.
---
# Getting started
Source: https://runek.nullorder.org/docs/getting-started
> Pull components into your project with the CLI and compose your first walkable world.
Runek ships as a source registry. The `runek` CLI copies editable component source into your project and installs what it needs.
## Install components
```bash
npx @runek/cli init # writes runek.config.json + the install dir
npx @runek/cli add player terrain bookshelf # pulls source + installs deps
npx @runek/cli list # browse the catalog
```
`add` resolves dependencies for you: it installs `@runek/core` (the `` provider, seeded `rng`, and contract types) from npm, and `house` pulls the walls, floor, roof, door, and window it composes from as source.
Working with a coding agent? Point it at [Runek for agents](https://runek.nullorder.org/docs/for-agents.md) (or [llms.txt](https://runek.nullorder.org/llms.txt)).
## Compose a world
The CLI copies components into `src/runek/` by default; they import the runtime from `@runek/core`, which it installs for you:
```tsx
import { World } from '@runek/core'
import { Bookshelf } from './runek/Bookshelf'
import { Player } from './runek/Player'
import { Terrain } from './runek/Terrain'
export function FirstWorld() {
return (
)
}
```
`` sets up the canvas, lighting, physics, and keyboard controls. `` is a first-person controller by default — drop in and walk: WASD moves, the arrow keys or a mouse-drag steer the camera. The world's [`avatar` and `controls` settings](https://runek.nullorder.org/docs/the-world-provider.md#controls) change the view and the bindings.
## Same seed, same world
Geometry is a pure function of props and `seed`. The same `seed` produces the same bookshelf — the same books, the same arrangement — on every machine, every render. Change the seed to roll a new variation; commit the number to lock it forever.
---
# The workshop
Source: https://runek.nullorder.org/docs/the-workshop
> Build a world in your browser, tune every prop, edit it as JSON or live TSX, put one component in the lab, and share it all as a link.
The [workshop](https://runek.nullorder.org/workshop) is Runek's in-browser workbench: the room next to the library, where the world you build renders live and the tools float over it. Nothing is installed and nothing leaves your browser. The whole session is a `WorldData` value, so a share link is just that value, compressed into the URL.
You can also walk there: in the [library](https://runek.nullorder.org/library), the door on the right wall with the green ring leads to it.
## Three modes
- **Build** composes a world. The **Outliner** lists the nodes (drag to reorder or nest them under a `Group`), and its **+ Add**, **Templates** and **Snippets** tabs drop things at the point the camera looks at. Click anything in the room to select it, then move or rotate it with the gizmo (`g`, `r`), or edit it in the **Inspector**.
- **Lab** puts one component on a pedestal with instruments: bounds in meters, a 1.8 m figure for scale, wireframe, normals, colliders, triangle and draw-call counts, and how long it took to build. The **seed grid** shows 9, 16 or 25 seeds side by side; **A / B** compares two prop sets; **check determinism** builds the component twice and compares geometry hashes.
- **Code** opens the code panel: `world.json`, a live `App.tsx`, and the install commands for your world.
## Every prop, generated
The inspector is not hand-written. At build time the docs site reads each component's TypeScript (its props interface, the defaults in its signature, the JSDoc) and turns it into a schema. Numbers get sliders, colors get swatches that show the palette slot they fall back to, string unions become segmented choices or dropdowns, `[x, y, z]` gets three scrubbable fields, and nested specs (a `Person`'s `clothes`, a `Wall`'s `openings`) get list editors. A dot marks props you changed, and ⟲ resets one to its default (it disappears from the file). Drag a number's label sideways to scrub it.
Callbacks and React children can't live in a world file; the inspector lists them under **Code-only props**, to use from `App.tsx`. **Export → Prop schema** downloads the whole schema as JSON.
## world.json and App.tsx
The `world.json` tab is the world itself, two ways: edit the room and the file updates; edit the file and the room follows (after a short pause). Completion knows component names, props, enum values and palette slots; inside a `nodes` array, type `node:` for a node or `snippet:` for a group. If the JSON is broken, the room keeps the last good world and the error is marked in the gutter. **minimal** hides props equal to their defaults.
`App.tsx` is a real React file that runs live. It starts generated from your world, either rendering the JSON with `WorldRenderer` (the *data* style) or spelled out component by component (the *JSX* style). `import world from './world.json'` always gives the current world, so the inspector keeps working while your code runs. Export a `registry` with your own components and the outliner accepts them as node types. The **new ▾** menu has examples: a custom seeded component, a `useFrame` animation, interactions and click handlers, and a world generated in a loop.
The code runs in a sandboxed iframe with its own origin: it can't read this site's storage, cookies or page, and it can import only React, three.js, React Three Fiber, drei, Rapier, `@runek/core` and `@runek/components`. A loop that never yields is stopped. Links that carry code ask before running it.
## Stages
The room around your world is a stage, not part of it, and it is never exported:
- **Room**: the Workshop hall, with a 20 × 20 m build plot.
- **Plot**: an open plot under the sky, for terrain and islands.
- **None**: your world alone, exactly as your app would render it.
## Walk it
**▶ Play** (or `p`) hides the tools and drops a first-person `Player` where the camera was looking (or uses your world's own `Player`). `Esc` brings you back. In the Room stage, walking out of the door leads back to the library.
## Take it home
- **Share → Copy link** puts the world, the stage and the camera in the URL. There is no server: anyone with the link gets the same world, rebuilt from the same seeds.
- **Export** downloads `world.json`, `App.tsx`, a PNG of the view (2×, optionally transparent), or the prop schema.
- The **Install** tab lists the exact `npx @runek/cli add …` command for the components your world uses.
- Drop a `.world.json` anywhere on the page to open it, or link to one: `/workshop?src=https://…/world.json` loads it from any static host that allows it.
Your work autosaves in the browser. **Templates** start you from a reading room, a village square, an island cove, an office, a campsite at dusk, or a line-up of people.
## Keyboard
| Keys | |
|---|---|
| `⌘K` | Command palette: add anything, load templates, jump to a node |
| `⌘Z` · `⇧⌘Z` | Undo · redo |
| `g` · `r` | Move · rotate |
| `f` · `Home` | Frame the selection · frame everything |
| `d` · `⌫` | Duplicate · delete |
| `⌘C` · `⌘V` | Copy · paste nodes as JSON (paste a whole world too) |
| `p` · `Esc` | Play · back |
| `Tab` | Hide or show all panels |
| `1` `2` `3` | Build, Lab, Code |
| `⌘↵` | Run App.tsx |
| `?` | All shortcuts |
---
# The World provider
Source: https://runek.nullorder.org/docs/the-world-provider
> sets up the canvas, lighting, physics, and controls — every component lives inside it.
`` is the root of every Runek scene. It wires up the React Three Fiber canvas, a default light rig, the Rapier physics world, and keyboard controls, then exposes scene-wide settings to its children through context.
```tsx
import { World } from '@runek/core'
import { Bookshelf } from './runek/Bookshelf'
import { Player } from './runek/Player'
```
## What it sets up
- A `
}>
)
}
```
The editor ships from its own entry, `@runek/core/editor`, and is the only part that imports `leva`. An app that never mounts `WorldEditor` doesn't need `leva` installed.
## Input that yields to the page
`World` reads the keyboard from the whole window, but never keys aimed at a text field, a select, or editable text, so typing "wasd" into a composer beside the canvas never walks the avatar. Pointer look listens on the canvas only, so panels drawn over it already take the pointer.
Switch the keyboard off entirely while a modal is open:
```tsx
…
```
Keys held when input switches off are released, so none stay stuck down.
## Pause when hidden
A world in a tab nobody is looking at should cost nothing. `paused` stops the frame loop and physics: no rendering, no steps, no input.
```tsx
…
```
Anything driven by the wall clock is simply where it should be on resume. A `Person` on a route was never stepping along it; its position is a function of the time, so pausing for an hour and resuming puts it exactly where an hour of walking would have.
## A view of the whole place
An app showing many figures at once usually wants to open on all of them. `view="overhead"` puts the camera high above the avatar at a fixed tilt; it follows as the avatar walks, scroll zooms, and WASD walks relative to the screen. Give the world a `view` control and the player can switch between overhead, third person, and first person:
```tsx
```
## Figures driven by app state
When people in the world stand for something live in your app (agents, players, orders), give each one a trip whenever its state moves it. A trip is a `route` with a start time:
```tsx
report(agent.id, trip.to)}
/>
```
Before `departAt` the figure waits at the first point; after the last it stays put, facing the way it came, in its `pose`. Because the position is a function of `(route, departAt, now)`, two windows showing the same world agree, and reopening the page puts everyone mid-stride where they should be without replaying anything.
`onArrive` fires once per trip, on the first frame the trip is over. That includes a trip that ended while the world was paused or not mounted at all, so the arrival is always reported; dedupe by `departAt` if your app remounts the world.
Pathfinding stays in your app: compute the waypoints (a small graph through your doorways is plenty) and hand them over. If a new trip starts before the last one ended, `tripAt(waypoints, departAt, Date.now())` gives where the figure had got to, in the same node-local frame, as the start of the next.
## Actions near a figure
Give a figure `actions` and a prompt appears over it when the avatar comes close, showing the key bound to each:
```tsx
(id === 'talk' ? openChat(agent) : openSheet(agent))}
/>
```
Only the nearest figure in range shows its prompt and takes the key, so two people standing together never both answer `T`. Keys come through the world's keyboard, so they yield to text fields, `input={false}`, and `paused` like everything else. The prompt reads its key names from the world's `controls`, so remapping `talk` relabels it.
Anything else can offer actions the same way by wrapping it in `Interactable` (with `use: ['KeyE']` in the world's `controls`):
```tsx
```
---
# World-to-world travel
Source: https://runek.nullorder.org/docs/world-to-world-travel
> Portal is a sensor gate that fires when the avatar (or a vehicle) walks through it — wire it to swap worlds, change levels, or teleport.
A Runek `` is a self-contained scene: its own canvas, its own physics world. To move between scenes — island to island, level to level, room to room — you need an in-world affordance that says "go here," and a host that listens. `Portal` is that affordance.
```tsx
import { Portal } from './runek/Portal'
// Data-only: walking through navigates the browser.
// Or drive an in-app transition yourself:
goTo('/foosha.world.json')} />
```
## The data and the event
`Portal` follows the contract's rule for interactive components ([§1](https://runek.nullorder.org/docs/the-component-contract.md)): the *data* of the interaction stays JSON-serializable, and the callback is optional, so a world still renders and round-trips from data without any code wired up.
- **`to`** — a URL or route, plain data. With no `onEnter`, entering the gate does `window.location.href = to` (the same fallback `Bookshelf` uses for a book's `href`). This is all a hand-authored, data-only world needs.
- **`onEnter(to)`** — an optional callback. When set, it runs *instead* of navigating, so your app can react however it likes: fade the screen, swap the mounted world JSON, push a route. The destination `to` is passed straight through, so one handler can serve many gates.
Because `onEnter` is optional and `to` is serializable, the gate is fully expressible in a `*.world.json` and still drives real navigation out of the box.
## The physics
The trigger is a **Rapier sensor** (a collider that reports overlaps without blocking movement), sized a little larger than the visible ring. Two details make it robust:
- **It fires for vehicles, not just walkers.** Rapier's default collision events only cover a *dynamic* body (the avatar capsule) against the fixed gate. `Portal` also enables `KINEMATIC_FIXED` active-collision types, so a **kinematic** vehicle — a scripted boat or cart moved with `setNextKinematicTranslation` — trips it too.
- **It ignores the scenery.** The handler skips any *fixed* body (`bodyType() === Fixed`), so the gate never fires against the terrain or props it sits among — only against something that moves through it. The sensor is also kept deep along the approach axis so a fast vehicle can't tunnel past it between frames.
The gate registers no solid collider — you can always walk through it; only the sensor matters.
## Reacting across the canvas boundary
There's a catch worth knowing. `` (and ``) mount their own React Three Fiber `