otherplane 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CLAUDE.md +130 -0
  2. package/LICENSE +21 -0
  3. package/README.md +146 -0
  4. package/bin/otherplane.mjs +489 -0
  5. package/engine/eslint.config.mjs +25 -0
  6. package/engine/next.config.ts +43 -0
  7. package/engine/package-lock.json +6848 -0
  8. package/engine/package.json +36 -0
  9. package/engine/postcss.config.mjs +5 -0
  10. package/engine/src/app/LandingRedirect.tsx +15 -0
  11. package/engine/src/app/[room]/RoomViewer.tsx +413 -0
  12. package/engine/src/app/[room]/page.tsx +30 -0
  13. package/engine/src/app/favicon.ico +0 -0
  14. package/engine/src/app/layout.tsx +45 -0
  15. package/engine/src/app/page.tsx +11 -0
  16. package/engine/src/app/providers.tsx +22 -0
  17. package/engine/src/components/controls/MobileHud.tsx +25 -0
  18. package/engine/src/components/controls/PlayerController.tsx +170 -0
  19. package/engine/src/components/controls/TouchLookController.tsx +93 -0
  20. package/engine/src/components/controls/VirtualStick.tsx +153 -0
  21. package/engine/src/components/edit/EditCapture.tsx +182 -0
  22. package/engine/src/components/edit/EditorPanel.tsx +265 -0
  23. package/engine/src/components/edit/Markers.tsx +91 -0
  24. package/engine/src/components/hud/Button.tsx +228 -0
  25. package/engine/src/components/hud/ClickToPlay.tsx +13 -0
  26. package/engine/src/components/hud/ContentOverlay.tsx +44 -0
  27. package/engine/src/components/hud/NavHeader.module.css +24 -0
  28. package/engine/src/components/scene/Artifacts.tsx +85 -0
  29. package/engine/src/components/scene/Exits.tsx +92 -0
  30. package/engine/src/components/scene/PointerLockBridge.tsx +28 -0
  31. package/engine/src/components/scene/WorldScene.tsx +164 -0
  32. package/engine/src/components/spark/SparkLayer.tsx +112 -0
  33. package/engine/src/components/spark/SplatWorld.tsx +156 -0
  34. package/engine/src/config/audio.ts +11 -0
  35. package/engine/src/data/editApi.ts +73 -0
  36. package/engine/src/data/presets.ts +34 -0
  37. package/engine/src/data/room.ts +100 -0
  38. package/engine/src/data/site.ts +50 -0
  39. package/engine/src/data/universeconfig.ts +19 -0
  40. package/engine/src/icons/ArrowLeft.tsx +20 -0
  41. package/engine/src/icons/ChevronDown.tsx +23 -0
  42. package/engine/src/icons/ChevronLeft.tsx +22 -0
  43. package/engine/src/icons/Home.tsx +22 -0
  44. package/engine/src/icons/Spinner.module.css +13 -0
  45. package/engine/src/icons/Spinner.tsx +28 -0
  46. package/engine/src/icons/VolumeMax.tsx +21 -0
  47. package/engine/src/icons/VolumeX.tsx +22 -0
  48. package/engine/src/icons/icons.interface.ts +7 -0
  49. package/engine/src/icons/index.ts +27 -0
  50. package/engine/src/physics/RapierProvider.tsx +302 -0
  51. package/engine/src/physics/index.ts +2 -0
  52. package/engine/src/physics/types.ts +9 -0
  53. package/engine/src/providers/audio.tsx +215 -0
  54. package/engine/src/providers/edit.tsx +357 -0
  55. package/engine/src/providers/pointerLock.tsx +88 -0
  56. package/engine/src/styles/globals.css +88 -0
  57. package/engine/tailwind.config.js +184 -0
  58. package/engine/tsconfig.json +27 -0
  59. package/otherplane.config.example.json +6 -0
  60. package/package.json +56 -0
  61. package/schema/room.schema.json +77 -0
  62. package/scripts/gen_world.py +147 -0
  63. package/skill.md +94 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,130 @@
1
+ # CLAUDE.md
2
+
3
+ Guidance for Claude Code working in the **Otherplane** repo.
4
+
5
+ ## What this is
6
+
7
+ A static site generator for walkable Gaussian-splat museums. This repo ships the
8
+ **engine/tool** only; a user's museum content is theirs and is gitignored. Two
9
+ layers:
10
+
11
+ - **the project** — a user's content root: `otherplane.config.json` (deploy config:
12
+ landing room, base path, site title, walk speed) plus `rooms/<slug>/room.json`,
13
+ `worlds/` (splats + colliders), `music/`. Gitignored here; it's the source of
14
+ truth for a given site. `otherplane init` scaffolds it.
15
+ - **`engine/`** — the renderer: a content-free Next.js viewer (Spark + R3F +
16
+ Rapier) that statically exports (`output: 'export'` — no server, any static
17
+ host). Its only input is a `room.json` by URL; it knows nothing about museums,
18
+ hosting, or how assets were made. Keep it content-free — no rooms baked in.
19
+
20
+ Plus the tooling: `bin/otherplane.mjs` (the CLI), `schema/` (room.json JSON
21
+ Schema), `scripts/gen_world.py` (Marble room generation), and [`skill.md`](skill.md)
22
+ (the authoring pipeline: generate → mark → wire → publish).
23
+
24
+ The CLI mirrors the project's `rooms/`, `worlds/`, `music/` into `engine/public/`
25
+ (gitignored build artifact) before dev/build, so Next's `generateStaticParams`
26
+ can enumerate `public/rooms/*`. Author at the project root, never in the mirror.
27
+ The project root defaults to the cwd, overridable via `$OTHERPLANE_PROJECT`.
28
+
29
+ ## Commands (run from the project root)
30
+
31
+ One CLI, aliased by the npm scripts: `npm run init` (scaffold a project) ·
32
+ `npm run new` (generate a room) · `npm run dev` · `npm run edit` (author in the
33
+ viewer) · `npm run check` (validate rooms) · `npm run build` (→ `engine/out`;
34
+ `-- --base /museum` to mount under a sub-path) · `npm run serve` · `npm run clean`.
35
+ Each aliases `otherplane <verb>` (`bin/otherplane.mjs`), which drives `engine/` and
36
+ `gen_world.py`.
37
+
38
+ ## The room primitive (`engine/src/data/room.ts`)
39
+
40
+ A room is `rooms/<slug>/room.json`. Types:
41
+
42
+ - `Entryway { id, pos, yaw }` — a named, addressable spawn spot. The URL fragment
43
+ (`/library/#from-study`) selects it; `id: "default"` is the no-fragment spawn.
44
+ - `Exit { pos, radius?, to }` — walk up + press **E** to follow `to` (a room URL +
45
+ `#entryway`; relative within a site, absolute across). Dead links 404 by design.
46
+ - `Artifact { id?, pos, radius, url }` — walk up + interact to open a web URL
47
+ overlay (no travel).
48
+ - `loadRoom(url)` resolves every asset URL **relative to the room.json's URL**, so
49
+ assets can be colocated, on object storage (R2/S3), or on the World Labs CDN.
50
+
51
+ A museum is just rooms linked by exits (like web pages linked by hrefs) — there is
52
+ no manifest and no global "museum" object; the graph is emergent.
53
+
54
+ ## Routing (`engine/src/app`)
55
+
56
+ - `app/[room]/page.tsx` — server wrapper; `generateStaticParams` enumerates
57
+ `public/rooms/*` so each room pre-renders to `/<slug>/index.html` in the export.
58
+ (generateStaticParams can't live in a `'use client'` file — hence the wrapper.)
59
+ - `app/[room]/RoomViewer.tsx` — the client viewer: loads the room, resolves the
60
+ spawn entryway from `location.hash`, renders the scene, handles exits (client
61
+ nav same-origin, full nav cross-origin).
62
+ - `app/page.tsx` — server component that bakes in `LANDING_ROOM` (`src/data/site.ts`,
63
+ read from `otherplane.config.json`) and renders `LandingRedirect.tsx`, a tiny
64
+ client redirect (static export has no server).
65
+ - `app/providers.tsx` — PointerLock / Audio / Edit providers (in the root layout).
66
+
67
+ ## Rendering & physics
68
+
69
+ - `components/scene/WorldScene.tsx` — the R3F `<Canvas>` and scene graph.
70
+ - `components/spark/SplatWorld.tsx` — loads the `.spz` via Spark. All Marble splats
71
+ use quaternion `[1,0,0,0]` (180° about X) + the room's `calibration.scale` (see
72
+ `roomToWorldDef` in `presets.ts`); the collider is baked with the same transform
73
+ so walls line up with what you see.
74
+ - `physics/RapierProvider.tsx` — builds a trimesh env collider from the room's
75
+ collider GLB; spawns the player (fixed-size dynamic capsule) at the entryway
76
+ `pos` (body-center) facing `yaw`. Falls back to a floor raycast (`findValidSpawn`)
77
+ only when a room declares no entryways. Rapier is `@dimforge/rapier3d-compat`
78
+ **v0.12** — ray hits use `.toi` (not `.timeOfImpact`).
79
+ - `components/controls/PlayerController.tsx` — WASD + mouse. The player capsule is
80
+ a **fixed real-world size**; `calibration.scale` sizes the *room* to fit it, so a
81
+ too-small scale leaves the player wedged (that's the room-scale footgun). No
82
+ jumping/sprinting; edit-mode specter (Z + arrows) is the only vertical movement.
83
+
84
+ ## Edit mode (`engine/src/providers/edit.tsx`)
85
+
86
+ A property of the running **instance** (`NEXT_PUBLIC_EDIT_MODE`, via `npm run
87
+ edit`), not a URL — so published builds have no edit mode and never mount any of
88
+ it. Edit mode **writes** `room.json`: `otherplane edit` runs a tiny local HTTP
89
+ writer alongside `next dev`, and `src/data/editApi.ts` POSTs marks to it; the CLI
90
+ merges only the coordinate arrays into the PROJECT source (never asset URLs, never
91
+ the mirror) and re-syncs. Pieces:
92
+
93
+ - `providers/edit.tsx` — owns the room's draft + mutators (each persists) + undo.
94
+ - `components/edit/EditCapture.tsx` — keys: **C** add/move entryway (floor-snapped),
95
+ **B** add/move artifact (beamed), **F** gaze-select, **Del** remove, **X**
96
+ deselect, **Z** specter fly (↑/↓).
97
+ - `components/edit/Markers.tsx` — labeled orbs in the scene (green entryway / cyan
98
+ exit / amber artifact), edit-only.
99
+ - `components/edit/EditorPanel.tsx` — the single DOM panel: help legend, undo/⌘Z,
100
+ name/wire/delete, exit dropdown-or-arbitrary-URL, promote-to-doorway, two-way
101
+ (reciprocal exit reusing the target's entryway), and two sliders that write
102
+ **shippable** data — room scale → `calibration.scale` in room.json (rescales the
103
+ markers with it), walk speed → `moveSpeed` in `otherplane.config.json` (per-museum,
104
+ ships to every viewer).
105
+
106
+ ## Controls (published viewer)
107
+
108
+ Desktop: WASD move, mouse look, **E** to use an exit, mute (top-right), Esc to
109
+ release the mouse. The first click or keypress engages look + audio (browsers
110
+ require a gesture). Mobile: virtual joystick + touch-drag to look.
111
+
112
+ ## Conventions
113
+
114
+ - TypeScript, functional components, hooks. Providers live OUTSIDE the R3F Canvas.
115
+ - Keep the renderer content-free: no specific rooms/assets baked in.
116
+ - Memoize the `WorldDef` passed to the scene (stable `position`/`quaternion`
117
+ references) or SplatWorld's load effect re-fires every render.
118
+ - Stable slugs + entryway ids: other rooms link to them; renaming rots links.
119
+ - Big binaries (`.spz`/`.glb`) belong in object storage referenced by URL from
120
+ `room.json`; keep git small.
121
+ - `.env` holds `WORLD_LABS_KEY` and is gitignored — never commit it (`.env.example`
122
+ is the template). Build output (`engine/.next`, `engine/out`, `node_modules`) and
123
+ content (`rooms/`, `worlds/`, `music/`, `generated/`) are gitignored too.
124
+
125
+ ## Tech stack
126
+
127
+ Next.js 15 (static export) · React 19 · TypeScript · three.js + @react-three/fiber ·
128
+ @sparkjsdev/spark (splat rendering) · @dimforge/rapier3d-compat (physics) ·
129
+ Tailwind CSS. Built on [WorldSplats](https://github.com/philchacko/worldsplats),
130
+ [Spark.js](https://sparkjs.dev), and [World Labs Marble](https://marble.worldlabs.ai).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Phil Chacko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Otherplane
2
+
3
+ A **static site generator for walkable Gaussian-splat museums.** Point it at the
4
+ sections of a personal site and it generates a 3D **room** for each one, lets you
5
+ mark spawn points / doorways / artifacts, links the rooms together, and publishes
6
+ a plain **static walkable site** — host it on GitHub Pages, Cloudflare R2/Pages,
7
+ S3, or any folder behind any web server.
8
+
9
+ A museum is just **rooms linked by exits** — like web pages linked by `href`s.
10
+ There is no manifest and no central "museum" object; the graph is emergent.
11
+
12
+ ## This repo is the engine
13
+
14
+ Otherplane ships the **tool**, not a museum. You build your own site on top: your
15
+ rooms, splats, colliders, music, and per-site config are **your content** and are
16
+ gitignored by default — clone the engine, then create a museum inside it (or in
17
+ any folder) and track that content in your own repo. `engine/` is the content-free
18
+ renderer; the rest is the CLI, schema, and authoring pipeline.
19
+
20
+ ```
21
+ otherplane/
22
+ ├── engine/ the renderer — a content-free Next.js viewer (Spark + R3F + Rapier)
23
+ ├── bin/otherplane.mjs the CLI (the npm scripts alias it)
24
+ ├── schema/ room.json JSON Schema (editor autocomplete + `otherplane check`)
25
+ ├── scripts/ gen_world.py — generate a room from a prompt (World Labs Marble)
26
+ ├── skill.md the authoring guide — how rooms get made, marked, linked, shipped
27
+ └── otherplane.config.example.json
28
+
29
+ # created by `otherplane init` / your content — GITIGNORED, build your own site here:
30
+ ├── otherplane.config.json deploy config (landing room, base path, title, walk speed)
31
+ ├── rooms/<slug>/room.json the rooms — one folder, one URL (/<slug>/)
32
+ ├── worlds/ splats + colliders (.spz / .glb) — host big blobs on R2/S3
33
+ └── music/ per-room loopable tracks
34
+ ```
35
+
36
+ The CLI mirrors your content into `engine/public/` (a gitignored build artifact)
37
+ so Next can serve and enumerate it; you never edit there. The renderer is a **deep
38
+ module**: its only input is a `room.json` (by URL) — it knows nothing about
39
+ museums, hosting, or how assets were made. See [`CLAUDE.md`](CLAUDE.md) for
40
+ renderer internals and [`skill.md`](skill.md) for the authoring pipeline.
41
+
42
+ ## Quick start
43
+
44
+ Use it as an installed CLI in any folder (the engine's deps install themselves on
45
+ first run):
46
+
47
+ ```bash
48
+ npx otherplane init # scaffold otherplane.config.json + rooms/ worlds/ music/
49
+ npx otherplane edit # open http://localhost:3000 — walk, mark, and wire rooms
50
+ npx otherplane build # → ./out (deploy anywhere) · -- --base /museum to sub-path
51
+ ```
52
+
53
+ …or clone the repo and use the `npm run` aliases (`npm run init` / `edit` /
54
+ `build`). Either way, `build` emits a plain static `out/` in your project — host
55
+ it on GitHub Pages, Cloudflare, S3, or any static host.
56
+
57
+ Add rooms one of two ways: **generate** them with `otherplane new` (World Labs
58
+ Marble — needs a key + `uv`, see below), or **bring your own** `.spz` splat +
59
+ `.glb` collider, drop them in `worlds/`, and point a `rooms/<slug>/room.json` at
60
+ them. Then `otherplane edit` to place spawns/doors/artifacts (it writes `room.json`
61
+ for you). No key or Python is needed for the bring-your-own path.
62
+
63
+ > **Publishing (maintainers):** `npm publish` ships the CLI + the engine source
64
+ > (`files` allowlist; build artifacts and `node_modules` are excluded). The engine
65
+ > is heavy (Next + Three.js + Rapier); its deps install on the user's first
66
+ > `dev`/`edit`/`build`, not at install time. Bump the version and `npm publish`.
67
+
68
+ ## CLI
69
+
70
+ Like other static site generators (Hugo, Astro, Eleventy), Otherplane exposes a
71
+ small set of verbs through one CLI (`bin/otherplane.mjs`). Run them as
72
+ `otherplane <verb>` or via the `npm run` aliases below:
73
+
74
+ | Command | What it does |
75
+ | :----------------- | :-------------------------------------------------------------------------- |
76
+ | `npm run setup` | Install the engine's dependencies (one-time). |
77
+ | `npm run init` | Scaffold a fresh project (config, `rooms/`, `.env.example`) in this folder. |
78
+ | `npm run new` | Generate a room from a prompt (World Labs Marble). See below. |
79
+ | `npm run dev` | Dev server with the viewer (http://localhost:3000). |
80
+ | `npm run edit` | Dev server with **edit mode** on — mark/wire rooms in the viewer; writes `room.json` for you. |
81
+ | `npm run check` | Validate every `rooms/*/room.json` against the schema. |
82
+ | `npm run build` | Static export to `engine/out` — deploy this folder anywhere. `-- --base /museum` to mount under a sub-path. |
83
+ | `npm run serve` | Serve the built `engine/out` at http://localhost:8000. |
84
+ | `npm run preview` | `build` then `serve`. |
85
+ | `npm run clean` | Remove build output + the synced mirror. |
86
+
87
+ ### Generating a room
88
+
89
+ ```bash
90
+ cp .env.example .env # then paste your WORLD_LABS_KEY
91
+ npm run setup
92
+ npm run new -- "a cozy wood-paneled library, warm lamplight" --slug library --name "Library"
93
+ ```
94
+
95
+ This calls World Labs Marble, downloads the splat (`.spz`) + collider (`.glb`)
96
+ into `worlds/`, saves the raw world object under `generated/<slug>/`, and writes a
97
+ `room.json` skeleton to `rooms/<slug>/`. Use `--model marble-1.0-draft` (the
98
+ default) while iterating — it's cheap (~$1.26).
99
+
100
+ Then mark coordinates and wire the room up in the viewer — see below.
101
+
102
+ ### Marking & wiring (edit mode)
103
+
104
+ `npm run edit` opens the viewer with an authoring panel and writes `room.json`
105
+ for you (no clipboard, no hand-editing). Walk into a room, then:
106
+
107
+ - **C** — drop (or move the selected) **entryway** at your floor-snapped spot.
108
+ - **B** — aim and drop (or move the selected) **artifact** at what you're looking at.
109
+ - **F** select the marker under your crosshair · **Del** remove it · **Z** fly.
110
+
111
+ Every marker shows as a labeled orb (green entryway, cyan exit, amber artifact).
112
+ In the panel you name entryways, wire **exits** by menu (any room → entryway) or
113
+ paste an arbitrary URL (link into someone else's museum), promote an entryway
114
+ into a doorway, and click **two-way** to auto-create the return door in the target
115
+ room. Changes save to `rooms/<slug>/room.json` as you go.
116
+
117
+ ## Hosting & repo size
118
+
119
+ Keep **text in git** (`room.json` + the shared bundle) and **blobs in object
120
+ storage**. A `.spz` is ~5 MB and a collider ~5 MB; for anything you want to keep,
121
+ mirror those to R2 (cheap, zero egress, CDN-backed) and point each `room.json`
122
+ URL there. Adding a room then costs the repo a few KB, not 10 MB. (Marble's own
123
+ CDN URLs have no documented retention SLA — self-host keepers.)
124
+
125
+ ### Mounting on an existing site (e.g. `/museum`)
126
+
127
+ To hang the museum off a personal site — an "Enter my museum" button that goes to
128
+ `example.com/museum/` — build it under that sub-path and drop the output into your
129
+ site's tree:
130
+
131
+ ```bash
132
+ npm run build -- --base /museum # or set "basePath": "/museum" in otherplane.config.json
133
+ # then copy engine/out/* into your site's /museum/ directory (or point the host at it)
134
+ ```
135
+
136
+ The link on your homepage is just `<a href="/museum/">`. The museum is plain
137
+ static files, so it coexists with any host; the two builds stay separate (the
138
+ viewer carries a WebGL toolchain your main site shouldn't inherit) and integrate
139
+ at the deploy layer.
140
+
141
+ ## License
142
+
143
+ MIT — see [`LICENSE`](LICENSE). Built on
144
+ [WorldSplats](https://github.com/philchacko/worldsplats) (the viewer),
145
+ [Spark.js](https://sparkjs.dev) (splat rendering), and
146
+ [World Labs Marble](https://marble.worldlabs.ai) (generation).