using System.Collections.Generic;
using System.Linq;
using Puzzy.MathUtils;
using UnityEngine;
namespace Puzzy
{
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(BoxCollider))]
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(Hoverable))]
[RequireComponent(typeof(Draggable))]
public class JigsawPiece : MonoBehaviour
{
private const float DefaultPieceThickness = .1f;
private static readonly int GridSize = Shader.PropertyToID("_GridSize");
private static readonly int Coordinates = Shader.PropertyToID("_Coordinates");
private static readonly int EmissionIntensity = Shader.PropertyToID("_EmissionIntensity");
[SerializeField] private bool _drawOutlineGizmos;
[SerializeField] private bool _drawMeshGizmos;
[SerializeField] private Material _topMaterial;
[SerializeField] private Material _sideMaterial;
[SerializeField] private Vector2Int _gridSize;
[SerializeField] private Vector2Int _gridCoordinates;
private MeshFilter _meshFilter;
private MeshRenderer _meshRenderer;
private BoxCollider _boxCollider;
private Rigidbody _rigidbody;
private Mesh _mesh;
private Hoverable _hoverable;
private Draggable _draggable;
private float _thickness = DefaultPieceThickness;
private readonly List<Color> _orderedGizmoColors = new();
private void Awake()
{
_meshFilter = GetComponent<MeshFilter>();
_meshRenderer = GetComponent<MeshRenderer>();
_boxCollider = GetComponent<BoxCollider>();
_boxCollider.size = new Vector3(0.99f, 0.99f, _thickness);
_boxCollider.center = new Vector3(0.5f, 0.5f, -_thickness / 2f);
_rigidbody = GetComponent<Rigidbody>();
_hoverable = GetComponent<Hoverable>();
_draggable = GetComponent<Draggable>();
_rigidbody.isKinematic = false;
GenerateGizmoColorSequence();
}
private void Start()
{
SetCoordinateInfo(_gridSize, _gridCoordinates);
_hoverable.HoverStateChanged += HoverableOnHoverStateChanged;
_draggable.DragStateChanged += DraggableOnDragStateChanged;
}
private void HoverableOnHoverStateChanged(bool isHovering)
{
if (_draggable.IsDragging)
{
return;
}
float intensity = isHovering ? 5f : 0f;
var materialPropertyBlock = new MaterialPropertyBlock();
_meshRenderer.GetPropertyBlock(materialPropertyBlock);
materialPropertyBlock.SetFloat(EmissionIntensity, intensity);
_meshRenderer.SetPropertyBlock(materialPropertyBlock);
}
private void DraggableOnDragStateChanged(bool isDragging)
{
if (!_draggable.IsDragging)
{
return;
}
var materialPropertyBlock = new MaterialPropertyBlock();
_meshRenderer.GetPropertyBlock(materialPropertyBlock);
materialPropertyBlock.SetFloat(EmissionIntensity, 0f);
_meshRenderer.SetPropertyBlock(materialPropertyBlock);
}
private void OnDrawGizmos()
{
if (!_mesh)
{
return;
}
DrawOutlineGizmos();
DrawMeshGizmos();
}
private void DrawOutlineGizmos()
{
if (!_drawOutlineGizmos)
{
return;
}
for (int i = 0; i < _mesh.vertexCount - 1; i++)
{
Gizmos.DrawWireSphere(transform.TransformPoint(_mesh.vertices[i]), 0.01f);
Gizmos.DrawLine(transform.TransformPoint(_mesh.vertices[i]), transform.TransformPoint(_mesh.vertices[i + 1]));
}
Gizmos.DrawWireSphere(transform.TransformPoint(_mesh.vertices[^1]), 0.01f);
}
private void DrawMeshGizmos()
{
if (!_drawMeshGizmos)
{
return;
}
for (int i = 0; i < _mesh.triangles.Length; i += 3)
{
Gizmos.color = _orderedGizmoColors[i % _orderedGizmoColors.Count];
Gizmos.DrawLine(transform.TransformPoint(_mesh.vertices[_mesh.triangles[i]]),
transform.TransformPoint(_mesh.vertices[_mesh.triangles[i + 1]]));
Gizmos.DrawLine(transform.TransformPoint(_mesh.vertices[_mesh.triangles[i + 1]]),
transform.TransformPoint(_mesh.vertices[_mesh.triangles[i + 2]]));
Gizmos.DrawLine(transform.TransformPoint(_mesh.vertices[_mesh.triangles[i + 2]]),
transform.TransformPoint(_mesh.vertices[_mesh.triangles[i]]));
}
}
private static bool ArePointsCollinear(Vector3 a, Vector3 b, Vector3 c)
{
Vector3 ab = b - a;
Vector3 bc = c - b;
return Vector3.Cross(ab.normalized, bc.normalized).magnitude < 0.0001f;
}
private static List<Vector3> JoinAndSimplifyVertices(IList<Vector3> top, IList<Vector3> right, IList<Vector3> bottom, IList<Vector3> left)
{
var vertices = new List<Vector3>();
vertices.AddRange(top);
vertices.AddRange(right.Skip(1));
vertices.AddRange(bottom.Skip(1));
vertices.AddRange(left.Skip(1));
var simplified = new List<Vector3>();
for (int i = 0; i < vertices.Count; i++)
{
Vector3 prev = vertices[(i - 1 + vertices.Count) % vertices.Count];
Vector3 curr = vertices[i];
Vector3 next = vertices[(i + 1) % vertices.Count];
if (Vector3.Distance(curr, prev) < 0.0001f)
{
continue;
}
if (i == 0 || i == vertices.Count - 1 || !ArePointsCollinear(prev, curr, next))
{
simplified.Add(curr);
}
}
return simplified;
}
public static bool FromEdges(
IList<Vector3> top,
IList<Vector3> right,
IList<Vector3> bottom,
IList<Vector3> left,
Material newTopMaterial,
Material newCardboardMaterial,
out JigsawPiece piece)
{
piece = null;
if (top.Last() != right.First() || right.Last() != bottom.First() ||
bottom.Last() != left.First() || left.Last() != top.First())
{
Debug.LogError("Edges are not properly joined.");
return false;
}
if (top.Count == 0)
{
top = new List<Vector3> { Vector3.zero };
}
List<Vector3> verts = JoinAndSimplifyVertices(top, right, bottom, left);
piece = new GameObject("JigsawPiece").AddComponent<JigsawPiece>();
piece.GenerateMeshFromVertices(verts);
piece.SetMaterials(newTopMaterial, newCardboardMaterial);
return true;
}
private void SetMaterials(Material newTopMaterial, Material newSideMaterial)
{
_topMaterial = newTopMaterial;
_sideMaterial = newSideMaterial;
_meshRenderer.SetMaterials(new List<Material> { newTopMaterial, newSideMaterial });
}
public void SetCoordinateInfo(Vector2Int gridSize, Vector2Int coordinates)
{
_gridSize = gridSize;
_gridCoordinates = coordinates;
var materialPropertyBlock = new MaterialPropertyBlock();
_meshRenderer.GetPropertyBlock(materialPropertyBlock);
materialPropertyBlock.SetVector(GridSize, new Vector4(gridSize.x, gridSize.y, 0f, 0f));
materialPropertyBlock.SetVector(Coordinates,
new Vector4(
1f / gridSize.x * coordinates.x,
1f / gridSize.y * coordinates.y,
0f,
0f));
_meshRenderer.SetPropertyBlock(materialPropertyBlock);
}
public void GenerateMeshFromVertices(IList<Vector3> vertices2D, float thickness = DefaultPieceThickness)
{
_thickness = thickness;
_mesh = new Mesh { name = "JigsawPieceMesh" };
_meshFilter.sharedMesh = _mesh;
int vertexCount = vertices2D.Count;
List<Vector3> verts = new();
List<Vector3> normals = new();
List<Vector2> uv0S = new(); // Primary UV set for texturing
List<int> topTris = new();
List<int> sideTris = new();
for (int i = 0; i < vertexCount; i++)
{
verts.Add(vertices2D[i]);
normals.Add(Vector3.zero);
uv0S.Add(new Vector2(vertices2D[i].x, vertices2D[i].y));
}
for (int i = 0; i < vertexCount; i++)
{
verts.Add(vertices2D[i] + Vector3.back * thickness);
normals.Add(Vector3.zero);
uv0S.Add(new Vector2(vertices2D[i].x, vertices2D[i].y));
}
// Triangulate top surface
List<int> tri = vertices2D.Triangulate();
topTris.AddRange(tri);
// Triangulate bottom surface
for (int i = 0; i < tri.Count; i += 3)
{
sideTris.Add(tri[i + 2] + vertexCount);
sideTris.Add(tri[i + 1] + vertexCount);
sideTris.Add(tri[i] + vertexCount);
}
for (int i = 0; i < vertexCount; i++)
{
int topCurr = i;
int topNext = (i + 1) % vertexCount;
int bottomCurr = i + vertexCount;
int bottomNext = (i + 1) % vertexCount + vertexCount;
// First triangle: bottomCurr -> topCurr -> bottomNext
sideTris.Add(bottomCurr);
sideTris.Add(topCurr);
sideTris.Add(bottomNext);
// Second triangle: topCurr -> topNext -> bottomNext
sideTris.Add(topCurr);
sideTris.Add(topNext);
sideTris.Add(bottomNext);
float edgeLength = Vector3.Distance(vertices2D[i], vertices2D[topNext]);
uv0S.Add(new Vector2(0, 0));
uv0S.Add(new Vector2(edgeLength, 0));
uv0S.Add(new Vector2(0, 1));
uv0S.Add(new Vector2(edgeLength, 1));
}
for (int i = 0; i < vertexCount; i++)
{
int topNext = (i + 1) % vertexCount;
uv0S[topNext] = new Vector2(Vector3.Distance(vertices2D[i], vertices2D[topNext]), 0); // Update topNext UV
}
_mesh.SetVertices(verts);
_mesh.subMeshCount = 2;
_mesh.SetTriangles(topTris, 0);
_mesh.SetTriangles(sideTris, 1);
_mesh.SetUVs(0, uv0S);
_mesh.SetNormals(normals);
ComputeSmoothNormals(_mesh, vertexCount);
_mesh.RecalculateBounds();
_mesh.RecalculateTangents();
}
private void ComputeSmoothNormals(Mesh mesh, int vertexCount)
{
Vector3[] vertices = mesh.vertices;
var normals = new Vector3[vertices.Length];
int[] triangles = mesh.GetIndices(0).Concat(mesh.GetIndices(1)).ToArray();
var normalSums = new Vector3[vertices.Length];
int[] counts = new int[vertices.Length];
for (int i = 0; i < triangles.Length; i += 3)
{
int i0 = triangles[i];
int i1 = triangles[i + 1];
int i2 = triangles[i + 2];
Vector3 v0 = vertices[i0];
Vector3 v1 = vertices[i1];
Vector3 v2 = vertices[i2];
Vector3 faceNormal = Vector3.Cross(v1 - v0, v2 - v0).normalized;
normalSums[i0] += faceNormal;
normalSums[i1] += faceNormal;
normalSums[i2] += faceNormal;
counts[i0]++;
counts[i1]++;
counts[i2]++;
}
for (int i = 0; i < vertices.Length; i++)
{
if (counts[i] > 0)
{
normals[i] = (normalSums[i] / counts[i]).normalized;
}
}
mesh.normals = normals;
}
private void GenerateGizmoColorSequence()
{
const int colorCount = 50;
for (int i = 0; i < colorCount; i++)
{
_orderedGizmoColors.Add(Color.HSVToRGB(i / (colorCount - 1f), 1f, 1f));
}
}
private void RecalculateSideNormals(Mesh mesh, int sideSubMeshIndex, int sideStartVertexIndex)
{
int[] triangles = mesh.GetTriangles(sideSubMeshIndex);
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
var normalSums = new Vector3[vertices.Length];
int[] counts = new int[vertices.Length];
for (int i = 0; i < triangles.Length; i += 3)
{
int i0 = triangles[i];
int i1 = triangles[i + 1];
int i2 = triangles[i + 2];
Vector3 v0 = vertices[i0];
Vector3 v1 = vertices[i1];
Vector3 v2 = vertices[i2];
Vector3 faceNormal = Vector3.Cross(v1 - v0, v2 - v0).normalized;
normalSums[i0] += faceNormal;
normalSums[i1] += faceNormal;
normalSums[i2] += faceNormal;
counts[i0]++;
counts[i1]++;
counts[i2]++;
}
for (int i = sideStartVertexIndex; i < vertices.Length; i++)
{
if (counts[i] > 0)
{
normals[i] = (normalSums[i] / counts[i]).normalized;
}
}
mesh.normals = normals;
}
}
}