using System.Collections.Generic; using UnityEngine; namespace ServiceSim { /// /// 床の塗り分け(見た目だけ。2026-10-23 に決め直した)。 /// /// 色は **サイトの JSON から決める**(Unity の NavMesh の形からは決めない)。 /// 使っていない NavMesh の形を塗ると、ロボットが道順で入る細い通路が塗られず、 /// 「入れない」ように見えてしまうため。 /// /// 水色  :ロボットがふだん走る床 /// サイトの道順(routes)に沿って、ロボットの体(navAgentRadius)が通る範囲 /// 濃い水色:細い通路(狭い道で入る)= 店の形の aisles の区画 /// /// use=narrow の道も「床」として塗る。狭い道が狭いのは通路の中だけなので、 /// 通路の外(広い通路を通っている部分)まで濃い水色にすると、 /// ⑦ではほとんどの床が「細い通路」になってしまう(水色 8.6 m2 / 濃い水色 79.1 m2)。 /// 区画で分けると ⑦は 水色 64.1 / 濃い水色 23.5 m2 になる。 /// /// どちらも体が通る範囲いっぱいまで塗る(道の中心線だけにしない)。 /// 置いてあるもの(壁・卓・椅子・ベンチ・カウンター・通れない部屋)の下は塗らない。 /// 2つが重なるところは、濃い水色(細い通路)を出す。 /// /// 塗りは当たりを持たない板(1色につき1枚のメッシュ)なので、 /// NavMesh には焼かれず、計算に使う NavMesh そのものは変わらない。 /// /// この script は見た目だけを扱う。店の形も人もロボットも動かさない。 /// public class FloorPaint : MonoBehaviour { /// 塗り分けの結果(報告に出す) public struct Result { public float robotAreaM2; // ロボットがふだん走る床の広さ(水色) public float peopleAreaM2; // 細い通路の広さ(濃い水色) public int robotQuads; // 塗りに使った四角の数 public int peopleQuads; public float cellSize; public bool ok; public string note; /// 読んだ道順の本数(ふだんの道/狭い道) public int normalRoutes, narrowRoutes; /// 細い通路の区画の数 public int aisleZones; } /// /// 塗り分けを作り直す(サイトの道順と細い通路の区画から)。 /// robotRadius はロボットの体の半径(navAgentRadius)。 /// public static Result Rebuild(Transform parent, StoreLayout layout, Bounds bounds, RouteBook routes, StorePlan plan, float robotRadius, Material normalMat, Material narrowMat) { Result res = new Result(); res.ok = false; Clear(parent); if (layout == null) { res.note = "StoreLayout がありません"; return res; } if (plan == null) { res.note = "店をまだ建てていません"; return res; } if (routes == null) { res.note = "サイトの道順(routes)がありません"; return res; } float cell = Mathf.Max(0.02f, layout.floorPaintCellSize); res.cellSize = cell; float x0 = bounds.min.x, z0 = bounds.min.z; int nx = Mathf.CeilToInt(bounds.size.x / cell) + 1; int nz = Mathf.CeilToInt(bounds.size.z / cell) + 1; if (nx <= 0 || nz <= 0 || (long)nx * nz > 4000000) { res.note = "塗る範囲が大きすぎます(floorPaintCellSize を大きくしてください)"; return res; } bool[] normal = new bool[nx * nz]; bool[] narrow = new bool[nx * nz]; // 道順に沿って、体が通る範囲を塗る。 // use=narrow の道も「床」として塗る。狭い道は通路の中だけなので、 // 通路の外(広い通路を通っている部分)まで濃い水色にはしない List all = routes.AllRoutes(); for (int i = 0; i < all.Count; i++) { RouteBook.Route r = all[i]; if (r == null || r.points.Count < 2) continue; if (r.IsNarrow) res.narrowRoutes++; else res.normalRoutes++; for (int k = 1; k < r.points.Count; k++) StrokeSegment(normal, r.points[k - 1], r.points[k], robotRadius, x0, z0, cell, nx, nz); } // 細い通路の区画(狭い道で入る所)。区画いっぱいが、体が通る範囲 for (int i = 0; i < plan.aisles.Count; i++) { if (!plan.aisles[i].narrow) continue; res.aisleZones++; Fill(narrow, plan.aisles[i].rect, x0, z0, cell, nx, nz); } // 置いてあるものの下は塗らない bool[] blocked = RasterizeSolids(plan, x0, z0, cell, nx, nz); if (blocked != null) { for (int i = 0; i < normal.Length; i++) if (blocked[i]) { normal[i] = false; narrow[i] = false; } } // 重なるところは、濃い水色(細い通路)を出す for (int i = 0; i < normal.Length; i++) if (narrow[i]) normal[i] = false; float y = Mathf.Max(0.001f, layout.floorPaintHeight); res.robotQuads = Build(parent, "FloorPaint_Robot", normal, nx, nz, x0, z0, cell, y, normalMat); res.peopleQuads = Build(parent, "FloorPaint_Narrow", narrow, nx, nz, x0, z0, cell, y, narrowMat); res.robotAreaM2 = Count(normal) * cell * cell; res.peopleAreaM2 = Count(narrow) * cell * cell; res.ok = true; res.note = "ok"; return res; } /// /// 道順の1区間に沿って、体(半径 radius)が通る範囲を塗る。 /// 区間の囲み箱の中だけを見て、点から線分までの距離で決める /// static void StrokeSegment(bool[] g, Vector3 a, Vector3 b, float radius, float x0, float z0, float cell, int nx, int nz) { float r = Mathf.Max(cell * 0.5f, radius); float minX = Mathf.Min(a.x, b.x) - r, maxX = Mathf.Max(a.x, b.x) + r; float minZ = Mathf.Min(a.z, b.z) - r, maxZ = Mathf.Max(a.z, b.z) + r; int ix0 = Mathf.Max(0, Mathf.FloorToInt((minX - x0) / cell)); int ix1 = Mathf.Min(nx - 1, Mathf.CeilToInt((maxX - x0) / cell)); int iz0 = Mathf.Max(0, Mathf.FloorToInt((minZ - z0) / cell)); int iz1 = Mathf.Min(nz - 1, Mathf.CeilToInt((maxZ - z0) / cell)); float r2 = r * r; for (int iz = iz0; iz <= iz1; iz++) { float pz = z0 + (iz + 0.5f) * cell; for (int ix = ix0; ix <= ix1; ix++) { int k = iz * nx + ix; if (g[k]) continue; float px = x0 + (ix + 0.5f) * cell; if (DistanceSqToSegment(px, pz, a, b) <= r2) g[k] = true; } } } /// 点から線分までの距離の2乗(XZ) static float DistanceSqToSegment(float px, float pz, Vector3 a, Vector3 b) { float ax = a.x, az = a.z, bx = b.x, bz = b.z; float dx = bx - ax, dz = bz - az; float len2 = dx * dx + dz * dz; float t = len2 > 1e-8f ? ((px - ax) * dx + (pz - az) * dz) / len2 : 0f; t = Mathf.Clamp01(t); float cx = ax + dx * t, cz = az + dz * t; float ex = px - cx, ez = pz - cz; return ex * ex + ez * ez; } /// 塗りを消す public static void Clear(Transform parent) { if (parent == null) return; for (int i = parent.childCount - 1; i >= 0; i--) { Transform t = parent.GetChild(i); if (!t.name.StartsWith("FloorPaint_")) continue; if (Application.isPlaying) Destroy(t.gameObject); else DestroyImmediate(t.gameObject); } } // ---------------- 格子に落とす ---------------- /// /// 置いてあるもの(通れないもの)の footprint を格子に塗る。 /// 床・通路の色板・座る場所の見た目は通れるので入れない。 /// static bool[] RasterizeSolids(StorePlan plan, float x0, float z0, float cell, int nx, int nz) { if (plan == null) return null; bool[] g = new bool[nx * nz]; for (int i = 0; i < plan.boxes.Count; i++) { PlanBox b = plan.boxes[i]; if (b.style == BoxStyle.Floor || b.style == BoxStyle.AisleMark || b.style == BoxStyle.BenchSeat) continue; Fill(g, b.rect, x0, z0, cell, nx, nz); } for (int i = 0; i < plan.places.Count; i++) { PlanPlace p = plan.places[i]; if (!p.solid || p.rect.IsEmpty) continue; Fill(g, p.rect, x0, z0, cell, nx, nz); } return g; } static void Fill(bool[] g, PlanRect r, float x0, float z0, float cell, int nx, int nz) { int ix0 = Mathf.Max(0, Mathf.FloorToInt((r.xMin - x0) / cell)); int ix1 = Mathf.Min(nx - 1, Mathf.CeilToInt((r.xMax - x0) / cell) - 1); int iz0 = Mathf.Max(0, Mathf.FloorToInt((r.zMin - z0) / cell)); int iz1 = Mathf.Min(nz - 1, Mathf.CeilToInt((r.zMax - z0) / cell) - 1); for (int iz = iz0; iz <= iz1; iz++) for (int ix = ix0; ix <= ix1; ix++) g[iz * nx + ix] = true; } static int Count(bool[] g) { int n = 0; for (int i = 0; i < g.Length; i++) if (g[i]) n++; return n; } // ---------------- 四角にまとめてメッシュにする ---------------- /// /// 塗るマスを大きな四角にまとめて、1枚のメッシュにする。 /// 当たりは付けない(NavMesh に焼かれないようにする)。四角の数を返す。 /// static int Build(Transform parent, string name, bool[] grid, int nx, int nz, float x0, float z0, float cell, float y, Material mat) { bool[] used = new bool[grid.Length]; List verts = new List(); List tris = new List(); int quads = 0; for (int iz = 0; iz < nz; iz++) { for (int ix = 0; ix < nx; ix++) { int k = iz * nx + ix; if (!grid[k] || used[k]) continue; // 右へ伸ばす int w = 1; while (ix + w < nx && grid[iz * nx + ix + w] && !used[iz * nx + ix + w]) w++; // 上へ伸ばす(同じ幅ぶん続くあいだ) int h = 1; while (iz + h < nz) { bool full = true; for (int i = 0; i < w; i++) { int kk = (iz + h) * nx + ix + i; if (!grid[kk] || used[kk]) { full = false; break; } } if (!full) break; h++; } for (int j = 0; j < h; j++) for (int i = 0; i < w; i++) used[(iz + j) * nx + ix + i] = true; float ax = x0 + ix * cell, az = z0 + iz * cell; float bx = ax + w * cell, bz = az + h * cell; int v = verts.Count; verts.Add(new Vector3(ax, 0f, az)); verts.Add(new Vector3(bx, 0f, az)); verts.Add(new Vector3(bx, 0f, bz)); verts.Add(new Vector3(ax, 0f, bz)); tris.Add(v); tris.Add(v + 2); tris.Add(v + 1); tris.Add(v); tris.Add(v + 3); tris.Add(v + 2); quads++; } } if (quads == 0) return 0; GameObject go = new GameObject(name); go.transform.SetParent(parent, false); go.transform.position = new Vector3(0f, y, 0f); Mesh mesh = new Mesh(); mesh.name = name; mesh.indexFormat = verts.Count > 65000 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16; mesh.SetVertices(verts); mesh.SetTriangles(tris, 0); mesh.RecalculateNormals(); mesh.RecalculateBounds(); go.AddComponent().sharedMesh = mesh; MeshRenderer mr = go.AddComponent(); if (mat != null) mr.sharedMaterial = mat; mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; mr.receiveShadows = false; return quads; } } }