# Internet Paradise SDK

Build your Internet Paradise plot outside the site, with your own AI agent or your own scripts. Building
happens only on mainland plots: build the whole plot on your computer and deploy it with your
wallet, like a Decentraland scene, or build one cell and import it into your plot editor. From
ChatGPT or Claude on the web, connect the site itself: see [Use it from ChatGPT or Claude](#use-it-from-chatgpt-or-claude).

| File                  | What it is                                                                            |
| --------------------- | ------------------------------------------------------------------------------------- |
| `third-life-mcp.mjs`  | An MCP server. Your agent (Claude Code, Codex, Claude Desktop, Cursor, VS Code…) builds through it. |
| `third-life-pack.mjs` | Packs a scene into the ZIP the plot editor imports; makes your plot's folder (`init`) and deploys it (`deploy`). |
| `world.schema.json`   | The scene format as a JSON Schema, for scripts in any language.                       |
| `teztopia_optimize.py` | A Blender script that takes a heavy scene to a GLB the town accepts ([Optimizing your models](#optimizing-your-models)). |

You need **Node.js 20 or newer** and nothing else: each `.mjs` file is self-contained. There is
nothing to install and no account. The server reads the files you point it at and writes your
scene and your ZIP, on your computer. It goes online for one thing only: when a scene uses a
prefab that carries model files (such as tiled roofs), the export downloads those files from the
site once and checks them by SHA-256. `--offline` turns that off.

## First scene in 2 minutes

1. Put the server in a folder of its own:

   ```sh
   mkdir internet-paradise && cd internet-paradise
   curl -O https://internetparadise.xyz/sdk/third-life-mcp.mjs
   node --version     # v20 or newer
   ```

   On Windows PowerShell, write `curl.exe` instead of `curl`.

2. Give it to Claude Code, from that folder (macOS and Linux):

   ```sh
   claude mcp add internet-paradise -- node "$PWD/third-life-mcp.mjs" --scene "$PWD/my-world.json"
   ```

   On Windows PowerShell:

   ```powershell
   claude mcp add internet-paradise -- node "$PWD\third-life-mcp.mjs" --scene "$PWD\my-world.json"
   ```

3. Start `claude` in the same folder and ask:

   > Use the internet-paradise tools. Read describe_rules, then build a small walled garden with a
   > fountain, benches, cypresses and a pergola. Use prefabs where they fit. After each round
   > look at preview and describe_scene, fix what they show, then export_zip.

4. Open your plot in the town ([internetparadise.xyz](https://internetparadise.xyz), **My space**), go to
   **Share**, choose **Import** and pick `my-world.zip`: the cell lands in your plot. Walk it.
   Then keep prompting for details ("make the pergola posts stone", "turn the benches to face
   the fountain"), export again and import again. Building happens only on mainland plots;
   for the whole plot at once, use a plot folder (below).

## Your plot, like a Decentraland scene

If you own a plot on the mainland, it is a folder on your computer: build it there with any
agent, look at it, and deploy it with a signature from your wallet. A plot is 16, 32, 48 or 80 m
square, made of 16 m cells; the folder holds all of them.

1. **Get your plot's folder.** On the site, open your plot editor, go to **Share** and press
   **Download my plot template** (or open
   [the deploy page](https://internetparadise.xyz/deploy.html?template=mine) signed
   in). It starts from your draft or your published plot. Or make it here, empty or from a scene
   you have:

   ```sh
   node third-life-pack.mjs init --plot 5
   ```

   `--plot` takes your plot's number or its north-west cell (`8,-3`); `--from my-world.json` starts
   from a scene (a one-cell scene goes into the cell at the street). The folder holds `plot.json`
   (the cells, the street entrance, the neighbouring plots, buildings and streets, the limits),
   `scene.json` (the plot), a README, an example prompt, both SDK files and the agent setups.

2. **Connect your agent** in that folder. Claude Code reads the `.mcp.json` that `init` writes:
   run `claude` there (a downloaded folder's README has the `claude mcp add` line for it). Codex,
   from the folder:

   ```sh
   codex mcp add internet-paradise -- node "$PWD/third-life-mcp.mjs" --scene "$PWD/scene.json"
   ```

   Cursor and VS Code read the folder's `.cursor/mcp.json` and `.vscode/mcp.json`. With the server
   already set up for a one-cell scene, ask the agent to call `new_plot` with your plot's number.

3. **Build.** Positions are metres from the plot's north-west corner, across the whole plot.
   Walls, floors, slabs and steps may cross the 16 m cell borders: they are cut into one piece per
   cell, which looks and collides as one. Anything else (columns, vaults across their arch,
   frames, models, a prefab's roof) must sit inside one cell, and the tools say where it fits.
   Every cell keeps its own limits (500 objects, 16 models…).

   > Use the internet-paradise tools. Read describe_rules, then build a small gallery quarter on my
   > 48 x 48 m plot, its front on the south side where the street arrives. After each round look
   > at preview and describe_scene and fix what they show. Then validate.

4. **Look.** `preview` draws the plot from above and from the street, with its numbered cells and
   the entrance; `describe_scene` lists objects per cell and walks the plot from the street with
   the game's own movement. `validate` checks every cell the way the site will.

5. **Look, then decide.** `node third-life-pack.mjs diff` says what changed against `base.json`
   (where the folder started). `node third-life-pack.mjs preview` writes `preview.png` and opens
   your plot on the site showing the folder, to walk; nothing is saved or published. Your agent
   has the same as `review_changes` and `preview_link`.

6. **Deploy**, only when you decide to, from the folder:

   ```sh
   node third-life-pack.mjs deploy
   ```

   It checks the plot, then opens the site's deploy page in your browser, which reads the plot
   from your computer: a server on `127.0.0.1` only, under a random link that works for 15
   minutes and answers only the site. On that page, signed in with the wallet that owns the plot,
   you see the plot drawn and what changes against the published version, cell by cell. Approve,
   and your wallet signs the publication; the site checks your claim again. Nothing is published
   without that signature, and neither the folder nor the command ever holds a key. If your
   browser will not let the page reach your computer, export the plot with `export_zip` and choose
   that file on the deploy page instead.

7. **Visit**: `https://internetparadise.xyz/?plot=<your plot's cell>`. Change
   anything and deploy again; each deploy replaces the published version.

## Use it from ChatGPT or Claude

Agents that live in the cloud (ChatGPT on the web, Claude on claude.ai, Claude Desktop and the
phone apps) cannot run a file on your computer. For them the site itself is the MCP server:

```
https://internetparadise.xyz/mcp
```

It has the same building tools as `third-life-mcp.mjs`, with the same checks and messages, and it
builds a **draft of your own plot** kept online for your wallet. You sign in once with your Tezos
wallet on a Internet Paradise page and allow the agent. **The agent can never publish**: you open its
draft in your plot editor, walk it, and publish it there, signed by your wallet. It cannot sign,
spend tez, upload files or see anything else of your account.

You need an invited wallet with a claimed artist plot.

**ChatGPT** (web; a plan whose workspace allows custom MCP servers / developer mode):

1. Open [chatgpt.com/plugins](https://chatgpt.com/plugins), press **+** and choose **Add custom
   MCP server**. (In accounts that still show the older menus: Settings › Apps & Connectors ›
   Advanced settings › turn on **Developer mode**, then **Create**.)
2. Name `Internet Paradise`. Under **Connection**, the public URL above, ending in `/mcp`.
3. Authentication: **OAuth**. Leave client ID and secret empty: ChatGPT registers itself.
4. Accept the warning (*I understand and want to continue*) and create it.
5. A Internet Paradise page opens. Sign in with your wallet, read what the agent may and may not do, and
   press **Allow**. You return to ChatGPT.
6. In a new chat, type `@` and pick Internet Paradise (or turn it on in the tools menu), then ask, for
   example: *"Use Internet Paradise. Read describe_rules, then build a small gallery on my plot with a
   courtyard and a fountain. After each round look at preview and describe_scene. When it is
   done, run validate and deploy."* ChatGPT asks you to confirm each change it makes; that is
   ChatGPT's own safety step.

This is the same for any model you pick inside ChatGPT.

**Claude** (claude.ai, Claude Desktop, mobile):

1. Settings › **Connectors** › **Add custom connector**.
2. Name `Internet Paradise`, URL the address above. Leave the advanced OAuth fields empty.
3. **Connect**: the Internet Paradise page opens; sign in with your wallet and press **Allow**.
4. In a chat, turn Internet Paradise on in the tools menu and ask as above.

**Claude Code**: `claude mcp add --transport http internet-paradise https://internetparadise.xyz/mcp`,
then `/mcp` in Claude Code to sign in. **Codex** and other agents that speak remote MCP with
OAuth: add the same URL (Codex: `codex mcp add internet-paradise --url <the URL>`, then
`codex mcp login internet-paradise`). An agent that only takes a header can use an access token from
your account › **Connected agents** › *Access token for agents without sign-in*, sent as
`Authorization: Bearer <token>`.

**Preview, then decide.** The agent shows you `preview` images as it builds. Before you decide,
ask it for `review_changes` (what changes against your published plot, cell by cell) and
`preview_link`: a link that shows the draft on your plot to walk, only to you, signed in. Nothing
is saved or published there; **Back to my draft** leaves everything as it was, **Use this draft**
puts it in your editor. `restore_published` starts the draft again from what is live;
`discard_draft` deletes it. The agent should call these, and `deploy`, only when you ask.

**From draft to published plot.** When you say "publish", the agent calls `deploy` and gives
you a link. It opens your plot editor with the draft under **Share › AI agents**:

- **Preview** shows it on your plot to walk, as `preview_link` does.
- **Open in the editor** puts the agent's version in your editor as one step. If your browser
  already has a draft of the plot, it first says how many objects it replaces; **Undo** brings
  them back.
- **Send my draft** gives the agent your browser's version instead, but never over a version the
  agent saved after you last opened it: the site refuses and tells you to open it first.
- **Give my draft to an agent** starts a new online draft from what is in your editor.
- Publish as always from **Share › Publish your whole plot**. Your wallet signs; no tez is spent.

What the remote server does not do: upload model files (the agent uses shapes, helpers and
prefabs; add your own models in the plot editor), hang NFTs (you do, in the plot editor), or keep
more than 4 drafts per wallet. Tool calls are limited to 60 a minute and 1,500 a day per wallet,
within the site's free plan.

**Disconnect** an agent at any time: your account (wallet button) › **Connected agents** ›
**Disconnect**. It stops at once; its drafts stay. A connection also ends by itself after 90 days.

## Connect your agent

Use absolute paths: most apps do not start the server in your project folder. In JSON on
Windows, write paths with forward slashes (`C:/Users/you/internet-paradise/my-world.json`) or doubled
backslashes. `node` is a program, not a script, so no `cmd /c` wrapper is needed on Windows.

**Claude Code.** As above. `--scope project` writes the setup to `.mcp.json` in the project so
it can be shared; `claude mcp list` and `/mcp` inside a session show whether it is connected.

```sh
claude mcp add --scope project internet-paradise -- node /Users/you/internet-paradise/third-life-mcp.mjs --scene /Users/you/internet-paradise/my-world.json
```

**Codex CLI** (OpenAI). `codex mcp add` writes the server to `~/.codex/config.toml`; `codex mcp
list` shows it, and `/mcp` inside a session shows its tools:

```sh
codex mcp add internet-paradise -- node /Users/you/internet-paradise/third-life-mcp.mjs --scene /Users/you/internet-paradise/my-world.json
```

The same, written into `~/.codex/config.toml` by hand:

```toml
[mcp_servers.internet-paradise]
command = "node"
args = ["/Users/you/internet-paradise/third-life-mcp.mjs", "--scene", "/Users/you/internet-paradise/my-world.json"]
```

**Claude Desktop.** Settings › Developer › Edit Config opens `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`). Add the server,
save, and restart Claude Desktop:

```json
{
  "mcpServers": {
    "internet-paradise": {
      "command": "node",
      "args": [
        "/Users/you/internet-paradise/third-life-mcp.mjs",
        "--scene",
        "/Users/you/internet-paradise/my-world.json"
      ]
    }
  }
}
```

**Cursor.** `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project).
Cursor fills in `${workspaceFolder}`:

```json
{
  "mcpServers": {
    "internet-paradise": {
      "command": "node",
      "args": [
        "${workspaceFolder}/third-life-mcp.mjs",
        "--scene",
        "${workspaceFolder}/my-world.json"
      ]
    }
  }
}
```

**VS Code** (Copilot agent mode). `.vscode/mcp.json` in your workspace; note the top-level key
is `servers`:

```json
{
  "servers": {
    "internet-paradise": {
      "type": "stdio",
      "command": "node",
      "args": [
        "${workspaceFolder}/third-life-mcp.mjs",
        "--scene",
        "${workspaceFolder}/my-world.json"
      ]
    }
  }
}
```

**Options.** `--scene <file.json>` is the scene the server edits (created on the first change).
`--assets <folder>` adds a folder to search for your GLB models when exporting (repeatable; the
scene's folder is always searched). `--offline` never downloads prefab model files.

## The tools

The same tools, with the same arguments and messages, are served by the site's remote MCP
server (`/mcp`) for the owner's plot drafts.

| Tool             | Does                                                                                      |
| ---------------- | ----------------------------------------------------------------------------------------- |
| `describe_rules` | Units, axes, the cell, shapes, materials, collision, the visitor's size, helpers, limits. |
| `describe_scene` | Groups and their bounds, a walk test, inside wall faces for frames, free ground, mistakes. |
| `preview`        | A picture: floor plans cut like an architect's, and elevations, as PNG (or SVG).          |
| `list_objects`   | One line per object, filtered by group or shape.                                          |
| `add_room`       | Four walls with a door and windows, a floor and an optional ceiling, as one group.        |
| `add_wall`       | One straight wall, cut for a door and windows.                                            |
| `add_floor`      | A slab whose top is the walking surface, with holes for stairs.                           |
| `add_stairs`     | A flight a visitor walks up, and the hole to leave in the floor above.                    |
| `add_frames`     | A row of empty picture frames on a wall face, for NFTs.                                   |
| `list_prefabs`   | Old-town pieces: three small buildings, walls, stairs, arches, roofs, garden furniture.   |
| `add_prefab`     | Places a prefab as one group: where, at what level, turned how far.                       |
| `add_objects`    | Boxes, cylinders, spheres, tori, vaults, foliage and frames, in one batch; `repeat` makes rows. |
| `update_objects` | Moves, resizes, turns, recolours, regroups objects, in one batch.                         |
| `move_group`     | Moves or turns a whole group as one.                                                      |
| `remove_objects` | Removes objects by id, or a whole group.                                                  |
| `undo`           | Steps back one change (up to 30).                                                         |
| `new_scene`, `open_scene`, `save_scene` | Start, open (a JSON or a ZIP from the builder) and save scenes.    |
| `new_plot`       | Writes your plot's folder and edits it as the whole plot, every cell.                      |
| `inspect_model`  | Measures a `.glb` against the budgets (size, triangles, draws, materials, textures) and says why the site would refuse it. Changes nothing. |
| `optimize_model` | Writes a lighter copy beside it with the builder's own import preparation, with a before/after report. |
| `add_model`      | Places your own `.glb`, checked and given collision the way the builder's import does.     |
| `validate`       | Checks the scene the way the site will (every cell of a plot), and that every model file can be found. |
| `export_zip`     | Writes the ZIP to import; for a plot, the package the deploy page takes as a file.         |
| `deploy`         | A plot only, when you ask: opens the deploy page for you to approve with your wallet.      |

Every change is saved to the scene file at once. A batch is all or nothing: if one object is
wrong, nothing is added and the reply lists every problem with what to change ("its centre x
must be between 1 and 15", "Did you mean position?").

## Optimizing your models

A model made for a render (a million triangles, 8K textures, dozens of materials) must be brought
down before visitors can walk past it on a phone. The full guide, with targets by object type and
every command, is at [internetparadise.xyz/docs/optimize](https://internetparadise.xyz/docs/optimize). In short:

1. `inspect_model` on the `.glb`: size, triangles, draw primitives, materials and textures against
   the target (180,000 triangles, 24 materials, 2048 px textures, about 3 MB for a whole building)
   and the hard limit (200,000 triangles, 20 MB), and why the site would refuse it.
2. Far over, or not a GLB yet: the Blender script (Blender 4.1 or newer, free from blender.org)
   applies modifiers, decimates, joins meshes by material, optionally bakes one atlas, shrinks
   textures and exports a GLB the town accepts:

   ```sh
   curl -O https://internetparadise.xyz/sdk/teztopia_optimize.py
   blender -b --python teztopia_optimize.py -- house.blend house.glb --triangles 100000 --texture 2048 --bake
   ```

3. `optimize_model` (`targetTriangles` and `maxTextureSize` optional) runs the builder's own import
   preparation on your computer: weld, dedup, joins by material, meshoptimizer simplification,
   textures to WebP, atlases, meshopt packing. It writes `<name>-optimized.glb` beside the original,
   which it never changes. Textures need the optional `sharp` package (`npm install sharp` in the
   SDK's folder; `npm install draco3dgltf` too for Draco files); without it the geometry is still
   optimized and the report names the textures to resize.
4. `add_model` with the optimized copy, then `validate` before you deploy.

## How an agent builds well

- **Units and axes.** Metres, y up, one 16 × 16 × 16 m cell. `position` is the **centre** of an
  object, from the cell's north-west corner, so a box of height h on the ground has y = h / 2.
  Angles are **degrees**. Visitors arrive from the south, at [8, 0, 18], looking north.
- **Rough out with helpers and prefabs**, refine with `add_objects` and `update_objects`. A
  building is usually `add_room` (or a prefab), `add_stairs`, `add_floor` with the stair's hole,
  then a roof.
- **Look after every round.** `preview` shows plans and elevations; `describe_scene` runs a walk
  test with the game's own movement and says which floors a visitor cannot reach, what rests on
  nothing, and which frames hang in front of nothing.
- **Upper storeys.** `add_room` says where the next storey starts. `add_stairs` returns a hole a
  little wider than the flight (a visitor on the stair's edge needs the headroom); leave it open
  in the slab above and rail its open edges.
- **NFT frames.** `add_room`, `add_prefab` and `describe_scene` list the solid inside faces of
  walls; pass one to `add_frames` with a count. The creator hangs the NFTs in the builder's
  **Art** tab after import.
- **Rows and groups.** `repeat: { count, offset }` turns one object into a row of posts, slats
  or columns. `move_group` moves or turns a finished building as one.

Example prompts, each built and walked with these tools before this guide was written:

> Build a two-storey stone loggia: an open arcade of three arches on the south front, a stair
> inside along the north wall, and a gallery room above with frames on its side walls.

> Build a gallery pavilion with eight frames on its walls for my NFTs, a stone forecourt and two
> cypresses by the door.

## Import into Internet Paradise

Building happens only on mainland plots; there are no standalone worlds.

- **Your plot on the mainland:** build the whole plot as its own folder and deploy it (see
  [Your plot, like a Decentraland scene](#your-plot-like-a-decentraland-scene)).
- **One cell:** a one-cell scene imports into the plot editor (**My space › Share › Import**), as
  the cell you are building in. A scene without models can also be imported as its JSON. The
  import replaces that cell, so export it first if you want to keep it.

The plot editor exports ZIPs too (**Share**, **Export with assets**). `open_scene` opens one, so a
world can go back and forth between the site and your agent.

## Troubleshooting

- **The agent does not see the tools.** Use absolute paths, check `node --version` is 20 or
  newer, and restart the app after editing its config. In Claude Code, `claude mcp list` and
  `/mcp` show the server's state. Claude Desktop writes logs to `~/Library/Logs/Claude/` (macOS)
  or `%APPDATA%\Claude\logs\` (Windows), one `mcp-server-internet-paradise.log` per server.
- **Try the server by hand.** It answers one JSON message per line on stdin, and logs only to
  stderr:

  ```sh
  echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{}}}' | node third-life-mcp.mjs --scene test.json
  ```

  It prints one line with `"serverInfo":{"name":"internet-paradise"…}` and exits when its input ends.
- **"could not be opened"** at start means the scene file is not valid JSON or not a scene. The
  tools say why; fix the file, or ask the agent to call `new_scene` (with `overwrite: true`) or
  `open_scene`.
- **"File not found" on export** means a model the scene declares is not in the scene's folder
  or an `--assets` folder. Files are matched by content, so a renamed file is found; an edited
  one is not: add it again with `add_model`.
- **Prefab model files offline.** With `--offline`, or without a connection, the export names the
  file to download and the folder to put it in (`third-life-prefabs` beside the scene).
- **A visitor cannot reach a floor.** Read the walk test in `describe_scene`: it says which floor
  and what to add. Then walk it in your plot editor.

The server speaks MCP over stdio: the `initialize` handshake of revisions 2024-11-05 to
2025-11-25, and the stateless 2026-07-28 revision (`server/discover` and per-request `_meta`).

## Packing without an agent

Write the scene JSON from any language following `world.schema.json`, then:

```sh
curl -O https://internetparadise.xyz/sdk/third-life-pack.mjs
node third-life-pack.mjs my-world.json --assets ./models -o my-world.zip
node third-life-pack.mjs my-world.json --check     # validate only
```

A model's declaration (its SHA-256, triangle budget and collision surfaces) is hard to write
by hand, so a script may name the file instead and let `pack` fill it in:

```json
{ "id": "fountain", "shape": "model", "file": "models/fountain.glb", "position": [8, 0.6, 5], "scale": 1, "yaw": 0.785 }
```

`file` is read from the scene's folder, then from each `--assets` folder. `scale` (or `size`)
and `yaw` are optional; without them the model keeps its own size in metres. In the scene file,
`yaw` and `rotation` are in radians, as three.js uses them; the tools take degrees.

## The scene format

A scene is one **16 × 16 m cell**, 16 m tall. Units are **metres** and **y is up**.

```
             north (-z)
   (0, 0) +--------------+ (16, 0)
          |              |
west (-x) |   the cell   | east (+x)
          |              |
  (0, 16) +--------------+ (16, 16)
             south (+z)
                 ^  visitors arrive here, looking north
```

- `position` is the **centre** of an object's bounding box, measured from the cell's north-west
  corner. A box standing on the ground has `y = height / 2`.
- `size` is the full extent `[x, y, z]`, each above 0 and at most 16.
- `rotation` is `[x, y, z]` in radians. Solid objects turn around y only: `[0, yaw, 0]`.
- Every object stays inside the cell: x and z from 0 to 16, bottom no lower than -0.3, top
  no higher than 16.

An example:

```json
{
  "spec": "tezworlds-dev/1",
  "name": "Loggia",
  "description": "A loggia by the sea.",
  "parcelSize": 16,
  "bounds": [0, 0, 48, 48],
  "spawn": [24, 0, 34],
  "parcels": [
    {
      "id": "1,1",
      "title": "Loggia",
      "common": false,
      "nodes": [
        {
          "id": "paving",
          "shape": "box",
          "position": [8, -0.1, 8],
          "size": [16, 0.2, 16],
          "color": "#b9ab92",
          "material": "stone",
          "collider": true
        },
        {
          "id": "column-1",
          "shape": "cylinder",
          "position": [2, 1.6, 13],
          "size": [0.5, 3.2, 0.5],
          "color": "#ddcfb5",
          "material": "plaster",
          "collider": true,
          "label": "Column",
          "buildGroup": { "id": "arcade", "name": "Arcade" }
        },
        {
          "id": "frame-1",
          "shape": "box",
          "galleryFrame": true,
          "position": [8, 1.6, 4.3],
          "size": [2, 1.6, 0.08],
          "color": "#3a2c20",
          "label": "Frame"
        }
      ]
    }
  ]
}
```

- `spec`, `parcelSize`: always these values.
- `bounds`, `spawn`: the builder's frame. The world is 48 m square with the cell in the middle
  (16 to 32 in world coordinates), and the visitor arrives at `[24, 0, 34]`, two metres south
  of it. Keep these as they are.
- `parcels`: one cell, `"1,1"`, with `common: false`. Its `nodes` are the objects.
- Shapes: `box`; `cylinder` (size is diameter x, height, diameter z); `sphere`; `torus` (an
  upright ring in the x-y plane); `vault` (a barrel-vault shell: an arch over x, extruded along
  z, open underneath, for arcades and roofs); `foliage`; `model` (a GLB, see above). A box with
  `galleryFrame: true` is an empty frame for an NFT; its picture faces +z.
- `color` is `#rrggbb`. `material` is one of stone, brick, plaster, metal, glass, copper,
  plywood, painted, concrete, fabric.
- `collider: true` makes an object solid. It collides as its bounding box; a vault as its
  arch; a model as its collision surfaces. `collisionOnly: true` makes an invisible solid box.
- `buildGroup` ties objects together: the builder selects and moves a group as one. One
  group id has one name.

### Walking

The visitor is 0.6 m wide and 1.72 m tall, and walks up any ledge of at most 0.28 m.

- Doors: at least 0.8 m wide and 1.82 m tall. The helpers use 1.2 × 2.3 m.
- Stairs: risers of at most 0.28 m; the helpers use 0.18 m with 0.3 m treads. Leave the
  stairwell open in the floor above, 0.6 m wider than the flight on each open side, or visitors
  hit their heads.
- Static GLB models get automatic surface collision, which keeps doorways and interiors open.
  Animated models and very complex surfaces stay decorative: add `collisionOnly` boxes.

### Limits per scene

| Limit                              | Value                                       |
| ---------------------------------- | ------------------------------------------- |
| Objects                            | 500                                         |
| Different model files              | 8                                           |
| Model instances                    | 16                                          |
| GLB file                           | 20 MB; 48 MB of files in total              |
| Triangles                          | 200,000 per model, 500,000 per scene        |
| Textures                           | PNG, JPEG or WebP embedded in the GLB, up to 4096 px and 16.7 megapixels per model |
| Model collision surfaces           | 8,192                                       |

GLB extensions accepted: meshopt compression, mesh quantization, WebP textures, unlit,
emissive strength, specular, IOR and texture transform. Draco and KTX2 are refused; export
without them. The builder's own import goes further and reduces models to 180,000 triangles
and about 3 MB; the SDK uses your file as it is and warns when it is above those targets.
