RoadIntersectionComponent/RoadIntersectionComponent.Sidewalk.cs

Partial class for RoadIntersectionComponent defining sidewalk graph modes and sidewalk geometry. It computes a closed loop of world-space points for the sidewalk centerline (circle or rounded rectangle) based on component dimensions and exposes SidewalkGraph configuration.

Native Interop
using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool;

/// <summary>
/// How much of a junction reaches the pedestrian graph. Geometry is never affected — the pavement mesh builds
/// the same either way; this only decides what pedestrians are allowed to route over.
///
/// Ordered most-to-least deliberately, so the zero value is the harmless one: a component that somehow arrives
/// without this field set behaves like every other junction rather than silently vanishing from the graph.
/// </summary>
public enum SidewalkGraphMode
{
	/// <summary>Pavement round the corners and a crossing at every arm.</summary>
	All,

	/// <summary>
	/// Corners only. The pavement stays joined all the way round and connects to every road that meets here,
	/// but there's nothing to step off the kerb onto — pedestrians walk round the junction instead of over it.
	/// </summary>
	NoCrossing,

	/// <summary>
	/// Nothing at all. The roads that meet here keep their own pavements; they just stop being connected
	/// THROUGH this junction, so expect their ends to show as dead ends.
	/// </summary>
	None
}



public partial class RoadIntersectionComponent
{
	/// <summary>
	/// What this junction contributes to the pedestrian graph — the pedestrian counterpart of
	/// <see cref="ExcludeTraffic"/>.
	///
	/// <see cref="SidewalkGraphMode.None"/> for somewhere nobody should be walking round at all: a slip road, a
	/// service yard, a junction whose pavement exists only because the mesh needs an edge.
	/// <see cref="SidewalkGraphMode.NoCrossing"/> for somewhere they may walk past but not across — a forecourt
	/// or car park entrance, where the kerb should stay continuous and stepping into the vehicle route is the
	/// thing you're trying to prevent.
	/// </summary>
	[Property, Feature("General"), Category("Sidewalk"), Order(3)] public SidewalkGraphMode SidewalkGraph { get; set; } = SidewalkGraphMode.All;



	/// <summary>
	/// The walking line around this junction's pavement, in world space — a closed loop running down the
	/// middle of the sidewalk slab.
	///
	/// Built from the junction's OWN outline, which is the whole point: a rectangular intersection's pavement
	/// runs along its edges and turns at its corners, and approximating that with an arc around the centre
	/// bows the path out into the road at the middle of each side and cuts the corners off. A circle really is
	/// an arc, so it gets one.
	///
	/// The loop runs all the way round, arm mouths included — whoever consumes it is expected to split it at
	/// the kerbs, because where the pavement is interrupted is the same question as where the crossings go.
	/// </summary>
	public List<Vector3> GetSidewalkOutline(float _Spacing)
	{
		var points = new List<Vector3>();

		if (!HasSidewalks)
			return points;

		float spacing = Math.Max(1.0f, _Spacing);

		// Centre of the slab, so it sits where someone would actually walk rather than on either kerb.
		float outset = SidewalkWidth * 0.5f;
		Vector3 lift = Vector3.Up * SidewalkHeight;

		if (Shape == IntersectionShape.Circle)
		{
			float radius = Radius + outset;
			int steps = Math.Max(8, (int)MathF.Ceiling(MathF.Tau * radius / spacing));

			for (int i = 0; i < steps; i++)
			{
				float angle = MathF.Tau * i / steps;

				points.Add(new Vector3(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius, 0.0f) + lift);
			}
		}
		else
		{
			// Written with the same right/forward vectors BuildRectangleRoad uses, rather than as raw x/y
			// components. Width runs along Right and Length along Forward, which in s&box axes is Y and X —
			// spelling that out by hand gets them the wrong way round, and a junction outline rotated 90°
			// looks almost plausible until nothing connects to it.
			Vector3 right = Vector3.Right;
			Vector3 forward = Vector3.Forward;

			float hw = Width * 0.5f + outset;
			float hl = Length * 0.5f + outset;

			// Round the rectangle in order, so the loop has a consistent winding for anything that walks it.
			Vector3[] corners =
			[
				-right * hw - forward * hl,
				 right * hw - forward * hl,
				 right * hw + forward * hl,
				-right * hw + forward * hl,
			];

			for (int i = 0; i < corners.Length; i++)
			{
				Vector3 from = corners[i];
				Vector3 to = corners[(i + 1) % corners.Length];

				int steps = Math.Max(1, (int)MathF.Ceiling(Vector3.DistanceBetween(from, to) / spacing));

				// Last point of each edge is skipped — it's the first of the next one, and a closed loop
				// mustn't repeat its corners.
				for (int s = 0; s < steps; s++)
					points.Add(Vector3.Lerp(from, to, (float)s / steps) + lift);
			}
		}

		for (int i = 0; i < points.Count; i++)
			points[i] = WorldTransform.PointToWorld(points[i]);

		return points;
	}



	/// <summary>Whether this junction has pavement to walk on at all.</summary>
	public bool HasSidewalks => SidewalkWidth > 0.0f;
}