using System;
using System.Collections.Generic;
using UnityEngine;
namespace ShieldField
{
public enum TilingKind
{
Hex, // regular hexagons
TriHex, // 3.6.3.6 — hexagons + triangles (dual: rhombille)
Square,
PenroseP3, // five-fold quasiperiodic rhombs, via substitution
Voronoi, // Lloyd-relaxed jittered lattice
VoronoiIFS // Voronoi over a chaos-game IFS attractor — fractal cell density
}
/// One affine map of an iterated function system, with its selection weight.
[Serializable]
public struct IfsMap
{
public Vector2 translate;
public float rotateDeg;
public float scale;
public float weight;
public Vector2 Apply(Vector2 p)
{
float r = rotateDeg * Mathf.Deg2Rad, c = Mathf.Cos(r), s = Mathf.Sin(r);
Vector2 q = new Vector2(p.x * c - p.y * s, p.x * s + p.y * c) * scale;
return q + translate;
}
}
public static class Tilings
{
public const float Phi = 1.6180339887f;
public static PolygonSoup Generate(TilingKind kind, float radius, float cellSize,
int seed, IfsMap[] ifs = null)
{
switch (kind)
{
case TilingKind.Hex: return Hex(radius, cellSize);
case TilingKind.TriHex: return TriHex(radius, cellSize);
case TilingKind.Square: return Square(radius, cellSize);
case TilingKind.PenroseP3: return Penrose(radius, cellSize);
case TilingKind.Voronoi: return Voronoi(JitteredSites(radius, cellSize, seed), radius, 3);
case TilingKind.VoronoiIFS: return Voronoi(ChaosGame(ifs ?? Pentaflake(), radius, cellSize, seed), radius, 1);
default: return Hex(radius, cellSize);
}
}
// ==============================================================
// Periodic lattices
// ==============================================================
public static PolygonSoup Hex(float radius, float size)
{
var soup = new PolygonSoup();
float w = size * Mathf.Sqrt(3f), h = size * 1.5f;
int nx = Mathf.CeilToInt(radius / w) + 2, ny = Mathf.CeilToInt(radius / h) + 2;
for (int j = -ny; j <= ny; j++)
for (int i = -nx; i <= nx; i++)
{
Vector2 c = new Vector2(i * w + (j & 1) * w * 0.5f, j * h);
if (c.magnitude > radius) continue;
soup.Add(Ngon(c, size, 6, 90f), 0);
}
return soup;
}
public static PolygonSoup TriHex(float radius, float size)
{
// Hexagons on a triangular lattice with gaps filled by upward/downward triangles.
var soup = new PolygonSoup();
float s = size; // hexagon circumradius
float a = s * Mathf.Sqrt(3f); // lattice spacing between hex centres
float w = a * Mathf.Sqrt(3f), h = a * 1.5f;
int nx = Mathf.CeilToInt(radius / w) + 2, ny = Mathf.CeilToInt(radius / h) + 2;
var hexCentres = new List();
for (int j = -ny; j <= ny; j++)
for (int i = -nx; i <= nx; i++)
{
Vector2 c = new Vector2(i * w + (j & 1) * w * 0.5f, j * h);
if (c.magnitude > radius + a) continue;
hexCentres.Add(c);
soup.Add(Ngon(c, s, 6, 90f), 0);
}
// Triangles sit in the interstices: each is bounded by three hex corners.
var weld = new VertexWelder(1e-3f);
var corners = new Dictionary();
foreach (var c in hexCentres)
for (int k = 0; k < 6; k++)
{
Vector2 p = c + Polar(s, 90f + k * 60f);
corners[weld.Get(p)] = p;
}
float triSide = s;
var seen = new HashSet();
foreach (var c in hexCentres)
for (int k = 0; k < 6; k++)
{
// interstitial triangle centre lies between two adjacent hexes
Vector2 dir = Polar(1f, 60f * k);
Vector2 tc = c + dir * (a / Mathf.Sqrt(3f));
if (tc.magnitude > radius) continue;
long key = ((long)Mathf.RoundToInt(tc.x * 1000f) << 32) ^ (uint)Mathf.RoundToInt(tc.y * 1000f);
if (!seen.Add(key)) continue;
float rot = (k % 2 == 0) ? 180f : 0f;
var tri = Ngon(tc, triSide / Mathf.Sqrt(3f), 3, rot);
soup.Add(tri, 1);
}
return soup;
}
public static PolygonSoup Square(float radius, float size)
{
var soup = new PolygonSoup();
int n = Mathf.CeilToInt(radius / size) + 1;
for (int j = -n; j <= n; j++)
for (int i = -n; i <= n; i++)
{
Vector2 c = new Vector2((i + 0.5f) * size, (j + 0.5f) * size);
if (c.magnitude > radius) continue;
float h = size * 0.5f;
soup.Add(new[]{ c + new Vector2(-h,-h), c + new Vector2(h,-h),
c + new Vector2(h, h), c + new Vector2(-h, h) }, 0);
}
return soup;
}
// ==============================================================
// Penrose P3 — Robinson triangle substitution.
// Run TilingBuilder.MergeRhombs on the result to recover the rhombs.
// ==============================================================
public static PolygonSoup Penrose(float radius, float cellSize)
{
// D18 fix: the substitution count used to be a fixed authored constant,
// so the whole tiling was just uniformly rescaled to `radius` afterward —
// cell count never grew with panel size. Every other tiling here (Hex,
// TriHex, Square, Voronoi) uses cellSize to hold local cell size constant
// while radius controls how far the tiling extends; derive an iteration
// count that does the same for Penrose. Each substitution divides edge
// length by phi (the inflation factor), and the seed triangles have unit
// edges before the final `* scale` below, so solve for the iteration
// count that brings a unit edge down to approximately cellSize once
// scaled by `radius`.
//
// D21 fix: a quasiperiodic substitution can only land ON one of these
// phi-quantised edge lengths, never between them — `cellSize` for Penrose
// means "the target edge length", and the right answer is the iteration
// count whose resulting edge length is NEAREST that target, not the count
// that guarantees the tiling is at least as fine as requested. The
// original `CeilToInt(...) + 1` always rounded up AND added an extra
// substitution on top, which is why Penrose landed an order of magnitude
// denser than Hex at the same cellSize (D21, §2.2/§2.3): every fractional
// log rounded away from cellSize toward finer, and the +1 then halved the
// edge length again. Rounding to the nearest integer substitution count is
// the fix; every other tiling here already reports the density the caller
// asked for, and Penrose can only ever approximate it to within one
// phi step, same as it does now, just centred on the target instead of
// always overshooting it.
int iterations = Mathf.Clamp(
Mathf.RoundToInt(Mathf.Log(Mathf.Max(radius / Mathf.Max(cellSize, 1e-4f), 1f), Phi)),
1, 12);
return Penrose(radius, iterations);
}
/// Penrose substitution with an explicit iteration count. Kept for probes
/// and callers that want to pin the level of detail directly.
public static PolygonSoup Penrose(float radius, int iterations)
{
var tris = new List<(int type, Vector2 a, Vector2 b, Vector2 c)>();
for (int i = 0; i < 10; i++)
{
Vector2 b = Polar(1f, (2 * i - 1) * 18f);
Vector2 c = Polar(1f, (2 * i + 1) * 18f);
if (i % 2 == 0) (b, c) = (c, b);
tris.Add((0, Vector2.zero, b, c));
}
for (int it = 0; it < iterations; it++)
{
var next = new List<(int, Vector2, Vector2, Vector2)>(tris.Count * 3);
foreach (var (type, A, B, C) in tris)
{
if (type == 0)
{
Vector2 P = A + (B - A) / Phi;
next.Add((0, C, P, B));
next.Add((1, P, C, A));
}
else
{
Vector2 Q = B + (A - B) / Phi;
Vector2 R = B + (C - B) / Phi;
next.Add((1, R, C, A));
next.Add((1, Q, R, B));
next.Add((0, R, Q, A));
}
}
tris = next;
}
float scale = radius / 1.0f;
var soup = new PolygonSoup();
foreach (var (type, A, B, C) in tris)
soup.Add(new[] { A * scale, B * scale, C * scale }, type);
return soup;
}
// ==============================================================
// Site generators
// ==============================================================
public static List JitteredSites(float radius, float spacing, int seed)
{
var rng = new System.Random(seed);
var pts = new List();
float w = spacing, h = spacing * 0.866f;
int nx = Mathf.CeilToInt(radius / w) + 1, ny = Mathf.CeilToInt(radius / h) + 1;
for (int j = -ny; j <= ny; j++)
for (int i = -nx; i <= nx; i++)
{
Vector2 p = new Vector2(i * w + (j & 1) * w * 0.5f, j * h);
p += new Vector2((float)rng.NextDouble() - 0.5f, (float)rng.NextDouble() - 0.5f) * spacing * 0.45f;
if (p.magnitude < radius * 1.15f) pts.Add(p);
}
return pts;
}
/// Chaos game over an IFS. Cell density inherits the attractor's fractal measure.
public static List ChaosGame(IfsMap[] maps, float radius, float spacing, int seed)
{
int target = Mathf.Clamp(Mathf.RoundToInt(Mathf.PI * radius * radius / (spacing * spacing)), 64, 6000);
var rng = new System.Random(seed);
float total = 0f; foreach (var m in maps) total += Mathf.Max(m.weight, 1e-4f);
Vector2 p = Vector2.zero;
for (int i = 0; i < 64; i++) p = Pick(maps, rng, total).Apply(p); // burn in
// Accept points onto a grid so we never emit two sites on top of each other.
float minDist = spacing * 0.42f;
var grid = new Dictionary<(int, int), List>();
float inv = 1f / minDist;
var pts = new List(target);
for (int i = 0; i < target * 220 && pts.Count < target; i++)
{
p = Pick(maps, rng, total).Apply(p);
Vector2 q = p * radius;
if (q.magnitude > radius * 1.1f) continue;
int gx = Mathf.FloorToInt(q.x * inv), gy = Mathf.FloorToInt(q.y * inv);
bool tooClose = false;
for (int dx = -1; dx <= 1 && !tooClose; dx++)
for (int dy = -1; dy <= 1 && !tooClose; dy++)
if (grid.TryGetValue((gx + dx, gy + dy), out var lst))
foreach (var o in lst)
if ((o - q).sqrMagnitude < minDist * minDist) { tooClose = true; break; }
if (tooClose) continue;
if (!grid.TryGetValue((gx, gy), out var b)) grid[(gx, gy)] = b = new List();
b.Add(q);
pts.Add(q);
}
return pts;
}
static IfsMap Pick(IfsMap[] maps, System.Random rng, float total)
{
float r = (float)rng.NextDouble() * total;
foreach (var m in maps) { r -= Mathf.Max(m.weight, 1e-4f); if (r <= 0f) return m; }
return maps[maps.Length - 1];
}
/// Five-fold pentaflake attractor. Pairs well with a Penrose outline layer.
public static IfsMap[] Pentaflake()
{
float r = 1f / (1f + Phi);
var maps = new IfsMap[5];
for (int i = 0; i < 5; i++)
maps[i] = new IfsMap
{
translate = Polar(1f - r, 90f + i * 72f),
rotateDeg = 0f,
scale = r,
weight = 1f
};
return maps;
}
/// Sierpinski triangle attractor — sparse, angular density falloff.
public static IfsMap[] Sierpinski()
{
var maps = new IfsMap[3];
for (int i = 0; i < 3; i++)
maps[i] = new IfsMap
{
translate = Polar(0.5f, 90f + i * 120f),
rotateDeg = 0f,
scale = 0.5f,
weight = 1f
};
return maps;
}
// ==============================================================
// Voronoi by half-plane clipping. O(n * k) with k nearest neighbours.
// Brute-force nearest search; fine at init for a few thousand sites.
// ==============================================================
public static PolygonSoup Voronoi(List sites, float radius, int lloydPasses)
{
for (int pass = 0; pass < lloydPasses; pass++)
{
var moved = new List(sites.Count);
foreach (var cell in VoronoiCells(sites, radius))
moved.Add(cell == null ? Vector2.zero : PolygonSoup.Centroid(cell));
for (int i = 0; i < sites.Count; i++)
if (moved[i] != Vector2.zero) sites[i] = moved[i];
}
var soup = new PolygonSoup();
foreach (var cell in VoronoiCells(sites, radius))
{
if (cell != null && cell.Count >= 3 && cell.Count <= Limits.MaxVerts)
soup.Add(cell.ToArray(), cell.Count % 3);
}
return soup;
}
static IEnumerable> VoronoiCells(List sites, float radius)
{
const int K = 24;
float R = radius * 1.4f;
var box = new List { new(-R,-R), new(R,-R), new(R,R), new(-R,R) };
var near = new List<(float d, int i)>(sites.Count);
for (int i = 0; i < sites.Count; i++)
{
Vector2 s = sites[i];
near.Clear();
for (int j = 0; j < sites.Count; j++)
{
if (j == i) continue;
near.Add(((sites[j] - s).sqrMagnitude, j));
}
near.Sort((a, b) => a.d.CompareTo(b.d));
var poly = new List(box);
int k = Mathf.Min(K, near.Count);
for (int m = 0; m < k && poly.Count > 0; m++)
{
Vector2 t = sites[near[m].i];
Vector2 n = t - s;
float len = n.magnitude;
if (len < 1e-6f) continue;
n /= len;
float d = Vector2.Dot((s + t) * 0.5f, n);
poly = ClipHalfPlane(poly, n, d);
}
yield return poly.Count >= 3 ? poly : null;
}
}
/// Sutherland–Hodgman clip; keeps the side where dot(p, n) <= d.
static List ClipHalfPlane(List poly, Vector2 n, float d)
{
var outp = new List(poly.Count + 2);
for (int i = 0; i < poly.Count; i++)
{
Vector2 a = poly[i], b = poly[(i + 1) % poly.Count];
float da = Vector2.Dot(a, n) - d, db = Vector2.Dot(b, n) - d;
bool ina = da <= 0f, inb = db <= 0f;
if (ina) outp.Add(a);
if (ina != inb)
{
float t = da / (da - db);
outp.Add(Vector2.Lerp(a, b, t));
}
}
return outp;
}
// ==============================================================
static Vector2 Polar(float r, float deg)
{
float a = deg * Mathf.Deg2Rad;
return new Vector2(Mathf.Cos(a) * r, Mathf.Sin(a) * r);
}
static Vector2[] Ngon(Vector2 c, float r, int n, float rotDeg)
{
var p = new Vector2[n];
for (int i = 0; i < n; i++) p[i] = c + Polar(r, rotDeg + i * 360f / n);
return p;
}
}
}