サービスロボット導入シミュレーション Log#48 | 配膳ロボットの店を、ブラウザで試せる形に

この記事の素材(コード・ログ・データ 12ファイル・まとめてダウンロードあり)
AIで読む ChatGPT・Claude などで、この記事に質問できます

配膳ロボットの導入シミュレーションは、Unity を入れなくても、ブラウザだけで人とロボットを動かして数字まで出せた。サイトに置いてあるファミレスの店のデータ(卓・壁・通路のマス目)を three.js で3Dに描き、ロボットの台数・スタッフの人数・混み具合のボタンを押すと、その場で店の1日が回り直す試作を作った。ただし、いまのブラウザ版はロボットを増やすほど通路での詰まりが増え、3台・混んだ日には料理がほとんど届かなくなる日が出た。数字はまだ試算・Unity と並べられる段階ではない。

この試作でできるようになったことです。

  • 見る人が、自分で条件を変えて動きを見られる。ロボット0〜6台・スタッフ2〜4人・混み具合3段・6店を選ぶと、ロボットとスタッフと客の動きと、入店待ち・会計待ち・料理が届くまでの時間が変わる
  • 1日の中の好きな時刻へ、スライダーで戻ったり進んだりできる。同じ「日」なら何度見ても同じ動きになるので、詰まった場面を何度でも見直せる
  • 3Dの絵だけでなく、ロボットとスタッフが「いま何をしているか」(料理を運んでいる・皿を下げている・卓で説明している)をパネルで読める。ロボットが立ち往生すると、その頭の上に問い合わせへの吹き出しが出る
  • 重さ:ページは1ファイル 3.07MB(gzip で 0.97MB)。店のデータ6店分(2.3MB)を中に含む。サーバーの CPU では、ロボット2台の1日(店の時間で約3時間40分)を約20秒で回せた。Unity で同じ1日を回す時間とは比べていない
  • テスト版として公開した。https://pixelstomotors.com/sim/serving-robot-3d で誰でも触れる(入口はサイトの TOP と /series8)

始まりは、エイイチが見つけた ブラウザで動く水族館のシミュレーション でした。インストールなしで、ページを開くだけで魚の群れが3Dで泳ぐ。これが何で作られているかを開発者ツールで確かめると、three.js の r187 と WebGPU(ブラウザから GPU を直接使う新しい仕組み)で、1本の JavaScript にまとめたページでした。それを見たエイイチから、次の一言がありました。

このやり方でブラウザ版を作ってみたいね。WEB画面上でパネルの数値や条件変更するとロボットや人の動きが変わるのがいいね。

このシリーズのシミュレーションは、これまで Unity で動かしていました。Unity の動きは動画にして見せるしかなく、見る人が「ロボットを1台減らしたら?」と思っても、その場では試せません。サイトの試算(計算だけの見積もり)は数字をすぐ返しますが、通路でロボットが止まる・人がよける、といった動きは見えません。ブラウザ版は、その間を埋めるものです。

何をなぜどうなったか
水族館のシミュレーションの作りを、開発者ツールで確かめた
ブラウザだけで3Dを動かすやり方を知るため
three.js r187・WebGPU・1本にまとめた JavaScript。魚や水は、絵や3Dモデルのファイルを読まずにコードで作っていた
ブラウザ版の決まりを、サイト・Unity・ブラウザで決めるの3つに分けた
4つ目の「正」を作らないため
店の形・条件・数字の数え方はサイト、通路での動きは Unity と同じ、残り7つをチャットで決めた
道を引くマス目の細かさと、人の動きの速さを先に測った
ブラウザで重すぎないかを、作る前に知るため
2.5cm刻みのままで1本約30ms。人30人の10分が約2.6秒。同じ種で2回回して位置が一致
試作を作り、ロボットのにらみ合いと会計の詰まりを直した
動かすと、ロボット2台が向き合ったまま止まり、レジが空かなかった
下がってゆずる・止まった台を道の邪魔として扱う・会計は席で待つ、などで1日が最後まで回るようになった(3台・混んだ日はまだ詰まる)
画面を直した(見た目を Unity にそろえる・いまの仕事のパネル・時刻のスライダー・スマホ・問い合わせ・ロゴ)
初めて見る人が、何が起きているか分かるように
案を並べて選ぶ形で、4か所を決めた
ロボット0〜3台・2つの混み具合で、3日ずつ回した
試作の数字がどこまで使えるかを見るため
料理が届くまでは0台の約1.1分から3台の約3分へ延び、立ち往生が増えた

配膳ロボットを自分の店に入れたらどうなるかは、このシリーズの試算をもとにした別のページで、いまも数字で試せます。ブラウザ版は、その数字の裏で人とロボットがどう動くかを見せる側として作っています。

店の立体図(席・キッチン・配膳ロボットの通り道)導入前シミュレーション配膳ロボットを、導入前に自分の店舗でシミュレーション店舗の図面と人数から、何人で回るか・どこで止まるか・月いくらかかるかを出します

作業ログ ― 水族館のシミュレーションは three.js r187 と WebGPU で作られていた

エイイチが共有した水族館のシミュレーションの画面録画を見て、何で作られているかを推し量り、そのあとブラウザの開発者ツールで確かめました。開発者ツールは、ブラウザに付いている、ページの中身を調べる道具です(Chrome なら右クリックの「検証」)。

  • コンソール(開発者ツールの「Console」の欄)で THREE.REVISION と打つと 187 が返った。three.js の r187 を使っている
  • ネットワーク(「Network」の欄)でページを読み直すと、読み込まれる JavaScript は1本で、名前の形から Vite(JavaScript をまとめる道具)で1つにまとめたものだった
  • 魚・水・岩の 3D モデルや画像のファイルは読み込まれていなかった。形と模様はコードの中で作っている
  • 描画は WebGPU。WebGPU が使えないブラウザでは、three.js が WebGL2(前からある、ブラウザで3Dを描く仕組み)に切り替える

three.js は、ブラウザで3Dを描くための JavaScript の道具

three.js は、ブラウザの中で3Dの絵を描くための、無料で使える JavaScript の道具です。箱・円柱・光・カメラを置くと、それを画面に描いてくれます。この回のブラウザ版も three.js で描いています(r181。npm で入れた版)。

この記事で使う言葉意味
WebGPU
ブラウザから GPU(絵を描くための部品)を直接使う新しい仕組み。three.js では three/webgpu の WebGPURenderer で使う
WebGL2
前からある、ブラウザで3Dを描く仕組み。WebGPU が使えないときに three.js が自動で切り替える
OrbitControls
three.js に付いている、マウスや指でカメラを回す・寄る仕組み
1本にまとめる(バンドル)
何十個もの JavaScript のファイルを、1つのファイルにまとめること。この回は esbuild を使った

このプロジェクトでの使い方:店の卓・壁・ベンチは箱、ロボットは円柱、人はカプセル(体)と球(頭)で描きます。形と色は、Unity の ServiceSim のプレハブとマテリアルの値をそのまま読みました(後の節)。

recast-navigation は、人が歩ける道を作って動かす道具

recast-navigation-js は、床の形から「人が歩ける面」(NavMesh)を作り、その上で何人もの人をぶつからないように動かす道具です。Unity の人の動き(NavMesh)と同じ、Recast という仕組みをブラウザで使えるようにしたものです(中身は WebAssembly)。

この記事で使う言葉意味
NavMesh
床のうち人が歩ける所を、三角形の面の集まりで表したもの。卓や壁の所は抜けている
Crowd
NavMesh の上で、何人もの人を、互いにぶつからないように動かす仕組み
WebAssembly
ブラウザの中で、C++ などで書いたプログラムを速く動かす仕組み

このプロジェクトでの使い方:人(客・スタッフ)だけを NavMesh で動かし、ロボットは使いません。ロボットは、サイトの「走れるマス目」の上で自分で道を引きます。Unity でも人は NavMesh、ロボットはマス目と分けているので、同じ分け方にしました。

作業ログ ― ブラウザ版の決まりを、サイト・Unity・ブラウザで決めるの3つに分けた

この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。

ファイルの一覧(12本)

01_Unity_3.md ― 作業ログ ― ブラウザ版の決まりを、サイト・Unity・ブラウザで決めるの3つに分けた・3,851字

02_sec03.js.txt ― 道の長さと重さ(gridcheck.mjs)・3,023字

03_sec04.js.txt ― 動かして数える所(src/sim.js)・36,660字

04_sec04.js.txt ― 描く所(src/main.js)・16,636字

05_sec04.html.txt ― ページの型(shell.html)・28,145字

06_sec07.txt ― サムネイルの並べ方(tools/sim_thumbs/make.py に足した browser_frames)・3,048字

07_sec09.txt ― 入口の部品(src/components/BrowserSimTest.astro)・3,208字

08_sec09.txt ― /series8 に節を足した差分(src/pages/[series].astro・src/lib/records.ts)・4,241字

09_sec09.txt ― TOP に節を置き、解説動画を下げた差分(src/pages/index.astro・BrowserSimTest.astro)・4,731字

10_sec09.txt ― 描く所の差分(main.js:試算にそろえる・住所の条件・店の読み込み)・8,789字

11_sec09.js.txt ― 描く所の全文(tools/serving_sim_3d/src/main.js)・18,938字

12_sec09.txt ― ページの型の差分(shell.html:テスト版の札・来客のペースの説明・その他の選ぶ欄)・21,956字

作る前に、ブラウザ版がどの決まりで動くかを、サイトの試算のコード(tools/serving_sim/haizen_sim.py・試算 v2.9)と、Mac の ServiceSim の Unity のコード(読むだけ)から拾いました。このシリーズでは、店の形・条件・数字の数え方はサイトが正で、Unity は通路で起きることを動かして数える側です。ブラウザ版も Unity と同じ側に置き、サイトのデータをそのまま読みます。

決まりの一覧は、プロジェクトの文書(claude/browser_sim_rules.md)に残しました。下はその全文です。

01_Unity_3.md3,851字・71行・markdown
# ブラウザ版 配膳ロボットシミュレーション — 決まりの一覧(下書き v0)

作成:2026-11-13
読んだもの:サイト p2m-site の tools/serving_sim/haizen_sim.py(試算 v2.9)・assumptions.json・public/sim/serving-robot-params.json/Mac の ServiceSim の Assets/ServiceSim/(Robot・People・Service の Spec と主なコード。読むだけ)

ブラウザ版の位置付け(2026-11-13 決定):題材は配膳ロボット。ブラウザの中で人とロボットを動かし、数字も出す。
「何について正か」は今までどおり。店の形・条件・回し方の決まり・数字の定義はサイトが正。ブラウザ版は Unity と同じく「通路で起きること」を動かして数える側。
別のタブで開く保存

一覧の C で、エイイチが決めた3つ

一覧の C(ブラウザ版で決めること)のうち、次の3つはチャットでエイイチが決めました。

  • 立ち往生と判定する秒数は30秒(Unity の RobotSpec の既定値は60秒)

  • 「5秒止まったら7m以内の人がロボットを動かす」は、ブラウザ版に入れる

  • 瞬間移動はさせない。Unity にある、客を60秒で家へ戻す逃がし弁(guestWarpHomeSec)はブラウザ版には入れない

  • ねらい:詰まりを、確率や瞬間移動でごまかさずに、起きたまま数える

  • 変わったこと:詰まった場面は、人が来て動かすか、下がってゆずるまで残る。立ち往生は数として出る

  • 見込める効果:見る人が「どこで、なぜ詰まったか」を自分の目で見られる。その代わり、決まりの穴はそのまま数字に出る(後の節の3台の日)

作業ログ ― 道を引くマス目の細かさと、人の動きの速さを Node で測った

ブラウザで何台ものロボットが道を引き直すと重くなるかもしれない、人を何十人も動かすと遅いかもしれない、という2つを、画面を作る前にサーバーの CPU(Node 22)で測りました。店は③ 効率重視・入口が角(p1_efficient_corner)。測った値は、ブラウザではなくサーバーの CPU のものです。

道の長さと重さ(gridcheck.mjs)

受け渡し口から42卓すべてへの道を、マス目の細かさを変えて引き、サイトに載っている道の長さと比べました。

02_sec03.js.txt3,023字・27行・js
import fs from 'fs'; import zlib from 'zlib';
const [mapP, layP] = process.argv.slice(2);
const M = JSON.parse(fs.readFileSync(mapP)); const L = JSON.parse(fs.readFileSync(layP));
const fc0 = zlib.gunzipSync(Buffer.from(M.free_class,'base64'));
const {nx,ny,res} = M;
function down(fc,nx,ny,f){ const X=Math.floor(nx/f),Y=Math.floor(ny/f),o=new Uint8Array(X*Y);
  for(let y=0;y<Y;y++)for(let x=0;x<X;x++){let m=2;for(let j=0;j<f;j++)for(let i=0;i<f;i++)m=Math.min(m,fc[(y*f+j)*nx+x*f+i]);o[y*X+x]=m;} return [o,X,Y]; }
// binary heap A*
別のタブで開く保存

記事を書くときに回し直した出力です(ベンチマークと同時に回したので、最初に測ったとき(1本30ms・5cmで7ms)より少し遅い)。

cell 2.5cm grid 960x640: 42 routes, fail 0, 31.1 ms/route, |len-site| mean 0.01 m max 0.04 m
cell 5.0cm grid 480x320: 42 routes, fail 0, 5.8 ms/route, |len-site| mean 0.11 m max 0.17 m
cell 10.0cm grid 240x160: 42 routes, fail 0, 1.1 ms/route, |len-site| mean 0.20 m max 0.47 m

どの細かさでも行けない卓は0。2.5cm刻みでもサイトの道の長さとの差は平均0.01mで、1本約30ms。道の引き直しは0.5秒に1回までにしているので、Unity と同じ2.5cm刻みのままにしました。

人の動きの速さと、同じ種で同じになるか(crowdcheck.mjs)

import fs from 'fs'; import zlib from 'zlib'; import crypto from 'crypto';
import { init, Crowd } from 'recast-navigation'; import { generateSoloNavMesh } from '@recast-navigation/generators';
const [mapP, layP] = process.argv.slice(2);
const M=JSON.parse(fs.readFileSync(mapP)),L=JSON.parse(fs.readFileSync(layP));
const fc=zlib.gunzipSync(Buffer.from(M.free_class,'base64'));const f=4,X=M.nx/f,Y=M.ny/f,r=M.res*f;
const pos=[],idx=[];
for(let y=0;y<Y;y++){let x=0;while(x<X){const ok=(xx)=>{for(let j=0;j<f;j++)for(let i=0;i<f;i++)if(!fc[(y*f+j)*M.nx+xx*f+i])return false;return true;};
  if(!ok(x)){x++;continue;}let e=x;while(e+1<X&&ok(e+1))e++;const b=pos.length/3;const x0=x*r,x1=(e+1)*r,z0=y*r,z1=(y+1)*r;
  pos.push(x0,0,z0,x1,0,z0,x1,0,z1,x0,0,z1);idx.push(b,b+2,b+1,b,b+3,b+2);x=e+1;}}
await init();
let t=performance.now();
const {success,navMesh}=generateSoloNavMesh(new Float32Array(pos),new Uint32Array(idx),{cs:0.05,ch:0.05,walkableRadius:2,walkableClimb:2,walkableHeight:30});
console.log('navmesh',success,(performance.now()-t).toFixed(0),'ms, tris',idx.length/3);
const stops=L.tables.filter(t=>t.stop).map(t=>t.stop);
function run(seed){
  const crowd=new Crowd(navMesh,{maxAgents:40,maxAgentRadius:0.3});let s=seed>>>0;const rnd=()=>{s=(Math.imul(s,1664525)+1013904223)>>>0;return s;};
  const ag=[];for(let i=0;i<30;i++){const p=stops[rnd()%stops.length];const a=crowd.addAgent({x:p[0],y:0,z:p[1]},{radius:0.2,height:1.7,maxSpeed:1.0,maxAcceleration:2.0});ag.push(a);}
  const steps=20*600;let t0=performance.now();
  for(let k=0;k<steps;k++){if(k%200===0)for(const a of ag){const p=stops[rnd()%stops.length];a.requestMoveTarget({x:p[0],y:0,z:p[1]});}crowd.update(1/20);}
  const ms=performance.now()-t0;const h=crypto.createHash('sha1');for(const a of ag){const p=a.position();h.update(new Float64Array([p.x,p.z]));}
  crowd.destroy();return {hash:h.digest('hex').slice(0,12),ms};}
const a=run(7),b=run(7),c=run(8);
console.log('seed7 run1',a.hash,'run2',b.hash,'same:',a.hash===b.hash,'| seed8',c.hash,`| 30 agents x 10 min sim-time: ${a.ms.toFixed(0)} ms`);
navmesh true 106 ms, tris 952
seed7 run1 364991238f00 run2 364991238f00 same: true | seed8 e49b95b63825 | 30 agents x 10 min sim-time: 2589 ms

店の床から NavMesh を作るのに約0.1秒、30人が店の時間で10分動くのが約2.6秒(約230倍の速さ)。同じ種(seed7)で2回回すと、全員の位置から作った値(ハッシュ)が一致しました。ブラウザや端末をまたいで同じになるかは、まだ確かめていません。

作業ログ ― 試作を作った:人は NavMesh、ロボットはマス目で動かし、数える所と描く所を分けた

試作は2つのファイルに分けました。sim.js は動かして数える所で、画面のことを何も知りません。main.js は three.js で描く所で、sim.js が出す1秒ごとの記録を読んで、間をなめらかにつないで描きます。

  • 1コマは0.05秒で固定。ロボットと人の動き・来客・料理・会計は、すべてこの刻みで進む
  • 乱数は種(日)から作る。来客の時刻・組の人数・料理ができる時間・食べる時間は、ロボットとは別の乱数にした。ロボットの台数を変えても、同じ日なら同じお客さんが同じ時刻に来る
  • 画面は、1コマ描くごとに約10ms ぶん先を計算し、1秒ごとの記録を貯める。スライダーは、計算が済んだ所までなら好きな時刻へ動かせる

ロボットの決まり(sim.js の RULES と、動きの順番)

  • 前のセンサーは0.6m先・幅0.5m。残りの道より先は見ない(道の終わりの先にいる人で止まらない)
  • 前がふさがって3秒たてば、回り込む道を探す
  • 5秒止まったら、7m以内の手の空いたスタッフが動かしに来る
  • 30秒動けなければ立ち往生。救出はスタッフの仕事の中でいちばん先(順番0)で、まだ持ち場から動き出していないスタッフの仕事を取り上げてでも行く
  • ロボットどうしが向き合ったら、料理を載せた台が先。どちらも空なら番号の大きい台がゆずり、近くの空いた所まで下がる(瞬間移動はしない)
  • 狭い通路(マス目の1)には、1台ずつしか入らない
  • 往復が終わったら待機場所へ戻る
  • 人はロボットにゆずる。スタッフはすぐ、客は1秒たってから
  • 卓や待機場所の近くまで来て、30秒進めなければ、着いたことにする

店の回し方(サイトの値)

  • 来客は、標準ファミレス35卓で 25・35・45 組/時(空いている・普通・混んでいる)を、卓の数に合わせて増やす。③は42卓なので、普通で42組/時、混んでいるで54組/時。ピークの120分だけ来る
  • 1組の人数は 1〜4人を .20・.35・.20・.25 の割合で(Unity の ServiceSpec)
  • 来たら待合で待ち、スタッフが卓へ案内する(45秒)
  • 料理は2皿。1皿目は5〜12分(いちばん多いのは8分)、2皿目は8〜18分(12分)でできる
  • 料理はロボットが先。2分待ってもロボットが来なければ、スタッフが運ぶ
  • 食べる時間は20〜45分(30分)
  • 会計は、レジが空くまで席で待ち、空いたらレジへ行って90秒
  • 帰ったあと、片付け40秒と卓拭き40秒。皿は受け渡し口へ運ぶ

動かすと出た詰まりと、直したこと

試作を動かすと、決まりどおりに書いたつもりでも、いくつも詰まりました。直した順です。

起きたこと原因直したこと
ロボット2台が向き合ったまま、横へよけ合って動かない
どちらも同じ側へよけ、また向き合う
ゆずる側を決め(料理を載せた台が先・空なら番号の大きい台)、ゆずる側は近くの空いた所まで下がる
下がった台の前に、また相手が来る
下がる距離が足りない
ゆずった台に、もっと離れるよう頼み直す
止まった台を、ほかの台の道が通る
道を引くとき、止まっている台を見ていなかった
止まっている台を、道を引くときの邪魔として扱う
道の終わりの先にいる人で止まる
センサーが、残りの道より先まで見ていた
センサーの長さを、残りの道までに切る
立ち往生しても、スタッフが来ない
スタッフが別の仕事を始めていた
救出をいちばん先にし、まだ動き出していないスタッフの仕事は取り上げる
誰も会計を済ませない
レジの後ろの立ち位置が、カウンターの中(歩けない所)にあった
レジの後ろの歩ける部屋に置き直した
トイレ・ドリンクバーへ立った客が動かない
席の点(NavMesh の外)から歩き出させていた
卓の前の立ち位置から歩き出させる
レジの前に客がたまり、通路をふさぐ
会計に立つ客を、空くのを待たずにレジへ向かわせていた
レジが空くまで席で待つ(サイトの試算 v2.8 と同じ)
卓の近くで、着かずに回り続ける
着いたかの判定が厳しすぎた
30秒進めなければ、着いたことにする

それでも、ロボットが2台以上で混んだ日には、まだ詰まりが残ります(後の節の数字)。

動かして数える所(src/sim.js)

03_sec04.js.txt36,660字・413行・js
// ブラウザ版 配膳ロボットシミュレーション — 動きと数えるところ(画面とは分ける)
// 決まりの一覧:Project の claude/browser_sim_rules.md
// 店の形・マス目はサイト(public/sim/layouts/unity/)のものをそのまま読む。
import { init as recastInit, Crowd } from 'recast-navigation';
import { generateSoloNavMesh } from '@recast-navigation/generators';

export const DT = 0.05; // 1コマ 0.05 秒(固定)
別のタブで開く保存

描く所(src/main.js)

04_sec04.js.txt16,636字・190行・js
import * as THREE from 'three/webgpu';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { createSim, DT, CROWD } from './sim.js';
import p0 from '../stores/p0_memory.json'; import p0m from '../stores/p0_memory_map.json';
import p1 from '../stores/p1_efficient_corner.json'; import p1m from '../stores/p1_efficient_corner_map.json';
import p2 from '../stores/p2_efficient_narrow.json'; import p2m from '../stores/p2_efficient_narrow_map.json';
import p3 from '../stores/p3_relaxed_center.json'; import p3m from '../stores/p3_relaxed_center_map.json';
import p4 from '../stores/p4_relaxed_side.json'; import p4m from '../stores/p4_relaxed_side_map.json';
別のタブで開く保存

ページの型(shell.html)

/*BUNDLE*/ の所に、esbuild でまとめた JavaScript を入れて1ファイルのページにします。

05_sec04.html.txt28,145字・90行・html
<!DOCTYPE html>
<html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Noto+Sans+JP:wght@400;600;700&display=swap" rel="stylesheet">
<title>配膳ロボット ブラウザ版(試作)</title>
<style>
:root{--ink:#1d1d1b;--sub:#6b6b66;--line:#d8d6d0;--bg:#f2f1ee;--accent:#2f6fd6}
*{box-sizing:border-box}body{margin:0;font-family:"Noto Sans JP","Inter",system-ui,sans-serif;color:var(--ink);background:var(--bg);height:100vh;display:grid;grid-template-columns:1fr 320px}
#view{position:relative;min-height:0}#view canvas{display:block}
別のタブで開く保存

1本にまとめるコマンド

# ブラウザ版の試作を1本にまとめる(作業フォルダ /home/claude/proto)
cd /home/claude/proto
npm install three@0.181.2 recast-navigation@0.44.0 esbuild@0.28.2
npx esbuild src/main.js --bundle --format=esm --minify --loader:.json=json --external:node:zlib --outfile=dist/app.js
python3 - <<'PY'
s = open('shell.html').read()
js = open('dist/app.js').read()
open('dist/index.html', 'w').write(s.replace('/*BUNDLE*/', js))
PY
ls -la dist/app.js dist/index.html

店のデータは、サイトの public/sim/layouts/unity/ の6店(p0_memory〜p5_crammed と、それぞれの _map.json)を stores/ に写して使っています。サイトの店の形の版は 9b8dae7ebf8c です。

作業ログ ― 画面を直した:Unity の色と形・いまの仕事のパネル・時刻のスライダー・スマホ・問い合わせ・ロゴ

試作は、まず点と箱だけで動きを確かめ、そのあと画面を作り込みました。ここからは、エイイチが画面を見て出した指示と、それで変えたことです。

人の色と形を、Unity のプレハブとマテリアルの値にそろえた

最初の画面では、客とスタッフの色と形を推し量って描いていました。Unity の ServiceSim のものと違う、と指摘を受け、Unity のプレハブ(人やロボットの型)とマテリアル(色)の値を読んで、そのまま使いました。

物
色(Unity の _Color)
形
客#facc21(黄)カプセルの体+球の頭
スタッフ#1761e6(青)カプセルの体+球の頭
待合の客(1組に1人)#8c4dbf(紫)同じ
ロボット#f5f5f7(白)
円柱+前の向きの印 #ff540a
床・壁・卓・ベンチ・椅子・背もたれ
#dbd6c9・#666e7a・#cc7d36・#9e544a・#215e6b・#8a909a
箱

ロボットとスタッフの「いまの仕事」を、3Dの右下に重ねて出した

ロボットが何をしているかわからないのでパネルで表示させて(料理運んでる?お皿下げてる?)

ロボットとスタッフの番号ごとに、いまの仕事を1行で出すパネルを作りました(「卓T34で料理を渡している(あと20秒)」「受け渡し口へ料理を取りに行く(卓T35の分)」「持ち場で待つ」など)。置き場所は、左の列を作らない条件で3案(右の操作パネルの一番上・3Dの下に横長の帯・3Dの右下に重ねる)を出し、3Dの右下に重ねる案になりました。

  • ねらい:3Dから目を離さずに、ロボットが何をしているかを読めるようにする
  • 変わったこと:3Dの中の番号(1・2・S1〜S3)と、パネルの行が対になった
  • 見込める効果:詰まった場面で「どの台が、何を運ぼうとして止まったか」を、映像と文の両方で追える(見込み。見る人に確かめてはいない)

スペースキーを押しながらドラッグで、3Dの位置を動かせるようにした

ドラッグはカメラを回す、ホイールは寄る、に加えて、スペースキーを押しながらドラッグすると3Dの絵を平行に動かせるようにしました。スマホは2本指で寄る・動かすです。

店の名前を副題にして、6店を切り替えられるようにした

題名は「配膳ロボット ブラウザ版(試作)」にし、店の名前(③ 効率重視・入口が左下の角 など)をその下の選ぶ欄にしました。選ぶと、その店のデータを読み直して1日を回し直します。

時間とともに、席の客が増えたり減ったりするようにした

時間経過とともにお客さんの入り方が変わる(席のお客さんが減ったり、増えたり)のはできる?

最初の試作は、決まった数の客が席にいるだけでした。サイトの試算と同じく、空の店から始めてピークの120分だけ来客し、食べ終えた組から会計して帰り、全員が帰るまで回す形にしました。いまの数字のパネルの「使っている卓」(例 11/42卓)が、時間とともに増えて減ります。

1日の好きな時刻へ、スライダーで動けるようにした

3Dの下にスライダーを置きました。計算は画面の裏で先へ進み、計算が済んだ所(「計算中…(43:53 まで)」)までなら、好きな時刻へ戻ったり進んだりできます。1秒ごとの記録の間を、なめらかにつないで描きます。

「種」を「日を変える」に言い換えた

種のことが初見の人はなんのことかわかんないね。

乱数の種(seed)を入れる欄を、「日を変える」の ‹ 1日目 › に置き換え、「条件はそのままで、お客さんの来る時刻や人数が違う日を見られます。同じ日なら、何度見ても同じ動きになります。」と書き添えました。

スマホでは、3D・スライダー・いまの仕事・操作の順に縦に並べた

幅760px 以下では、3Dを画面の高さの45%にし、その下にスライダー・カメラのボタン・いまの仕事・色の説明・操作を縦に並べます。起きたことの時刻の数字がスマホで明朝体になっていたので、Inter と Noto Sans JP をページで読むようにしました。

スマホ(幅390px)で見たブラウザ版の全体。上から題名と店を選ぶ欄、3D、時刻のスライダー、カメラのボタン、いまの仕事、色の説明、操作のボタン、日を変える、いまの数字

問い合わせの入口は、ロボットが止まった瞬間にその頭の上へ出す

問い合わせの入口は、3Dの上に半透明で小さく置く、という指示で、まず3案、続けて別案4つを出しました。選ばれたのは「問題が起きた瞬間に出る」案で、その見た目をさらに4案出し、ロボットの頭の上の吹き出しになりました。吹き出しは、立ち往生したロボットについて動き、少なくとも6秒出します。押すと、サイトのメッセージのページ(話題はフィジカルAI)が開きます。

  • ねらい:ずっと出ている「相談する」のボタンにせず、見る人が「うちの店ならどうなる?」と思う所でだけ入口を出す
  • 変わったこと:入口が出るのは、ロボットが30秒止まって立ち往生した時だけ
  • 見込める効果:押されたかは、公開したあとのメッセージのページの記録で確かめる(いまは試作で公開していないので、数字はない)

ロゴは、右の操作パネルの一番上(スマホはページの一番下)

Pixels to Motors のロゴは、最初に題名の札の中に入れましたが、置き場所がいまいちと言われ、6案を出して、右の操作パネルの一番上(スマホはページの一番下)になりました。あわせて、操作パネルの説明の文(この試作について)を外し、いまの数字の行の間を詰めました。

4か所で並べた案と、選んだ案です。

いまの PC の画面です。

PC で見たブラウザ版。③ 効率重視・入口が左下の角、ロボット2台・スタッフ3人・混んでいる、店の時間で8分52秒。右上に色の説明、右下にいまの仕事、右の列に操作といまの数字、3Dの下に時刻のスライダー

⓪の店(記憶の店)に切り替えた所です。

ブラウザ版で⓪の店を斜め左30°から見た所。ロボット3台・混んでいる

作業ログ ― ロボット0〜3台・2つの混み具合で、1日ずつ回した数字

画面を作り終えたあと、試作の数字がどこまで使えるかを見るため、画面なしで1日を回しました。店は③、スタッフ3人、ロボット0〜3台、混み具合は普通と混んでいる、日は1〜3日目。店の時間で5時間(300分)たっても全員が帰り終わらなければ、そこで打ち切ります。

回したコード(bench506.mjs)

import fs from 'fs'; import { createSim } from './src/sim.js';
const L=JSON.parse(fs.readFileSync('layout.json')),M=JSON.parse(fs.readFileSync('map.json'));
console.log('robots,crowd,staff,day,end_min,ms,groups,seat_wait_min,pay_wait_min,dish_wait_min,delivered,by_robot,sensor_stops,detours,helps,stucks');
for (const crowd of ['normal','busy']) for (const robots of [0,1,2,3]) for (const seed of [1,2,3]) {
  const sim=await createSim(L,M,{robots,staff:3,crowd,seed}); const t0=performance.now();
  for(let k=0;k<20*60*300;k++){ sim.step(); if (k%200===0 && sim.summary().allDone) break; }
  const s=sim.summary();
  console.log([robots,crowd,3,seed,(sim.t/60).toFixed(0),(performance.now()-t0|0),sim.groups.length,(s.avgSeatWait/60).toFixed(1),(s.avgPayWait/60).toFixed(1),(s.avgDishWait/60).toFixed(1),s.delivered,s.byRobot,s.sensorStops,s.detours,s.helps,s.stucks].join(','));
}

出力(bench506.csv)

列は、ロボットの台数・混み具合・スタッフ・日・終わった店の時間(分)・かかった時間(ms)・来た組・入店待ちの平均(分)・会計待ちの平均(分)・料理ができてから届くまでの平均(分)・届けた料理・うちロボット・センサーで止まった回数・回り込み・人が動かしに行った回数・立ち往生。

robots,crowd,staff,day,end_min,ms,groups,seat_wait_min,pay_wait_min,dish_wait_min,delivered,by_robot,sensor_stops,detours,helps,stucks
0,normal,3,1,300,5826,113,31.4,14.4,1.2,226,0,0,0,0,0
0,normal,3,2,220,3775,85,8.0,19.4,1.1,170,0,0,0,0,0
0,normal,3,3,208,3670,85,8.1,12.4,1.1,170,0,0,0,0,0
1,normal,3,1,268,16451,113,24.6,22.6,2.4,226,154,133,12,3,1
1,normal,3,2,222,10548,85,5.2,21.5,2.4,170,116,96,8,1,0
1,normal,3,3,214,10460,85,6.6,15.2,2.3,170,114,121,11,5,3
2,normal,3,1,271,21462,113,32.5,19.6,2.5,226,172,533,140,69,41
2,normal,3,2,223,20672,85,8.0,19.9,2.3,170,134,586,66,30,19
2,normal,3,3,222,18166,85,7.2,20.1,2.3,170,119,546,103,55,20
3,normal,3,1,300,32467,113,33.8,19.2,3.4,226,181,1226,475,239,111
3,normal,3,2,236,29608,85,10.6,22.0,3.2,170,111,1179,185,99,33
3,normal,3,3,300,28453,85,20.2,12.0,2.8,168,45,1431,972,454,111
0,busy,3,1,300,6812,140,55.3,19.4,1.2,280,0,0,0,0,0
0,busy,3,2,263,4832,113,26.8,19.3,1.2,226,0,0,0,0,0
0,busy,3,3,241,4127,102,19.8,18.0,1.2,204,0,0,0,0,0
1,busy,3,1,300,16961,140,49.0,26.3,2.3,280,202,167,22,3,2
1,busy,3,2,264,13011,113,23.4,23.8,2.3,226,157,189,17,2,1
1,busy,3,3,248,10486,102,16.2,24.9,2.2,204,139,149,11,0,0
2,busy,3,1,300,27036,140,56.3,26.3,2.5,280,193,903,164,69,43
2,busy,3,2,274,26410,113,25.3,26.1,2.7,226,144,961,106,50,28
2,busy,3,3,248,26016,102,18.1,23.6,2.5,204,143,807,102,50,29
3,busy,3,1,300,40292,140,77.7,42.0,3.3,264,176,2141,330,127,67
3,busy,3,2,300,50594,113,5.9,0.0,16.6,85,19,1319,1106,988,417
3,busy,3,3,300,29648,102,1.9,1.5,2.5,96,19,720,14,9,5

数字から分かったこと

  • 料理ができてから届くまでは、0台で約1.1〜1.2分、1台で約2.2〜2.4分、2台で約2.3〜2.7分、3台で約2.5〜3.4分(混んだ日の2日目は16.6分)。ロボット優先の決まり(2分待ってもロボットが来なければ人が運ぶ)があるので、ロボットを入れると、手の空いたスタッフがすぐ運ぶ0台より遅くなる
  • 立ち往生は、1台では1日0〜3回、2台では19〜43回、3台では5〜417回。台数を増やすと、ロボットどうしの詰まりが急に増える
  • 3台・混んだ日の2日目は、届けた料理が85皿(0台では226皿)で、店が5時間たっても空にならなかった。ロボットが通路で詰まり、人も料理もそこを通れなくなった
  • 0台でも、1日目(来客113〜140組)は5時間で打ち切りになった。入店待ちが31〜55分になり、会計待ちも12〜19分と長い。ロボットとは別に、会計とスタッフの回し方にも、試算と合っていない所がある
  • 1日を回す時間は、0台で約4〜7秒、2台で約18〜27秒、3台で約28〜51秒(サーバーの CPU)。台数と詰まりが増えるほど、道の引き直しが増えて重くなる

ブラウザ版の試作は、条件を変えて動きを見る道具としては動いた。しかし、ロボットを増やしたときの詰まり方と会計待ちが、サイトの試算 v2.9・Unity の基準データとまだ合わない。 数字をサイトで見せるのは、答え合わせをしてからにします。

作業ログ ― 公開したあと:サムネイルを、ブラウザで動く画面だと分かる絵に替えた

記事を公開したあと、スマホで記事を開いたエイイチから指摘がありました。

サムネイルがブラウザ版をイメージできてないね

最初のサムネイルは、ブラウザ版で描いた店の3Dを、大1枚・小2枚で並べたものでした。店の絵としては Unity の回のサムネイルと見分けがつかず、「ブラウザで、自分で条件を変えて動かせる」ことが伝わっていませんでした。

最初のサムネイル。ブラウザ版で描いた店の3Dを、左に大きく1枚、右に小さく2枚並べた

作り直したサムネイルは、ブラウザの窓の中に PC で見たブラウザ版の画面全体(ロボットの台数・混み具合のボタン、いまの数字、時刻のスライダー、いまの仕事のパネル)を入れ、右にスマホで見た同じ画面を置きました。最初の作り直しではスマホが PC の操作の列に重なったので、窓を少し狭くして重ならないようにしました。

作り直したサムネイル。左はブラウザの窓に入れた PC の画面、右はスマホの画面
  • ねらい:一覧で見た人に、ボタンを押して試せる画面だと一目で伝える
  • 変わったこと:絵の主役が「店の3D」から「操作のある画面」になった
  • 見込める効果:記事一覧と SNS で、Unity の回との違いが伝わる(見込み。押された数は、サイトの記録で前後を比べる)

サムネイルの並べ方(tools/sim_thumbs/make.py に足した browser_frames)

06_sec07.txt3,048字・44行・diff
commit 01515b5ed2ce9e59dc4a3fd9221a04b0206ca5fb
Author: Claude <noreply@anthropic.com>
Date:   Sat Nov 14 07:20:53 2026 +0900

    LOG506 のサムネイルを、ブラウザの窓の PC の画面とスマホの画面を並べる形に替えた(ブラウザ版だと分かるように)。sim_thumbs に browser_frames を足す
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LevEXU3Z3M9LXKJxzqN9Ew
別のタブで開く保存

作業ログ ― ブラウザ版の入口を、サイトのどこに置くかを3回考え直した

記事の公開のあと、ブラウザ版のリンクをサイトに置く形を相談されました(サイトへの反映はしない、という条件つき)。案は3回出し直しています。

最初の案は、試算の結果の下にリンク、/series8 に入口、LP は答え合わせのあと、TOP には出さない、というものでした。これに対して、次の問いが返ってきました。

現行サイトをよく調査しましたか?

実際には、LP の見出しの行と /series8 のテンプレートの数行しか見ていませんでした。/series8・試算のページ(/sim/serving-robot)・LP を、公開されている画面とコードの両方で見直すと、次のことが分かりました。

分かったこと案への影響
試算の「店の形を選ぶ」の ②〜⑦ は、ブラウザ版の6店(p0〜p5)とまったく同じ店
店ごとにつなげられる
試算には店ごとに「平面図/Unity で動かすと」の切り替えがあり、Unity は録画した回の条件のまま(試算で選んだ人数・台数とは別)
ブラウザ版は選んだ条件のまま動く。そこが違い
試算は LP の「まず、近い店舗で試す」に ?lp=1 で埋め込まれている
試算にリンクを足すと LP にも出る
試算は /series8 にも埋め込まれている
/series8 に別の入口を作らなくても出る
混み具合の言葉(試算は「混雑」)、台数(試算は0〜3台+その他)、同じ条件の数字(料理が届くまで 試算1分03秒・ブラウザ版約2.3分)がずれている
そろえないと同じ画面で食い違う

そこで、試算の切り替えの3つ目に「ブラウザで動かす」を足す案を出しました。その答えが次の指示です。

あくまでもまだテスト版というのをベースにしましょう。言葉と幅を試算に揃えるのは行ってください。

試算の中に置くと、同じ条件で試算と違う数字が並びます。テスト版のうちは、試算・LP・TOP には置かず、研究の記録を見に来た人が見つけて触れる /series8 に節を1つだけ置く案にし、その形で公開しました。立ち往生の吹き出しの行き先は、メッセージのページのままです。

公開後、TOP にも置くよう指示がありました。

TOPにも置くようにしてください。なるべく上の方

FV(いちばん上の動画の画面)には「導入シミュレーション」と「研究室」の2枚の入口があり、3枚目に足すと見出しと同じ重さに見えます。そこで FV のすぐ下、スクロールして最初に出る節にしました。続けて、解説動画を下げてよいと指示がありました。

YouTubeもっと下に下げていいよ(マンガの近く)

TOP の並びは、FV → ブラウザで動かす(テスト版)→ Pixels to Motors の1年 → 導入前ガイド → 解説動画 → 4コママンガ になりました。

  • ねらい:テスト版を、決まった試算や LP の数字と並べずに、見つけた人が触れる形で出す
  • 変わったこと:入口は /series8 の節と TOP の FV の下の2か所。試算・LP には置いていない
  • 見込める効果:TOP と /series8 のどちらから何回開かれたかを、data-ev の「TOP:」の有無で分けて数えられる(公開直後なので、まだ数字はない)

作業ログ ― ブラウザ版を試算にそろえ、テスト版として /sim/serving-robot-3d に公開した

試算にそろえたこと

項目前後(試算と同じ)
店の名前
③ 効率重視型・入口が左下の角 など(店のデータの name)
③ 効率重視・入口が角 など(試算の short)
スタッフ2・3・4人
店ごとの3つのボタン+その他(③なら3・4・5人、その他で2〜8人)
ロボット0〜6台のボタン
0〜3台のボタン+その他(4〜6台)
混み具合空いている・普通・混んでいる空いている・普通・混雑
来客のペース
標準35卓の 25・35・45組/時を卓の数で増やす
試算の店ごとの値(③なら30・42・54組/時)
題名
配膳ロボット ブラウザ版(試作)
配膳ロボット ブラウザ版[テスト版]の札
いまの数字説明なし
「テスト版の数字です。試算(v2.9)とは別に、人とロボットを動かして数えています(答え合わせ前)」

あわせて、住所に条件を付けて開けるようにしました(例 /sim/serving-robot-3d?store=p1&staff=3&robots=2&crowd=normal&day=1)。答え合わせが済んで試算からつなぐときに、選んでいる条件のまま開くためです。店を読み込んでいる間に条件を変えると変更が捨てられていたので、読み終えてからもう一度建て直すようにしました。

サイトでは、店のデータをサイトの公開データから読む

チャットの画面で試すときは6店のデータをページの中に含めていましたが(3.07MB)、サイトではサイトの公開データ(public/sim/layouts/unity/)を、店を選んだときに読みます。試算・Unity と同じ正を読むことになり、ページは 1.7MB になりました。どちらにするかは、まとめるときに p2m-stores の行き先で選びます。

JavaScript を動かさない読み手(検索・AI)向けに、ページの説明、正規の住所、SNS 用の画像、何のページか・動きの決まり・数字の定義を書いた文(noscript)を足しました。

公開したテスト版のページ。⑤ ゆったり・入口が正面、スタッフ4人・ロボット5台(その他から選択)・混雑。題名の横にテスト版の札、右の列の混み具合の下に来客のペース

/series8 と TOP の入口の節

/series8 の「ブラウザで動かす(テスト版)| Test」の節。左に説明・まだのこと3つ・テスト版を開くボタン・LOG506 へのリンク、右に PC とスマホの画面の絵
TOP の FV のすぐ下に置いた同じ節

入口の部品(src/components/BrowserSimTest.astro)

07_sec09.txt3,208字・46行・astro
---
// サービスロボット導入シミュレーション(/series8)の「ブラウザで動かす(テスト版)」(2026-11-14・LOG506)
// 配膳ロボットのブラウザ版(/sim/serving-robot-3d)への入口。テスト版なので、入口はこの1か所だけ(試算・LP・トップには置かない)。
// 試算・Unity と答え合わせが済んだら、試算の「平面図/Unity で動かすと」の隣に移す(住所に条件を付けて開ける作りは入れてある)。
// TOP にも置く(2026-11-14 エイイチの指示「TOPにも置くようにしてください。なるべく上の方」)。FV のすぐ下・解説動画の前。押された数を分けて数えるため、place で data-ev の頭を変える
const { place = 'series8' } = Astro.props as { place?: 'series8' | 'top' };
const href = '/sim/serving-robot-3d';
const ev = (t: string) => (place === 'top' ? `TOP:${t}` : t);
別のタブで開く保存

/series8 に節を足した差分(src/pages/[series].astro・src/lib/records.ts)

08_sec09.txt4,241字・51行・diff
commit e01906cede777f66e7a741e732db011d06ee4c92
Author: Claude <noreply@anthropic.com>
Date:   Sat Nov 14 07:40:55 2026 +0900

    配膳ロボットのブラウザ版(テスト版)を /sim/serving-robot-3d に公開し、/series8 に入口の節「ブラウザで動かす(テスト版)」を足した。店の名前・人数・混み具合・台数を試算にそろえ、店のデータは public/sim/layouts/unity/ を読む。コードは tools/serving_sim_3d/
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LevEXU3Z3M9LXKJxzqN9Ew
別のタブで開く保存

TOP に節を置き、解説動画を下げた差分(src/pages/index.astro・BrowserSimTest.astro)

09_sec09.txt4,731字・82行・diff
commit 244b497a8677aee1ff3feb073c1797a3dac65195
Author: Claude <noreply@anthropic.com>
Date:   Sat Nov 14 07:50:49 2026 +0900

    TOP:FV のすぐ下に配膳ロボットのブラウザ版(テスト版)の入口を置き、解説動画を4コマの前へ下げた。入口の押された数は TOP と /series8 で分けて数える
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LevEXU3Z3M9LXKJxzqN9Ew
別のタブで開く保存

描く所の差分(main.js:試算にそろえる・住所の条件・店の読み込み)

10_sec09.txt8,789字・115行・diff
--- /tmp/main.bak.js	2026-11-14 07:32:29.557516637 +0900
+++ tools/serving_sim_3d/src/main.js	2026-11-14 07:38:43.166492010 +0900
@@ -1,18 +1,33 @@
 import * as THREE from 'three/webgpu';
 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
 import { createSim, DT, CROWD } from './sim.js';
-import p0 from '../stores/p0_memory.json'; import p0m from '../stores/p0_memory_map.json';
-import p1 from '../stores/p1_efficient_corner.json'; import p1m from '../stores/p1_efficient_corner_map.json';
別のタブで開く保存

描く所の全文(tools/serving_sim_3d/src/main.js)

11_sec09.js.txt18,938字・224行・js
import * as THREE from 'three/webgpu';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { createSim, DT, CROWD } from './sim.js';
import { loadStore } from 'p2m-stores'; // 店のデータ:試作(チャットの画面)は中に含む・サイトは public/sim/layouts/unity/ を読む(build のときに選ぶ)
// サイトの公開データ(public/sim/layouts/unity/)の6店
const STORE_KEYS = ['p0', 'p1', 'p2', 'p3', 'p4', 'p5'];
let layout = null, map = null;
別のタブで開く保存

動かして数える所の差分(sim.js:混雑の言葉・試算の来客のペース)

--- /tmp/sim.bak.js	2026-11-14 07:32:29.559256711 +0900
+++ tools/serving_sim_3d/src/sim.js	2026-11-14 07:38:43.166521375 +0900
@@ -23,7 +23,7 @@
 export const CROWD = {
   empty:  { arrivalsPerH35: 25, label: '空いている' },
   normal: { arrivalsPerH35: 35, label: '普通' },
-  busy:   { arrivalsPerH35: 45, label: '混んでいる' },
+  busy:   { arrivalsPerH35: 45, label: '混雑' },
 };
 // サイトの試算・Unity の値(決まりの一覧 A・B)
 export const STORE = {
@@ -103,7 +103,7 @@
     this.tableState = new Map(this.tables.map(t => [t.id, { t, state: 'free', group: null }])); // free / taken / dirty / clearing
     this.seated = []; this.groups = []; this.waiting = []; this.payQueue = []; this.jobs = [];
     this.queue = []; // 受け渡し口で待っている料理
-    this.arrPerH = CROWD[this.o.crowd].arrivalsPerH35 * this.tables.length / 35;
+    this.arrPerH = this.o.arrPerH || CROWD[this.o.crowd].arrivalsPerH35 * this.tables.length / 35; // 試算の店ごとの値(組/時)があればそれを使う
     this.nextArrival = this._expG(3600 / this.arrPerH); this.gid = 0;
     this.c.groups = 0; this.c.seatWaitSum = 0; this.c.seatedGroups = 0; this.c.payWaitSum = 0; this.c.paid = 0; this.c.left = 0;
   }

ページの型の差分(shell.html:テスト版の札・来客のペースの説明・その他の選ぶ欄)

12_sec09.txt21,956字・45行・diff
--- /tmp/shell.bak.html	2026-11-14 07:32:29.560893894 +0900
+++ tools/serving_sim_3d/shell.html	2026-11-14 07:38:43.167890948 +0900
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
 <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Noto+Sans+JP:wght@400;600;700&display=swap" rel="stylesheet">
-<title>配膳ロボット ブラウザ版(試作)</title>
+<title>配膳ロボット ブラウザ版(テスト版)</title>
別のタブで開く保存

店のデータの読み方(stores_site.js・stores_bundle.js)

// サイト(pixelstomotors.com/sim/serving-robot-3d)用:店のデータはサイトの公開データ(試算・Unity と同じ正)を、選んだときに読む
const FILES = { p0: 'p0_memory', p1: 'p1_efficient_corner', p2: 'p2_efficient_narrow', p3: 'p3_relaxed_center', p4: 'p4_relaxed_side', p5: 'p5_crammed' };
const cache = {};
export function loadStore(k) {
  if (!cache[k]) {
    const get = (n) => fetch(`/sim/layouts/unity/${n}.json`).then((r) => { if (!r.ok) throw new Error(`${n}.json を読めませんでした(${r.status})`); return r.json(); });
    cache[k] = Promise.all([get(FILES[k]), get(FILES[k] + '_map')]).catch((e) => { delete cache[k]; throw e; });
  }
  return cache[k];
}
// 試作(チャットの画面)用:6店のデータをページの中に含める
import p0 from '../stores/p0_memory.json'; import p0m from '../stores/p0_memory_map.json';
import p1 from '../stores/p1_efficient_corner.json'; import p1m from '../stores/p1_efficient_corner_map.json';
import p2 from '../stores/p2_efficient_narrow.json'; import p2m from '../stores/p2_efficient_narrow_map.json';
import p3 from '../stores/p3_relaxed_center.json'; import p3m from '../stores/p3_relaxed_center_map.json';
import p4 from '../stores/p4_relaxed_side.json'; import p4m from '../stores/p4_relaxed_side_map.json';
import p5 from '../stores/p5_crammed.json'; import p5m from '../stores/p5_crammed_map.json';
const S = { p0: [p0, p0m], p1: [p1, p1m], p2: [p2, p2m], p3: [p3, p3m], p4: [p4, p4m], p5: [p5, p5m] };
export async function loadStore(k) { return S[k]; }

サイト用に書き出すスクリプト(build_site.py)と README

"""サイト用のブラウザ版(pixelstomotors.com/sim/serving-robot-3d)を作る(2026-11-14)。
試作(チャットの画面)との違い:店のデータを中に含めず、サイトの public/sim/layouts/unity/ を読む。
head に説明・正規の住所・SNS 用の画像を足し、JavaScript を動かさない読み手(検索・AI)向けに、何のページかを文で置く。
  python3 build_site.py <p2m-site の場所>
"""
import subprocess, sys, html
site = sys.argv[1]
subprocess.run(['npx', 'esbuild', 'src/main.js', '--bundle', '--format=esm', '--minify', '--loader:.json=json',
                '--external:node:zlib', '--alias:p2m-stores=./src/stores_site.js', '--outfile=dist/app_site.js'], check=True)
s = open('shell.html', encoding='utf-8').read()
js = open('dist/app_site.js', encoding='utf-8').read()
DESC = ('配膳ロボットのブラウザ版(テスト版)。試算と同じファミレスの店(②〜⑦)を3Dで建て、スタッフ・ロボットの台数・混み具合を選ぶと、'
        'ブラウザの中で人とロボットを動かして1日を回し、入店待ち・会計待ち・料理が届くまでの時間を数える。数字は試算・Unity と答え合わせ前。')
URL = 'https://pixelstomotors.com/sim/serving-robot-3d'
head = (f'<meta name="description" content="{html.escape(DESC)}">\n<link rel="canonical" href="{URL}">\n'
        f'<meta property="og:title" content="配膳ロボット ブラウザ版(テスト版)| Pixels to Motors">\n<meta property="og:description" content="{html.escape(DESC)}">\n'
        f'<meta property="og:url" content="{URL}">\n<meta property="og:image" content="https://pixelstomotors.com/og/LOG506.jpg">\n<meta name="twitter:card" content="summary_large_image">\n')
about = '''<noscript><div style="max-width:720px;margin:24px auto;padding:0 16px;font-size:14px;line-height:1.8">
<h1>配膳ロボット ブラウザ版(テスト版)</h1>
<p>このページは JavaScript で動きます。サービスロボット導入シミュレーションの試算(https://pixelstomotors.com/sim/serving-robot)と同じファミレスの店の形(② 定番の郊外型・③ 効率重視・入口が角・④ 効率重視・小さめの店・⑤ ゆったり・入口が正面・⑥ ゆったり・入口が横・⑦ ③の通路を95cmに)を three.js で3Dに描き、スタッフの人数・配膳ロボットの台数(0〜6台)・混み具合(空いている・普通・混雑)を選ぶと、ブラウザの中で客・スタッフ・ロボットを動かして、空の店から始めてピーク120分の来客、全員が帰るまでの1日を回します。</p>
<p>テスト版です。数字は、試算 v2.9 や Unity の基準データとの答え合わせをしていません。ロボット3台以上・混雑の日は、ロボットどうしが通路で詰まりやすくなります。下げ膳はまだ入っていません。</p>
<p>動きの決まり:人は NavMesh(recast-navigation)、ロボットは店の走れるマス目(2.5cm刻み)の上で道を引く。ロボットは直径0.5m・最高0.6m/秒、前0.6mのセンサーで止まり、3秒ふさがれば回り込む。5秒止まったら7m以内の手の空いたスタッフが動かし、30秒動けなければ立ち往生としてスタッフが救出する。瞬間移動はしない。料理はロボットが先に運び、2分たってもロボットが来なければスタッフが運ぶ。会計はレジが空くまで席で待ち、90秒。</p>
<p>数字の定義:入店待ち=来てから卓に着くまで。会計待ち=食べ終えてからレジで会計が始まるまで。料理が届くまで=料理ができてから卓に届くまで。同じ「日」を選べば、何度見ても同じ動きになります。</p>
<p>作り方とコードの記録:https://pixelstomotors.com/record/serving-robot-store-sim-in-the-browser (LOG506)。シリーズ:https://pixelstomotors.com/series8</p>
</div></noscript>
'''
s = s.replace('<title>配膳ロボット ブラウザ版(テスト版)</title>', '<title>配膳ロボット ブラウザ版(テスト版)| Pixels to Motors</title>\n' + head)
s = s.replace('</style></head><body>', '</style></head><body>\n' + about, 1)
s = s.replace('href="https://pixelstomotors.com/message?topic=physical-ai"', 'href="/message?topic=physical-ai"')
assert '/*BUNDLE*/' in s
out = s.replace('/*BUNDLE*/', js)
open(f'{site}/public/sim/serving-robot-3d.html', 'w', encoding='utf-8').write(
    '<!-- 配膳ロボット ブラウザ版(テスト版)。作り方は LOG506・元のコードは記事の分けたファイル。tools ではなく試作の build_site.py で書き出した(2026-11-14) -->\n' + out if False else out)
print('ok', len(out))
# 配膳ロボット ブラウザ版(テスト版)のコード

公開先:`/sim/serving-robot-3d`(`public/sim/serving-robot-3d.html`)。入口は `/series8` の「ブラウザで動かす(テスト版)」だけ(`src/components/BrowserSimTest.astro`)。
作り方・決まり・測った数字は LOG506(https://pixelstomotors.com/record/serving-robot-store-sim-in-the-browser)。

- `src/sim.js`:動かして数える所(画面を知らない)。1コマ0.05秒・種(日)で同じ動き
- `src/main.js`:three.js で描く所。店の名前・人数・来客のペースは試算(`public/sim/serving-robot.html` の STORES)にそろえてある
- `src/stores_site.js`:サイト用。店のデータは `public/sim/layouts/unity/` を読む(試算・Unity と同じ正)
- `src/stores_bundle.js`:チャットの画面で試すとき用。店のデータを中に含める(`stores/` に layouts/unity の6店を写して使う)
- `shell.html`:ページの型。`/*BUNDLE*/` に JavaScript を入れる

書き出し(このフォルダで):

```
npm install
python3 build_site.py ../..
```

住所に条件を付けて開ける:`/sim/serving-robot-3d?store=p1&staff=3&robots=2&crowd=normal&day=1`(store は p0〜p5 =試算の ②〜⑦)。

コードは p2m-site の tools/serving_sim_3d/ に置きました。MASTER の保存先テーブルには、まだこの行がありません。

作業ログ ― 残っていること

  • ロボット2台以上・混んだ日の詰まり。3台・混んだ日の2日目は店が止まる。Unity で同じ詰まり方が出るかを並べて見る
  • 会計待ちが0台でも12〜19分と長い。サイトの試算(レジ1つ・90秒)と、どこで食い違っているかを分ける
  • 数字を、試算 v2.9 と Unity の基準データ v5 と並べて、向きが合うかを見る(ブラウザ版を4つ目の正にしない)
  • 下げ膳(皿をロボットで下げる使い方)はまだ入れていない。パネルには「料理を運ぶだけ(下げ膳はスタッフ)」と出している
  • 大型店(⑧⑨)は、Unity 用の店のデータがないので入れていない
  • 立ち往生の30秒と「5秒止まったら7m以内の人が動かす」が、いまの Unity に入っているかは、Unity 側で確かめる
  • ブラウザや端末をまたいで、同じ日が同じ動きになるかは、まだ確かめていない
  • テスト版として /sim/serving-robot-3d に公開した(入口は TOP と /series8)。答え合わせが済んだら、試算の「平面図/Unity で動かすと」の隣へ入口を移す
  • MASTER の保存先テーブルに、ブラウザ版のコード(p2m-site の tools/serving_sim_3d/)の行を足す