Code/RoadComponent/RoadComponent.Sidewalk.cs
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RoadComponent
{
	[Property(Title = "🔒 Locked"), Feature("Sidewalk")] private bool IsSidewalkLocked { get; set; } = false;
	[Property, FeatureEnabled("Sidewalk", Icon = "directions_walk", Tint = EditorTint.Blue)] private bool HasSidewalk { get; set { field = value; IsDirty = true; } } = true;
	[Property(Title = "Material"), Feature("Sidewalk")] private Material SidewalkMaterial { get; set { field = value; IsDirty = true; } }
	[Property(Title = "Width"), Feature("Sidewalk"), Range(10.0f, 500.0f)] private float SidewalkWidth { get; set { field = value; IsDirty = true; } } = 150.0f;
	[Property(Title = "Height"), Feature("Sidewalk"), Range(0.1f, 100.0f)] private float SidewalkHeight { get; set { field = value; IsDirty = true; } } = 5.0f;
	[Property(Title = "Texture Repeat"), Feature("Sidewalk")] private float SidewalkTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 200.0f;



	private void BuildSidewalkMesh()
	{
		if (!HasSidewalk)
			return;

		GetSplineFrameData(out var frames, out var segmentsToKeep);

		if (segmentsToKeep.Count < 2)
			return;

		var polygonMesh = new PolygonMesh();
		var material = SidewalkMaterial ?? Material.Load("materials/dev/reflectivity_70.vmat");
		var frameVertices = new HalfEdgeMesh.VertexHandle[segmentsToKeep.Count][];

		float roadEdgeOffset = RoadWidth * 0.5f;

		float leftInnerEdge = -roadEdgeOffset;
		float leftOuterEdge = -(roadEdgeOffset + SidewalkWidth);

		float rightInnerEdge = roadEdgeOffset;
		float rightOuterEdge = roadEdgeOffset + SidewalkWidth;

		float leftAvgUVDist = 0f;
		float rightAvgUVDist = 0f;

		for (int i = 0; i < segmentsToKeep.Count; i++)
		{
			Transform frame = frames[segmentsToKeep[i]];
			Vector3 u = frame.Rotation.Up;
			Vector3 r = frame.Rotation.Right;
			Vector3 p = frame.Position;

			Vector3 lb = p + r * leftInnerEdge;
			Vector3 lo = p + r * leftOuterEdge;
			Vector3 lt = lb + u * SidewalkHeight;
			Vector3 lto = lo + u * SidewalkHeight;

			Vector3 rb = p + r * rightInnerEdge;
			Vector3 ro = p + r * rightOuterEdge;
			Vector3 rt = rb + u * SidewalkHeight;
			Vector3 rto = ro + u * SidewalkHeight;

			frameVertices[i] = polygonMesh.AddVertices(lb, lo, lt, lto, rb, ro, rt, rto);
		}

		for (int i = 0; i < segmentsToKeep.Count - 1; i++)
		{
			int idx0 = segmentsToKeep[i];
			int idx1 = segmentsToKeep[i + 1];

			float v2 = SidewalkHeight / SidewalkTextureRepeat;

			Transform f0 = frames[idx0];
			Transform f1 = frames[idx1];

			Vector3 r0 = f0.Rotation.Right;
			Vector3 r1 = f1.Rotation.Right;

			Vector3 p0 = f0.Position;
			Vector3 p1 = f1.Position;

			// Left sidewalk positions
			Vector3 lb0 = p0 + r0 * leftInnerEdge;
			Vector3 lb1 = p1 + r1 * leftInnerEdge;

			Vector3 lo0 = p0 + r0 * leftOuterEdge;
			Vector3 lo1 = p1 + r1 * leftOuterEdge;

			// Right sidewalk positions
			Vector3 rb0 = p0 + r0 * rightInnerEdge;
			Vector3 rb1 = p1 + r1 * rightInnerEdge;

			Vector3 ro0 = p0 + r0 * rightOuterEdge;
			Vector3 ro1 = p1 + r1 * rightOuterEdge;

			float leftInnerLen3D = Vector3.DistanceBetween(lb0, lb1);
			float leftOuterLen3D = Vector3.DistanceBetween(lo0, lo1);
			float rightInnerLen3D = Vector3.DistanceBetween(rb0, rb1);
			float rightOuterLen3D = Vector3.DistanceBetween(ro0, ro1);

			float leftAvgV0 = leftAvgUVDist;
			float rightAvgV0 = rightAvgUVDist;

			leftAvgUVDist += ((leftInnerLen3D + leftOuterLen3D) * 0.5f) / SidewalkTextureRepeat;
			rightAvgUVDist += ((rightInnerLen3D + rightOuterLen3D) * 0.5f) / SidewalkTextureRepeat;

			float leftAvgV1 = leftAvgUVDist;
			float rightAvgV1 = rightAvgUVDist;

			MeshUtility.AddTexturedQuad(
				polygonMesh,
				material,
				frameVertices[i][2], frameVertices[i + 1][2], frameVertices[i + 1][3], frameVertices[i][3],
				new Vector2(0, leftAvgV0), new Vector2(0, leftAvgV1), new Vector2(1, leftAvgV1), new Vector2(1, leftAvgV0));

			MeshUtility.AddTexturedQuad(
				polygonMesh,
				material,
				frameVertices[i][0], frameVertices[i + 1][0], frameVertices[i + 1][2], frameVertices[i][2],
				new Vector2(v2, leftAvgV0), new Vector2(v2, leftAvgV1), new Vector2(0, leftAvgV1), new Vector2(0, leftAvgV0));

			MeshUtility.AddTexturedQuad(
				polygonMesh,
				material,
				frameVertices[i][1], frameVertices[i][3], frameVertices[i + 1][3], frameVertices[i + 1][1],
				new Vector2(1 - v2, 1 - leftAvgV0), new Vector2(1, 1 - leftAvgV0), new Vector2(1, 1 - leftAvgV1), new Vector2(1 - v2, 1 - leftAvgV1));

			MeshUtility.AddTexturedQuad(
				polygonMesh,
				material,
				frameVertices[i][6], frameVertices[i][7], frameVertices[i + 1][7], frameVertices[i + 1][6],
				new Vector2(0, rightAvgV0), new Vector2(1, rightAvgV0), new Vector2(1, rightAvgV1), new Vector2(0, rightAvgV1));

			MeshUtility.AddTexturedQuad(
				polygonMesh,
				material,
				frameVertices[i][4], frameVertices[i][6], frameVertices[i + 1][6], frameVertices[i + 1][4],
				new Vector2(v2, rightAvgV0), new Vector2(0, rightAvgV0), new Vector2(0, rightAvgV1), new Vector2(v2, rightAvgV1));

			MeshUtility.AddTexturedQuad(
				polygonMesh,
				material,
				frameVertices[i][5], frameVertices[i + 1][5], frameVertices[i + 1][7], frameVertices[i][7],
				new Vector2(1 - v2, 1 - rightAvgV0), new Vector2(1 - v2, 1 - rightAvgV1), new Vector2(1, 1 - rightAvgV1), new Vector2(1, 1 - rightAvgV0));
		}

		CreateSidewalkMeshComponent(polygonMesh);
	}



	private void CreateSidewalkMeshComponent(PolygonMesh _PolygonMesh)
	{
		var child = new GameObject(GameObject, true, "Sidewalk");
		child.Tags.Add(RoadMeshTag);
		child.Tags.Add(SidewalkSurfaceTag);

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



	private void EnsureSidewalkMeshExist()
	{
		if (SandboxUtility.IsInPlayMode)
			return;

		if (IsSidewalkLocked)
			return;

		if (HasGeneratedMeshChildren(SidewalkSurfaceTag))
			return;

		BuildSidewalkMesh();
	}
}