プラレール風 3D シミュレータ plasim-3d を作った。ブラウザで線路を組んで、Claude にも組ませる
ブラウザでプラレール風のレイアウトを 3D で組んで、列車を走らせて遊べるシミュレータを作った。 線路だけでなく道路と車も走るし、MCP 経由で Claude や ChatGPT からレイアウトを編集したり走らせたりできる。
https://plasim-3d.polidogs.workers.dev/
ログインなしの「お試しモード」があるので、アカウントを作らなくても触れる。
どんなもの
ざっくり言うと「プラレールの箱を開けて床に線路を並べるやつ」をブラウザでやるもの。
- 3D エディタ: パーツをクリックで配置。コネクタ (緑の球) に吸着して、連打すると線路が伸びていく。ドラッグ移動、回転/反転、ポイント切替、Undo/Redo、範囲選択、コピー&ペースト、自動保存
- 走行シミュレーション: 複数列車・多両編成、ターンアウト / Y字 / 単線複線の分岐、坂レール (橋脚は自動で描画)、U ターン、ストップレール
- 車両基地と駅: ターンアウトで分岐した側線にストップレールと車庫を置いて、列車を待機させたり発車させたりする
- 道路と自動車: レールとは別ネットワークの道路 (直線・カーブ・十字路・T字路・駐車枠) を敷いて、乗用車 / バス / トラックを走らせる
- 踏切: 列車が近づくと遮断機が下りて警報ランプが点滅し、車が手前で止まる。通過すると勝手に開いて車も発車する
- 運転モード / ドライブ視点: 運転席カメラで列車を加減速したり、車をハンドル操作で走らせたりできる
- 情景: 家・ビル・木・街灯・川・鉄橋・山・トンネル・岩・崖・駐車場・芝生・水面。空は晴れ / 夕方 / 夜 / 曇りのプリセット
- レイアウトセット: 「少ないレールで簡単レイアウト」「坂レールを使ってみよう」みたいなテーマ別の完成レイアウトから始められる
- 公開・フォーク: 自分のレイアウトを公開すると他のユーザーが閲覧できて、自分のアカウントにフォークして編集できる
- iPad 対応: タッチ操作で編集も閲覧もできる
- MCP 連携: Claude Code / Claude Desktop / Cursor からは API トークン、Claude.ai / ChatGPT のコネクタからは OAuth で接続する
作ろうと思ったきっかけ
元ネタは binzume/plarail-sim という 2D のプラレールシミュレータ (MIT)。 これを見て「3D にしたい」「列車を走らせたい」「Claude に線路を組ませたい」「せっかくならログインして保存したい」と欲張った結果がこれ。
最初に決めた要件は 4 つだけ。
- three.js で 3D 表示する
- 列車の走行をシミュレーションする
- MCP から Claude / ChatGPT で編集できる
- ログインしてレイアウトを保存できる
レールパーツの定義とレイアウト JSON の形式は plarail-sim の v1 形式と互換にして、サンプルレイアウトをそのまま流用できるようにした。 道路や車はあとから独自拡張として足したので、そこは互換の範囲外。
技術構成
| 層 | 使ったもの |
|---|---|
| フロント | Vite + three.js (素の TypeScript、フレームワークなし) |
| サーバ | Hono + Cloudflare Workers |
| DB | Cloudflare D1 |
| リアルタイム反映 | Durable Objects 経由の SSE |
| MCP | @modelcontextprotocol/sdk (Streamable HTTP) |
| 認証 | メール + パスワード (scrypt)、セッション Cookie、API トークン、OAuth 2.1 |
| テスト | node:test のユニットテスト + playwright-core の E2E スモーク |
ソースは src/core / src/client / src/server の 3 つに分かれていて、合わせて 9,000 行弱。
core をブラウザとサーバで共有する
いちばん気に入っている設計は、src/core を DOM 非依存にしてブラウザとサーバの両方から使っているところ。
パーツ定義、座標変換、コネクタの接続とスナップ、それにヘッドレスの走行シミュレータがここに入っている。
ブラウザの再生ボタンで動いているのも、MCP の simulate ツールがサーバ側で「30 秒走らせて通過したレールと停止イベントを返す」のも、同じコードだ。
走行ロジックを直すときはここだけ触ればいいし、Claude に「このレイアウト、列車ちゃんと一周する?」と聞くとサーバ側で走らせて答えてくれる。
座標系
x が右、y が奥、z が高さで、1 unit = 21.6mm。直線レールが 10 unit、45° カーブが半径 10 unit (8 本で円)。 坂レールの先に直線をつなぐと高さ 10 (橋脚 1 段分) に上がって、橋脚は自動で描かれる。 「実物のプラレールの寸法をそのまま unit にする」という plarail-sim の設計がよくできていて、そのまま乗っかった。
MCP 連携と OAuth を自前実装した話
MCP エンドポイントは /mcp の 1 本で、Streamable HTTP。
Claude Code から使うぶんには API トークンを Bearer ヘッダで渡せば済む。
claude mcp add --transport http plasim https://plasim-3d.polidogs.workers.dev/mcp \
--header "Authorization: Bearer <トークン>"
問題は ChatGPT のコネクタで、Bearer ヘッダを設定する UI がない。OAuth 一択。
なので OAuth 2.1 (PKCE + 動的クライアント登録) をサーバに自前で実装した。
/.well-known/oauth-authorization-server を返しておくと、ChatGPT も Claude.ai も URL を登録するだけで動的登録 → ログイン → 許可 → トークン発行まで勝手にやってくれる。
発行されたトークンは「MCP 接続」ダイアログに並ぶので、そこから失効できる。
用意しているツールは 30 個ほどで、たとえばこんな頼み方ができる。
レイアウト X に楕円ループを作って列車を 3 両で走らせて、30 秒シミュレーションして
ターンアウトから側線を出してストップレールと車庫を置き、青い列車を停車させておいて
空を夕方にして
MCP からの変更は Durable Object 経由の SSE でブラウザに即時反映されるので、Claude が線路を足していく様子を画面で眺められる。これが地味に楽しい。
Cloudflare Workers への移行
最初は Node 24 の node:sqlite でローカル DB を持つ構成だった。
公開するにあたって Vercel + Next.js 化も考えたけど、Hono + Vite の構成が気に入っていたので、そのまま活かせる Cloudflare Workers にした。
- DB は D1 に移行 (SQLite なのでスキーマはほぼそのまま)
- ブラウザへの SSE は Workers 単体では持てないので、Durable Objects の
LayoutHubに接続をぶら下げる - フロントは Workers Static Assets で配信して、
/api/*/mcp/oauth/*だけ Worker が先に受ける - GitHub と接続して push のたびに Workers Builds で自動デプロイ
ハマったのは 2 つ。Workers には node:fs がないので examples/ 配下のサンプル JSON はビルド時の静的 import で登録する必要があったこと。それと D1 のマイグレーションは Cloudflare 側で自動適用されないので、Deploy command で毎回流すようにしたこと。
道路と車はレールと別ネットワークにした
道路を足すとき、レールと同じ仕組みに乗せるかで少し悩んだ。
結局、道路は専用のパーツ (コネクタと走行パスつき) として新設して、レールとは絶対に接続できない別ネットワークにした。
validate_layout は道路とレールの誤接続や「車が道路の上に乗っていない」も警告する。
車はハンドル操作つきだけど、常にパス上に留まる。A / D キーは「次の交差点でどっちに曲がるか」の進路予約として働くだけで、脱線も当たり判定もない。プラレールの世界観としてはこれで十分だった。
踏切だけは両方のネットワークにまたがる複合パーツで、レール側は直線 1 本分、道路側は道路直線 1 本分のコネクタを持つ。列車の接近を検知して遮断機を下ろし、道路側の車を止める。
3 日で作った。ほぼ Claude Code
ここからは作り方の話。初日から数えて 3 日、コミット 30 ほど、マージした PR は 17 本。 コードはほぼ全部 Claude Code に書いてもらった。
初日: 対話で土台を作る
1 日目は普通に Claude Code と対話しながら、要件 4 つを伝えて土台を作った。 three.js のエディタ、コネクタ吸着、走行シミュレータ、ログイン、MCP サーバ、OAuth まで初日でひととおり動いている。
この段階で「core を DOM 非依存にしてブラウザとサーバで共用する」という方針だけは自分で決めて伝えた。ここが後々ずっと効いた。
2 日目以降: Issue を積んでエージェントループで回す
機能が増えてきたところで、進め方を変えた。 やりたいことを GitHub Issues に「要件」と「受け入れ条件」つきで起票しておいて、Claude Code のループが 1 件ずつ claim して worktree で実装し、typecheck とテストを通して PR を作る、というやり方。
Issue 一覧を見返すとこんな感じ。
- 公開・閲覧・フォーク
- 運転モード (運転席視点 + 加減速 + 分岐器操作)
- Cloudflare Workers 化
- 川タイルと鉄橋
- iPad タッチ対応
- 道路と自動車
- 車のドライブ視点
- 駐車場と駐車枠
- 踏切
- 範囲選択削除
- レイアウトセット
- ドラッグでレールを配置・延長
- 空のプリセット
- Shift+ドラッグ範囲選択と Ctrl+C/V
- 山・トンネル・岩・崖
自分がやるのは、Issue に「何が欲しいか」と「どうなったら OK か」を書くことと、上がってきた PR をブラウザで触ってマージすること。
複数のループを並行で走らせたので、main が先に進んで PR がコンフリクトすることが何度かあった。これはループの終了処理側で origin/main をマージして解消してから push する手順にしたら落ち着いた。
ハマったこと
- Vite の proxy 設定:
/apiと書いたらsrc/client/api.tsへのリクエストまでプロキシされた。^/api/のように正規表現で書く - WebGL のスクリーンショット: E2E で
readPixelsしてもpreserveDrawingBufferなしだと空になる。コネクタの位置はスクリーンショットを見て固定座標で指定する方式に逃げた pnpm deploy: pnpm 組み込みのコマンド名と衝突するのでpnpm run deployでないと動かない- ChatGPT のコネクタ: 前述のとおり Bearer が使えないので OAuth を書くはめになった
やってみて思ったこと
「Issue に受け入れ条件を書く」のがそのままプロンプトになるので、自分の仕事が要件定義とレビューに寄っていった。 逆に言うと、受け入れ条件が曖昧な Issue は曖昧な PR になって返ってくる。「列車が中に入るとブラウザ上で見えなくなり、両端の開口部から出入りする」くらいまで書くと、だいたい一発で通った。
あと、MCP で自分のアプリを Claude から触れるようにしておくと、開発中の動作確認も Claude にやらせられるのが便利だった。「楕円を作って列車を走らせて、simulate で一周することを確認して」で済む。
これから
- 同じ高さで交わる / 重なるレールと道路を
validate_layoutで警告したい (交差レールも欲しい) - 車両の形状を新幹線 3 種と東急田園都市線にもう少し近づけたい
- 情景パーツはいくらでも欲しくなるので、たぶんずっと足し続ける
まとめ
プラレールを床に広げる代わりにブラウザで広げて、Claude に「ここに駅を置いて」と頼めるようになった。 ログインなしで触れるので、よければ遊んでみてほしい。
https://plasim-3d.polidogs.workers.dev/
パーツ定義とサンプルレイアウトは binzume/plarail-sim (MIT) を元にしている。感謝。