"""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))