Code/RoadParkingLotComponent/RoadParkingLotComponent.Curbs.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RoadParkingLotComponent
{
	[Property, FeatureEnabled("Curbs", Icon = "block", Tint = EditorTint.Pink), Change] private bool HasCurbs { get; set; } = false;
	[Property, Feature("Curbs")] private Material CurbsMaterial { get; set { field = value; m_IsDirty = true; } }
	[Property, Feature("Curbs"), Range(1, 5)] private int CurbsSegments { get; set { field = value; m_IsDirty = true; } } = 3;
	[Property, Feature("Curbs"), Range(0.1f, 1.0f)] private float CurbsFillRatio { get; set { field = value; m_IsDirty = true; } } = 0.667f; // 2/3 of the SpotWidth
	[Property, Feature("Curbs"), Range(1.0f, 100.0f)] private float CurbsHeight { get; set { field = value; m_IsDirty = true; } } = 8.0f;
	[Property, Feature("Curbs"), Range(1.0f, 100.0f)] private float CurbsDepth { get; set { field = value; m_IsDirty = true; } } = 12.0f;
	[Property, Feature("Curbs"), Range(-50.0f, 50.0f)] private float CurbsOffset { get; set { field = value; m_IsDirty = true; } } = 6.0f;
	[Property(Title = "Texture Repeat"), Feature("Curbs")] private float CurbsTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); m_IsDirty = true; } } = 10.0f;



	private void OnHasCurbsChanged(bool _OldValue, bool _NewValue)
	{
		m_IsDirty = true;
	}



	private void BuildCurbs()
	{
		if (!HasCurbs || SpotCount <= 0)
			return;

		var material = CurbsMaterial ?? Material.Load("materials/dev/reflectivity_90.vmat");
		var mesh = new PolygonMesh();
		var cache = new Dictionary<Vector3, HalfEdgeMesh.VertexHandle>();

		float spacing = CalculateSpacing();

		for (int i = 0; i < SpotCount; i++)
		{
			float xCenter = (i * spacing) + (SpotWidth * 0.5f);

			if (CurbsSegments <= 1)
				DrawSimpleCurb(mesh, material, cache, xCenter);
			else
				DrawBeveledCurb(mesh, material, cache, xCenter);
		}

		var child = new GameObject(GameObject, true, "ParkingCurbs");
		child.Tags.Add(CurbsTag);

		var meshComponent = child.AddComponent<MeshComponent>();
		meshComponent.Mesh = mesh;
		meshComponent.SmoothingAngle = 40.0f;
	}



	private void DrawSimpleCurb(PolygonMesh _Mesh, Material _Material, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, float _CenterX)
	{
		Vector3 up = Vector3.Up;

		float angleRad = SpotAngle.DegreeToRadian();
		float sinAngle = float.Sin(angleRad);
		float cosAngle = float.Cos(angleRad);

		float curbWidth = SpotWidth * CurbsFillRatio;
		float halfW = curbWidth * 0.5f;
		float halfD = CurbsDepth * 0.5f;
		float h = CurbsHeight;

		float spotX = _CenterX - (SpotWidth * 0.5f);
		float backOffsetFromFront = SpotLength - halfD - CurbsOffset;
		float curbCenterX = spotX + (SpotWidth * 0.5f * cosAngle) - (backOffsetFromFront * sinAngle);
		float curbCenterY = (SpotWidth * 0.5f * sinAngle) + (backOffsetFromFront * cosAngle);

		Vector3 perpDir = new Vector3(cosAngle, sinAngle, 0);
		Vector3 lengthDir = new Vector3(-sinAngle, cosAngle, 0);
		Vector3 center = new Vector3(curbCenterX, curbCenterY, 0);

		float uW = curbWidth / CurbsTextureRepeat;
		float uD = CurbsDepth / CurbsTextureRepeat;
		float uH = CurbsHeight / CurbsTextureRepeat;

		Vector3 fbl = center - perpDir * halfW - lengthDir * halfD;
		Vector3 fbr = center + perpDir * halfW - lengthDir * halfD;
		Vector3 ftl = fbl + up * h;
		Vector3 ftr = fbr + up * h;
		Vector3 bbl = center - perpDir * halfW + lengthDir * halfD;
		Vector3 bbr = center + perpDir * halfW + lengthDir * halfD;
		Vector3 btl = bbl + up * h;
		Vector3 btr = bbr + up * h;

		var vFbl = MeshUtility.GetOrAddVertex(_Mesh, _Cache, fbl);
		var vFbr = MeshUtility.GetOrAddVertex(_Mesh, _Cache, fbr);
		var vFtl = MeshUtility.GetOrAddVertex(_Mesh, _Cache, ftl);
		var vFtr = MeshUtility.GetOrAddVertex(_Mesh, _Cache, ftr);
		var vBbl = MeshUtility.GetOrAddVertex(_Mesh, _Cache, bbl);
		var vBbr = MeshUtility.GetOrAddVertex(_Mesh, _Cache, bbr);
		var vBtl = MeshUtility.GetOrAddVertex(_Mesh, _Cache, btl);
		var vBtr = MeshUtility.GetOrAddVertex(_Mesh, _Cache, btr);

		// Top face
		MeshUtility.AddTexturedQuad(_Mesh, _Material, vFtl, vFtr, vBtr, vBtl,
			new Vector2(0, 0), new Vector2(uW, 0), new Vector2(uW, uD), new Vector2(0, uD));

		// Front face
		MeshUtility.AddTexturedQuad(_Mesh, _Material, vFbl, vFbr, vFtr, vFtl,
			new Vector2(0, 0), new Vector2(uW, 0), new Vector2(uW, uH), new Vector2(0, uH));

		// Back face
		MeshUtility.AddTexturedQuad(_Mesh, _Material, vBbr, vBbl, vBtl, vBtr,
			new Vector2(0, 0), new Vector2(uW, 0), new Vector2(uW, uH), new Vector2(0, uH));

		// Left face
		MeshUtility.AddTexturedQuad(_Mesh, _Material, vBbl, vFbl, vFtl, vBtl,
			new Vector2(0, 0), new Vector2(uD, 0), new Vector2(uD, uH), new Vector2(0, uH));

		// Right face
		MeshUtility.AddTexturedQuad(_Mesh, _Material, vFbr, vBbr, vBtr, vFtr,
			new Vector2(0, 0), new Vector2(uD, 0), new Vector2(uD, uH), new Vector2(0, uH));
	}



	private void DrawBeveledCurb(PolygonMesh _Mesh, Material _Material, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, float _CenterX)
	{
		Vector3 up = Vector3.Up;

		float angleRad = SpotAngle * MathF.PI / 180.0f;
		float sinAngle = float.Sin(angleRad);
		float cosAngle = float.Cos(angleRad);

		float curbWidth = SpotWidth * CurbsFillRatio;
		float halfW = curbWidth * 0.5f;
		float halfD = CurbsDepth * 0.5f;
		float h = CurbsHeight;

		float spotX = _CenterX - (SpotWidth * 0.5f);
		float backOffsetFromFront = SpotLength - halfD - CurbsOffset;
		float curbCenterX = spotX + (SpotWidth * 0.5f * cosAngle) - (backOffsetFromFront * sinAngle);
		float curbCenterY = (SpotWidth * 0.5f * sinAngle) + (backOffsetFromFront * cosAngle);

		Vector3 perpDir = new Vector3(cosAngle, sinAngle, 0);
		Vector3 lengthDir = new Vector3(-sinAngle, cosAngle, 0);
		Vector3 center = new Vector3(curbCenterX, curbCenterY, 0);

		// Define 4 corners of a classic curb profile
		var anchors = new[]
		{
			new Vector2(-halfD, 0),          // Front bottom
			new Vector2(-halfD * 0.5f, h),   // Front top
			new Vector2(halfD * 0.5f, h),    // Back top
			new Vector2(halfD, 0)            // Back bottom
		};

		// Generate the profile based on segments
		var profile = new Vector2[CurbsSegments + 1];

		for (int i = 0; i <= CurbsSegments; i++)
		{
			float t = (float)i / CurbsSegments;

			if (t <= 0.333f)
				profile[i] = Vector2.Lerp(anchors[0], anchors[1], t / 0.333f);
			else if (t <= 0.666f)
				profile[i] = Vector2.Lerp(anchors[1], anchors[2], (t - 0.333f) / 0.333f);
			else
				profile[i] = Vector2.Lerp(anchors[2], anchors[3], (t - 0.666f) / 0.334f);
		}

		float uW = curbWidth / CurbsTextureRepeat;

		// Body — one quad per profile segment, shared vertices across adjacent segments
		for (int i = 0; i < CurbsSegments; i++)
		{
			Vector2 p1 = profile[i];
			Vector2 p2 = profile[i + 1];

			Vector3 bl = center - perpDir * halfW + lengthDir * p1.x + up * p1.y;
			Vector3 br = center + perpDir * halfW + lengthDir * p1.x + up * p1.y;
			Vector3 tr = center + perpDir * halfW + lengthDir * p2.x + up * p2.y;
			Vector3 tl = center - perpDir * halfW + lengthDir * p2.x + up * p2.y;

			float v1 = (p1.x + halfD) / CurbsTextureRepeat;
			float v2 = (p2.x + halfD) / CurbsTextureRepeat;

			var vBl = MeshUtility.GetOrAddVertex(_Mesh, _Cache, bl);
			var vBr = MeshUtility.GetOrAddVertex(_Mesh, _Cache, br);
			var vTr = MeshUtility.GetOrAddVertex(_Mesh, _Cache, tr);
			var vTl = MeshUtility.GetOrAddVertex(_Mesh, _Cache, tl);

			MeshUtility.AddTexturedQuad(_Mesh, _Material, vBl, vBr, vTr, vTl,
				new Vector2(0, v1), new Vector2(uW, v1), new Vector2(uW, v2), new Vector2(0, v2));
		}

		// End caps — fan of triangles from the center of each cap face
		Vector3 centerL = center - perpDir * halfW;
		Vector3 centerR = center + perpDir * halfW;

		for (int i = 0; i < CurbsSegments; i++)
		{
			Vector3 v1L = centerL + lengthDir * profile[i].x + up * profile[i].y;
			Vector3 v2L = centerL + lengthDir * profile[i + 1].x + up * profile[i + 1].y;
			Vector3 v1R = centerR + lengthDir * profile[i].x + up * profile[i].y;
			Vector3 v2R = centerR + lengthDir * profile[i + 1].x + up * profile[i + 1].y;

			Vector2 uvC = new Vector2(0.5f, 0) * (CurbsDepth / CurbsTextureRepeat);
			Vector2 uv1 = new Vector2((profile[i].x + halfD) / CurbsTextureRepeat, profile[i].y / CurbsTextureRepeat);
			Vector2 uv2 = new Vector2((profile[i + 1].x + halfD) / CurbsTextureRepeat, profile[i + 1].y / CurbsTextureRepeat);

			var vCL = MeshUtility.GetOrAddVertex(_Mesh, _Cache, centerL);
			var vV1L = MeshUtility.GetOrAddVertex(_Mesh, _Cache, v1L);
			var vV2L = MeshUtility.GetOrAddVertex(_Mesh, _Cache, v2L);
			var vCR = MeshUtility.GetOrAddVertex(_Mesh, _Cache, centerR);
			var vV1R = MeshUtility.GetOrAddVertex(_Mesh, _Cache, v1R);
			var vV2R = MeshUtility.GetOrAddVertex(_Mesh, _Cache, v2R);

			MeshUtility.AddTexturedTriangle(_Mesh, _Material, vCL, vV1L, vV2L, uvC, uv1, uv2);
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, vCR, vV2R, vV1R, uvC, uv2, uv1);
		}
	}
}