やってみた 2026年7月30日

WebGPUの3D建築エディタ「Pascal Editor」を使い捨てコンテナで動かす——monorepoとdev/prodで二度ハマった話

XECIN TypeScriptWebGPUReact Three Fibermonorepo

GitHubのトレンドを眺めていたら、pascalorg/editor が上のほうに来ていました。説明は「Create and share 3D architectural projects.」、中身は README を読むと React Three Fiber と WebGPU で作った3Dの建築エディタらしい。

自分はフロント寄りの実装が多いのですが、WebGPU をまともに使ったプロダクトのコードをローカルで動かした経験はまだ浅くて。ブラウザで3Dの建物を組めるツールが実際どのくらいの手数で立ち上がるのか、気になったので使い捨てのコンテナで動かしてみました。

結論から言うと、アプリは動いたし壁も引けたんですが、そこに辿り着くまでに monorepo の作法と「開発モードか本番ビルドか」で二回ハマりました。どちらも、公式ドキュメントの最短手順を自己流で崩したのが原因だったりします。

試した環境

いつも通り、ホスト(Windows)を汚さないように使い捨ての node:22-bookworm コンテナの中だけで動かしています。ブラウザのキャプチャだけはホスト側の Chrome から撮っています。

  • 対象: pascalorg/editor(commit 08e2279、TypeScript、MITライセンス、スター約19,800)
  • ランタイム: Node.js v22.23.1 / bun 1.3.14(package.jsonpackageManagerbun@1.3.0 ピン留め)
  • フレームワーク: Next.js 16.2(Turbopack)/ Turborepo 2.9.17 / three ^0.185 / @react-three/fiber ^9

リポジトリは Turborepo の monorepo で、apps/editor(Next.js アプリ)と packages/core packages/viewer packages/editor packages/nodes などに分かれています。SETUP.md のクイックスタートは bun install して bun dev、それだけ。

一つ目のハマり:アプリ単体を next dev したら @pascal-app/core が消える

bun install はかなり速くて、1386パッケージが実測で7秒くらい。node_modules は 1.3GB まで膨らみますが、bun のおかげで待たされる感じはなかったです。

自分は最初、公開ポートに出したいのもあって bun dev(= turbo run dev)ではなく、apps/editor の中で next dev を直接叩きました。そうしたら初回リクエストでいきなり500。

⨯ ./apps/editor/components/build-tab.tsx:3:1
Module not found: Can't resolve '@pascal-app/core'

原因を追うと、packages/core/package.jsonexports./dist/index.js を指していて、その dist がまだ生成されていませんでした。

// packages/core/package.json(抜粋)
"main": "./dist/index.js",
"exports": { ".": { "import": "./dist/index.js" } },
"scripts": { "build": "tsc --build", "dev": "tsgo --build --watch" }

turbo.json を見ると dev タスクに "dependsOn": ["^build"] が付いていて、bun dev なら依存パッケージのビルドが先に走る仕組みでした。公式手順の bun dev を崩してアプリ単体を起動したから、ワークスペースのパッケージが未ビルドで解決できなかった、という話なんですよね。ソースじゃなく dist を参照する設計だと、こういうときに黙って転びます。

先にパッケージだけビルドすれば解決します。

bunx turbo run build --filter="./packages/*"
# Tasks: 5 successful, 5 total  /  Time: 41.467s

これで packages/core/dist/index.js が出てきて、next dev も通るように。初回コンパイルは GET / 200 in 12.2s、起動ログにはビルトインのレジストリが46種類のノード(wall slab door window roof stair solar-panelhvac-equipment まで)を読み込んだと出ていました。建築系のプリミティブが最初から一通り揃っているのは、触る前からちょっとワクワクする作りです。

二つ目のハマり:開発モードだと3Dビューがローダーから戻ってこない

ところが、next dev でページは200になったのに、肝心の3Dビューがぐるぐる回るローダーのまま一向に表示されない。左のツールパレット(Wall / Slab / Door …)は出ているのに、キャンバスだけ真っ白でした。

コンソールを見ると WebGPU 由来のエラーは無く、唯一の赤はこれ。

WebSocket connection to 'ws://127.0.0.1:34130/_next/webpack-hmr...'
failed: Error during WebSocket handshake: net::ERR_INVALID_HTTP_RESPONSE [5 times]

念のため WebGPU が使えているかをブラウザ側で確認したら、アダプタもデバイスも普通に取れました。

const a = await navigator.gpu.requestAdapter();
const d = await a.requestDevice();   // 30msで成功
// vendor: "nvidia", architecture: "lovelace"

つまり WebGPU は問題なし。エディタ側のソースを追うと、ローダーの表示条件は showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady で、isViewerSceneReady が来ない場合でも8秒(SCENE_READY_FALLBACK_MS = 8000)でフォールバックして画面を出す作りになっていました。ところがその8秒フォールバックすら発火しない。

ここは自分の環境要因も混ざっていて断定はしづらいのですが、Docker のポートマッピング越しに Turbopack の HMR が張れず(上のWSエラー)、そのせいでコンポーネントの再マウントが繰り返され、8秒タイマーが毎回リセットされている、という筋に見えました。HMR は開発モードだけの仕組みなので、だったら本番ビルドで確かめればいい。

bunx next build   # 25秒、/ は静的ページとして生成
PORT=3000 bunx next start --hostname 0.0.0.0 --port 3000

これが効きました。本番サーバでリロードした瞬間、キャンバスが 300x150(HTMLのデフォルト)から 1256x1000 に広がり、ローダーは1秒以内に消えて、WebGPU の3D空間がちゃんと出てきました。

Pascal Editorの3D編集画面。中央に薄いグリッドの地面(Ground Floor 2.5m)が広がり、左端に壁・床・ドア・窓・階段・MEPなどの建築ツールパレット、上部にPan/Rotate/Zoomのカメラ操作ヘルプ、右下に「Snapping: Grid / Grid: 0.50m / Set wall start / end」という壁ツールのヒントが表示されている

同じアプリでも開発サーバと本番ビルドでここまで挙動が違うと、最初「動かないツールなのか」と勘違いしかけました。今思えば HMR が張れない環境で dev を粘ったのが遠回りで、素直に next build へ切り替えるべきだったなと。

ついでに分かった、ローカルとバックエンドの線引き

トップページ(/)は projectId="local-editor" で動く完全ローカルモードで、上部に「Local editor — scenes are not saved.」と出ます。保存は IndexedDB 頼みで、いわゆるお試し用ですね。

一方、そこにある「Create new / Open recent scenes」のリンク先 /scenes を開くと500になりました。

GET /scenes 500
[cause]: Error: connect ECONNREFUSED 127.0.0.1:34130

これはサーバコンポーネントが host ヘッダを使って自分自身(=ホスト側の公開ポート34130)へ fetch しにいく作りで、コンテナの中からはその番号が居ないので繋がらない、というものでした。加えて turbo.json のビルド環境変数に BETTER_AUTH_SECRET POSTGRES_URL NEXT_PUBLIC_SUPABASE_URL などが並んでいて、保存を伴うシーン機能は素の状態だとバックエンド前提。触って初めて「ローカルで完結するのは / のお試し編集まで」という線引きがはっきりしました。ここは自分の使い方(コンテナ+ポートマッピング)と噛み合わなかった部分でもあります。

実際に壁を引いてみる

お試しモードでも編集自体はちゃんと動きます。Wall ツールでグリッド上を4点クリックしてみたら、壁が立ち上がりました。スナップはグリッド0.50m刻み。

3D空間に立ち上がった3枚の壁。内側の面にスタッド(間柱)のフレーミングが見え、床の始点に緑色の頂点マーカーが表示されている。囲いの一角が開いた状態で、右側には壁ツールの継続モードのヒントが出ている

Scene パネルを開くと、Site > Building > Ground Floor(2.5m) > Structure の下に Wall 1 Wall 2 Wall 3 が並んでいて、4クリック=3セグメントぶんの壁ができていました。壁の内側にスタッド(間柱)のフレーミングまで描かれていて、単なる板ではなく構造を持ったモデルなのが伝わってきます。

途中で「囲いを閉じよう」と c キーを押したら、閉じるどころか継続モードが Room (auto-close) から Single wall にトグルされて、4本目が引かれず開いたまま止まりました。ヒント表示をちゃんと読んでいれば分かる挙動なんですが、キーの意味を勝手に決めつけて手を動かすと、こういう小さな事故が起きたりします。

試してみての所感

bun install から実際に壁を引くまで、ハマりを抜きにすれば手数は少ないツールでした。WebGPU が使える環境なら描画は速いし、建築プリミティブが最初から46種そろっているのも好印象です。

一方で、今回の学びはツール本体というより周辺のほうが大きくて。dist 参照の monorepo は公式手順(bun dev / turbo)に乗らないとすぐ解決エラーになること、そして HMR が張れない環境では開発サーバに固執せず本番ビルドで切り分けたほうが速い、という二点は、他の Turborepo + Next.js のプロジェクトでもそのまま効く教訓だと思います。

自分としては、まずローカルの / でUIの手触りを確かめる用途で様子を見つつ、保存やシーン共有まで踏み込むならバックエンド構成(認証・DB)ごと評価する、という進め方にしたいところ。もっと良い立ち上げ方や、dev モードで3Dビューを素直に出すコツがあれば教えてください。