"""Unity が店を建てるための細かい形を、店の形（layout）と解析（analysis）から決まった規則で作る。

2026-10-22 エイイチと決めた「何について正か」による（README）。Unity は店の形を自分で作らず、ここで書き出したものを読む。
サイトの試算（haizen_sim.py）はこれを使わない。数字（卓の数・卓までの距離・止まる位置）は analysis と同じものをそのまま渡す。

書き出すもの（public/sim/layouts/unity/{名前}.json）
- outer_walls：外壁（入口を空けた矩形の並び。外形の外側に壁の厚みぶん）
- floor：床を敷く矩形（建物の外を除く）
- aisles：細い通路（ボックス席の列と列の間。幅・区画・両端の入口と受け渡し口の前からの道のり）
- tables：卓ごとの部品（天板・ベンチ・椅子・座る位置・席に着くために立つ点）と、止まる位置・通る細い通路
- places：場所の点（受け渡し口の前・下げ膳の返却・パントリー・入口・待合・レジ・レジ内・トイレ・ドリンクバー・待機場所・スタッフの待つ点）

規則（寸法は famires_v3.FURNITURE_TYPES と同じ）
- 細い通路：同じ向きのボックス席がすき間なく並んだ「列」を見つけ、隣り合う2つの列のすき間（0.5m以上、1.5m以下）を細い通路とする。
  区画は2つの列の長さを合わせた範囲。両端の少し外（0.8m）に入口の点を置き、受け渡し口の前からの道のりが短い方を entrance とする
- 卓が通る細い通路：止まる位置がその通路の区画の中にあるもの
- 下げ膳の返却口は受け渡し口と同じ点（試算の dish_extra_m=0 と同じ前提）
- ロボットの道順（routes）：受け渡し口の前→各卓の止まる位置（from_pass）と、卓→卓（between。"T01|T02" は T01 から T02。逆向きは点を逆にたどる）。
  ふだん走らせる幅（normal_width）の道で行けるならその道、行けなければ通れる幅（pass_width）の道（試算と同じ規則。use が normal か narrow）。
  マス目の上の最短の道を、通れる範囲でまっすぐにつないで、曲がり角の点だけにした。卓→受け渡し口は from_pass を逆にたどる
- 部屋は通れない箱。ただし walkable=true の部屋（レジ内）は人が中に立つ所で、箱にしない（staff_only=true はスタッフだけが入れる）
- 席に着くために立つ点は、障害物から0.2m（人の半径）以上離れた所だけ
"""
import math

import numpy as np

SETBACK = 0.8            # 通路の端から、入口の点を外へ出す距離
NARROW_MAX = 1.5         # これ以下のすき間を細い通路とする（ゆったり型の通路幅）
NARROW_MIN = 0.5         # これより狭いすき間は通路にしない（人が通れる幅。島の中の背中合わせのすき間0.1mなどを除く）
SPOT_OUT = 0.3           # 席に着くために立つ点：座る側の辺から外へ
HEIGHTS = dict(wall=2.7, partition=1.2, toilet_wall=2.7, table=0.72, bench=1.1, bench_seat=0.45, chair=0.45, counter=1.0)


def r2(v):
    return round(float(v), 3)


def rect(*a):
    return [r2(v) for v in a]


# ---------------------------------------------------------------- 細い通路
def booth_columns(tables):
    """同じ向きのボックス席が、すき間なく（0.01m以内）並んだ列。axis=v は縦の列（x の範囲が同じ）"""
    cols = []
    for axis in ("v", "h"):
        groups = {}
        for i, t in enumerate(tables):
            if t["type"] != "booth" or t["axis"] != axis:
                continue
            k = (round(t["x"], 3), round(t["x"] + t["w"], 3)) if axis == "v" else (round(t["y"], 3), round(t["y"] + t["h"], 3))
            groups.setdefault(k, []).append(i)
        for k, idx in groups.items():
            idx.sort(key=lambda i: tables[i]["y"] if axis == "v" else tables[i]["x"])
            run = [idx[0]]
            for a, b in zip(idx, idx[1:]):
                ea = tables[a]["y"] + tables[a]["h"] if axis == "v" else tables[a]["x"] + tables[a]["w"]
                sb = tables[b]["y"] if axis == "v" else tables[b]["x"]
                if abs(sb - ea) <= 0.01:
                    run.append(b)
                else:
                    cols.append((axis, k, run))
                    run = [b]
            cols.append((axis, k, run))
    out = []
    for axis, (c0, c1), run in cols:
        if axis == "v":
            l0, l1 = tables[run[0]]["y"], tables[run[-1]]["y"] + tables[run[-1]]["h"]
        else:
            l0, l1 = tables[run[0]]["x"], tables[run[-1]]["x"] + tables[run[-1]]["w"]
        out.append(dict(axis=axis, c0=c0, c1=c1, l0=l0, l1=l1, tables=run))
    return out


def find_aisles(L):
    cols = booth_columns(L["tables"])
    aisles = []
    for a in cols:
        for b in cols:
            if a is b or a["axis"] != b["axis"] or b["c0"] <= a["c1"] - 1e-6:
                continue
            gap = b["c0"] - a["c1"]
            if not (NARROW_MIN - 1e-6 <= gap <= NARROW_MAX + 1e-6):
                continue
            if min(a["l1"], b["l1"]) - max(a["l0"], b["l0"]) <= 1e-6:
                continue
            # a と b の間に、別の列が挟まっていないか
            if any(c is not a and c is not b and c["axis"] == a["axis"] and c["c0"] >= a["c1"] - 1e-6 and c["c1"] <= b["c0"] + 1e-6
                   and min(c["l1"], a["l1"], b["l1"]) - max(c["l0"], a["l0"], b["l0"]) > 1e-6 for c in cols):
                continue
            l0, l1 = min(a["l0"], b["l0"]), max(a["l1"], b["l1"])
            if a["axis"] == "v":
                r = rect(a["c1"], l0, b["c0"], l1)
                ends = [(r2((a["c1"] + b["c0"]) / 2), r2(l0 - SETBACK)), (r2((a["c1"] + b["c0"]) / 2), r2(l1 + SETBACK))]
            else:
                r = rect(l0, a["c1"], l1, b["c0"])
                ends = [(r2(l0 - SETBACK), r2((a["c1"] + b["c0"]) / 2)), (r2(l1 + SETBACK), r2((a["c1"] + b["c0"]) / 2))]
            aisles.append(dict(rect=r, width=r2(gap), axis=a["axis"], narrow=True, ends=[list(e) for e in ends]))
    aisles.sort(key=lambda a: (a["axis"], a["rect"][0], a["rect"][1]))
    for i, a in enumerate(aisles):
        a["id"] = f"A{i + 1:02d}"
    return aisles


def aisle_entrances(aisles, grid, dist, dist_pass):
    """両端の入口の点の、受け渡し口の前からの道のり（ふだんの幅の道。行けなければ通れる幅の道）。短い方を entrance にする"""
    for a in aisles:
        ds = []
        for x, y in a["ends"]:
            cy, cx = grid.cell(x, y)
            d = None
            if 0 <= cy < grid.ny and 0 <= cx < grid.nx:
                for D in (dist, dist_pass):
                    if D is not None and np.isfinite(D[cy, cx]):
                        d = r2(D[cy, cx])
                        break
            ds.append(d)
        a["end_dist"] = ds
        ok = [i for i, d in enumerate(ds) if d is not None]
        a["entrance"] = a["ends"][min(ok, key=lambda i: ds[i])] if ok else None


# ---------------------------------------------------------------- 卓の部品
def booth_parts(t, FT):
    B = FT["booth"]
    bd, td = B["bench_depth"], B["table_depth"]
    x, y, w, h = t["x"], t["y"], t["w"], t["h"]
    n = t["seats"] // 2   # ベンチ1本あたりの人数
    if t["axis"] == "v":   # ベンチが上下、座る側は左右
        benches = [rect(x, y, x + w, y + bd), rect(x, y + h - bd, x + w, y + h)]
        top = rect(x, y + bd, x + w, y + bd + td)
        seats = [[r2(x + w * (k + 0.5) / n), r2(y + bd / 2)] for k in range(n)] + \
                [[r2(x + w * (k + 0.5) / n), r2(y + h - bd / 2)] for k in range(n)]
    else:
        benches = [rect(x, y, x + bd, y + h), rect(x + w - bd, y, x + w, y + h)]
        top = rect(x + bd, y, x + bd + td, y + h)
        seats = [[r2(x + bd / 2), r2(y + h * (k + 0.5) / n)] for k in range(n)] + \
                [[r2(x + w - bd / 2), r2(y + h * (k + 0.5) / n)] for k in range(n)]
    return dict(top=top, benches=benches, chairs=[], seat_points=seats)


def t2_parts(t, FT):
    x, y, w, h, s = t["x"], t["y"], t["w"], t["h"], t["side"]
    b = FT["t2"]["bench_depth"]
    cr = FT["t2"]["chair_radius"]
    if s == "bottom":
        bench, top, chair, pull = rect(x, y, x + w, y + b), rect(x + 0.05, y + 0.5, x + 0.75, y + 1.1), (x + w / 2, y + 1.2), (0, 1)
    elif s == "top":
        bench, top, chair, pull = rect(x, y + h - b, x + w, y + h), rect(x + 0.05, y + 0.3, x + 0.75, y + 0.9), (x + w / 2, y + 0.2), (0, -1)
    elif s == "left":
        bench, top, chair, pull = rect(x, y, x + b, y + h), rect(x + 0.5, y + 0.05, x + 1.1, y + 0.75), (x + 1.2, y + h / 2), (1, 0)
    else:
        bench, top, chair, pull = rect(x + w - b, y, x + w, y + h), rect(x + 0.3, y + 0.05, x + 0.9, y + 0.75), (x + 0.2, y + h / 2), (-1, 0)
    bc = [r2((bench[0] + bench[2]) / 2), r2((bench[1] + bench[3]) / 2)]
    ch = dict(rect=rect(chair[0] - cr, chair[1] - cr, chair[0] + cr, chair[1] + cr), pullable=True, pull_dir=list(pull))
    return dict(top=top, benches=[bench], chairs=[ch], seat_points=[bc, [r2(chair[0]), r2(chair[1])]])


def lsofa_parts(t):
    """L字ソファ：下の帯（高さ0.45h）と左の列（幅0.55w）がソファ。flip で上下、mirror で左右を返す"""
    x, y, w, h = t["x"], t["y"], t["w"], t["h"]

    def tf(px, py):
        if t.get("flip"):
            py = 2 * y + h - py
        if t.get("mirror"):
            px = 2 * x + w - px
        return px, py

    def tr(r):
        (ax, ay), (bx, by) = tf(r[0], r[1]), tf(r[2], r[3])
        return rect(min(ax, bx), min(ay, by), max(ax, bx), max(ay, by))
    band = tr([x, y, x + w, y + 0.45 * h])
    column = tr([x, y + 0.45 * h, x + 0.55 * w, y + h])
    cx, cy = (x + 0.55 * w + x + w) / 2, (y + 0.45 * h + y + h) / 2
    top = tr([cx - 0.375, cy - 0.6, cx + 0.375, cy + 0.6])
    sd = 0.55   # 座面の奥行き（ソファの内側の縁から）
    seats = [tf(x + 0.55 * w + (w * 0.45) * (k + 0.5) / 3, y + 0.45 * h - sd / 2) for k in range(3)] + \
            [tf(x + 0.55 * w - sd / 2, y + 0.45 * h + (h * 0.55) * (k + 0.5) / 3) for k in range(3)]
    return dict(top=top, benches=[band, column], chairs=[], seat_points=[[r2(a), r2(b)] for a, b in seats])


def table_parts(t, FT):
    if t["type"] == "booth":
        return booth_parts(t, FT)
    if t["type"] == "t2":
        return t2_parts(t, FT)
    if t["type"] == "booth6L":
        return lsofa_parts(t)
    return None   # t4・r4・long6（② のみ）は椅子と座る位置がまだない（README の「まだ入っていないこと」）


PERSON_R = 0.2           # 人の半径（席に着く点は、障害物からこれ以上離れた所だけ）


def seat_spots(sides, grid):
    """席に着くために立つ点：座る側の辺の真ん中から外へ SPOT_OUT。障害物から PERSON_R 未満の点は使わない
    （壁ぞいの2人席の横は隣の卓まで0.3mしかないため）"""
    out = []
    for k, ((ax, ay), (bx, by), (nx, ny)) in sides.items():
        x, y = (ax + bx) / 2 + nx * SPOT_OUT, (ay + by) / 2 + ny * SPOT_OUT
        cy, cx = grid.cell(x, y)
        if 0 <= cy < grid.ny and 0 <= cx < grid.nx and grid.clear[cy, cx] >= PERSON_R - 1e-9:
            out.append(dict(side=k, point=[r2(x), r2(y)]))
    return out


# ---------------------------------------------------------------- 外壁・床・場所
def outer_walls(L, thick=0.15):
    """外壁：外形（または輪郭）の各辺の、建物の外側に厚み thick の壁。角は厚みぶん延ばしてふさぐ。入口の所は空ける"""
    W, D = L["outer"]["w"], L["outer"]["d"]
    pts = [tuple(map(float, p)) for p in (L.get("outline") or [(0, 0), (W, 0), (W, D), (0, D)])]
    e = L["entrance"]
    segs = []
    for (xa, ya), (xb, yb) in zip(pts, pts[1:] + pts[:1]):
        if abs(ya - yb) < 1e-9:   # 横の辺
            lo, hi, c = min(xa, xb), max(xa, xb), ya
            inside_up = _inside(pts, (lo + hi) / 2, c + 0.01)
            a0, a1 = (c - thick, c) if inside_up else (c, c + thick)
            gate = (e["side"] == "bottom" and abs(c) < 1e-9) or (e["side"] == "top" and abs(c - D) < 1e-9)
            pieces = [(lo - thick, max(lo, e["a0"])), (min(hi, e["a1"]), hi + thick)] if gate else [(lo - thick, hi + thick)]
            segs += [rect(p, a0, q, a1) for p, q in pieces if q - p > 1e-6]
        else:
            lo, hi, c = min(ya, yb), max(ya, yb), xa
            inside_right = _inside(pts, c + 0.01, (lo + hi) / 2)
            a0, a1 = (c - thick, c) if inside_right else (c, c + thick)
            gate = (e["side"] == "left" and abs(c) < 1e-9) or (e["side"] == "right" and abs(c - W) < 1e-9)
            pieces = [(lo - thick, max(lo, e["a0"])), (min(hi, e["a1"]), hi + thick)] if gate else [(lo - thick, hi + thick)]
            segs += [rect(a0, p, a1, q) for p, q in pieces if q - p > 1e-6]
    return segs


def _inside(pts, x, y):
    from matplotlib.path import Path as MPath
    return MPath(np.array(pts, dtype=float)).contains_point((x, y))


def floor_rects(L):
    """床：外形から建物の外（kind=outside）を除いた矩形（縦に切り分ける）"""
    W, D = L["outer"]["w"], L["outer"]["d"]
    outs = [r["rect"] for r in L["rooms"] if r["kind"] == "outside"]
    if not outs:
        return [rect(0, 0, W, D)]
    xs = sorted({0.0, W} | {c for o in outs for c in (o[0], o[2])})
    out = []
    for xa, xb in zip(xs, xs[1:]):
        cx = (xa + xb) / 2
        ys = sorted({0.0, D} | {c for o in outs if o[0] < cx < o[2] for c in (o[1], o[3])})
        for ya, yb in zip(ys, ys[1:]):
            cy = (ya + yb) / 2
            if not any(o[0] < cx < o[2] and o[1] < cy < o[3] for o in outs):
                out.append(rect(xa, ya, xb, yb))
    return out


def front_point(r, floor_free, step=0.4):
    """部屋（カウンター）の前に立つ点：4辺のうち、外へ step 出た所が床として空いている辺の真ん中（空きの多い順）"""
    x0, y0, x1, y1 = r
    cand = [("bottom", ((x0 + x1) / 2, y0 - step)), ("top", ((x0 + x1) / 2, y1 + step)),
            ("left", (x0 - step, (y0 + y1) / 2)), ("right", (x1 + step, (y0 + y1) / 2))]
    best = max(cand, key=lambda c: floor_free(*c[1]))
    return best[0], [r2(best[1][0]), r2(best[1][1])]


REG_QUEUE = 3            # レジに並ぶ点の数
# 通れない箱にしない部屋（人が中に立つ所）。staff_only はスタッフだけが入れる（ロボットと客は入れない）
WALKABLE = {"レジ内": {"walkable": True, "staff_only": True}}
DRINK_QUEUE = 3          # ドリンクバーに並ぶ点の数
STAFF_STANDBY = 6        # スタッフが待つ点の数（図面のある店で選べる人数の最大まで）


def _side_point(r, side, out):
    """矩形 r の辺 side の真ん中から外へ out の点と、外向きの向き"""
    x0, y0, x1, y1 = r
    n = {"bottom": (0, -1), "top": (0, 1), "left": (-1, 0), "right": (1, 0)}[side]
    c = {"bottom": ((x0 + x1) / 2, y0), "top": ((x0 + x1) / 2, y1), "left": (x0, (y0 + y1) / 2), "right": (x1, (y0 + y1) / 2)}[side]
    return [r2(c[0] + n[0] * out), r2(c[1] + n[1] * out)], n


def _opposite_side(r, b):
    """矩形 r に接している矩形 b の、反対側の辺"""
    if abs(b[3] - r[1]) < 1e-6:
        return "top"
    if abs(b[1] - r[3]) < 1e-6:
        return "bottom"
    if abs(b[2] - r[0]) < 1e-6:
        return "right"
    return "left"


def staff_standby_points(L, free, aisles):
    """スタッフが待つ点：パントリー（と接する壁）の客席側の辺にそって、壁から 0.25m、0.6m おき。
    受け渡し口に近い端から並べ、その外側にロボットのふだんの道（1.0m）が残る所だけ使う（仮の規則）。
    受け渡し口の前（ロボットの出入り）と細い通路には置かない"""
    pan = [r["rect"] for r in L["rooms"] if r["label"] == "パントリー"]
    if not pan:
        return []
    x0, y0, x1, y1 = pan[0]
    for w in L["walls"]:   # パントリーに接する壁も含めた外形
        r = w["rect"]
        if touch_segment_simple(r, (x0, y0, x1, y1)):
            x0, y0, x1, y1 = min(x0, r[0]), min(y0, r[1]), max(x1, r[2]), max(y1, r[3])
    d = L["dish_up"][0]["rect"]
    dc = ((d[0] + d[2]) / 2, (d[1] + d[3]) / 2)
    sides = {"left": ((x0, y0), (x0, y1), (-1, 0)), "right": ((x1, y0), (x1, y1), (1, 0)),
             "bottom": ((x0, y0), (x1, y0), (0, -1)), "top": ((x0, y1), (x1, y1), (0, 1))}
    pts = []
    for k, ((ax, ay), (bx, by), (nx, ny)) in sides.items():
        wide = (d[2] - d[0]) >= (d[3] - d[1])   # 受け渡し口が横長なら上か下の辺、縦長なら左か右の辺にある
        if (k == "bottom" and wide and abs(d[1] - y0) < 0.05) or (k == "top" and wide and abs(d[3] - y1) < 0.05) or \
           (k == "left" and not wide and abs(d[0] - x0) < 0.05) or (k == "right" and not wide and abs(d[2] - x1) < 0.05):
            continue   # 受け渡し口のある辺
        n = int(math.hypot(bx - ax, by - ay) / 0.6)
        for q in range(n):
            f = (q + 0.5) / n
            px, py = ax + (bx - ax) * f + nx * 0.25, ay + (by - ay) * f + ny * 0.25
            lx, ly = px + nx * 0.75, py + ny * 0.75   # 外側に残る道の真ん中
            if free(px, py) >= 0.2 and free(lx, ly) >= 0.5 and not _in_aisle(aisles, px, py):
                pts.append([r2(px), r2(py)])
    pts.sort(key=lambda p: math.hypot(p[0] - dc[0], p[1] - dc[1]))   # 受け渡し口に近い順
    return pts[:STAFF_STANDBY]


def touch_segment_simple(a, b):
    """2つの矩形が接しているか重なっているか"""
    return not (a[2] < b[0] - 1e-6 or b[2] < a[0] - 1e-6 or a[3] < b[1] - 1e-6 or b[3] < a[1] - 1e-6)


def _in_aisle(aisles, x, y, m=0.0):
    return any(a["rect"][0] - m <= x <= a["rect"][2] + m and a["rect"][1] - m <= y <= a["rect"][3] + m for a in aisles)


def places(L, grid, standby_slots, aisles):
    W, D = L["outer"]["w"], L["outer"]["d"]

    def free(x, y):   # 障害物までの余裕（m）。店の外は −1
        cy, cx = grid.cell(x, y)
        if not (0 <= cy < grid.ny and 0 <= cx < grid.nx):
            return -1.0
        return float(grid.clear[cy, cx])
    P = []
    st = L["start"]
    P.append(dict(kind="FoodPass", label="受け渡し口の前（出発点）", rect=L["dish_up"][0]["rect"], point=list(st)))
    P.append(dict(kind="DishReturn", label="下げ膳の返却（受け渡し口と同じ点）", rect=L["dish_up"][0]["rect"], point=list(st)))
    e = L["entrance"]
    m = (e["a0"] + e["a1"]) / 2
    ep = {"bottom": (m, 0.5), "top": (m, D - 0.5), "left": (0.5, m), "right": (W - 0.5, m)}[e["side"]]
    P.append(dict(kind="Entrance", label="入口", point=[r2(ep[0]), r2(ep[1])]))
    kinds = {"パントリー": "Pantry", "待合": "Waiting", "レジ": "Register", "レジ内": "RegisterBack", "トイレ": "Toilet",
             "ドリンクバー": "DrinkBar", "キッチン": "Kitchen"}
    for r in L["rooms"]:
        k = kinds.get(r["label"])
        if not k:
            continue
        if k == "Toilet":
            dz = L["toilet_doors"][0]
            P.append(dict(kind=k, label="トイレの入口の前", rect=r["rect"], point=[r2((dz[0] + dz[2]) / 2), r2((dz[1] + dz[3]) / 2)]))
        elif k == "Pantry":
            P.append(dict(kind=k, label="パントリー（受け渡し口の前と同じ点）", rect=r["rect"], point=list(st)))
        elif k == "RegisterBack":
            x0, y0, x1, y1 = r["rect"]
            P.append(dict(kind=k, label="レジ内（店員が立つ所）", rect=r["rect"], point=[r2((x0 + x1) / 2), r2((y0 + y1) / 2)]))
        elif k == "Kitchen":
            continue
        elif k == "Register":
            # 客の側は、レジ内（店員の立つ所）の反対側
            back = [b["rect"] for b in L["rooms"] if b["label"] == "レジ内"]
            side = _opposite_side(r["rect"], back[0]) if back else front_point(r["rect"], free)[0]
            pt, nrm = _side_point(r["rect"], side, 0.4)
            P.append(dict(kind=k, label="レジの前（会計に立つ点）", rect=r["rect"], point=pt, front=side))
            for q in range(1, REG_QUEUE + 1):   # 並ぶ点：会計に立つ点から外へ 0.5m ずつ。障害物から0.25m未満・細い通路に入る所で打ち切る
                qx, qy = pt[0] + nrm[0] * 0.5 * q, pt[1] + nrm[1] * 0.5 * q
                if free(qx, qy) < 0.25 or _in_aisle(aisles, qx, qy):
                    break
                P.append(dict(kind="RegisterQueue", label=f"レジに並ぶ点 {q}", point=[r2(qx), r2(qy)]))
        elif k == "DrinkBar":
            side, _ = front_point(r["rect"], free)
            x0, y0, x1, y1 = r["rect"]
            horiz = side in ("bottom", "top")
            length = (x1 - x0) if horiz else (y1 - y0)
            n = max(1, int(length / 0.9))   # 前に立つ点：0.9m に1人
            pt, nrm = _side_point(r["rect"], side, 0.4)
            P.append(dict(kind=k, label="ドリンクバーの前", rect=r["rect"], point=pt, front=side))
            for q in range(n):
                f = (q + 0.5) / n
                base = (x0 + (x1 - x0) * f, y0 if side == "bottom" else y1) if horiz else (x0 if side == "left" else x1, y0 + (y1 - y0) * f)
                P.append(dict(kind="DrinkBarStand", label=f"ドリンクバーの前に立つ点 {q + 1}", point=[r2(base[0] + nrm[0] * 0.4), r2(base[1] + nrm[1] * 0.4)]))
            for q in range(1, DRINK_QUEUE + 1):   # 並ぶ点：真ん中の前から外へ。障害物から0.25m未満・細い通路に入る所で打ち切る
                qx, qy = pt[0] + nrm[0] * (0.4 + 0.5 * q), pt[1] + nrm[1] * (0.4 + 0.5 * q)
                if free(qx, qy) < 0.25 or _in_aisle(aisles, qx, qy):
                    break
                P.append(dict(kind="DrinkBarQueue", label=f"ドリンクバーに並ぶ点 {q}", point=[r2(qx), r2(qy)]))
        else:
            side, pt = front_point(r["rect"], free)
            P.append(dict(kind=k, label=r["label"] + "の前", rect=r["rect"], point=pt, front=side))
    # スタッフが待つ点：受け渡し口の前の横（待機場所と反対の側）に、パントリーの壁ぞいへ 0.6m おき（仮の規則）
    for i, pt in enumerate(staff_standby_points(L, free, aisles)):
        P.append(dict(kind="StaffStandby", label=f"スタッフが待つ点 {i + 1}", point=pt))
    for i, s in enumerate(standby_slots):
        P.append(dict(kind="RobotStandby", label=f"ロボットの待機場所 {i + 1}", rect=rect(*s), point=[r2((s[0] + s[2]) / 2), r2((s[1] + s[3]) / 2)]))
    return P



# ---------------------------------------------------------------- ロボットの道順（2026-10-22 エイイチと決めた）
def _trace_cells(pred, nx, src, dst):
    """dijkstra の predecessors から、src から dst までのマス目の番号の並び"""
    out, i = [], dst
    while i != src:
        if i < 0:
            return None
        out.append(i)
        i = pred[i]
    out.append(src)
    return out[::-1]


def _visible(free, grid, a, b):
    """点 a から b までの線分が、通れるマス目だけを通るか（マス目の4分の1の間隔で確かめる）"""
    n = max(2, int(math.hypot(b[0] - a[0], b[1] - a[1]) / (grid.res / 4)) + 1)
    xs = np.linspace(a[0], b[0], n)
    ys = np.linspace(a[1], b[1], n)
    cx = np.clip((xs / grid.res).astype(int), 0, grid.nx - 1)
    cy = np.clip((ys / grid.res).astype(int), 0, grid.ny - 1)
    return bool(free[cy, cx].all())


def _pull(points, free, grid):
    """マス目の道を、通れる範囲でまっすぐにつないだ曲がり角の点だけにする（始点と終点は残す）"""
    out, i, n = [points[0]], 0, len(points)
    while i < n - 1:
        lo, hi = i + 1, n - 1
        if _visible(free, grid, points[i], points[hi]):
            j = hi
        else:   # 見通せるいちばん遠い点を二分で探す（i+1 は必ず見通せる）
            while hi - lo > 1:
                mid = (lo + hi) // 2
                if _visible(free, grid, points[i], points[mid]):
                    lo = mid
                else:
                    hi = mid
            j = lo
        out.append(points[j])
        i = j
    return out


def _poly_len(pts):
    return sum(math.hypot(b[0] - a[0], b[1] - a[1]) for a, b in zip(pts, pts[1:]))


def build_routes(L, analysis_tables, grid, normal_width, pass_width):
    """受け渡し口の前→各卓の止まる位置、卓→卓 の道順。
    規則：ふだん走らせる幅（normal_width）の道で行けるならその道、行けなければ通れる幅（pass_width）の道（試算と同じ）。
    道はマス目の上の最短の道を、通れる範囲でまっすぐにつないで曲がり角の点だけにする。終点は止まる位置そのもの"""
    from scipy.sparse.csgraph import dijkstra
    f_norm, f_pass = grid.free(normal_width), grid.free(pass_width)
    G_norm, G_pass = grid.graph(f_norm), grid.graph(f_pass)
    cell = lambda x, y: (lambda c: c[0] * grid.nx + c[1])(grid.cell(x, y))
    xy = lambda i: ((i % grid.nx + 0.5) * grid.res, (i // grid.nx + 0.5) * grid.res)
    nodes = [("PASS", tuple(L["start"]))] + [(t["id"], tuple(a["stop"])) for t, a in zip(L["tables"], analysis_tables) if a.get("stop")]
    ids = [cell(*p) for _, p in nodes]
    out = {"from_pass": {}, "between": {}}
    for si, (sname, sp) in enumerate(nodes):
        dn, pn = dijkstra(G_norm, indices=ids[si], return_predecessors=True) if f_norm.ravel()[ids[si]] else (None, None)
        dp, pp = dijkstra(G_pass, indices=ids[si], return_predecessors=True)
        for ti in range(si + 1, len(nodes)):
            tname, tp = nodes[ti]
            use = None
            if dn is not None and np.isfinite(dn[ids[ti]]):
                use, free, pred, width = "normal", f_norm, pn, normal_width
            elif np.isfinite(dp[ids[ti]]):
                use, free, pred, width = "narrow", f_pass, pp, pass_width
            if use is None:
                rec = None
            else:
                cells = _trace_cells(pred, grid.nx, ids[si], ids[ti])
                pts = [(r2(x), r2(y)) for x, y in [sp] + [xy(c) for c in cells[1:-1]] + [tp]]   # 書き出す座標（mm）で確かめる
                pr = _pull(pts, free, grid)
                rec = dict(use=use, width=width, points=[[x, y] for x, y in pr],
                           length=r2(_poly_len(pr)), grid_length=r2(_poly_len(pts)))
            if sname == "PASS":
                out["from_pass"][tname] = rec
            else:
                out["between"][f"{sname}|{tname}"] = rec
    return out


def build(L, analysis_tables, grid, dist, dist_pass, sides, standby_slots, FT, meta):
    aisles = find_aisles(L)
    aisle_entrances(aisles, grid, dist, dist_pass)
    tables = []
    for t, a, sd in zip(L["tables"], analysis_tables, sides):
        parts = table_parts(t, FT)
        stop = a.get("stop")
        ai = None
        if stop:
            for x in aisles:
                r = x["rect"]
                if r[0] - 0.05 <= stop[0] <= r[2] + 0.05 and r[1] - 0.05 <= stop[1] <= r[3] + 0.05:
                    ai = x["id"]
                    break
        tables.append(dict(id=t["id"], type=t["type"], seats=t["seats"], footprint=rect(t["x"], t["y"], t["x"] + t["w"], t["y"] + t["h"]),
                           category=a["category"], stop=stop, side=a.get("side"), dist=a.get("dist"), aisle=ai,
                           parts=parts, spots=seat_spots(sd, grid)))
    walls = [dict(label=w["label"], rect=w["rect"], height=HEIGHTS["partition"] if w.get("joint") or w["label"] in ("仕切り", "目隠し") else
                  (HEIGHTS["toilet_wall"] if w["label"] == "トイレの壁" else HEIGHTS["wall"])) for w in L["walls"]]
    st = (meta or {}).get("settings") or {}
    model = (meta or {}).get("robot_model") or {}
    routes = build_routes(L, analysis_tables, grid, st.get("normal_width", 1.0), model.get("pass_width", 0.8)) if st else None
    return dict(meta=dict(meta, heights=HEIGHTS, rules=__doc__.strip()), routes=routes,
                name=L["name"], outer=L["outer"], outline=L.get("outline"), entrance=L["entrance"],
                floor=floor_rects(L), outer_walls=outer_walls(L), walls=walls,
                rooms=[dict(label=r["label"], kind=r["kind"], rect=r["rect"], **WALKABLE.get(r["label"], {"walkable": False}))
                       for r in L["rooms"] if r["kind"] != "outside"],
                doors=L.get("doors", []), aisles=aisles, tables=tables, places=places(L, grid, standby_slots, aisles))
