commit 1ee9d8e8590c9391280774c292ee266b918d26cc 店の形:Unity のロボットが自分で道を引くための地図を書き出す(版 2fa10547a2b7) diff --git a/tools/sim_layouts/README.md b/tools/sim_layouts/README.md index 22d2ece..1aa11e9 100644 --- a/tools/sim_layouts/README.md +++ b/tools/sim_layouts/README.md @@ -30,7 +30,8 @@ - 場所の点:受け渡し口の前・下げ膳の返却(受け渡し口と同じ)・入口・待合・レジ(レジ内の反対側)と並ぶ点・レジ内・トイレの入口の前・ドリンクバーの前と立つ点・並ぶ点・ロボットの待機場所(1台ずつ)・スタッフが待つ点 - スタッフが待つ点:パントリー(と接する壁)の客席側の辺にそって壁から0.25m、0.6mおき。外側にロボットのふだんの道(1.0m)が残る所だけ。受け渡し口に近い順に6つ(仮の規則) - 並ぶ点・立つ点は、障害物から0.25m未満の所と細い通路には置かない -- ロボットの道順(`routes`):受け渡し口の前→各卓の止まる位置(`from_pass`)と、卓→卓(`between`。"T01|T02" は T01 から T02、逆向きは点を逆にたどる)。ふだん走らせる幅の道で行けるならその道、行けなければ通れる幅の道(試算と同じ規則。`use` が normal か narrow。試算の卓の分け方と一致することをテストで確かめる)。マス目の最短の道を、通れる範囲でまっすぐにつないだ曲がり角の点の並び。Unity のロボットはこの点を順にたどる(Unity で道を決めない)。2026-10-22、Unity で狭い通路の区画を NavMesh に書き込んだら NavMesh が壊れたため、この形にした +- ロボットの道順(`routes`):受け渡し口の前→各卓の止まる位置(`from_pass`)と、卓→卓(`between`。"T01|T02" は T01 から T02、逆向きは点を逆にたどる)。ふだん走らせる幅の道で行けるならその道、行けなければ通れる幅の道(試算と同じ規則。`use` が normal か narrow。試算の卓の分け方と一致することをテストで確かめる)。マス目の最短の道を、通れる範囲でまっすぐにつないだ曲がり角の点の並び。2026-10-22 から Unity のロボットはこの点を順にたどっていたが、2026-10-24 から下の地図の上で自分で道を引く形に変える(線に縛ると、行きと帰りが同じ線になり、ロボットどうしが正面から出会って回り込めなかったため)。道順は、回り込みで延びた距離を測る物差しと、前の動き方に戻す切り替えのために残す +- ロボットの地図(`unity/{名前}_map.json`。2026-10-24):試算が卓までの距離を測るのと同じ2.5cmのマス目で、マスごとに「ふだんの道(1.0m)が通れる=2/通れる幅(0.8m)の道だけ通れる=1/通れない=0」(`free_class`)と、いちばん近い障害物までの距離(`clear_mm`)を入れたもの。分け方は試算と同じ式(`Grid.free`)。会計の列は通れない。待機場所は床として入れる(止まっているロボットは Unity で動くものとして扱う。待機場所の真ん中は壁から0.4m前後なので1のマス)。Unity のロボットはこの上で自分で道を引く(行き先までふだんの道で行けるならその道だけ、行けなければ狭い道も使う。試算と同じ規則)。地図だけで試算の卓の分け方(robot・narrow)が再現できることをテストで確かめる。中身は gzip で縮めた base64(読み方は `unity_export.unpack_map`) - 会計の列(`robot_keepout`):ロボットの道にしない矩形。人は通れるので、Unity では部屋や壁として建てない(道順はこれをよけて作ってある) - まだないもの:② の4人席・丸テーブル・6人の長テーブルの椅子と座る位置(parts が null)、人がロボットをよける逃げ場(refuges) diff --git a/tools/sim_layouts/famires_v3.py b/tools/sim_layouts/famires_v3.py index 89e8738..2c28568 100644 --- a/tools/sim_layouts/famires_v3.py +++ b/tools/sim_layouts/famires_v3.py @@ -1010,6 +1010,12 @@ def export_unity(L, res_t, ctx, nm, out, gen_id): 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) + # ロボットの地図(2026-10-24):同じマス目で、待機場所を床にしたもの。ロボットはこの上で自分で道を引く + gm = Grid(L, SETTINGS["res"], standby=[]) + M = unity_export.build_map(gm, SETTINGS["normal_width"], ROBOT_MODELS[SETTINGS["model"]]["pass_width"], + dict(generation=gen_id, units="m", source=f"{nm}.json と同じ店の形・同じマス目(settings.res)から作った", + robot_model=meta["robot_model"]["name"])) + json.dump(M, open(out / "unity" / f"{nm}_map.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1) return U diff --git a/tools/sim_layouts/test_famires_v3.py b/tools/sim_layouts/test_famires_v3.py index 9d5274c..b43624b 100644 --- a/tools/sim_layouts/test_famires_v3.py +++ b/tools/sim_layouts/test_famires_v3.py @@ -384,6 +384,38 @@ for nm, fn, width in (("p1", gen.p1_efficient_corner, 1.2), ("p5", gen.p5_cramme 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"))) +# ---- ロボットの地図(unity/{名前}_map.json。2026-10-24) +from scipy.sparse.csgraph import connected_components +for nm, fn in (("p1", gen.p1_efficient_corner), ("p5", gen.p5_crammed), ("p0", famires_p0.p0_memory)): + L = v.prepare(fn()) + ctx = {} + res, _ = v.analyze(L, v.SETTINGS["res"], ctx) + g = ctx["grid"] + nw, pw = v.SETTINGS["normal_width"], v.ROBOT_MODELS[v.SETTINGS["model"]]["pass_width"] + gm = v.Grid(L, v.SETTINGS["res"], standby=[]) + M = ux.build_map(gm, nw, pw, {}) + cls, mm = ux.unpack_map(M) + expect(f"{nm}:地図を読み戻すと、マス目の数が同じ", cls.shape == (g.ny, g.nx) == (M["ny"], M["nx"])) + expect(f"{nm}:地図を2回作ると同じ文字列(毎回同じ)", ux.build_map(gm, nw, pw, {})["free_class"] == M["free_class"]) + expect(f"{nm}:障害物までの距離は、計算の値と0.5mm以内", float(abs(mm / 1000.0 - np.minimum(gm.clear, 65.535)).max()) <= 0.0005 + 1e-9) + expect(f"{nm}:試算でふだんの道が通れるマスは、地図でも2(待機場所を床にした分だけ増える)", + bool((cls[g.free(nw)] == 2).all()) and bool((cls[g.free(pw)] >= 1).all())) + expect(f"{nm}:会計の列のマスは、地図では通れない", + all(cls[gm.cell((k[0] + k[2]) / 2, (k[1] + k[3]) / 2)] == 0 for k in v.checkout_keepout(L))) + # 地図だけで、試算と同じ卓の分け方になるか(受け渡し口の前から、2のマスだけでつながる卓=ふだんの道の卓) + _, lab2 = connected_components(gm.graph(cls == 2), directed=False) + _, lab1 = connected_components(gm.graph(cls >= 1), directed=False) + c = lambda x, y: (lambda q: q[0] * gm.nx + q[1])(gm.cell(x, y)) + s = c(*L["start"]) + expect(f"{nm}:地図の上でも、ふだんの道で行ける卓は試算の robot の卓と同じ、狭い道でだけ行ける卓は narrow の卓と同じ", + all(((lab2[s] == lab2[c(*a["stop"])]) == (a["category"] == "robot")) and + ((lab1[s] == lab1[c(*a["stop"])]) == (a["category"] in ("robot", "narrow"))) + for a in res if a.get("stop"))) + slots = v.standby_slots(L["robot_standby"]) + # 待機場所は壁ぎわに止める形(真ん中が壁から0.4m前後)なので、最後は狭い道(1)のマスになる + expect(f"{nm}:どの待機場所の真ん中にも、受け渡し口の前から地図の上で行ける(通れる幅の道)", + all(lab1[s] == lab1[c((r[0] + r[2]) / 2, (r[1] + r[3]) / 2)] for r in slots)) + # 確かめの仕組みの確かめ:会計の列がなければ、②はロボットが会計の列の前を横切り、確かめに引っかかる _ko = v.checkout_keepout v.checkout_keepout = lambda L: [] diff --git a/tools/sim_layouts/unity_export.py b/tools/sim_layouts/unity_export.py index 168c9ef..41d4fcb 100644 --- a/tools/sim_layouts/unity_export.py +++ b/tools/sim_layouts/unity_export.py @@ -22,7 +22,14 @@ - 席に着くために立つ点は、障害物から0.2m(人の半径)以上離れた所だけ - 会計の列(robot_keepout):レジの客の側で、ロボットの道にしない矩形(famires_v3.checkout_keepout)。人は通れるので、部屋や壁としては建てない。 ロボットの道順(routes)はこれをよけて作ってある。Unity では見せるための目印として使ってよい +- ロボットの地図(unity/{名前}_map.json。2026-10-24):試算が卓までの距離を測るのと同じマス目(settings.res)の、マスごとの値。 + free_class はマスの分け方(2=ふだん走らせる幅 normal_width の道が通れる、1=通れる幅 pass_width の道だけ通れる、0=通れない)。 + clear_mm はマスの中心からいちばん近い障害物(壁・部屋・卓・会計の列)までの距離(mm)。 + 分け方は試算と同じ式(famires_v3.Grid.free)で、ロボットはこの地図の上で自分で道を引く(ふだんの道で行けるならそこ、行けなければ狭い道。試算と同じ規則)。 + 待機場所は床として入れる(止まっているロボットは、Unity で動くものとして扱う)。routes は残す(回り込みで延びた距離を測る物差し) """ +import base64 +import gzip import math import numpy as np @@ -517,3 +524,36 @@ def build(L, analysis_tables, grid, dist, dist_pass, sides, standby_slots, FT, m 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", []), 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)) + + +MAP_SCHEMA = "famires-unity-map/0.1" + + +def _pack(a): + """配列を、行(y=0 の手前から)ごとに並べた生のバイト列 → gzip → base64 の文字列にする(gzip の時刻は 0。同じ中身なら毎回同じ文字列)""" + return base64.b64encode(gzip.compress(np.ascontiguousarray(a).tobytes(), mtime=0)).decode("ascii") + + +def unpack_map(M): + """書き出した地図を読み戻す(確かめ用。Unity でも同じ手順で読む)""" + ny, nx = M["ny"], M["nx"] + cls = np.frombuffer(gzip.decompress(base64.b64decode(M["free_class"])), dtype=np.uint8).reshape(ny, nx) + mm = np.frombuffer(gzip.decompress(base64.b64decode(M["clear_mm"])), dtype="