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;
}
}
}