--- /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 => `
${fmt(e.t)}${e.msg}`).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 => ``).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 => ``).join('')
+ + (more.length ? `` : '');
+ 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]]) => ``).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) => ``).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)));
// ---------------- 回す ----------------