--- /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';
-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';
+import { loadStore } from 'p2m-stores'; // 店のデータ：試作（チャットの画面）は中に含む・サイトは public/sim/layouts/unity/ を読む（build のときに選ぶ）
 // サイトの公開データ（public/sim/layouts/unity/）の6店
-const STORES = { p0: [p0, p0m], p1: [p1, p1m], p2: [p2, p2m], p3: [p3, p3m], p4: [p4, p4m], p5: [p5, p5m] };
-let layout = p1, map = p1m;
+const STORE_KEYS = ['p0', 'p1', 'p2', 'p3', 'p4', 'p5'];
+let layout = null, map = null;
 
 const $ = (s) => document.querySelector(s);
 const state = { store: 'p1', robots: 2, staff: 3, crowd: 'normal', seed: 1, speed: 1, playing: true };
+// 試算（/sim/serving-robot）の店の形 ②〜⑦ と同じ名前・人数・来客のペース（試算 v2.9 の STORES の short・staff・staff_all・arrivals）
+const CALC = {
+  p0: { short: '② 定番の郊外型', staff: [1, 2, 3], staffAll: [1, 2, 3, 4, 5, 6], arrivals: [14, 19, 24] },
+  p1: { short: '③ 効率重視・入口が角', staff: [3, 4, 5], staffAll: [2, 3, 4, 5, 6, 7, 8], arrivals: [30, 42, 54] },
+  p2: { short: '④ 効率重視・小さめの店', staff: [1, 2, 3], staffAll: [1, 2, 3, 4, 5, 6], arrivals: [20, 28, 36] },
+  p3: { short: '⑤ ゆったり・入口が正面', staff: [3, 4, 5], staffAll: [2, 3, 4, 5, 6, 7, 8], arrivals: [33, 46, 59] },
+  p4: { short: '⑥ ゆったり・入口が横', staff: [2, 3, 4], staffAll: [1, 2, 3, 4, 5, 6, 7], arrivals: [27, 38, 49] },
+  p5: { short: '⑦ ③の通路を95cmに', staff: [3, 4, 5], staffAll: [2, 3, 4, 5, 6, 7, 8], arrivals: [30, 42, 54] },
+};
+const CROWD_KEYS = ['empty', 'normal', 'busy'];
+const ROBOTS_MAIN = [0, 1, 2, 3], ROBOTS_ALL = [0, 1, 2, 3, 4, 5, 6]; // 試算と同じ（4〜6台は「その他」）
+// 住所の条件から始める（?store=p1&staff=3&robots=2&crowd=normal&day=1）。試算から同じ条件で開くため
+{ const q = new URLSearchParams(location.search);
+  if (CALC[q.get('store')]) state.store = q.get('store');
+  if (CROWD_KEYS.includes(q.get('crowd'))) state.crowd = q.get('crowd');
+  const n = (k) => parseInt(q.get(k), 10);
+  if (ROBOTS_ALL.includes(n('robots'))) state.robots = n('robots');
+  if (CALC[state.store].staffAll.includes(n('staff'))) state.staff = n('staff');
+  else if (!CALC[state.store].staffAll.includes(state.staff)) state.staff = CALC[state.store].staff[1];
+  if (n('day') >= 1) state.seed = n('day'); }
 let sim = null, building = false;
 
 // ---------------- 画面 ----------------
@@ -34,7 +49,7 @@
 scene.add(new THREE.HemisphereLight(0xffffff, 0xb8b8b0, 2.2));
 const sun = new THREE.DirectionalLight(0xffffff, 1.6); sun.position.set(8, 20, 6); scene.add(sun);
 
-let W = layout.outer.w, D = layout.outer.d;
+let W = 24, D = 16; // 店を読むまでの仮の大きさ（読んだら buildStore で入れ替わる）
 // 店の座標 (x, y) → three (x, 0, -y)。奥が画面の奥
 const P = (x, y, h = 0) => new THREE.Vector3(x - W / 2, h, -(y - D / 2));
 const mat = (c, o = {}) => new THREE.MeshStandardMaterial({ color: c, roughness: 0.85, ...o });
@@ -134,29 +149,48 @@
   $('#n-stops').textContent = s.sensorStops; $('#n-helps').textContent = s.helps; $('#n-stuck').textContent = s.stucks;
   $('#log').innerHTML = (sim.allEvents || []).filter(e => e.t <= f.t).slice(-5).reverse().map(e => `<li><b>${fmt(e.t)}</b>${e.msg}</li>`).join('');
 }
+let rebuildAgain = false; // 建てている間に条件が変わったら、終わってからもう一度建てる
 async function rebuild() {
-  if (building) return; building = true; $('#status').textContent = '店を建てています…';
+  if (building) { rebuildAgain = true; return; } building = true; $('#status').textContent = '店を建てています…';
+  try { [layout, map] = await loadStore(state.store); }
+  catch (e) { building = false; $('#status').textContent = '店のデータを読めませんでした。ページを読み込み直してください'; return; }
   if (sim) { sim.crowd.destroy(); }
-  [layout, map] = STORES[state.store]; buildStore(); $('#store').value = state.store;
-  sim = await createSim(layout, map, { robots: state.robots, staff: state.staff, crowd: state.crowd, seed: state.seed });
+  buildStore(); $('#store').value = state.store;
+  sim = await createSim(layout, map, { robots: state.robots, staff: state.staff, crowd: state.crowd, seed: state.seed, arrPerH: CALC[state.store].arrivals[CROWD_KEYS.indexOf(state.crowd)] });
   bubbleRobot = -1; rebuildDynamic(); frames = [sim.snapshot()]; viewT = 0; simDone = false; draw(frames[0]); panel(frames[0]); building = false; $('#status').textContent = '';
+  if (rebuildAgain) { rebuildAgain = false; rebuild(); }
 }
-function seg(id, values, key, label = (v) => v) {
-  const el = $(id); el.innerHTML = values.map(v => `<button data-v="${v}">${label(v)}</button>`).join('');
-  const sync = () => el.querySelectorAll('button').forEach(b => b.classList.toggle('on', String(state[key]) === b.dataset.v));
-  el.addEventListener('click', (e) => { const b = e.target.closest('button'); if (!b) return; const v = b.dataset.v; state[key] = isNaN(+v) ? v : +v; sync(); if (key !== 'speed') rebuild(); });
+function seg(id, values, key, label = (v) => v, more = []) {
+  // 試算と同じ形：よく使う値はボタン、残りは「その他」の選ぶ欄
+  const el = $(id);
+  el.innerHTML = values.map(v => `<button data-v="${v}">${label(v)}</button>`).join('')
+    + (more.length ? `<select class="more" aria-label="その他"><option value="">その他</option>${more.map(v => `<option value="${v}">${label(v)}</option>`).join('')}</select>` : '');
+  const sel = el.querySelector('select');
+  const sync = () => {
+    el.querySelectorAll('button').forEach(b => b.classList.toggle('on', String(state[key]) === b.dataset.v));
+    if (sel) { const hit = more.map(String).includes(String(state[key])); sel.value = hit ? String(state[key]) : ''; sel.classList.toggle('on', hit); }
+  };
+  const set = (v) => { state[key] = isNaN(+v) ? v : +v; sync(); if (key !== 'speed') rebuild(); };
+  el.onclick = (e) => { const b = e.target.closest('button'); if (b) set(b.dataset.v); };
+  if (sel) sel.onchange = () => { if (sel.value !== '') set(sel.value); };
   sync();
 }
-$('#store').innerHTML = Object.entries(STORES).map(([k, [l]]) => `<option value="${k}">${l.name}</option>`).join('');
-$('#store').addEventListener('change', (e) => { state.store = e.target.value; rebuild(); });
-seg('#c-robots', [0, 1, 2, 3, 4, 5, 6], 'robots', v => `${v}台`);
-seg('#c-staff', [2, 3, 4], 'staff', v => `${v}人`);
-seg('#c-crowd', ['empty', 'normal', 'busy'], 'crowd', v => CROWD[v].label);
+function staffSeg() {
+  const c = CALC[state.store];
+  if (!c.staffAll.includes(state.staff)) state.staff = c.staff[1];
+  seg('#c-staff', c.staff, 'staff', v => `${v}人`, c.staffAll.filter(v => !c.staff.includes(v)));
+}
+function crowdSeg() { seg('#c-crowd', CROWD_KEYS, 'crowd', v => `${CROWD[v].label}`); $('#crowdNote').textContent = `${CALC[state.store].short}の来客は、空いている ${CALC[state.store].arrivals[0]}・普通 ${CALC[state.store].arrivals[1]}・混雑 ${CALC[state.store].arrivals[2]} 組/時（試算と同じ）。混む時間は120分。`; }
+$('#store').innerHTML = STORE_KEYS.map((k) => `<option value="${k}">${CALC[k].short}</option>`).join('');
+$('#store').addEventListener('change', (e) => { state.store = e.target.value; staffSeg(); crowdSeg(); rebuild(); });
+seg('#c-robots', ROBOTS_MAIN, 'robots', v => `${v}台`, ROBOTS_ALL.filter(v => !ROBOTS_MAIN.includes(v)));
+staffSeg();
+crowdSeg();
 seg('#c-speed', [1, 5, 20], 'speed', v => v === 1 ? '実際の速さ' : `${v}倍`);
 $('#play').addEventListener('click', () => { state.playing = !state.playing; $('#play').textContent = state.playing ? '一時停止' : '再生'; });
 $('#restart').addEventListener('click', () => rebuild());
 const setDay = (d) => { state.seed = Math.max(1, d); $('#dayLabel').textContent = `${state.seed}日目`; $('#dayPrev').disabled = state.seed <= 1; rebuild(); };
-$('#dayPrev').addEventListener('click', () => setDay(state.seed - 1)); $('#dayNext').addEventListener('click', () => setDay(state.seed + 1)); $('#dayPrev').disabled = true;
+$('#dayPrev').addEventListener('click', () => setDay(state.seed - 1)); $('#dayNext').addEventListener('click', () => setDay(state.seed + 1)); $('#dayPrev').disabled = state.seed <= 1; $('#dayLabel').textContent = `${state.seed}日目`;
 document.querySelectorAll('[data-view]').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
 
 // ---------------- 回す ----------------
