diff --git a/tools/sim_layouts/unity_export.py b/tools/sim_layouts/unity_export.py index e320ba7..323f459 100644 --- a/tools/sim_layouts/unity_export.py +++ b/tools/sim_layouts/unity_export.py @@ -8,7 +8,7 @@ - floor:床を敷く矩形(建物の外を除く) - aisles:細い通路(ボックス席の列と列の間。幅・区画・両端の入口と受け渡し口の前からの道のり) - tables:卓ごとの部品(天板・ベンチ・椅子・座る位置・席に着くために立つ点)と、止まる位置・通る細い通路 -- places:場所の点(受け渡し口の前・下げ膳の返却・パントリー・入口・待合・レジ・レジ内・トイレ・ドリンクバー・待機場所・スタッフの待つ点) +- places:場所の点(受け渡し口の前・下げ膳の返却・パントリー・入口・待合・レジ・レジ内・トイレ・ドリンクバー・待機場所・スタッフの待つ点・スタッフの持ち場) 規則(寸法は famires_v3.FURNITURE_TYPES と同じ) - 細い通路:同じ向きのボックス席がすき間なく並んだ「列」を見つけ、隣り合う2つの列のすき間(0.5m以上、1.5m以下)を細い通路とする。 @@ -409,6 +409,70 @@ def staff_standby_points(L, free, aisles): return pts[:STAFF_STANDBY] +STAFF_POST = 4 # スタッフの持ち場の数(入口番を除く。図面のある店で選べる人数の最大 5 − 1) +POST_BAND = 0.6 # パントリー(と接する壁)からこの距離以内 +POST_GAP = 1.8 # 持ち場どうしをこれ以上離す +POST_AVOID = 1.2 # 入口・レジ・待合・受け渡し口・トイレ・ドリンクバー・待機場所からこれ以上離す +POST_STEP = 0.25 # 候補を探すマス目の間隔 + + +def staff_post_points(L, grid, aisles, P, standby_slots): + """スタッフの持ち場(2026-10-31 エイイチと決めた)。 + 店員は準備・片付け・掃除・配膳の準備のためパントリーの近くにいることが多いが、1か所に固まらず散らばっている。 + 1人は入口が見えるよう、レジ内(RegisterBack)で待つ(入口番。ここでは作らない)。ほかの人はこの点で待つ。 + - 候補:POST_STEP おきの点で、人が立てて(ロボットの余裕 0.22m以上)何かに寄っていて(0.6m以下)、 + 余裕が増える向きへ 0.75m 先にロボットのふだんの道(余裕 0.5m 以上)が残る所。細い通路・会計の列(0.3m 外まで)には置かない。 + 入口・レジ・待合・受け渡し口・トイレ・ドリンクバー・待機場所の点から POST_AVOID 以上離す + - パントリー(と接する壁を含めた外形)から POST_BAND 以内の候補を、受け渡し口に近い順に見て、 + すでに選んだ点から POST_GAP 以上離れたものを選ぶ(STAFF_POST まで) + - 使い方(Unity):入口番の1人を除いた人数ぶん、1 から順に使う。手が空いた人は、使う点のうち空いている一番近い所へ""" + 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]) + W, D = L["outer"]["w"], L["outer"]["d"] + + def clr(x, y): + 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]) + avoid = [p["point"] for p in P if p["kind"] in ("Entrance", "Register", "RegisterQueue", "RegisterBack", "Waiting", "FoodPass", + "Toilet", "DrinkBarQueue", "DrinkBarStand")] + avoid += [[(s[0] + s[2]) / 2, (s[1] + s[3]) / 2] for s in standby_slots] + keep = L.get("robot_keepout", []) + fp = L["start"] + cands = [] + for x in np.arange(POST_STEP, W, POST_STEP): + for y in np.arange(POST_STEP, D, POST_STEP): + if math.hypot(max(x0 - x, 0, x - x1), max(y0 - y, 0, y - y1)) > POST_BAND: + continue + c = clr(x, y) + if not (0.22 <= c <= 0.6) or _in_aisle(aisles, x, y): + continue + if any(k[0] - 0.3 <= x <= k[2] + 0.3 and k[1] - 0.3 <= y <= k[3] + 0.3 for k in keep): + continue + if any(math.hypot(x - a[0], y - a[1]) < POST_AVOID for a in avoid): + continue + gx, gy = clr(x + 0.1, y) - clr(x - 0.1, y), clr(x, y + 0.1) - clr(x, y - 0.1) + g = math.hypot(gx, gy) + if g < 1e-6 or clr(x + gx / g * 0.75, y + gy / g * 0.75) < 0.5: + continue + cands.append((float(x), float(y))) + cands.sort(key=lambda p: (round(math.hypot(p[0] - fp[0], p[1] - fp[1]), 6), p[0], p[1])) + out = [] + for p in cands: + if all(math.hypot(p[0] - q[0], p[1] - q[1]) >= POST_GAP for q in out): + out.append(p) + if len(out) == STAFF_POST: + break + return [[r2(x), r2(y)] for x, y in out] + + 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) @@ -507,6 +571,9 @@ def places(L, grid, standby_slots, aisles): # スタッフが待つ点:受け渡し口の前の横(待機場所と反対の側)に、パントリーの壁ぞいへ 0.6m おき(仮の規則) for i, pt in enumerate(staff_standby_points(L, free, aisles)): P.append(dict(kind="StaffStandby", label=f"スタッフが待つ点 {i + 1}", point=pt)) + # スタッフの持ち場(2026-10-31 エイイチ):パントリーのまわりに散らした点。入口番はレジ内(RegisterBack) + for i, pt in enumerate(staff_post_points(L, grid, aisles, P, standby_slots)): + P.append(dict(kind="StaffPost", 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