diff --git a/tools/sim_layouts/famires_v3.py b/tools/sim_layouts/famires_v3.py index 816f6de..89e8738 100644 --- a/tools/sim_layouts/famires_v3.py +++ b/tools/sim_layouts/famires_v3.py @@ -15,6 +15,10 @@ codex の2回目の検証(review_result_v3.md)を受けて直したこと( - 2人席:止まってよいのは、椅子の側とテーブルの横(ベンチの横は使わない) - 待機場所:1台分が0.8×0.8m以上か、台数が正しいかを確かめる。1台ずつ、ほかの台が止まったままで受け渡し口の前まで出られるかを確かめる +レジのまわり(2026-10-23 エイイチと決めた) +- レジの置き方で通路を狭くしない。会計に立つ点・レジに並ぶ点は、ロボットの道(受け渡し口から卓・卓から卓)から0.75m以上 +- 会計の列(カウンターの客の側、幅1.0m・長さ2.15m)はロボットの道にしない。人は通れる。レジ横はロボットが通ってよい + 使い方: python3 famires_v3.py (出力はサイトの中なら public/sim/layouts/、外なら variants3/) python3 famires_v3.py Servi (機種を替えて試算) """ @@ -69,6 +73,8 @@ SETTINGS = dict( joint_max_depth=0.08, # 仕切りが家具に食い込んでよい深さ(背もたれの上に立つ仕切り) register_min_len=2.5, register_clear=0.75, # 会計に立つ点・レジに並ぶ点は、ロボットの道からこの距離以上(人がロボットをよける距離と同じ) + checkout_keepout_w=1.0, # 会計の列(ロボットだけ通さない所)の幅。列の真ん中から左右0.5m + checkout_keepout_len=2.15, # 会計の列の長さ:カウンターから会計に立つ点(0.4m)+並ぶ点3つ(0.5mおき)+人の半径ぶん lsofa_end_reach=0.6, # L字席:開いた辺から、ソファの端に沿ってこの長さまで止まってよい(端の席の人が受け取れる所) standby_slot=0.8, # 待機場所の1台分の最小の大きさ(m×m) ) @@ -242,6 +248,34 @@ def all_obstacles(L, skip=None, standby=None): return obs +def checkout_keepout(L): + """会計の列:レジの客の側(レジ内の反対側)で、カウンターの真ん中から外へ checkout_keepout_len、幅 checkout_keepout_w の矩形。 + ロボットの道にだけ効く(人は通れる。部屋や壁ではない)。店の外にはみ出す分は切る。 + 会計の列の前をロボットが横切らないようにする(2026-10-23 エイイチと決めた。レジ横はロボットが通ってよい)""" + S = SETTINGS + W, D = L["outer"]["w"], L["outer"]["d"] + regs = [r["rect"] for r in L["rooms"] if r["label"] == "レジ"] + backs = [r["rect"] for r in L["rooms"] if r["label"] == "レジ内"] + out = [] + for x0, y0, x1, y1 in regs: + if not backs: + continue + b = backs[0] + h, n = S["checkout_keepout_w"] / 2, S["checkout_keepout_len"] + cx, cy = (x0 + x1) / 2, (y0 + y1) / 2 + if b[3] <= y0 + 1e-6: # レジ内が下 → 客は上 + r = (cx - h, y1, cx + h, y1 + n) + elif b[1] >= y1 - 1e-6: # レジ内が上 → 客は下 + r = (cx - h, y0 - n, cx + h, y0) + elif b[2] <= x0 + 1e-6: # レジ内が左 → 客は右 + r = (x1, cy - h, x1 + n, cy + h) + else: # レジ内が右 → 客は左 + r = (x0 - n, cy - h, x0, cy + h) + r = (max(0.0, r[0]), max(0.0, r[1]), min(W, r[2]), min(D, r[3])) + out.append(tuple(round(v, 3) for v in r)) + return out + + def open_sides(L, i, probe=0.2): """座る側のうち、外側 probe の帯に何もない辺(壁・仕切り・ほかの卓・店の外がない辺)""" t = L["tables"][i] @@ -595,13 +629,22 @@ class Grid: dx = np.maximum(np.maximum(x0 - X, X - x1), 0.0) dy = np.maximum(np.maximum(y0 - Y, Y - y1), 0.0) np.minimum(clear, np.hypot(dx, dy), out=clear) - self.clear = clear + self.clear_person = clear.copy() # 人の余裕(会計の列を含まない) + for x0, y0, x1, y1 in checkout_keepout(L): # ロボットの余裕だけ、会計の列も障害物にする + dx = np.maximum(np.maximum(x0 - X, X - x1), 0.0) + dy = np.maximum(np.maximum(y0 - Y, Y - y1), 0.0) + np.minimum(clear, np.hypot(dx, dy), out=clear) + self.clear = clear # ロボットの余裕 self.occ = clear <= 1e-9 def free(self, width): # 幅 width 以上の通路は必ず通れる。width−res 未満は必ず通れない(マス目の中心が通路の真ん中に来るとは限らないため) return self.clear >= width / 2 - self.res / 2 - 1e-9 + def free_person(self, width): + """人が通れる所(会計の列も通れる)""" + return self.clear_person >= width / 2 - self.res / 2 - 1e-9 + def cell(self, x, y): return int(y / self.res), int(x / self.res) @@ -705,7 +748,7 @@ def analyze(L, res, ctx=None): errs = [] if not (0 <= cy < g.ny and 0 <= cx < g.nx): raise ValueError("出発点が店の外にある") - f_pass, f_norm, f_hum = g.free(pw), g.free(nw), g.free(hw) + f_pass, f_norm, f_hum = g.free(pw), g.free(nw), g.free_person(hw) if not f_pass[cy, cx]: raise ValueError(f"出発点(受け渡し口の前)がロボットの通れない場所にある(余裕 {g.clear[cy, cx]:.2f}m)") s0 = cy * g.nx + cx @@ -912,6 +955,8 @@ def draw(ax, L, res_t, summ, conv): for i in range(sb["slots"]): ax.add_patch(Circle((x0 + (x1 - x0) * (i + 0.5) / sb["slots"], (y0 + y1) / 2), body, fc=ROBOT, ec="white", lw=0.8, zorder=6)) ax.add_patch(Circle(tuple(L["start"]), 0.12, fc="white", ec=ROBOT, lw=1.5, zorder=6)) + for kx0, ky0, kx1, ky1 in checkout_keepout(L): # 会計の列(ロボットは通らない) + ax.add_patch(Rectangle((kx0, ky0), kx1 - kx0, ky1 - ky0, fc="none", ec=ROBOT, lw=0.8, ls="--", hatch="///", alpha=0.5, zorder=4)) s = summ ax.text(0, -1.0, f'{L["name"]} {s["seats"]}席・{W:g}m×{D:g}m', fontsize=10.5, color=INK, va="center", weight="bold") @@ -961,7 +1006,7 @@ def export_unity(L, res_t, ctx, nm, out, gen_id): coords="原点は店の外壁の内側の左下の角。x は右、y は奥(上)。Unity では x→X、y→Z、高さ→Y とする", source=f"{nm}.json と {nm}_analysis.json(同じ generation)から決まった規則で作った。数字(卓・距離・止まる位置)は analysis と同じ", robot_model=dict(name=SETTINGS["model"], **ROBOT_MODELS[SETTINGS["model"]]), settings=SETTINGS) - U = unity_export.build(L, res_t, ctx["grid"], ctx["d_norm"], ctx["d_pass"], ctx["sides"], + U = unity_export.build(dict(L, robot_keepout=checkout_keepout(L)), res_t, ctx["grid"], ctx["d_norm"], ctx["d_pass"], ctx["sides"], standby_slots(L["robot_standby"]), FURNITURE_TYPES, meta) (out / "unity").mkdir(exist_ok=True) json.dump(U, open(out / "unity" / f"{nm}.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1) diff --git a/tools/sim_layouts/test_famires_v3.py b/tools/sim_layouts/test_famires_v3.py index b0bc649..5c52775 100644 --- a/tools/sim_layouts/test_famires_v3.py +++ b/tools/sim_layouts/test_famires_v3.py @@ -356,9 +356,9 @@ for nm, fn, width in (("p1", gen.p1_efficient_corner, 1.2), ("p5", gen.p5_cramme g = ctx["grid"] pts = [p["point"] for p in U["places"] if p["kind"] in ("StaffStandby", "RegisterQueue", "DrinkBarQueue", "DrinkBarStand")] expect(f"{nm}:スタッフが待つ点・並ぶ点・立つ点は、障害物から0.2m以上離れ、細い通路に入らない", - all(g.clear[g.cell(x, y)] >= 0.2 - 1e-9 and not ux._in_aisle(U["aisles"], x, y) for x, y in pts)) + all(g.clear_person[g.cell(x, y)] >= 0.2 - 1e-9 and not ux._in_aisle(U["aisles"], x, y) for x, y in pts)) expect(f"{nm}:席に着く点は障害物から0.2m以上離れ、どの卓にも1つ以上ある", - all(t["spots"] for t in U["tables"]) and all(g.clear[g.cell(*s["point"])] >= 0.2 - 1e-9 for t in U["tables"] for s in t["spots"])) + all(t["spots"] for t in U["tables"]) and all(g.clear_person[g.cell(*s["point"])] >= 0.2 - 1e-9 for t in U["tables"] for s in t["spots"])) expect(f"{nm}:レジ内は通れる部屋(スタッフだけ)、ほかの部屋は通れない箱", all((r["walkable"] and r.get("staff_only")) if r["label"] == "レジ内" else not r["walkable"] for r in U["rooms"])) # ロボットの道順(2026-10-22) @@ -375,6 +375,27 @@ for nm, fn, width in (("p1", gen.p1_efficient_corner, 1.2), ("p5", gen.p5_cramme allr = list(fp.values()) + list(R["between"].values()) expect(f"{nm}:道順のどの区間も、その幅で通れるマス目だけを通る", all(ux._visible(f_n if r["use"] == "normal" else f_p, g, a, b) for r in allr for a, b in zip(r["points"], r["points"][1:]))) + # レジのまわり(2026-10-23) + expect(f"{nm}:会計に立つ点・並ぶ点は、ロボットの道から0.75m以上", v.check_register_clear(dict(places=U["places"], routes=R)) == []) + ko = v.checkout_keepout(L) + expect(f"{nm}:会計の列が1つあり、ロボットは通れず、人は通れる", + len(ko) == 1 and all(g.clear[g.cell((k[0] + k[2]) / 2, (k[1] + k[3]) / 2)] <= 1e-9 and + g.clear_person[g.cell((k[0] + k[2]) / 2, (k[1] + k[3]) / 2)] >= 0.2 for k in ko)) + expect(f"{nm}:会計に立つ点・並ぶ点は会計の列の中", all(any(k[0] <= p["point"][0] <= k[2] and k[1] <= p["point"][1] <= k[3] for k in ko) + for p in U["places"] if p["kind"] in ("Register", "RegisterQueue"))) + +# 確かめの仕組みの確かめ:会計の列がなければ、②はロボットが会計の列の前を横切り、確かめに引っかかる +_ko = v.checkout_keepout +v.checkout_keepout = lambda L: [] +try: + L = v.prepare(famires_p0.p0_memory()) + ctx = {} + res, _ = v.analyze(L, v.SETTINGS["res"], ctx) + U = ux.build(L, res, ctx["grid"], ctx["d_norm"], ctx["d_pass"], ctx["sides"], v.standby_slots(L["robot_standby"]), v.FURNITURE_TYPES, {}) + R = ux.build_routes(L, res, ctx["grid"], v.SETTINGS["normal_width"], v.ROBOT_MODELS[v.SETTINGS["model"]]["pass_width"]) + expect("会計の列がないと、②は会計に立つ点の近くをロボットが通り、確かめに引っかかる", len(v.check_register_clear(dict(places=U["places"], routes=R))) > 0) +finally: + v.checkout_keepout = _ko L = v.prepare(gen.p1_efficient_corner()) L["tables"][5]["x"] += 0.05 # 列の中の1卓をずらす → 列が切れる cols = ux.booth_columns(L["tables"]) diff --git a/tools/sim_layouts/unity_export.py b/tools/sim_layouts/unity_export.py index eacde65..168c9ef 100644 --- a/tools/sim_layouts/unity_export.py +++ b/tools/sim_layouts/unity_export.py @@ -20,6 +20,8 @@ マス目の上の最短の道を、通れる範囲でまっすぐにつないで、曲がり角の点だけにした。卓→受け渡し口は from_pass を逆にたどる - 部屋は通れない箱。ただし walkable=true の部屋(レジ内)は人が中に立つ所で、箱にしない(staff_only=true はスタッフだけが入れる) - 席に着くために立つ点は、障害物から0.2m(人の半径)以上離れた所だけ +- 会計の列(robot_keepout):レジの客の側で、ロボットの道にしない矩形(famires_v3.checkout_keepout)。人は通れるので、部屋や壁としては建てない。 + ロボットの道順(routes)はこれをよけて作ってある。Unity では見せるための目印として使ってよい """ import math @@ -201,7 +203,7 @@ def seat_spots(sides, grid): 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: + if 0 <= cy < grid.ny and 0 <= cx < grid.nx and grid.clear_person[cy, cx] >= PERSON_R - 1e-9: out.append(dict(side=k, point=[r2(x), r2(y)])) return out @@ -338,7 +340,7 @@ def places(L, grid, standby_slots, aisles): 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]) + return float(grid.clear_person[cy, cx]) # 場所の点は人が立つ所なので、人の余裕で見る P = [] st = L["start"] P.append(dict(kind="FoodPass", label="受け渡し口の前(出発点)", rect=L["dish_up"][0]["rect"], point=list(st))) @@ -514,4 +516,4 @@ def build(L, analysis_tables, grid, dist, dist_pass, sides, standby_slots, FT, m 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)) + doors=L.get("doors", []), robot_keepout=[dict(label="会計の列(ロボットは通らない)", rect=list(r)) for r in L.get("robot_keepout", [])], aisles=aisles, tables=tables, places=places(L, grid, standby_slots, aisles))