s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (111 total matches found)
redsnail.roadtool / Editor/IntersectionTool.cs
Editor library
using Sandbox;
using Editor;

namespace RedSnail.RoadTool.Editor;

/// <summary>
/// Create and manage road and road intersection.
/// </summary>
[Title("Create Road/Intersection")]
[Icon("roundabout_left")]
[Alias("intersection")]
[Group("1")]
[Order(0)]
public class IntersectionTool : EditorTool
{
	public override void OnEnabled()
	{

	}

	public override Widget CreateToolSidebar()
	{
		ToolSidebarWidget sidebar = new ToolSidebarWidget();
		sidebar.AddTitle("Intersection", "roundabout_left");

		Layout group = sidebar.AddGroup("Create");
		Layout row = Layout.Row();

		IconButton road = sidebar.CreateButton("Create Road", "route", null, CreateRoad, true, row);
		IconButton inter = sidebar.CreateButton("Create Intersection", "roundabout_left", null, CreateIntersection, true, row);

		row.Spacing = 5;
		row.AddStretchCell();

		group.Add(row);

		sidebar.Layout.Add(group);
		sidebar.Layout.AddStretchCell();
		return sidebar;
	}

	private static void CreateRoad()
	{
		GameObject go = SceneEditorSession.Active.Scene.CreateObject();
		go.Name = "Road";
		go.AddComponent<RoadComponent>();
	}

	private static void CreateIntersection()
	{
		GameObject go = SceneEditorSession.Active.Scene.CreateObject();
		go.Name = "Road Intersection";
		go.AddComponent<RoadIntersectionComponent>();
	}
}
redsnail.roadtool / RoadIntersectionComponent/RoadIntersectionComponent.Sidewalk.cs
Game library
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;
}
redsnail.roadtool / RoadIntersectionComponent/RoadIntersectionComponent.Terrain.cs
Game library
using System;
using System.Linq;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RoadIntersectionComponent
{
	[Property, Feature("Terrain", Icon = "landscape", Tint = EditorTint.Green), Hide]
	private Terrain TerrainTarget { get; set; }

	[Property, Feature("Terrain"), Range(0f, 2000f)]
	public float TerrainFalloffRadius { get; set; } = 500f;

	[Property, Feature("Terrain"), Range(-10f, 10f)]
	public float TerrainHeightOffset { get; set; } = 0f;

	[Property, Feature("Terrain"), Range(0f, 100f)]
	public float TerrainRoadInset { get; set; } = 10f;

	[Property, Feature("Terrain"), Group("Texture"), Range(100f, 1000f)]
	public float TerrainEdgeRadius { get; set; } = 500f;

	[Property, Feature("Terrain"), Group("Texture")]
	public TerrainTextureLayer TerrainTargetLayer { get; set; } = TerrainTextureLayer.Overlay;

	[Property, Feature("Terrain"), Group("Texture"), Range(0f, 1f)]
	public float TerrainTextureNoise { get; set; } = 0.2f;

	[Property, Feature("Terrain"), Group("Texture")]
	public TerrainMaterial[] TerrainEdgeMaterials { get; set; } = Array.Empty<TerrainMaterial>();

	[Property, Feature("Terrain"), Group("Texture")]
	public Gradient TerrainEdgeBlendGradient = new Gradient(
		new Gradient.ColorFrame(0, Color.White),
		new Gradient.ColorFrame(1, Color.White.WithAlpha(0f))
	);



	[Button("Apply to the Ground"), Feature("Terrain")]
	private void ApplyTerrainToGround()
	{
		if (!Scene.IsEditor)
			return;

		AdaptTerrainToIntersection();
	}



	public void AdaptTerrainToIntersection()
	{
		if (!TerrainTarget.IsValid())
		{
			// Always take the closest terrain
			TerrainTarget = Scene.GetAllComponents<Terrain>().OrderBy(x => x.WorldPosition.DistanceSquared(WorldPosition)).FirstOrDefault();
		}

		if (!TerrainTarget.IsValid())
		{
			Log.Warning("RoadTool: No Terrain found in scene.");
			return;
		}

		var storage = TerrainTarget.Storage;
		if (storage == null || storage.HeightMap == null) return;

		// 1. Setup Parameters 
		int resolution = storage.Resolution;
		float terrainSize = storage.TerrainSize;
		float terrainMaxHeight = storage.TerrainHeight;
		float halfSize = terrainSize * 0.5f;

		// Calculate bounds including falloff
		float boundSize = (Shape == IntersectionShape.Rectangle ? Math.Max(Width, Length) * 0.5f : Radius) + TerrainFalloffRadius;
		BBox worldBounds = new BBox(WorldPosition - new Vector3(boundSize), WorldPosition + new Vector3(boundSize));

		var heightMap = storage.HeightMap;

		// Capture initial state for Undo
		bool hasModified = false;

		// Initialize buffers for height calculation 
		var updatedHeights = new float[heightMap.Length];
		var bestDistance = new float[heightMap.Length];

		for (int i = 0; i < heightMap.Length; i++)
		{
			// Decode: Map [0..1] ushort to [0 .. MaxHeight] to match RoadComponent
			updatedHeights[i] = (heightMap[i] / (float)ushort.MaxValue) * terrainMaxHeight;
			bestDistance[i] = float.MaxValue;
		}

		BuildRectangleExitCorridors();

		// 2. Grid Traversal
		for (int ix = 0; ix < resolution; ix++)
		{
			for (int iy = 0; iy < resolution; iy++)
			{
				// 1. Adaptive coordinate detection (Center vs Corner) matching RoadComponent
				float nodeLocalX_corner = (ix / (float)(resolution - 1)) * terrainSize;
				float nodeLocalY_corner = (iy / (float)(resolution - 1)) * terrainSize;

				float nodeLocalX = nodeLocalX_corner;
				float nodeLocalY = nodeLocalY_corner;

				// Check if the intersection is in the centered range
				var checkPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);
				if (checkPos.x < 0f || checkPos.x > terrainSize || checkPos.y < 0f || checkPos.y > terrainSize)
				{
					nodeLocalX = nodeLocalX_corner - halfSize;
					nodeLocalY = nodeLocalY_corner - halfSize;
				}

				Vector3 pixelWorldPos = TerrainTarget.Transform.World.PointToWorld(new Vector3(nodeLocalX, nodeLocalY, 0));

				if (!worldBounds.Contains(pixelWorldPos)) continue;

				int index = iy * resolution + ix;

				// 2. Distance to intersection shape 
				Vector3 relativePos = WorldTransform.PointToLocal(pixelWorldPos);
				float distance = GetDistanceToIntersectionShape(relativePos.WithZ(0));

				if (distance > TerrainFalloffRadius) continue;

				// 3. Target height matching RoadComponent (0 to MaxHeight range)
				Vector3 intersectionLocalPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);
				float roadSurfaceHeight = Math.Clamp(intersectionLocalPos.z + TerrainHeightOffset, 0f, terrainMaxHeight);
				float roadInsetHeight = Math.Clamp(roadSurfaceHeight - TerrainRoadInset, 0f, terrainMaxHeight);
				float currentPixelHeight = (heightMap[index] / (float)ushort.MaxValue) * terrainMaxHeight;

				float candidateHeight;
				if (distance <= 0) // Inside the intersection — sink terrain below road to prevent Z-fighting
				{
					candidateHeight = roadInsetHeight;
				}
				else if (SidewalkWidth > 0f && distance <= SidewalkWidth) // Sidewalk ring — flush with road surface
				{
					candidateHeight = roadSurfaceHeight;
				}
				else // Falloff — blend from surface/inset back to original terrain
				{
					float transitionStart = SidewalkWidth > 0f ? SidewalkWidth : 0f;
					float transitionBaseHeight = SidewalkWidth > 0f ? roadSurfaceHeight : roadInsetHeight;
					float t = Math.Clamp((distance - transitionStart) / TerrainFalloffRadius, 0f, 1f);
					float smoothT = t * t * (3f - 2f * t);
					candidateHeight = MathX.Lerp(transitionBaseHeight, currentPixelHeight, smoothT);
				}

				if (distance < bestDistance[index])
				{
					bestDistance[index] = distance;
					updatedHeights[index] = candidateHeight;
					hasModified = true;
				}
			}
		}

		if (hasModified)
		{
			// 4. Final encoding to ushort (Mapping back to 0..1 without the 0.5 offset)
			for (int i = 0; i < heightMap.Length; i++)
			{
				heightMap[i] = (ushort)MathF.Round(Math.Clamp(updatedHeights[i], 0f, terrainMaxHeight) / terrainMaxHeight * ushort.MaxValue);
			}

			storage.HeightMap = heightMap;
			storage.StateHasChanged();
			TerrainTarget.Create();
		}
	}

	public void PaintTerrainToIntersection()
	{
		if (!TerrainTarget.IsValid() || TerrainEdgeMaterials == null || TerrainEdgeMaterials.Length == 0) return;

		var storage = TerrainTarget.Storage;
		if (storage == null || storage.ControlMap == null) return;

		int resolution = storage.Resolution;
		float terrainSize = storage.TerrainSize;
		float halfSize = terrainSize * 0.5f;

		// Identify all material indices in the terrain storage 
		bool materialsAdded = false;
		var materialIndices = new int[TerrainEdgeMaterials.Length];
		for (int m = 0; m < TerrainEdgeMaterials.Length; m++)
		{
			if (TerrainEdgeMaterials[m] == null) continue;

			int idx = storage.Materials.IndexOf(TerrainEdgeMaterials[m]);
			if (idx == -1)
			{
				storage.Materials.Add(TerrainEdgeMaterials[m]);
				idx = storage.Materials.Count - 1;
				materialsAdded = true;
			}

			if (idx > 31)
			{
				Log.Error($"RoadTool: Terrain has too many materials ({idx}). Material '{TerrainEdgeMaterials[m].ResourceName}' cannot be painted.");
				idx = 0;
			}

			materialIndices[m] = idx;
		}

		if (materialsAdded)
		{
			storage.StateHasChanged();
			TerrainTarget.Create();
		}

		float boundSize = (Shape == IntersectionShape.Rectangle ? Math.Max(Width, Length) * 0.5f : Radius) + TerrainEdgeRadius; // This line is unchanged 
		BBox worldBounds = new BBox(WorldPosition - new Vector3(boundSize), WorldPosition + new Vector3(boundSize)); // This line is unchanged

		BuildRectangleExitCorridors();

		var controlMap = storage.ControlMap;
		bool hasModified = false;

		for (int ix = 0; ix < resolution; ix++)
		{
			for (int iy = 0; iy < resolution; iy++)
			{
				float nodeLocalX = (ix / (float)(resolution - 1)) * terrainSize;
				float nodeLocalY = (iy / (float)(resolution - 1)) * terrainSize;

				var checkPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);
				if (checkPos.x < 0f || checkPos.x > terrainSize || checkPos.y < 0f || checkPos.y > terrainSize)
				{
					nodeLocalX -= halfSize;
					nodeLocalY -= halfSize;
				}

				Vector3 pixelWorldPos = TerrainTarget.Transform.World.PointToWorld(new Vector3(nodeLocalX, nodeLocalY, 0));
				if (!worldBounds.Contains(pixelWorldPos)) continue;

				Vector3 relativePos = WorldTransform.PointToLocal(pixelWorldPos);
				float distance = GetDistanceToIntersectionShape(relativePos.WithZ(0));

				if (distance > TerrainEdgeRadius) continue;

				int index = iy * resolution + ix;
				float t = Math.Clamp(distance / TerrainEdgeRadius, 0f, 1f);
				float blendStrength = TerrainEdgeBlendGradient.Evaluate(t).a;

				if (blendStrength > 0.01f)
				{
					// Add deterministic noise to blend textures together (Dithering)
					float pixelNoise = ((float)((index * 1103515245 + 12345) & 0x7FFFFFFF) / 0x7FFFFFFF) * TerrainTextureNoise - (TerrainTextureNoise * 0.5f);
					float noisyT = Math.Clamp(t + pixelNoise, 0f, 1f);
					float noisyDistance = distance + (pixelNoise * TerrainEdgeRadius);

					int materialIndex;
					if (noisyDistance <= 0)
					{
						materialIndex = materialIndices[0];
					}
					else
					{
						int edgeMatCount = materialIndices.Length - 1;
						// Using noisyT for index selection 
						int edgeIdx = edgeMatCount > 0 ? Math.Clamp((int)(noisyT * edgeMatCount), 0, edgeMatCount - 1) + 1 : 0;
						materialIndex = materialIndices[edgeIdx];
					}

					uint packed = controlMap[index];
					var mat = new CompactTerrainMaterial(packed);

					if (TerrainTargetLayer == TerrainTextureLayer.Base)
					{
						mat.BaseTextureId = (byte)materialIndex;
						mat.BlendFactor = (byte)MathX.Lerp(mat.BlendFactor, 0, blendStrength);
					}
					else
					{
						// Otherwise, we place it in Overlay and increase the BlendFactor to display it
						mat.OverlayTextureId = (byte)materialIndex;
						mat.BlendFactor = (byte)MathX.Lerp(mat.BlendFactor, 255, blendStrength);
					}

					controlMap[index] = mat.Packed;
					hasModified = true;
				}
			}
		}

		if (hasModified)
		{
			storage.ControlMap = controlMap;
			storage.StateHasChanged();
			TerrainTarget.SyncGPUTexture();
		}
	}

	// Per-opening exit corridors in local space, rebuilt once per flatten so the per-pixel distance test stays cheap.
	// Each entry is an opening's road-edge centre, its outward direction, its lateral direction, and half its width.
	private (Vector3 Center, Vector3 Outward, Vector3 Lateral, float Half)[] m_RectangleExitCorridors = Array.Empty<(Vector3, Vector3, Vector3, float)>();

	private void BuildRectangleExitCorridors()
	{
		if (Shape != IntersectionShape.Rectangle)
		{
			m_RectangleExitCorridors = Array.Empty<(Vector3, Vector3, Vector3, float)>();
			return;
		}

		EnsureRectangleExits();

		m_RectangleExitCorridors = Exits
			.Where(exit => exit != null)
			.Select(exit =>
			{
				Transform t = GetRectangleExitLocalTransform(exit.Side, false, exit.Offset);
				return (t.Position, t.Rotation.Forward, t.Rotation.Right, exit.Width * 0.5f);
			})
			.ToArray();
	}



	private float GetDistanceToIntersectionShape(Vector3 localPixelPos)
	{
		if (Shape == IntersectionShape.Rectangle)
		{
			float hl = Length * 0.5f;
			float hw = Width * 0.5f;
			float dx = MathF.Max(MathF.Abs(localPixelPos.x) - hl, 0);
			float dy = MathF.Max(MathF.Abs(localPixelPos.y) - hw, 0);
			float dist = MathF.Sqrt(dx * dx + dy * dy);

			// Treat each open exit's corridor as inside, so terrain doesn't poke up through a road opening. A corridor is
			// the band beyond an opening's road edge (outward) and within that opening's width (lateral) — one per opening.
			if (dist > 0)
			{
				foreach (var corridor in m_RectangleExitCorridors)
				{
					Vector3 toPixel = localPixelPos - corridor.Center;

					if (Vector3.Dot(toPixel, corridor.Outward) >= 0.0f && MathF.Abs(Vector3.Dot(toPixel, corridor.Lateral)) <= corridor.Half)
						return 0;
				}
			}

			return dist;
		}

		// Circle
		float radDist = MathF.Max(localPixelPos.WithZ(0).Length - Radius, 0);

		if (radDist > 0 && CircleExits != null && CircleExits.Length > 0)
		{
			Vector3 pixelDir = localPixelPos.WithZ(0);
			if (pixelDir.LengthSquared > 0.0001f)
				pixelDir = pixelDir.Normal;

			foreach (var exit in CircleExits)
			{
				// Use dot product to stay independent of angle conventions
				Vector3 exitDir = Rotation.FromYaw(exit.AngleDegrees).Forward;
				float cosHalfAngle = MathF.Cos(MathF.Atan(exit.RoadWidth / Radius));
				if (Vector3.Dot(pixelDir, exitDir) >= cosHalfAngle)
					return 0;
			}
		}

		return radDist;
	}
}
redsnail.roadtool / RoadIntersectionComponent/RoadIntersectionComponent.Traffic.cs
Game library
using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RoadIntersectionComponent
{
	/// <summary>
	/// A single drivable exit of an intersection, expressed in world space.
	/// <see cref="Transform"/>.Forward points outward (away from the intersection), matching the snap targets.
	/// </summary>
	public readonly struct TrafficExit
	{
		public Transform Transform { get; init; }
		public float RoadWidth { get; init; }

		/// <summary>Pedestrians get no crossing over this arm's mouth. Vehicles are unaffected.</summary>
		public bool NoCrossing { get; init; }
	}

	/// <summary>
	/// When enabled, this intersection is ignored by the traffic system: vehicles will not route through it.
	/// </summary>
	[Property, Feature("General"), Category("Traffic"), Order(2)] public bool ExcludeTraffic { get; set; } = false;

	/// <summary>Speed limit for traffic crossing this intersection, in km/h.</summary>
	[Property, Feature("General"), Category("Traffic"), Order(2), Range(5.0f, 130.0f)] public float SpeedLimit { get; set; } = 30.0f;



	/// <summary>
	/// Enumerates every active exit of this intersection (rectangle or circle) as a world transform plus road width.
	/// These are the same outer-edge positions that <see cref="SnapNearbyRoads"/> snaps roads to, so the traffic
	/// graph can match road endpoints against them by proximity.
	/// </summary>
	public List<TrafficExit> GetTrafficExits()
	{
		var exits = new List<TrafficExit>();

		if (Shape == IntersectionShape.Rectangle)
		{
			EnsureRectangleExits();

			foreach (var exit in Exits)
			{
				if (exit is null)
					continue;

				exits.Add(new TrafficExit
				{
					Transform = GetRectangleExitTransform(exit.Side, true, exit.Offset),
					RoadWidth = exit.Width,
					NoCrossing = exit.NoCrossing
				});
			}
		}
		else
		{
			var circleExits = CircleExits ?? Array.Empty<CircleExit>();

			for (int i = 0; i < circleExits.Length; i++)
			{
				exits.Add(new TrafficExit
				{
					Transform = GetCircleExitTransform(i, true),
					RoadWidth = circleExits[i].RoadWidth,
					NoCrossing = circleExits[i].NoCrossing
				});
			}
		}

		return exits;
	}
}
redsnail.roadtool / RoadIntersectionComponent/RoadIntersectionComponent.Utility.cs
Game library
using System;

namespace RedSnail.RoadTool;

public partial class RoadIntersectionComponent
{
	// Computes a quadratic Bezier control point at the intersection of the two tangent lines.
	// Returns true if the lines intersect, false if parallel (in which case the midpoint is used as a fallback).
	// The control distance along _StartTan is clamped to the chord length so asymmetric tangents (e.g. an
	// off-grid exit angle whose disc tangent points well past the outer corner) don't drive the bezier past
	// the outer endpoint — overshoot produces samples beyond the endpoint and flips downstream triangle winding.
	private static bool TryBezierControl(Vector3 _Start, Vector3 _StartTan, Vector3 _End, Vector3 _EndTan, out Vector3 _Control)
	{
		float det = _StartTan.x * _EndTan.y - _EndTan.x * _StartTan.y;

		if (MathF.Abs(det) < 0.0001f)
		{
			_Control = (_Start + _End) * 0.5f;
			return false;
		}

		Vector3 d = _End - _Start;
		float r = (d.x * _EndTan.y - _EndTan.x * d.y) / det;
		float rMax = d.Length;
		float rClamped = Math.Clamp(r, 0.0f, rMax);
		_Control = _Start + rClamped * _StartTan;
		return true;
	}

	private static Vector3 SampleQuadBezier(Vector3 _B0, Vector3 _B1, Vector3 _B2, float _T)
	{
		float u = 1.0f - _T;
		return u * u * _B0 + 2.0f * u * _T * _B1 + _T * _T * _B2;
	}
}
redsnail.roadtool / RoadManager/RoadTrafficGraph.Routing.cs
Game library
using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool;

/// <summary>
/// Routing queries over the lane graph — how far it actually is to drive from one place to another.
///
/// "Actually" is the point: straight-line distance is useless for anything that has to obey roads. A delivery
/// two blocks away as the crow flies can be a mile of one-way streets, and a job that times you on crow-flight
/// distance is unwinnable in exactly the places that are most interesting to drive.
/// </summary>
public sealed partial class RoadTrafficGraph
{
	/// <summary>
	/// How far either end of a route may be from a lane and still count as being on it. A road carries a lane
	/// per direction, so a single point sits near several — see <see cref="FindNearbyLanes"/> for why taking
	/// them all matters.
	/// </summary>
	public const float DefaultRouteSnapRadius = 1000.0f;



	/// <summary>
	/// Driving distance from <paramref name="_From"/> to <paramref name="_To"/>, following lanes in their legal
	/// direction. False when neither end is anywhere near a road, or when no route exists at all — a one-way
	/// system can genuinely have no way round.
	///
	/// Dijkstra over whole lanes rather than individual waypoints: lanes are the unit the graph is linked in,
	/// and a city's worth of them is a few thousand nodes. Costs are measured to the START of each lane, with
	/// the partial lengths at both ends added on, so the answer is measured between the actual points.
	/// </summary>
	public bool TryGetDrivingDistance(Vector3 _From, Vector3 _To, out float _Distance)
	{
		_Distance = 0.0f;

		List<(TrafficLane Lane, int Index)> starts = FindNearbyLanes(_From);
		List<(TrafficLane Lane, int Index)> goals = FindNearbyLanes(_To);

		if (starts.Count == 0 || goals.Count == 0)
			return false;

		var goalIndices = new Dictionary<TrafficLane, int>();

		foreach ((TrafficLane lane, int index) in goals)
			goalIndices[lane] = index;

		var best = new Dictionary<TrafficLane, float>();
		var queue = new PriorityQueue<TrafficLane, float>();

		float bestTotal = float.MaxValue;

		foreach ((TrafficLane lane, int index) in starts)
		{
			// Goal on the same lane and ahead of us: straight down the road, no junction involved. Behind us
			// doesn't count — a lane is one-way, so that really does mean driving round and coming back, which
			// the search below works out properly.
			if (goalIndices.TryGetValue(lane, out int goalIndex) && goalIndex >= index)
				bestTotal = Math.Min(bestTotal, lane.DistanceFromStart(goalIndex) - lane.DistanceFromStart(index));

			// We start partway along, so what's reachable is the successors, at the cost of finishing this lane.
			float toEnd = lane.DistanceToEnd(index);

			foreach (TrafficLane next in lane.Successors)
				Relax(next, toEnd, best, queue);
		}

		while (queue.TryDequeue(out TrafficLane lane, out float cost))
		{
			// Min-ordered, so once the cheapest thing left already costs more than an answer we have, nothing
			// better can come out of it.
			if (cost >= bestTotal)
				break;

			if (best.TryGetValue(lane, out float known) && cost > known)
				continue;

			// Reaching a goal lane doesn't end the search: another route might arrive at a different goal
			// candidate — the other side of the same street, say — for less.
			if (goalIndices.TryGetValue(lane, out int goalIndex))
				bestTotal = Math.Min(bestTotal, cost + lane.DistanceFromStart(goalIndex));

			float exit = cost + lane.Length;

			foreach (TrafficLane next in lane.Successors)
				Relax(next, exit, best, queue);
		}

		if (bestTotal >= float.MaxValue)
			return false;

		_Distance = Math.Max(0.0f, bestTotal);

		return true;
	}



	/// <summary>
	/// The actual waypoints to DRIVE from one point to another, following lanes in their legal direction. False
	/// when there's no route, same as <see cref="TryGetDrivingDistance"/>.
	///
	/// This is the vehicle counterpart of a navmesh path, and it has to be a separate structure rather than the
	/// navmesh itself. A navmesh is baked around a person — it runs over pavements, through doorways and up
	/// stairs, and its corridors are person-wide. A route down one is a perfectly valid walk and an impossible
	/// drive, and unlike a pedestrian scraping a wall, a car routed somewhere it doesn't fit is stuck for good.
	/// Lanes are the drivable surface by construction, and they carry direction, which a navmesh has no concept
	/// of at all.
	///
	/// The list is reused rather than returned, because anything chasing anything re-routes constantly.
	/// </summary>
	public bool TryGetDrivingRoute(Vector3 _From, Vector3 _To, List<Vector3> _Route)
	{
		_Route.Clear();

		if (!TrySearchRoute(_From, _To, out RouteSearch search))
			return false;

		BuildRoute(search.Starts, search.GoalIndices, search.CameFrom, search.Goal, search.Start, search.StartIndex, _Route);

		return _Route.Count > 0;
	}



	/// <summary>
	/// The same route as <see cref="TryGetDrivingRoute"/>, but as the LANES to drive rather than the waypoints
	/// along them — including the one we're starting on.
	///
	/// This is the form anything that already knows how to drive a lane wants. <see cref="TrafficVehicle"/>
	/// follows lanes and picks a successor at every junction; handing it a lane list turns "wander" into "go
	/// here" without touching a single line of how it actually steers, brakes or corners.
	/// </summary>
	public bool TryGetDrivingLaneRoute(Vector3 _From, Vector3 _To, List<TrafficLane> _Route)
	{
		return TryGetDrivingLaneRoute(FindNearbyLanes(_From), _To, _Route);
	}



	/// <summary>
	/// The same, but starting from the lane a vehicle is ALREADY DRIVING rather than from its position.
	///
	/// This is the overload anything mid-journey wants, and the difference is not subtle. Asking by position
	/// seeds the search with every lane in range — including the one going the other way down the same road —
	/// and Dijkstra will happily return the shortest route from whichever of those is cheapest. That route
	/// starts on a lane the vehicle is not on, so it never matches, and a driver that can't find itself in its
	/// own route falls back to picking turns at random. The symptom is a car that mostly goes the right way and
	/// occasionally sets off round the block for no visible reason.
	/// </summary>
	public bool TryGetDrivingLaneRoute(TrafficLane _FromLane, Vector3 _FromPosition, Vector3 _To, List<TrafficLane> _Route)
	{
		_Route.Clear();

		if (_FromLane is null || _FromLane.Waypoints.Count == 0)
			return false;

		return TryGetDrivingLaneRoute([(_FromLane, NearestWaypointIndex(_FromLane, _FromPosition))], _To, _Route);
	}



	/// <summary>Which waypoint of a lane is closest to a point.</summary>
	private static int NearestWaypointIndex(TrafficLane _Lane, Vector3 _Point)
	{
		int best = 0;
		float bestDistance = float.MaxValue;

		for (int i = 0; i < _Lane.Waypoints.Count; i++)
		{
			float distance = _Lane.Waypoints[i].DistanceSquared(_Point);

			if (distance >= bestDistance)
				continue;

			bestDistance = distance;
			best = i;
		}

		return best;
	}



	private bool TryGetDrivingLaneRoute(List<(TrafficLane Lane, int Index)> _Starts, Vector3 _To, List<TrafficLane> _Route)
	{
		_Route.Clear();

		if (!TrySearchRoute(_Starts, _To, out RouteSearch search))
			return false;

		// Never left the start lane — the route is just that one.
		if (search.Goal is null)
		{
			if (search.Start is null)
				return false;

			_Route.Add(search.Start);

			return true;
		}

		List<TrafficLane> chain = BuildLaneChain(search.CameFrom, search.Goal);

		if (chain.Count == 0)
			return false;

		// The lane we're ON isn't in the chain (the chain begins at one of its successors), and a driver already
		// travelling it needs to see it in the list or its very first junction is an unplanned one.
		foreach ((TrafficLane lane, int _) in search.Starts)
		{
			if (!lane.Successors.Contains(chain[0]))
				continue;

			_Route.Add(lane);

			break;
		}

		_Route.AddRange(chain);

		return true;
	}



	/// <summary>What a completed search found: the winning route's ends, and the map to walk it back with.</summary>
	private struct RouteSearch
	{
		public List<(TrafficLane Lane, int Index)> Starts;
		public Dictionary<TrafficLane, int> GoalIndices;
		public Dictionary<TrafficLane, TrafficLane> CameFrom;

		/// <summary>The lane the route ends on, or null when it never left the lane it started on.</summary>
		public TrafficLane Goal;

		public TrafficLane Start;
		public int StartIndex;
	}



	/// <summary>
	/// The Dijkstra itself, shared by both route shapes so there's one search to be correct rather than two to
	/// keep in step.
	/// </summary>
	private bool TrySearchRoute(Vector3 _From, Vector3 _To, out RouteSearch _Result)
	{
		return TrySearchRoute(FindNearbyLanes(_From), _To, out _Result);
	}



	/// <inheritdoc cref="TrySearchRoute(Vector3, Vector3, out RouteSearch)"/>
	private bool TrySearchRoute(List<(TrafficLane Lane, int Index)> _Starts, Vector3 _To, out RouteSearch _Result)
	{
		_Result = default;

		List<(TrafficLane Lane, int Index)> starts = _Starts;
		List<(TrafficLane Lane, int Index)> goals = FindNearbyLanes(_To);

		if (starts is null || starts.Count == 0 || goals.Count == 0)
			return false;

		var goalIndices = new Dictionary<TrafficLane, int>();

		foreach ((TrafficLane lane, int index) in goals)
			goalIndices[lane] = index;

		var best = new Dictionary<TrafficLane, float>();
		var cameFrom = new Dictionary<TrafficLane, TrafficLane>();
		var queue = new PriorityQueue<TrafficLane, float>();

		// The winning route so far: where it ends, and which of the several starts it began at.
		float bestTotal = float.MaxValue;
		TrafficLane bestGoal = null;
		TrafficLane bestStart = null;
		int bestStartIndex = 0;

		foreach ((TrafficLane lane, int index) in starts)
		{
			// Goal on the same lane and ahead of us — no junction involved, so the route is just this stretch.
			if (goalIndices.TryGetValue(lane, out int sameLaneGoal) && sameLaneGoal >= index)
			{
				float direct = lane.DistanceFromStart(sameLaneGoal) - lane.DistanceFromStart(index);

				if (direct < bestTotal)
				{
					bestTotal = direct;
					bestGoal = null;      // null goal marks "never left the start lane"
					bestStart = lane;
					bestStartIndex = index;
				}
			}

			float toEnd = lane.DistanceToEnd(index);

			foreach (TrafficLane next in lane.Successors)
			{
				if (Relax(next, toEnd, best, queue))
					cameFrom[next] = lane;
			}
		}

		while (queue.TryDequeue(out TrafficLane lane, out float cost))
		{
			if (cost >= bestTotal)
				break;

			if (best.TryGetValue(lane, out float known) && cost > known)
				continue;

			if (goalIndices.TryGetValue(lane, out int goalIndex))
			{
				float total = cost + lane.DistanceFromStart(goalIndex);

				if (total < bestTotal)
				{
					bestTotal = total;
					bestGoal = lane;
				}
			}

			float exit = cost + lane.Length;

			foreach (TrafficLane next in lane.Successors)
			{
				if (Relax(next, exit, best, queue))
					cameFrom[next] = lane;
			}
		}

		if (bestTotal >= float.MaxValue)
			return false;

		_Result = new RouteSearch
		{
			Starts = starts,
			GoalIndices = goalIndices,
			CameFrom = cameFrom,
			Goal = bestGoal,
			Start = bestStart,
			StartIndex = bestStartIndex
		};

		return true;
	}



	/// <summary>
	/// Walks the predecessor chain back from the winning goal lane to whichever start it came from, then lays
	/// the waypoints down in travel order.
	///
	/// The two ends are partial lanes — we join partway along the first and stop partway along the last — which
	/// is why they're handled separately from the whole lanes in between.
	/// </summary>
	private void BuildRoute(List<(TrafficLane Lane, int Index)> _Starts, Dictionary<TrafficLane, int> _GoalIndices,
	                        Dictionary<TrafficLane, TrafficLane> _CameFrom, TrafficLane _Goal,
	                        TrafficLane _Start, int _StartIndex, List<Vector3> _Route)
	{
		// Never left the start lane: one straight run down it.
		if (_Goal is null)
		{
			if (_Start is null || !_GoalIndices.TryGetValue(_Start, out int stop))
				return;

			for (int i = _StartIndex; i <= stop; i++)
				_Route.Add(_Start.Waypoints[i]);

			return;
		}

		List<TrafficLane> chain = BuildLaneChain(_CameFrom, _Goal);

		if (chain.Count == 0)
			return;

		// The first lane in the chain is a successor of the start lane, so the start lane itself isn't in it —
		// find which of the candidates fed it and lay down the tail of that one first.
		TrafficLane head = chain[0];

		foreach ((TrafficLane lane, int index) in _Starts)
		{
			if (!lane.Successors.Contains(head))
				continue;

			for (int i = index; i < lane.Waypoints.Count; i++)
				_Route.Add(lane.Waypoints[i]);

			break;
		}

		for (int c = 0; c < chain.Count; c++)
		{
			TrafficLane lane = chain[c];

			// The last one stops at the goal waypoint rather than running to the end of the road.
			int stop = c == chain.Count - 1 && _GoalIndices.TryGetValue(lane, out int goalIndex)
				? goalIndex
				: lane.Waypoints.Count - 1;

			for (int i = 0; i <= stop; i++)
				_Route.Add(lane.Waypoints[i]);
		}
	}



	/// <summary>Walks the predecessor map back from a goal lane and returns the chain in travel order.</summary>
	private List<TrafficLane> BuildLaneChain(Dictionary<TrafficLane, TrafficLane> _CameFrom, TrafficLane _Goal)
	{
		var chain = new List<TrafficLane>();
		TrafficLane current = _Goal;

		// Bounded by the lane count so a cycle in the map can't spin forever.
		for (int step = 0; step <= Lanes.Count && current is not null; step++)
		{
			chain.Add(current);

			if (!_CameFrom.TryGetValue(current, out TrafficLane previous))
				break;

			current = previous;
		}

		chain.Reverse();

		return chain;
	}



	/// <summary>True when this was an improvement, so the caller knows whether to record the predecessor.</summary>
	private static bool Relax(TrafficLane _Lane, float _Cost, Dictionary<TrafficLane, float> _Best, PriorityQueue<TrafficLane, float> _Queue)
	{
		if (_Best.TryGetValue(_Lane, out float existing) && existing <= _Cost)
			return false;

		_Best[_Lane] = _Cost;

		_Queue.Enqueue(_Lane, _Cost);

		return true;
	}



	/// <summary>
	/// Every drivable lane with a waypoint within <paramref name="_Radius"/> of the point, and which waypoint
	/// that was — at most one entry per lane.
	///
	/// Taking ALL of them, rather than just the closest, is what makes routing reliable. A road carries a lane
	/// per direction, so any point on it is near at least two; picking only the nearest is a coin flip that can
	/// land on the one pointing away from where you're going, or on one nothing feeds into. The route then comes
	/// back as impossible even though the lane a few metres over is trivially routable. Seeding the search with
	/// every candidate — and accepting any of them at the far end — also gets the natural answer for free:
	/// either side of the street will do, whichever is closer to drive.
	///
	/// Road lanes only. Snapping an endpoint onto an intersection cross-lane would measure from the middle of
	/// a junction.
	/// </summary>
	public List<(TrafficLane Lane, int Index)> FindNearbyLanes(Vector3 _Point, float _Radius = DefaultRouteSnapRadius)
	{
		var results = new List<(TrafficLane, int)>();

		TrafficLane nearestLane = null;
		int nearestIndex = 0;
		float nearestDistance = float.MaxValue;

		float radiusSquared = _Radius * _Radius;

		foreach (TrafficLane lane in Lanes)
		{
			if (!lane.IsRoadLane)
				continue;

			int laneIndex = -1;
			float laneDistance = float.MaxValue;

			for (int i = 0; i < lane.Waypoints.Count; i++)
			{
				float distance = lane.Waypoints[i].DistanceSquared(_Point);

				if (distance >= laneDistance)
					continue;

				laneDistance = distance;
				laneIndex = i;
			}

			if (laneIndex < 0)
				continue;

			if (laneDistance < nearestDistance)
			{
				nearestDistance = laneDistance;
				nearestLane = lane;
				nearestIndex = laneIndex;
			}

			if (laneDistance <= radiusSquared)
				results.Add((lane, laneIndex));
		}

		// Off-road entirely (a car park, a field) — the closest lane is still the honest answer, so don't come
		// back empty and turn a long route into "no route".
		if (results.Count == 0 && nearestLane is not null)
			results.Add((nearestLane, nearestIndex));

		return results;
	}



	/// <summary>
	/// The single drivable lane closest to a point, and which waypoint that was. Prefer
	/// <see cref="FindNearbyLanes"/> for routing — one lane is rarely the whole answer for a two-way road.
	/// </summary>
	public TrafficLane FindNearestLane(Vector3 _Point, out int _Index)
	{
		TrafficLane bestLane = null;
		float bestDistance = float.MaxValue;

		_Index = 0;

		foreach (TrafficLane lane in Lanes)
		{
			if (!lane.IsRoadLane)
				continue;

			for (int i = 0; i < lane.Waypoints.Count; i++)
			{
				float distance = lane.Waypoints[i].DistanceSquared(_Point);

				if (distance >= bestDistance)
					continue;

				bestDistance = distance;
				bestLane = lane;

				_Index = i;
			}
		}

		return bestLane;
	}
}
redsnail.roadtool / RoadManager/RoadVehicleDriver.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool;

/// <summary>
/// A primitive control surface the traffic AI (<see cref="TrafficVehicle"/>) uses to drive ONE vehicle. It's a plain
/// bag of delegates — there is no interface for a vehicle controller to implement, and your vehicle code never needs
/// to reference this library.
///
/// The seam is filled in by whoever uses both this tool AND a vehicle controller — i.e. your GAME — via
/// <see cref="RoadManager.ResolveVehicleDriver"/>. The game maps whatever its controller looks like onto these few
/// delegates. Any field left null is simply skipped. The demo wires <see cref="DemoCarController"/> automatically.
/// </summary>
public sealed class RoadVehicleDriver
{
	/// <summary>True while a player is at the wheel — the AI then hands this car over for good and never reclaims it.</summary>
	public Func<bool> IsPlayerDriving;

	/// <summary>The vehicle body's world velocity. The brain reads it to chase a target speed and to detect being jammed.</summary>
	public Func<Vector3> Velocity;

	/// <summary>Tell the controller whether the AI is currently driving this vehicle (vs parked / player-driven). Pushed every frame.</summary>
	public Action<bool> SetAiControlled;

	/// <summary>Push the AI's per-frame inputs: throttle and steer in [-1, 1] (steer + = left), plus handbrake.</summary>
	public Action<float, float, bool> Drive;

	/// <summary>Optional: max steering angle in degrees, used to widen the entity look-ahead toward where the car is turning.</summary>
	public Func<float> MaxSteering;
}
redsnail.roadtool / Editor/RoadToolWindow/RoadToolWindow.Gizmo.cs
Editor library
using Sandbox;

namespace RedSnail.RoadTool.Editor;

public partial class RoadToolWindow
{
	private const float GIZMO_BOX_SIZE = 2.0f;
	private const float LINE_THICKNESS = 2.0f;
	private const float TANGENT_LINE_THICKNESS = 0.8f;



	private void DrawGizmos()
	{
		using (Gizmo.Scope("road_editor", _targetComponent.WorldTransform))
		{
			DrawSplineSegments();
			DrawPositionGizmo();
			DrawPointControls();
		}
	}



	private void DrawSplineSegments()
	{
		_targetComponent.Spline.ConvertToPolyline(ref _polyLine);

		for (var i = 0; i < _polyLine.Count - 1; i++)
		{
			DrawSegment(i, _polyLine[i], _polyLine[i + 1]);
		}
	}



	private void DrawSegment(int index, Vector3 start, Vector3 end)
	{
		using (Gizmo.Scope("segment" + index))
		using (Gizmo.Hitbox.LineScope())
		{
			Gizmo.Draw.LineThickness = LINE_THICKNESS;
			Gizmo.Hitbox.AddPotentialLine(start, end, LINE_THICKNESS * 2f);
			Gizmo.Draw.Line(start, end);

			if (Gizmo.IsHovered && Gizmo.HasMouseFocus)
			{
				HandleSegmentHover(start, end);
			}
		}
	}



	private void HandleSegmentHover(Vector3 start, Vector3 end)
	{
		Gizmo.Draw.Color = Color.Cyan;

		if (!new Line(start, end).ClosestPoint(Gizmo.CurrentRay.ToLocal(Gizmo.Transform),
			out Vector3 pointOnLine, out _))
			return;

		var hoverSample = _targetComponent.Spline.SampleAtClosestPosition(pointOnLine);
		DrawHoverHandle(pointOnLine, hoverSample.Tangent);

		if (Gizmo.HasClicked && Gizmo.Pressed.This)
		{
			InsertPointAtHover(hoverSample.Distance);
		}
	}



	private void DrawHoverHandle(Vector3 position, Vector3 tangent)
	{
		using (Gizmo.Scope("hover_handle", new Transform(position, Rotation.LookAt(tangent))))
		using (Gizmo.GizmoControls.PushFixedScale())
		{
			Gizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));
		}
	}



	private void InsertPointAtHover(float distance)
	{
		using (CreateUndoScope("Added spline point"))
		{
			var newPointIndex = _targetComponent.Spline.AddPointAtDistance(distance, true);
			SelectedPointIndex = newPointIndex;
			_inTangentSelected = false;
			_outTangentSelected = false;
		}
	}



	private void DrawPositionGizmo()
	{
		// The first point sits on the component origin, so its move handle lands right on top of the GameObject's
		// own transform gizmo — two move gizmos fighting over the same spot. Hide the point-body handle there and
		// let the object transform gizmo move the whole track instead. Tangent editing still gets its own gizmo.
		if (SelectedPointIndex == 0 && !_inTangentSelected && !_outTangentSelected)
			return;

		var gizmoPosition = CalculateGizmoPosition();

		if (!Gizmo.IsShiftPressed)
		{
			_draggingOutNewPoint = false;
		}

		using (Gizmo.Scope("position", new Transform(gizmoPosition)))
		{
			HandlePositionControl();
		}
	}



	private Vector3 CalculateGizmoPosition()
	{
		var position = _selectedPoint.Position;

		if (_inTangentSelected)
			position += _selectedPoint.In;
		else if (_outTangentSelected)
			position += _selectedPoint.Out;

		return position;
	}



	private void HandlePositionControl()
	{
		_moveInProgress = false;

		if (Gizmo.Control.Position("spline_control_", Vector3.Zero, out var delta, GetHandleRotation()))
		{
			_moveInProgress = true;
			_movementUndoScope ??= CreateUndoScope("Moved spline point");

			if (_inTangentSelected)
				MoveSelectedPointInTangent(delta);
			else if (_outTangentSelected)
				MoveSelectedPointOutTangent(delta);
			else
				HandlePointMove(delta);
		}

		if (!_moveInProgress && Gizmo.WasLeftMouseReleased)
		{
			_movementUndoScope?.Dispose();
			_movementUndoScope = null;
		}
	}



	/// <summary>
	/// Orientation for the move gizmo's arrows, honouring the editor's Global/Local space toggle
	/// (the same <see cref="Gizmo.Settings.GlobalSpace"/> button the object move tool uses).
	/// Global keeps the arrows world-aligned; Local aligns them to the selected point's tangent frame
	/// so a point can be dragged straight along the track. We are already inside the component-transform
	/// scope, so returning the point's <em>local</em> frame is what places the arrows on the world tangent —
	/// and <see cref="Gizmo.Control.Position"/> hands the delta back in component-local space, matching how
	/// <see cref="MoveSelectedPoint"/> applies it.
	/// </summary>
	private Rotation GetHandleRotation()
	{
		if (Gizmo.Settings.GlobalSpace)
			return Rotation.Identity;

		if (!IsSelectedPointValid())
			return Rotation.Identity;

		var spline = _targetComponent.Spline;
		var sample = spline.SampleAtDistance(spline.GetDistanceAtPoint(SelectedPointIndex));

		if (sample.Tangent.IsNearlyZero())
			return Rotation.Identity;

		var up = Rotation.FromAxis(sample.Tangent, sample.Roll) * sample.Up;

		return Rotation.LookAt(sample.Tangent, up);
	}



	private void HandlePointMove(Vector3 delta)
	{
		if (Gizmo.IsShiftPressed && !_draggingOutNewPoint)
		{
			_draggingOutNewPoint = true;
			var currentPoint = _targetComponent.Spline.GetPoint(SelectedPointIndex);
			_targetComponent.Spline.InsertPoint(SelectedPointIndex + 1, currentPoint);
			SelectedPointIndex++;
		}
		else
		{
			MoveSelectedPoint(delta);
		}
	}



	private void DrawPointControls()
	{
		var spline = _targetComponent.Spline;

		for (var i = 0; i < spline.PointCount; i++)
		{
			if (spline.IsLoop && i == spline.SegmentCount)
				continue;

			var point = spline.GetPoint(i);
			DrawPointControl(i, point);
		}
	}



	private void DrawPointControl(int index, Spline.Point point)
	{
		using (Gizmo.Scope("point_controls" + index, new Transform(point.Position)))
		{
			Gizmo.Draw.IgnoreDepth = true;
			DrawPointPositionHandle(index);

			if (SelectedPointIndex == index)
			{
				DrawTangentHandles(point);
			}
		}
	}



	private void DrawPointPositionHandle(int index)
	{
		using (Gizmo.Scope("position"))
		using (Gizmo.GizmoControls.PushFixedScale())
		{
			Gizmo.Hitbox.DepthBias = 0.1f;
			Gizmo.Hitbox.BBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));

			bool isSelected = index == SelectedPointIndex && !_inTangentSelected && !_outTangentSelected;

			if (Gizmo.IsHovered || isSelected)
			{
				Gizmo.Draw.Color = Color.Cyan;
			}

			Gizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));

			if (Gizmo.HasClicked && Gizmo.Pressed.This)
			{
				SelectPoint(index);
			}
		}
	}



	private void DrawTangentHandles(Spline.Point point)
	{
		Gizmo.Draw.Color = Color.White;
		Gizmo.Draw.LineThickness = TANGENT_LINE_THICKNESS;

		DrawTangentHandle("in_tangent", point.In, -point.In, ref _inTangentSelected, ref _outTangentSelected);
		DrawTangentHandle("out_tangent", point.Out, -point.Out, ref _outTangentSelected, ref _inTangentSelected);
	}



	private void DrawTangentHandle(string name, Vector3 offset, Vector3 lineStart, ref bool thisSelected, ref bool otherSelected)
	{
		using (Gizmo.Scope(name, new Transform(offset)))
		{
			bool isMirroredOrAuto = _selectedPointTangentMode is HandleModeTemp.Mirrored or HandleModeTemp.Auto;
			if (isMirroredOrAuto && (thisSelected || otherSelected))
			{
				Gizmo.Draw.Color = Color.Cyan;
			}

			Gizmo.Draw.Line(lineStart, Vector3.Zero);

			if (_selectedPointTangentMode != HandleModeTemp.Linear)
			{
				DrawTangentBox(ref thisSelected, ref otherSelected);
			}
		}
	}



	private void DrawTangentBox(ref bool thisSelected, ref bool otherSelected)
	{
		using (Gizmo.GizmoControls.PushFixedScale())
		{
			Gizmo.Hitbox.DepthBias = 0.1f;
			Gizmo.Hitbox.BBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));

			if (Gizmo.IsHovered || thisSelected)
			{
				Gizmo.Draw.Color = Color.Cyan;
			}

			Gizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));

			if (Gizmo.HasClicked && Gizmo.Pressed.This)
			{
				thisSelected = true;
				otherSelected = false;
			}
		}
	}
}
redsnail.roadtool / Editor/RoadToolWindow/RoadToolWindow.UI.cs
Editor library
using Sandbox;
using Editor;

namespace RedSnail.RoadTool.Editor;

public partial class RoadToolWindow
{
	private const int HEADER_HEIGHT = 32;



	private void Rebuild()
	{
		Layout.Clear(true);
		Layout.Margin = 0;
		Icon = _isClosed ? "" : "route";
		UpdateWindowTitle();
		IsGrabbable = !_isClosed;

		if (_isClosed)
		{
			BuildClosedState();
			return;
		}

		MinimumWidth = 400;
		BuildHeader();

		if (_targetComponent.IsValid())
		{
			BuildControlSheet();
		}

		Layout.Margin = 4;
	}



	private void BuildClosedState()
	{
		var closedRow = Layout.AddRow();

		closedRow.Add(new IconButton("route", () => { _isClosed = false; Rebuild(); })
		{
			ToolTip = "Open Spline Point Editor",
			FixedHeight = HEADER_HEIGHT,
			FixedWidth = HEADER_HEIGHT,
			Background = Color.Transparent
		});

		MinimumWidth = 0;
	}



	private void BuildHeader()
	{
		var headerRow = Layout.AddRow();

		headerRow.AddStretchCell();

		headerRow.Add(new IconButton("info")
		{
			ToolTip = GetInfoTooltip(),
			FixedHeight = HEADER_HEIGHT,
			FixedWidth = HEADER_HEIGHT,
			Background = Color.Transparent
		});

		headerRow.Add(new IconButton("close", CloseWindow)
		{
			ToolTip = "Close Editor",
			FixedHeight = HEADER_HEIGHT,
			FixedWidth = HEADER_HEIGHT,
			Background = Color.Transparent
		});
	}



	private string GetInfoTooltip()
	{
		return "Controls to edit the spline points.\n" +
			   "In addition to modifying the properties in the control sheet, you can also use the 3D Gizmos.\n" +
			   "Clicking on the spline between points will split the spline at that position.\n" +
			   "Holding shift while dragging a point's position will drag out a new point.";
	}



	private void BuildControlSheet()
	{
		var serialized = this.GetSerialized();
		var controlSheet = new ControlSheet();

		// Add property rows
		controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointTangentMode)));
		controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointPosition)));
		_inTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointIn)));
		_outTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointOut)));

		// Add advanced group
		var roll = serialized.GetProperty(nameof(_selectedPointRoll));
		var scale = serialized.GetProperty(nameof(_selectedPointScale));
		var up = serialized.GetProperty(nameof(_selectedPointUp));
		controlSheet.AddGroup("Advanced", [roll, scale, up]);

		// Add control buttons
		controlSheet.AddLayout(BuildControlButtons());

		Layout.Add(controlSheet);
		ToggleTangentInput();
	}



	private Layout BuildControlButtons()
	{
		var row = Layout.Row();
		row.Spacing = 16;
		row.Margin = 8;

		row.Add(CreateNavigationButton("skip_previous", -1, "Go to previous point"));
		row.Add(CreateNavigationButton("skip_next", 1, "Go to next point"));
		row.Add(CreateDeleteButton());
		row.Add(CreateAddButton());

		return row;
	}



	private IconButton CreateNavigationButton(string icon, int direction, string tooltip)
	{
		return new IconButton(icon, () =>
		{
			if (direction < 0)
				SelectedPointIndex = int.Max(0, SelectedPointIndex - 1);
			else
				SelectedPointIndex = int.Min(_targetComponent.Spline.PointCount - 1, SelectedPointIndex + 1);

			UpdateWindowTitle();
			Focus();
		})
		{ ToolTip = tooltip };
	}



	private IconButton CreateDeleteButton()
	{
		return new IconButton("delete", () =>
		{
			using (CreateUndoScope("Delete Spline Point"))
			{
				_targetComponent.Spline.RemovePoint(SelectedPointIndex);
				SelectedPointIndex = int.Max(0, SelectedPointIndex - 1);
			}
			UpdateWindowTitle();
			Focus();
		})
		{ ToolTip = "Delete point" };
	}



	private IconButton CreateAddButton()
	{
		return new IconButton("add", () =>
		{
			using (CreateUndoScope("Added Spline Point"))
			{
				InsertNewPoint();
			}
			SelectedPointIndex++;
			UpdateWindowTitle();
			Focus();
		})
		{
			ToolTip = "Insert point after current point.\n" +
					  "You can also hold shift while dragging a point to create a new point."
		};
	}



	private void InsertNewPoint()
	{
		var spline = _targetComponent.Spline;

		if (SelectedPointIndex == spline.PointCount - 1)
		{
			var distance = spline.GetDistanceAtPoint(SelectedPointIndex);
			var tangent = spline.SampleAtDistance(distance).Tangent;
			var newPosition = _selectedPoint.Position + tangent * 200;

			spline.InsertPoint(SelectedPointIndex + 1, _selectedPoint with { Position = newPosition });
		}
		else
		{
			var currentDist = spline.GetDistanceAtPoint(SelectedPointIndex);
			var nextDist = spline.GetDistanceAtPoint(SelectedPointIndex + 1);
			var midDist = (currentDist + nextDist) / 2;

			spline.AddPointAtDistance(midDist, true);
		}
	}



	private void UpdateWindowTitle()
	{
		WindowTitle = _isClosed ? "" : $"Spline Point [{SelectedPointIndex}] Editor - {_targetComponent?.GameObject?.Name ?? ""}";
	}



	private void CloseWindow()
	{
		_isClosed = true;
		Rebuild();
		Position = Parent.Size - 32;
	}
}
redsnail.roadtool / Code/RoadComponent/RoadComponent.Lampposts.cs
Game library
using System;
using System.Linq;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RoadComponent
{
	private bool m_DoesLamppostsNeedRebuild = false;

	[Property, FeatureEnabled("Lampposts", Icon = "light_mode", Tint = EditorTint.Red), Change] private bool HasLampposts { get; set; } = false;
	[Property, Feature("Lampposts")] public GameObject LamppostPrefab { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } }
	[Property, Feature("Lampposts"), Range(50.0f, 2000.0f)] private float LamppostSpacing { get; set { field = value.Clamp(10.0f, 100000.0f); m_DoesLamppostsNeedRebuild = true; } } = 50.0f;
	[Property, Feature("Lampposts"), Range(-200.0f, 200.0f)] private float LamppostOffsetFromSidewalk { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 10.0f;
	[Property, Feature("Lampposts"), Range(0.0f, 10.0f)] private float LamppostHeightOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;
	[Property, Feature("Lampposts")] private LamppostSide LamppostPlacement { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = LamppostSide.Both;
	[Property, Feature("Lampposts")] private bool AlignToSplineRotation { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = true;
	[Property(Title = "Keep Vertical (World Up)"), Feature("Lampposts")] private bool KeepVertical { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = true;
	[Property, Feature("Lampposts"), Range(0.0f, 360.0f)] private float LamppostRotationOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;
	[Property, Feature("Lampposts"), Range(0.0f, 100.0f)] private float StartOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;
	[Property, Feature("Lampposts"), Range(0.0f, 100.0f)] private float EndOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;

	public enum LamppostSide
	{
		Left,
		Right,
		Both,
		Alternating
	}



	private void OnHasLamppostsChanged(bool _OldValue, bool _NewValue)
	{
		m_DoesLamppostsNeedRebuild = true;
	}



	private void CreateLampposts()
	{
		RemoveLampposts();

		if (!HasLampposts || !LamppostPrefab.IsValid())
			return;

		BuildLampposts();
	}



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

		GameObject containerObject = GameObject.Children.FirstOrDefault(x => x.Name == "Lampposts");

		if (containerObject.IsValid())
		{
			containerObject.Destroy();
		}
	}



	private void UpdateLampposts()
	{
		if (m_DoesLamppostsNeedRebuild)
		{
			CreateLampposts();

			m_DoesLamppostsNeedRebuild = false;
		}
	}



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

		GameObject containerObject = new GameObject(GameObject, true, "Lampposts");
		containerObject.Tags.Add("road_props");

		float splineLength = Spline.Length;
		float effectiveLength = splineLength - StartOffset - EndOffset;

		if (effectiveLength <= 0)
			return;

		GetSplineFrameData(out var frames, out var segmentsToKeep);

		var simplifiedPositions = new List<(Transform _Frame, float _Distance)>();

		foreach (int index in segmentsToKeep)
		{
			float t = (float)index / (frames.Length - 1);
			float distance = t * splineLength;

			simplifiedPositions.Add((frames[index], distance));
		}

		int lamppostCount = Math.Max(1, (int)MathF.Ceiling(effectiveLength / LamppostSpacing));
		int frameCount = lamppostCount + 1;

		float roadEdgeOffset = RoadWidth * 0.5f;
		float sidewalkOffset = HasSidewalk ? SidewalkWidth : 0.0f;
		float totalOffset = roadEdgeOffset + sidewalkOffset + LamppostOffsetFromSidewalk;

		for (int i = 0; i < frameCount; i++)
		{
			float t = (float)i / (frameCount - 1);
			float distance = t * splineLength;

			// Skip if outside the start/end offset range
			if (distance < StartOffset || distance > splineLength - EndOffset)
				continue;

			// Interpolate frame at this distance along the simplified spline
			Transform frame = InterpolateFrameAtDistance(simplifiedPositions, distance);

			Vector3 basePosition = frame.Position;
			Vector3 forward = frame.Rotation.Forward;
			Vector3 up = frame.Rotation.Up;
			Vector3 right = frame.Rotation.Right;

			bool placeLeft = false;
			bool placeRight = false;

			switch (LamppostPlacement)
			{
				case LamppostSide.Left:
					placeLeft = true;
					break;
				case LamppostSide.Right:
					placeRight = true;
					break;
				case LamppostSide.Both:
					placeLeft = true;
					placeRight = true;
					break;
				case LamppostSide.Alternating:
					if (i % 2 == 0)
						placeLeft = true;
					else
						placeRight = true;
					break;
				default:
					placeLeft = true;
					placeRight = true;
					break;
			}

			if (placeLeft)
			{
				Vector3 leftPosition = basePosition - right * totalOffset + up * (LamppostHeightOffset + (HasSidewalk ? SidewalkHeight : 0.0f));
				Rotation leftRotation = CalculateLamppostRotation(forward, up, LamppostRotationOffset);

				CreateLamppost(containerObject, leftPosition, leftRotation);
			}

			if (placeRight)
			{
				Vector3 rightPosition = basePosition + right * totalOffset + up * (LamppostHeightOffset + (HasSidewalk ? SidewalkHeight : 0.0f));
				Rotation rightRotation = CalculateLamppostRotation(forward, up, LamppostRotationOffset + 180.0f);

				CreateLamppost(containerObject, rightPosition, rightRotation);
			}
		}
	}



	private void CreateLamppost(GameObject _Parent, Vector3 _Position, Rotation _Rotation)
	{
		if (!LamppostPrefab.IsValid())
			return;

		GameObject lamppostObject = LamppostPrefab.Clone(_Parent, _Position, _Rotation, Vector3.One);

		if (!lamppostObject.IsValid())
			return;

		lamppostObject.LocalPosition = _Position;
		lamppostObject.LocalRotation = _Rotation;
	}



	private Rotation CalculateLamppostRotation(Vector3 _Forward, Vector3 _SplineUp, float _YawOffset)
	{
		if (!AlignToSplineRotation)
			return Rotation.FromYaw(_YawOffset);

		Rotation finalRotation;

		if (KeepVertical)
		{
			Vector3 flatForward = _Forward.WithZ(0).Normal;

			if (flatForward.Length > 0.001f)
			{
				finalRotation = Rotation.LookAt(flatForward, Vector3.Up);
			}
			else
			{
				finalRotation = Rotation.FromYaw(_YawOffset);
			}
		}
		else
		{
			finalRotation = Rotation.LookAt(_Forward, _SplineUp);
		}

		return finalRotation * Rotation.FromYaw(_YawOffset);
	}
}
redsnail.roadtool / Code/RoadComponent/RoadComponent.Lines.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RoadComponent
{
	[Property, FeatureEnabled("Lines", Icon = "show_chart", Tint = EditorTint.Yellow), Change] private bool HasLines { get; set; } = false;
	[Property(Title = "Lines"), Feature("Lines")] public RoadLineDefinition[] LineDefinitions { get; set { field = value; IsDirty = true; } }
	[Property(Title = "Offset"), Feature("Lines"), Range(0.01f, 1.0f)] private float LinesOffset { get; set { field = value; IsDirty = true; } } = 0.1f;
	[Property(Title = "Width"), Feature("Lines"), Range(1.0f, 50.0f)] private float LinesWidth { get; set { field = value; IsDirty = true; } } = 5.0f;
	[Property(Title = "Extra Spacing"), Feature("Lines"), Range(0.0f, 1000.0f)] private float LinesExtraSpacing { get; set { field = value; IsDirty = true; } } = 0.0f;
	[Property(Title = "Texture Repeat"), Feature("Lines")] private float LinesTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 10.0f;



	private void OnHasLinesChanged(bool _OldValue, bool _NewValue)
	{
		IsDirty = true;
	}



	private void CreateLines()
	{
		EnsureLinesMeshExist();
	}



	private void UpdateLines()
	{
	}



	private void RemoveLines()
	{
		RemoveGeneratedMeshChildren(LineSurfaceTag);
	}



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

		if (HasGeneratedMeshChildren(LineSurfaceTag))
			return;

		BuildLinesMesh();
	}



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

		RemoveGeneratedMeshChildren(LineSurfaceTag);
		BuildLinesMesh();
	}



	private void BuildLinesMesh()
	{
		if (!HasLines || LineDefinitions == null || LineDefinitions.Length == 0)
			return;

		GetSplineFrameData(out var frames, out var segmentsToKeep);

		int finalSegmentCount = segmentsToKeep.Count - 1;

		if (finalSegmentCount <= 0)
			return;

		float roadWidth = RoadWidth + LinesExtraSpacing;
		float lineSpacing = roadWidth / (LineDefinitions.Length + 1);

		var polygonMeshes = new PolygonMesh[LineDefinitions.Length];
		for (int i = 0; i < LineDefinitions.Length; i++)
			polygonMeshes[i] = new PolygonMesh();

		float[] lineDistances = new float[LineDefinitions.Length];

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

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

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

			Vector3 right0 = f0.Rotation.Right;

			for (int line = 0; line < LineDefinitions.Length; line++)
			{
				float offsetFromCenter = ((line + 1) * lineSpacing) - (roadWidth * 0.5f);

				Vector3 center0 =
					p0 +
					f0.Rotation.Right * offsetFromCenter +
					f0.Rotation.Up * LinesOffset;

				Vector3 center1 =
					p1 +
					f1.Rotation.Right * offsetFromCenter +
					f1.Rotation.Up * LinesOffset;

				float segmentLength = Vector3.DistanceBetween(center0, center1);
				Vector3 dir = (center1 - center0).Normal;

				float remaining = segmentLength;
				Vector3 curCenter = center0;

				float dashSpacing = LineDefinitions[line]?.DashSpacing ?? 0.0f;
				float dashFillRatio = LineDefinitions[line]?.DashFillRatio ?? 1.0f;
				float dashLength = dashSpacing * dashFillRatio;
				float halfWidth = LinesWidth * 0.5f;

				var polygonMesh = polygonMeshes[line];
				var material = LineDefinitions[line]?.Material ?? Material.Load("materials/default.vmat");

				while (remaining > 0.001f)
				{
					float linePos = lineDistances[line];
					float cyclePos = dashSpacing > 0 ? linePos % dashSpacing : 0;

					if (cyclePos < 0.0001f)
						cyclePos = 0.0f;

					if (dashSpacing > 0 && dashSpacing - cyclePos < 0.0001f)
						cyclePos = dashSpacing;

					bool inDash = dashSpacing <= 0 || cyclePos <= dashLength - 0.0001f;

					float step;

					if (dashSpacing <= 0)
						step = remaining;
					else if (inDash)
						step = dashLength - cyclePos;
					else
						step = dashSpacing - cyclePos;

					step = Math.Max(step, 0.01f);
					step = Math.Min(step, remaining);

					Vector3 nextCenter = curCenter + dir * step;

					if (inDash)
					{
						Vector3 l0 = curCenter - right0 * halfWidth;
						Vector3 r0 = curCenter + right0 * halfWidth;
						Vector3 l1 = nextCenter - right0 * halfWidth;
						Vector3 r1 = nextCenter + right0 * halfWidth;

						float v0 = linePos / LinesTextureRepeat;
						float v1 = (linePos + step) / LinesTextureRepeat;

						var verts = polygonMesh.AddVertices(l0, r0, r1, l1);
						MeshUtility.AddTexturedQuad(polygonMesh, material, verts[0], verts[1], verts[2], verts[3],
							new Vector2(0, v0), new Vector2(1, v0),
							new Vector2(1, v1), new Vector2(0, v1));
					}

					lineDistances[line] += step;
					curCenter = nextCenter;
					remaining -= step;
				}
			}
		}

		for (int line = 0; line < LineDefinitions.Length; line++)
		{
			var child = new GameObject(GameObject, true, $"Line_{line}");
			child.Tags.Add(LineSurfaceTag);

			var meshComponent = child.AddComponent<MeshComponent>();
			meshComponent.Mesh = polygonMeshes[line];
			meshComponent.Collision = MeshComponent.CollisionType.None;
			meshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;
			meshComponent.SmoothingAngle = 40.0f;
			meshComponent.Static = true;
		}
	}
}
redsnail.roadtool / Code/RoadParkingLotComponent/RoadParkingLotComponent.cs
Game library
using System.Linq;
using Sandbox;

namespace RedSnail.RoadTool;

/// <summary>
/// Generates parking lot lines for parking spaces
/// </summary>
[Icon("local_parking")]
public partial class RoadParkingLotComponent : Component, Component.ExecuteInEditor
{
	private bool m_IsDirty;

	private const string LinesTag = "parking_lines";
	private const string CurbsTag = "parking_curbs";

	/// <summary>
	/// An optional prefab, if non-empty the parking lot will generate a bunch of child gameobjects positioned at each parking spots center.
	/// (e.g. this allows you to use a gameobject prefab with a car spawner system component attached to it)
	/// </summary>
	[Property, Feature("General", Icon = "public", Tint = EditorTint.White)] private GameObject SpotPrefab { get; set; }

	/// <summary>
	/// The amount of parking spots you want to generate.
	/// </summary>
	[Property, Feature("General"), Range(1, 50)] private int SpotCount { get; set { field = value; m_IsDirty = true; } } = 10;

	/// <summary>
	/// Well that's the parking spot length
	/// </summary>
	[Property, Feature("General"), Range(10.0f, 1000.0f)] private float SpotLength { get; set { field = value; m_IsDirty = true; } } = 250.0f;

	/// <summary>
	/// and width...
	/// </summary>
	[Property, Feature("General"), Range(10.0f, 1000.0f)] private float SpotWidth { get; set { field = value; m_IsDirty = true; } } = 150.0f;

	/// <summary>
	/// The angle of the parking spots in degrees (0 = perpendicular, 45 = angled, 90 = parallel)
	/// </summary>
	[Property, Feature("General"), Range(-90.0f, 90.0f), Step(1.0f)] private float SpotAngle { get; set { field = value; m_IsDirty = true; } } = 0.0f;
	[Property, Feature("General"), Range(0.5f, 1.0f)] private float SpotAngleThreshold { get; set { field = value; m_IsDirty = true; } } = 0.5f;



	protected override void OnEnabled()
	{
		BuildAllMeshes();
	}



	protected override void OnDisabled()
	{
		DestroyMeshChildren();
		RemoveParkingSpots();
	}



	protected override void OnUpdate()
	{
		if (m_IsDirty)
		{
			if (!SandboxUtility.IsInPlayMode)
			{
				DestroyMeshChildren();
				BuildAllMeshes();
			}

			m_IsDirty = false;
		}
	}



	private void DestroyMeshChildren()
	{
		var toRemove = GameObject.Children
			.Where(c => c.Tags.Has(LinesTag) || c.Tags.Has(CurbsTag))
			.ToList();

		foreach (var child in toRemove)
			child.Destroy();
	}



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

		BuildParkingLines();
		BuildCurbs();

		RemoveParkingSpots();
		CreateParkingSpots();
	}



	protected override void DrawGizmos()
	{
		if (!Gizmo.IsSelected)
			return;

		Gizmo.Draw.LineThickness = 2.0f;
		Gizmo.Draw.Color = Color.Green.WithAlpha(0.5f);

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

		float spacing = CalculateSpacing();

		// Draw parking spot outlines
		for (int i = 0; i < SpotCount; i++)
		{
			float xPos = i * spacing;

			Vector3 frontLeft = new Vector3(xPos, 0, LinesOffset);
			Vector3 frontRight = new Vector3(xPos + SpotWidth * cosAngle, SpotWidth * sinAngle, LinesOffset);
			Vector3 backLeft = new Vector3(xPos - SpotLength * sinAngle, SpotLength * cosAngle, LinesOffset);
			Vector3 backRight = new Vector3(xPos + SpotWidth * cosAngle - SpotLength * sinAngle, SpotWidth * sinAngle + SpotLength * cosAngle, LinesOffset);

			Gizmo.Draw.Line(frontLeft, frontRight);
			Gizmo.Draw.Line(frontRight, backRight);
			Gizmo.Draw.Line(backRight, backLeft);
			Gizmo.Draw.Line(backLeft, frontLeft);
		}
	}



	private void CreateParkingSpots()
	{
		// If we're in play mode, do not build (Since they're already saved in the scene file)
		if (LoadingScreen.IsVisible || Game.IsPlaying)
			return;

		if (!SpotPrefab.IsValid())
			return;

		GameObject containerObject = GameObject.Children.FirstOrDefault(x => x.Name == "ParkingSpots");

		if (!containerObject.IsValid())
			containerObject = new GameObject(GameObject, true, "ParkingSpots");

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

		float spacing = CalculateSpacing();

		for (int i = 0; i < SpotCount; i++)
		{
			float xPos = i * spacing;

			float centerX = xPos + (SpotWidth * 0.5f * cosAngle) - (SpotLength * 0.5f * sinAngle);
			float centerY = (SpotWidth * 0.5f * sinAngle) + (SpotLength * 0.5f * cosAngle);

			Vector3 position = new Vector3(centerX, centerY, 0);

			GameObject gameObject = SpotPrefab.Clone(new Transform(), containerObject);
			gameObject.LocalPosition = position;
			gameObject.LocalRotation = Rotation.FromYaw(SpotAngle);
			gameObject.NetworkMode = NetworkMode.Object;
			gameObject.Network.SetOrphanedMode(NetworkOrphaned.Host);
		}
	}



	private void RemoveParkingSpots()
	{
		// If we're in play mode, do not remove (Since they're already saved in the scene file)
		if (LoadingScreen.IsVisible || Game.IsPlaying)
			return;

		GameObject containerObject = GameObject.Children.FirstOrDefault(x => x.Name == "ParkingSpots");

		if (!containerObject.IsValid())
			return;

		foreach (var gameObject in containerObject.Children.Where(x => x.IsValid()))
		{
			gameObject.Destroy();
		}
	}



	/// <summary>
	/// Utility button to directly snap the parking lot to the nearest solid ground
	/// </summary>
	[Button("Snap to Ground"), Feature("General"), Order(100)]
	public void SnapToGround()
	{
		SceneTraceResult trace = Scene.Trace.Ray(WorldPosition, WorldPosition + Vector3.Down * 10000.0f).Run();

		if (trace.Distance < 0.1f) // Ignore really close hits, bcs that mean the parking lot is already properly grounded
			return;

		if (!trace.Hit)
			return;

		WorldPosition = trace.HitPosition;
		WorldRotation = Rotation.LookAt(trace.Normal, Vector3.Up) * Rotation.FromPitch(90.0f);
	}



	private float CalculateSpacing()
	{
		float angleRad = SpotAngle.DegreeToRadian();
		float cosAngle = float.Cos(angleRad);

		return SpotWidth / float.Max(cosAngle, SpotAngleThreshold);
	}
}
redsnail.roadtool / Code/Utility/VehicleSpawnResource.cs
Game library
using Sandbox;

namespace RedSnail.RoadTool;

[AssetType(Name = "Vehicle Spawn", Extension = "vspawn", Category = "Road Tool")]
public sealed class VehicleSpawnResource : GameResource
{
	[Property] public GameObject Prefab { get; set; }

	[Property, Range(0.0f, 10.0f)] public float Probability { get; set; } = 1.0f;
	
	
	
	protected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)
	{
		return CreateSimpleAssetTypeIcon("car_rental", _Width, _Height, "#00ccff", "black");
	}
}
redsnail.roadtool / ISplineComponent.cs
Game library
using Sandbox;

namespace RedSnail.RoadTool;

/// <summary>
/// A component that owns an editable <see cref="Sandbox.Spline"/>. Lets the spline editor tool/window drive any of
/// them (roads, rails, …) without being tied to a single component type.
/// </summary>
public interface ISplineComponent : IValid
{
	Spline Spline { get; }
	Transform WorldTransform { get; }
	GameObject GameObject { get; }
}
redsnail.roadtool / RoadComponent/RoadComponent.cs
Game library
using System.Linq;
using Sandbox;

namespace RedSnail.RoadTool;

/// <summary>
/// Represents a road component that can be manipulated within the editor and at runtime.
/// </summary>
[Icon("signpost")]
public partial class RoadComponent : Component, Component.ExecuteInEditor, Component.IHasBounds, ISplineComponent
{
	/// <summary>
	/// Undo/redo and prefab reloads deserialize a brand new <see cref="Sandbox.Spline"/> into this component
	/// instead of mutating the existing one, so the setter has to move our change listener over. Otherwise the
	/// subscription stays on the discarded instance and later edits never mark the mesh dirty again.
	/// </summary>
	[Property, Feature("General"), Hide]
	public Spline Spline
	{
		get;
		set
		{
			field = value;

			SubscribeToSpline();
			UpdateData();
		}
	} = new();

	private Spline m_SubscribedSpline;

	private bool m_DoesRoadMeshNeedRebuild;

	private const string RoadMeshTag = "road_mesh";
	private const string RoadSurfaceTag = "road_surface";
	private const string SidewalkSurfaceTag = "road_sidewalk";
	private const string LineSurfaceTag = "road_lines";

	[Property, Feature("General", Icon = "public", Tint = EditorTint.White), Category("Optimization")] private bool AutoSimplify { get; set { field = value; IsDirty = true; } } = false;
	[Property, Feature("General"), Category("Optimization"), Range(0.1f, 10.0f)] private float StraightThreshold { get; set { field = value; IsDirty = true; } } = 1.0f; // Degrees - how straight before merging
	[Property, Feature("General"), Category("Optimization"), Range(2, 50)] private int MinSegmentsToMerge { get; set { field = value; IsDirty = true; } } = 3; // Minimum consecutive straight segments before merging

	[Property, Feature("General"), Category("Miscellaneous")] public bool UseRotationMinimizingFrames { get; set { field = value; IsDirty = true; } }

	[Property, FeatureEnabled("Bridge", Icon = "architecture", Tint = EditorTint.Blue)]
	private bool HasBridge { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = false;

	/// <summary>
	/// This will prevent the bridge from being rebuilt if any property is edited or if the road component get disable and re-enabled.
	/// Really useful if you plan to edit the mesh with the mapping tool so you don't accidently erase/rebuild the bridge.
	/// </summary>
	[Property(Title = "🔒 Locked"), Feature("Bridge")]
	private bool IsLocked { get; set; } = false;

	/// <summary>
	/// This is your bridge material you wanna use.
	/// I recommend using a tileable texture for better result.
	/// </summary>
	[Property(Title = "Material"), Feature("Bridge"), Group("Texturing"), Order(1)]
	private Material BridgeMaterial { get; set { field = value; m_DoesBridgeNeedRebuild = true; } }

	[Property(Title = "Texture Repeat"), Feature("Bridge"), Group("Texturing"), Order(1), Step(1)]
	private float BridgeTextureRepeat { get; set { field = value.Clamp(10.0f, 10000.0f); m_DoesBridgeNeedRebuild = true; } } = 500.0f;

	[Property(Title = "Border Width"), Feature("Bridge"), Group("Shape"), Order(0), Range(10.0f, 500.0f)]
	private float BridgeBorderWidth { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;

	[Property(Title = "Border Height"), Feature("Bridge"), Group("Shape"), Range(10.0f, 500.0f)]
	private float BridgeBorderHeight { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;

	[Property(Title = "Bottom Depth"), Feature("Bridge"), Group("Shape"), Range(10.0f, 500.0f)]
	private float BridgeBottomDepth { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;

	[Property(Title = "Close Caps"), Feature("Bridge"), Group("Shape")]
	private bool BridgeCloseCaps { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;

	[Property(Title = "Pillars"), Feature("Bridge"), ToggleGroup("Pillars"), Order(2)]
	private bool Pillars { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;

	[Property(Title = "Shape"), Feature("Bridge"), ToggleGroup("Pillars")]
	private BridgePillarShape BridgePillarType { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = BridgePillarShape.Square;

	[Property(Title = "Size"), Feature("Bridge"), ToggleGroup("Pillars"), Range(10.0f, 1000.0f)]
	private float BridgePillarSize { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 200.0f;

	[Property(Title = "Height"), Feature("Bridge"), ToggleGroup("Pillars"), Range(10.0f, 5000.0f)]
	private float BridgePillarHeight { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 600.0f;

	[Property(Title = "Spacing"), Feature("Bridge"), ToggleGroup("Pillars"), Range(100.0f, 10000.0f)]
	private float BridgePillarSpacing { get; set { field = value.Clamp(100.0f, 100000.0f); m_DoesBridgeNeedRebuild = true; } } = 1200.0f;

	[Property(Title = "Inset"), Feature("Bridge"), ToggleGroup("Pillars"), Range(0.0f, 200.0f)]
	private float BridgePillarInset { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 20.0f;

	[Property(Title = "Segments"), Feature("Bridge"), ToggleGroup("Pillars"), Range(3, 24), ShowIf(nameof(BridgePillarType), BridgePillarShape.Cylinder)]
	private int BridgePillarRoundSegments { get; set { field = value.Clamp(3, 64); m_DoesBridgeNeedRebuild = true; } } = 12;

	/// <summary>
	/// Does the pillars follow world up vector or follow the road shape ?
	/// </summary>
	[Property(Title = "Keep Vertical (World Up)"), Feature("Bridge"), ToggleGroup("Pillars")]
	private bool BridgePillarsKeepVertical { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;
	
	[Property, FeatureEnabled("Crosswalks", Icon = "menu", Tint = EditorTint.Pink), Change] private bool HasCrosswalks { get; set; } = false;
	[Property(Title = "Config"), Feature("Crosswalks")] public CrosswalkConfig CrosswalkConfig { get; set { field = value; m_DoesCrosswalksNeedsRebuild = true; } } = CrosswalkConfig.Both;
	[Property(Title = "Decal Definition"), Feature("Crosswalks")] public DecalDefinition CrosswalkDefinition { get; set { field = value; m_DoesCrosswalksNeedsRebuild = true; } }
	[Property(Title = "Decal Size"), Feature("Crosswalks"), Range(0.1f, 10.0f)] private Vector2 CrosswalkSize { get; set { field = value; m_DoesCrosswalksNeedsRebuild = true; } } = Vector2.One;
	
	private bool IsDirty
	{
		get;
		set
		{
			field = value;

			m_DoesRoadMeshNeedRebuild = value;
			m_DoesLamppostsNeedRebuild = value;
		}
	}

	public BBox LocalBounds => Spline.Bounds;



	public RoadComponent()
	{
		Spline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(0, 0, 0) });
		Spline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1000, 0, 0) });
		Spline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1600, 1000, 0) });
	}



	protected override void OnEnabled()
	{
		SubscribeToSpline();

		EnsureLanes(); // migrate/seed the lane layout before anything that reads it (lines mesh, traffic graph) runs

		EnsureRoadMeshExist();
		EnsureSidewalkMeshExist();
		EnsureBridgeMeshExist();

		CreateLines();
		CreateDecals();
		CreateLampposts();
		CreateCrosswalks();
	}



	protected override void OnDisabled()
	{
		UnsubscribeFromSpline();

		RemoveRoadMesh();
		RemoveSidewalkMesh();
		RemoveLines();
		RemoveDecals();
		RemoveLampposts();
		RemoveCrosswalks();
		RemoveBridge();
	}



	/// <summary>
	/// Undo/redo restores the serialized spline data in place, which does not raise
	/// <see cref="Sandbox.Spline.SplineChanged"/>. Rebuilding from here is what makes the mesh follow an undo.
	/// </summary>
	protected override void OnValidate()
	{
		SubscribeToSpline();
		UpdateData();
	}



	protected override void OnUpdate()
	{
		SyncSplineSubscription();

		UpdateRoadMeshes();
		UpdateLines();
		UpdateDecals();
		UpdateLampposts();
		UpdateCrosswalks();
		UpdateBridge();
	}



	private void UpdateRoadMeshes()
	{
		if (!m_DoesRoadMeshNeedRebuild)
			return;

		RebuildRoadMesh();
		RebuildSidewalkMesh();
		RebuildLinesMesh();

		m_DoesRoadMeshNeedRebuild = false;
	}



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

		if (IsRoadLocked)
			return;

		RemoveGeneratedMeshChildren(RoadSurfaceTag);
		BuildRoadMesh();
	}



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

		if (IsSidewalkLocked)
			return;

		RemoveGeneratedMeshChildren(SidewalkSurfaceTag);
		BuildSidewalkMesh();
	}



	private void RemoveRoadMesh()
	{
		if (IsRoadLocked)
			return;

		RemoveGeneratedMeshChildren(RoadSurfaceTag);
	}



	private void RemoveSidewalkMesh()
	{
		if (IsSidewalkLocked)
			return;

		RemoveGeneratedMeshChildren(SidewalkSurfaceTag);
	}



	private void RemoveGeneratedMeshChildren(string _Tag)
	{
		var toRemove = GameObject.Children.Where(child => child.Tags.Has(_Tag)).ToList();

		foreach (var child in toRemove)
			child.Destroy();
	}



	private bool HasGeneratedMeshChildren(string _Tag)
	{
		return GameObject.Children.Any(child => child.Tags.Has(_Tag));
	}



	/// <summary>
	/// Safety net for editor state changes that swap the spline instance without ever touching the
	/// property setter or <see cref="OnValidate"/>. A reference compare per frame is cheap enough to
	/// be worth never silently losing the subscription again.
	/// </summary>
	private void SyncSplineSubscription()
	{
		if (ReferenceEquals(m_SubscribedSpline, Spline))
			return;

		SubscribeToSpline();
		UpdateData();
	}



	private void SubscribeToSpline()
	{
		if (ReferenceEquals(m_SubscribedSpline, Spline))
			return;

		UnsubscribeFromSpline();

		m_SubscribedSpline = Spline;

		if (m_SubscribedSpline is not null)
			m_SubscribedSpline.SplineChanged += UpdateData;
	}



	private void UnsubscribeFromSpline()
	{
		if (m_SubscribedSpline is null)
			return;

		m_SubscribedSpline.SplineChanged -= UpdateData;
		m_SubscribedSpline = null;
	}



	private void UpdateData()
	{
		if (!GameObject.IsValid() || !Scene.IsEditor)
			return;

		IsDirty = true;
	}
}
redsnail.roadtool / RoadComponent/RoadComponent.Lines.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RoadComponent
{
	[Property, FeatureEnabled("Lines", Icon = "show_chart", Tint = EditorTint.Yellow), Change] private bool HasLines { get; set; } = false;
	[Property(Title = "Lines"), Feature("Lines")] public RoadLineDefinition[] LineDefinitions { get; set { field = value; IsDirty = true; } }
	[Property(Title = "Offset"), Feature("Lines"), Range(0.01f, 1.0f)] private float LinesOffset { get; set { field = value; IsDirty = true; } } = 0.1f;
	[Property(Title = "Width"), Feature("Lines"), Range(1.0f, 50.0f)] private float LinesWidth { get; set { field = value; IsDirty = true; } } = 5.0f;
	[Property(Title = "Extra Spacing"), Feature("Lines"), Range(0.0f, 1000.0f)] private float LinesExtraSpacing { get; set { field = value; IsDirty = true; } } = 0.0f;
	[Property(Title = "Texture Repeat"), Feature("Lines")] private float LinesTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 10.0f;



	private void OnHasLinesChanged(bool _OldValue, bool _NewValue)
	{
		IsDirty = true;
	}



	private void CreateLines()
	{
		EnsureLinesMeshExist();
	}



	private void UpdateLines()
	{
	}



	private void RemoveLines()
	{
		RemoveGeneratedMeshChildren(LineSurfaceTag);
	}



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

		if (HasGeneratedMeshChildren(LineSurfaceTag))
			return;

		BuildLinesMesh();
	}



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

		RemoveGeneratedMeshChildren(LineSurfaceTag);
		BuildLinesMesh();
	}



	private void BuildLinesMesh()
	{
		if (!HasLines || LineDefinitions == null || LineDefinitions.Length == 0)
			return;

		GetSplineFrameData(out var frames, out var segmentsToKeep);

		int finalSegmentCount = segmentsToKeep.Count - 1;

		if (finalSegmentCount <= 0)
			return;

		float roadWidth = RoadWidth + LinesExtraSpacing;
		float lineSpacing = roadWidth / (LineDefinitions.Length + 1);

		var polygonMeshes = new PolygonMesh[LineDefinitions.Length];
		for (int i = 0; i < LineDefinitions.Length; i++)
			polygonMeshes[i] = new PolygonMesh();

		float[] lineDistances = new float[LineDefinitions.Length];

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

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

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

			Vector3 right0 = f0.Rotation.Right;

			for (int line = 0; line < LineDefinitions.Length; line++)
			{
				float offsetFromCenter = ((line + 1) * lineSpacing) - (roadWidth * 0.5f);

				Vector3 center0 =
					p0 +
					f0.Rotation.Right * offsetFromCenter +
					f0.Rotation.Up * LinesOffset;

				Vector3 center1 =
					p1 +
					f1.Rotation.Right * offsetFromCenter +
					f1.Rotation.Up * LinesOffset;

				float segmentLength = Vector3.DistanceBetween(center0, center1);
				Vector3 dir = (center1 - center0).Normal;

				float remaining = segmentLength;
				Vector3 curCenter = center0;

				float dashSpacing = LineDefinitions[line]?.DashSpacing ?? 0.0f;
				float dashFillRatio = LineDefinitions[line]?.DashFillRatio ?? 1.0f;
				float dashLength = dashSpacing * dashFillRatio;
				float halfWidth = LinesWidth * 0.5f;

				var polygonMesh = polygonMeshes[line];
				var material = LineDefinitions[line]?.Material ?? Material.Load("materials/default.vmat");

				while (remaining > 0.001f)
				{
					float linePos = lineDistances[line];
					float cyclePos = dashSpacing > 0 ? linePos % dashSpacing : 0;

					if (cyclePos < 0.0001f)
						cyclePos = 0.0f;

					if (dashSpacing > 0 && dashSpacing - cyclePos < 0.0001f)
						cyclePos = dashSpacing;

					bool inDash = dashSpacing <= 0 || cyclePos <= dashLength - 0.0001f;

					float step;

					if (dashSpacing <= 0)
						step = remaining;
					else if (inDash)
						step = dashLength - cyclePos;
					else
						step = dashSpacing - cyclePos;

					step = Math.Max(step, 0.01f);
					step = Math.Min(step, remaining);

					Vector3 nextCenter = curCenter + dir * step;

					if (inDash)
					{
						Vector3 l0 = curCenter - right0 * halfWidth;
						Vector3 r0 = curCenter + right0 * halfWidth;
						Vector3 l1 = nextCenter - right0 * halfWidth;
						Vector3 r1 = nextCenter + right0 * halfWidth;

						float v0 = linePos / LinesTextureRepeat;
						float v1 = (linePos + step) / LinesTextureRepeat;

						var verts = polygonMesh.AddVertices(l0, r0, r1, l1);
						MeshUtility.AddTexturedQuad(polygonMesh, material, verts[0], verts[1], verts[2], verts[3],
							new Vector2(0, v0), new Vector2(1, v0),
							new Vector2(1, v1), new Vector2(0, v1));
					}

					lineDistances[line] += step;
					curCenter = nextCenter;
					remaining -= step;
				}
			}
		}

		for (int line = 0; line < LineDefinitions.Length; line++)
		{
			var child = new GameObject(GameObject, true, $"Line_{line}");
			child.Tags.Add(LineSurfaceTag);

			var meshComponent = child.AddComponent<MeshComponent>();
			meshComponent.Mesh = polygonMeshes[line];
			meshComponent.Collision = MeshComponent.CollisionType.None;
			meshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;
			meshComponent.SmoothingAngle = 40.0f;
			meshComponent.Static = true;
		}
	}
}
redsnail.roadtool / Utility/MeshUtility.cs
Game library
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool;

public static class MeshUtility
{
	public static HalfEdgeMesh.VertexHandle GetOrAddVertex(PolygonMesh _Mesh, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, Vector3 _Pos)
	{
		if (!_Cache.TryGetValue(_Pos, out var handle))
		{
			handle = _Mesh.AddVertices(_Pos)[0];
			_Cache[_Pos] = handle;
		}

		return handle;
	}



	public static void AddTexturedQuad(PolygonMesh _Mesh, Material _Material,
		HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,
		HalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D,
		Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)
	{
		if (HasDuplicateVertex(_A, _B, _C, _D))
			return;

		var face = _Mesh.AddFace(_A, _B, _C, _D);

		if (!face.IsValid)
			return;

		_Mesh.SetFaceMaterial(face, _Material);
		_Mesh.SetFaceTextureCoords(face, new List<Vector2> { _UvA, _UvB, _UvC, _UvD });
	}



	public static void AddTexturedTriangle(PolygonMesh _Mesh, Material _Material,
		HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,
		HalfEdgeMesh.VertexHandle _C,
		Vector2 _UvA, Vector2 _UvB, Vector2 _UvC)
	{
		if (HasDuplicateVertex(_A, _B, _C))
			return;

		var face = _Mesh.AddFace(_A, _B, _C);

		if (!face.IsValid)
			return;

		_Mesh.SetFaceMaterial(face, _Material);
		_Mesh.SetFaceTextureCoords(face, new List<Vector2> { _UvA, _UvB, _UvC });
	}



	private static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C)
	{
		return _A.Equals(_B) || _A.Equals(_C) || _B.Equals(_C);
	}



	private static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D)
	{
		return _A.Equals(_B) || _A.Equals(_C) || _A.Equals(_D) ||
			_B.Equals(_C) || _B.Equals(_D) ||
			_C.Equals(_D);
	}
}
redsnail.roadtool / Code/RailComponent/RailComponent.Fishplates.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RailComponent
{
	[Property, FeatureEnabled("Fishplates", Icon = "link", Tint = EditorTint.Green)] private bool HasFishplates { get; set { field = value; IsDirty = true; } } = true;
	[Property(Title = "Material"), Feature("Fishplates")] private Material FishplateMaterial { get; set { field = value; IsDirty = true; } }

	/// <summary>Distance along the track between joints. Real rail comes in long sections, so this is much larger than the sleeper spacing.</summary>
	[Property(Title = "Spacing"), Feature("Fishplates"), Range(100.0f, 5000.0f)] private float FishplateSpacing { get; set { field = value; IsDirty = true; } } = 1200.0f;

	/// <summary>Plate length along the track (it straddles the joint between two rail sections).</summary>
	[Property(Title = "Length"), Feature("Fishplates"), Range(10.0f, 500.0f)] private float FishplateLength { get; set { field = value; IsDirty = true; } } = 80.0f;

	/// <summary>Plate height — sized to sit against the rail web, between the foot and the head.</summary>
	[Property(Title = "Height"), Feature("Fishplates"), Range(2.0f, 100.0f)] private float FishplateHeight { get; set { field = value; IsDirty = true; } } = 14.0f;

	/// <summary>How far the plate stands out from the rail web.</summary>
	[Property(Title = "Thickness"), Feature("Fishplates"), Range(1.0f, 50.0f)] private float FishplateThickness { get; set { field = value; IsDirty = true; } } = 6.0f;

	/// <summary>Radius of the hexagonal bolt heads.</summary>
	[Property(Title = "Bolt Radius"), Feature("Fishplates"), Range(0.5f, 20.0f)] private float FishplateBoltRadius { get; set { field = value; IsDirty = true; } } = 3.5f;

	/// <summary>How far the bolt heads stand out from the plate.</summary>
	[Property(Title = "Bolt Depth"), Feature("Fishplates"), Range(0.5f, 20.0f)] private float FishplateBoltDepth { get; set { field = value; IsDirty = true; } } = 3.0f;

	[Property(Title = "Texture Repeat"), Feature("Fishplates")] private float FishplateTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 100.0f;



	private void BuildFishplates(Transform[] _Frames)
	{
		float length = Spline.Length;

		if (length <= 0.0f)
			return;

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

		int count = (int)MathF.Floor(length / spacing);

		if (count < 1)
			return; // track is shorter than one rail section — no joints

		var material = FishplateMaterial ?? Material.Load("materials/dev/reflectivity_50.vmat");
		var polygonMesh = new PolygonMesh();

		float textureRepeat = Math.Max(1.0f, FishplateTextureRepeat);
		float baseHeight = HasSleepers ? SleeperHeight : 0.0f;
		float halfGauge = RailGauge * 0.5f;

		int placed = 0;

		for (int s = 1; s <= count; s++)
		{
			float distance = s * spacing;

			if (distance >= length)
				break; // don't drop a joint right on the end of the track

			var frame = SampleFrameAtDistance(_Frames, distance, length);

			// One plate on the OUTER side of each rail (the outward sign flips the side for the left rail).
			AddFishplate(polygonMesh, material, frame, halfGauge, 1.0f, baseHeight, textureRepeat);
			AddFishplate(polygonMesh, material, frame, halfGauge, -1.0f, baseHeight, textureRepeat);

			placed++;
		}

		if (placed == 0)
			return;

		CreateFishplateChild("Fishplates", polygonMesh);
	}



	/// <summary>Adds one bolted joint plate against the outer face of a single rail's web.</summary>
	private void AddFishplate(PolygonMesh _Mesh, Material _Material, Transform _Frame, float _HalfGauge, float _OutwardSign, float _BaseHeight, float _TextureRepeat)
	{
		Vector3 origin = _Frame.Position;
		Vector3 right = _Frame.Rotation.Right;
		Vector3 forward = _Frame.Rotation.Forward;
		Vector3 up = _Frame.Rotation.Up;

		Vector3 outward = right * _OutwardSign; // points away from the track centre, toward the outer side of this rail

		float halfWeb = RailWidth * RailWebScale * 0.5f;
		float webMid = _BaseHeight + RailHeight * 0.5f;

		float embed = MathF.Min(1.0f, halfWeb * 0.5f); // bite into the web so the hidden inner face never z-fights it
		float innerDist = halfWeb - embed;
		float outerDist = halfWeb + FishplateThickness;
		float centreDist = (innerDist + outerDist) * 0.5f;
		float halfOut = (outerDist - innerDist) * 0.5f;

		float halfLength = FishplateLength * 0.5f;
		float halfHeight = FishplateHeight * 0.5f;

		Vector3 railCentre = origin + right * (_HalfGauge * _OutwardSign);
		Vector3 plateCentre = railCentre + outward * centreDist + up * webMid;

		AddBox(_Mesh, _Material, plateCentre, outward, forward, up, halfOut, halfLength, halfHeight, _TextureRepeat);

		// Two hex bolts standing out of the plate's outer face, spread along the joint.
		Vector3 outerFace = plateCentre + outward * halfOut;
		float boltOffset = FishplateLength * 0.25f;

		AddHexBolt(_Mesh, _Material, outerFace + forward * boltOffset, outward, forward, FishplateBoltRadius, FishplateBoltDepth, _TextureRepeat);
		AddHexBolt(_Mesh, _Material, outerFace - forward * boltOffset, outward, forward, FishplateBoltRadius, FishplateBoltDepth, _TextureRepeat);
	}



	/// <summary>A closed box built from three frame axes, textured with a uniform box (cube) projection.</summary>
	private static void AddBox(PolygonMesh _Mesh, Material _Material, Vector3 _Centre, Vector3 _AxisR, Vector3 _AxisF, Vector3 _AxisU, float _HalfR, float _HalfF, float _HalfU, float _TextureRepeat)
	{
		// The faces below wind outward only for a right-handed basis. The outward axis flips for the left rail, which
		// would make it left-handed and flip every face to a backface — mirror one axis to keep it right-handed (the
		// box is symmetric, so this changes nothing but the winding).
		if (Vector3.Dot(Vector3.Cross(_AxisR, _AxisF), _AxisU) < 0.0f)
			_AxisF = -_AxisF;

		Vector3 Corner(float r, float f, float u) => _Centre + _AxisR * r + _AxisF * f + _AxisU * u;

		var c000 = Corner(-_HalfR, -_HalfF, -_HalfU);
		var c100 = Corner(_HalfR, -_HalfF, -_HalfU);
		var c110 = Corner(_HalfR, _HalfF, -_HalfU);
		var c010 = Corner(-_HalfR, _HalfF, -_HalfU);
		var c001 = Corner(-_HalfR, -_HalfF, _HalfU);
		var c101 = Corner(_HalfR, -_HalfF, _HalfU);
		var c111 = Corner(_HalfR, _HalfF, _HalfU);
		var c011 = Corner(-_HalfR, _HalfF, _HalfU);

		float r0 = -_HalfR / _TextureRepeat, r1 = _HalfR / _TextureRepeat;
		float f0 = -_HalfF / _TextureRepeat, f1 = _HalfF / _TextureRepeat;
		float u0 = -_HalfU / _TextureRepeat, u1 = _HalfU / _TextureRepeat;

		AddQuad(_Mesh, _Material, c100, c110, c111, c101, new Vector2(f0, u0), new Vector2(f1, u0), new Vector2(f1, u1), new Vector2(f0, u1)); // +R
		AddQuad(_Mesh, _Material, c010, c000, c001, c011, new Vector2(f1, u0), new Vector2(f0, u0), new Vector2(f0, u1), new Vector2(f1, u1)); // -R
		AddQuad(_Mesh, _Material, c110, c010, c011, c111, new Vector2(r1, u0), new Vector2(r0, u0), new Vector2(r0, u1), new Vector2(r1, u1)); // +F
		AddQuad(_Mesh, _Material, c000, c100, c101, c001, new Vector2(r0, u0), new Vector2(r1, u0), new Vector2(r1, u1), new Vector2(r0, u1)); // -F
		AddQuad(_Mesh, _Material, c001, c101, c111, c011, new Vector2(r0, f0), new Vector2(r1, f0), new Vector2(r1, f1), new Vector2(r0, f1)); // +U
		AddQuad(_Mesh, _Material, c000, c010, c110, c100, new Vector2(r0, f0), new Vector2(r0, f1), new Vector2(r1, f1), new Vector2(r1, f0)); // -U
	}



	/// <summary>A hexagonal bolt head — a 6-sided prism standing out of the plate along <paramref name="_AxisOut"/>, capped on the outer end.</summary>
	private static void AddHexBolt(PolygonMesh _Mesh, Material _Material, Vector3 _Base, Vector3 _AxisOut, Vector3 _Forward, float _Radius, float _Depth, float _TextureRepeat)
	{
		const int sides = 6;

		// t2 completes a right-handed (out, t1, t2) basis so the winding below stays outward-facing on either rail.
		Vector3 t1 = _Forward;
		Vector3 t2 = Vector3.Cross(_AxisOut, t1);

		var baseRing = new Vector3[sides];
		var topRing = new Vector3[sides];

		for (int k = 0; k < sides; k++)
		{
			float angle = MathF.PI * 2.0f * k / sides;
			Vector3 radial = t1 * MathF.Cos(angle) + t2 * MathF.Sin(angle);

			baseRing[k] = _Base + radial * _Radius;
			topRing[k] = baseRing[k] + _AxisOut * _Depth;
		}

		float edge = _Radius; // a regular hexagon's side length equals its radius
		float v1 = _Depth / _TextureRepeat;

		for (int k = 0; k < sides; k++)
		{
			int next = (k + 1) % sides;
			float u0 = edge * k / _TextureRepeat;
			float u1 = edge * (k + 1) / _TextureRepeat;

			AddQuad(_Mesh, _Material, baseRing[k], baseRing[next], topRing[next], topRing[k],
				new Vector2(u0, 0.0f), new Vector2(u1, 0.0f), new Vector2(u1, v1), new Vector2(u0, v1));
		}

		Vector2 CapUv(Vector3 _P) => new Vector2(Vector3.Dot(_P - _Base, t1), Vector3.Dot(_P - _Base, t2)) / _TextureRepeat;

		for (int k = 1; k < sides - 1; k++)
		{
			AddTri(_Mesh, _Material, topRing[0], topRing[k], topRing[k + 1],
				CapUv(topRing[0]), CapUv(topRing[k]), CapUv(topRing[k + 1]));
		}
	}



	// Own vertices per face so every plate edge and bolt facet stays hard.
	private static void AddQuad(PolygonMesh _Mesh, Material _Material, Vector3 _A, Vector3 _B, Vector3 _C, Vector3 _D, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)
	{
		var v = _Mesh.AddVertices(_A, _B, _C, _D);

		MeshUtility.AddTexturedQuad(_Mesh, _Material, v[0], v[1], v[2], v[3], _UvA, _UvB, _UvC, _UvD);
	}



	private static void AddTri(PolygonMesh _Mesh, Material _Material, Vector3 _A, Vector3 _B, Vector3 _C, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC)
	{
		var v = _Mesh.AddVertices(_A, _B, _C);

		MeshUtility.AddTexturedTriangle(_Mesh, _Material, v[0], v[1], v[2], _UvA, _UvB, _UvC);
	}



	private void CreateFishplateChild(string _Name, PolygonMesh _PolygonMesh)
	{
		var child = new GameObject(GameObject, true, _Name);
		child.Tags.Add(RailMeshTag);
		child.Tags.Add(FishplateSurfaceTag);

		var meshComponent = child.AddComponent<MeshComponent>();
		meshComponent.Mesh = _PolygonMesh;
	}
}
redsnail.roadtool / Code/RailComponent/RailComponent.Utility.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RailComponent
{
	private static Transform[] CalculateTangentFramesUsingUpDir(Spline _Spline, int _FrameCount)
	{
		var frames = new Transform[_FrameCount];
		float totalSplineLength = _Spline.Length;

		var sample = _Spline.SampleAtDistance(0.0f);
		sample.Up = Vector3.Up;

		// Fall back to a different up if the tangent runs (nearly) straight up.
		if (MathF.Abs(Vector3.Dot(sample.Tangent, sample.Up)) > 0.999f)
			sample.Up = Vector3.Right;

		for (int i = 0; i < _FrameCount; i++)
		{
			float t = _FrameCount > 1 ? (float)i / (_FrameCount - 1) : 0.0f;
			float distance = t * totalSplineLength;

			sample = _Spline.SampleAtDistance(distance);

			var up = Rotation.FromAxis(sample.Tangent, sample.Roll) * sample.Up;
			Rotation rotation = Rotation.LookAt(sample.Tangent, up);

			frames[i] = new Transform(sample.Position, rotation, sample.Scale);
		}

		return frames;
	}



	private static Transform[] CalculateRotationMinimizingTangentFrames(Spline _Spline, int _FrameCount)
	{
		var frames = new Transform[_FrameCount];
		float totalSplineLength = _Spline.Length;

		var previousSample = _Spline.SampleAtDistance(0.0f);
		Vector3 up = Vector3.Up;

		if (MathF.Abs(Vector3.Dot(previousSample.Tangent, up)) > 0.999f)
			up = Vector3.Right;

		up = Rotation.FromAxis(previousSample.Tangent, previousSample.Roll) * up;

		frames[0] = new Transform(previousSample.Position, Rotation.LookAt(previousSample.Tangent, up), previousSample.Scale);

		for (int i = 1; i < _FrameCount; i++)
		{
			float t = _FrameCount > 1 ? (float)i / (_FrameCount - 1) : 0.0f;
			float distance = t * totalSplineLength;

			var sample = _Spline.SampleAtDistance(distance);

			// Parallel-transport the up vector so the profile doesn't twist through 3D curves.
			up = GetRotationMinimizingNormal(previousSample.Position, previousSample.Tangent, up, sample.Position, sample.Tangent);

			float deltaRoll = sample.Roll - previousSample.Roll;
			up = Rotation.FromAxis(sample.Tangent, deltaRoll) * up;

			Rotation rotation = Rotation.LookAt(sample.Tangent, up);
			frames[i] = new Transform(sample.Position, rotation, sample.Scale);

			previousSample = sample;
		}

		return frames;
	}



	private static Vector3 GetRotationMinimizingNormal(Vector3 _PosA, Vector3 _TangentA, Vector3 _NormalA, Vector3 _PosB, Vector3 _TangentB)
	{
		// Source: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf
		Vector3 v1 = _PosB - _PosA;

		float v1DotV1Half = Vector3.Dot(v1, v1) / 2.0f;

		if (v1DotV1Half <= 0.0001f)
			return _NormalA;

		float r1 = Vector3.Dot(v1, _NormalA) / v1DotV1Half;
		float r2 = Vector3.Dot(v1, _TangentA) / v1DotV1Half;

		Vector3 nL = _NormalA - r1 * v1;
		Vector3 tL = _TangentA - r2 * v1;
		Vector3 v2 = _TangentB - tL;

		float r3 = Vector3.Dot(v2, nL) / Vector3.Dot(v2, v2);

		return (nL - 2.0f * r3 * v2).Normal;
	}
}
redsnail.roadtool / Code/RoadIntersectionComponent/RoadIntersectionComponent.Sidewalk.cs
Game library
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;
}
Debug: View Raw JSON Response
{
    "TotalCount": 111,
    "Files": [
        {
            "Ident": "redsnail.roadtool",
            "Path": "Editor/IntersectionTool.cs",
            "FileName": "IntersectionTool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\nusing Editor;\r\n\r\nnamespace RedSnail.RoadTool.Editor;\r\n\r\n/// <summary>\r\n/// Create and manage road and road intersection.\r\n/// </summary>\r\n[Title(\"Create Road/Intersection\")]\r\n[Icon(\"roundabout_left\")]\r\n[Alias(\"intersection\")]\r\n[Group(\"1\")]\r\n[Order(0)]\r\npublic class IntersectionTool : EditorTool\r\n{\r\n\tpublic override void OnEnabled()\r\n\t{\r\n\r\n\t}\r\n\r\n\tpublic override Widget CreateToolSidebar()\r\n\t{\r\n\t\tToolSidebarWidget sidebar = new ToolSidebarWidget();\r\n\t\tsidebar.AddTitle(\"Intersection\", \"roundabout_left\");\r\n\r\n\t\tLayout group = sidebar.AddGroup(\"Create\");\r\n\t\tLayout row = Layout.Row();\r\n\r\n\t\tIconButton road = sidebar.CreateButton(\"Create Road\", \"route\", null, CreateRoad, true, row);\r\n\t\tIconButton inter = sidebar.CreateButton(\"Create Intersection\", \"roundabout_left\", null, CreateIntersection, true, row);\r\n\r\n\t\trow.Spacing = 5;\r\n\t\trow.AddStretchCell();\r\n\r\n\t\tgroup.Add(row);\r\n\r\n\t\tsidebar.Layout.Add(group);\r\n\t\tsidebar.Layout.AddStretchCell();\r\n\t\treturn sidebar;\r\n\t}\r\n\r\n\tprivate static void CreateRoad()\r\n\t{\r\n\t\tGameObject go = SceneEditorSession.Active.Scene.CreateObject();\r\n\t\tgo.Name = \"Road\";\r\n\t\tgo.AddComponent<RoadComponent>();\r\n\t}\r\n\r\n\tprivate static void CreateIntersection()\r\n\t{\r\n\t\tGameObject go = SceneEditorSession.Active.Scene.CreateObject();\r\n\t\tgo.Name = \"Road Intersection\";\r\n\t\tgo.AddComponent<RoadIntersectionComponent>();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "RoadIntersectionComponent/RoadIntersectionComponent.Sidewalk.cs",
            "FileName": "RoadIntersectionComponent.Sidewalk.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// <summary>\n/// How much of a junction reaches the pedestrian graph. Geometry is never affected \u2014 the pavement mesh builds\n/// the same either way; this only decides what pedestrians are allowed to route over.\n///\n/// Ordered most-to-least deliberately, so the zero value is the harmless one: a component that somehow arrives\n/// without this field set behaves like every other junction rather than silently vanishing from the graph.\n/// </summary>\npublic enum SidewalkGraphMode\n{\n\t/// <summary>Pavement round the corners and a crossing at every arm.</summary>\n\tAll,\n\n\t/// <summary>\n\t/// Corners only. The pavement stays joined all the way round and connects to every road that meets here,\n\t/// but there's nothing to step off the kerb onto \u2014 pedestrians walk round the junction instead of over it.\n\t/// </summary>\n\tNoCrossing,\n\n\t/// <summary>\n\t/// Nothing at all. The roads that meet here keep their own pavements; they just stop being connected\n\t/// THROUGH this junction, so expect their ends to show as dead ends.\n\t/// </summary>\n\tNone\n}\n\n\n\npublic partial class RoadIntersectionComponent\n{\n\t/// <summary>\n\t/// What this junction contributes to the pedestrian graph \u2014 the pedestrian counterpart of\n\t/// <see cref=\"ExcludeTraffic\"/>.\n\t///\n\t/// <see cref=\"SidewalkGraphMode.None\"/> for somewhere nobody should be walking round at all: a slip road, a\n\t/// service yard, a junction whose pavement exists only because the mesh needs an edge.\n\t/// <see cref=\"SidewalkGraphMode.NoCrossing\"/> for somewhere they may walk past but not across \u2014 a forecourt\n\t/// or car park entrance, where the kerb should stay continuous and stepping into the vehicle route is the\n\t/// thing you're trying to prevent.\n\t/// </summary>\n\t[Property, Feature(\"General\"), Category(\"Sidewalk\"), Order(3)] public SidewalkGraphMode SidewalkGraph { get; set; } = SidewalkGraphMode.All;\n\n\n\n\t/// <summary>\n\t/// The walking line around this junction's pavement, in world space \u2014 a closed loop running down the\n\t/// middle of the sidewalk slab.\n\t///\n\t/// Built from the junction's OWN outline, which is the whole point: a rectangular intersection's pavement\n\t/// runs along its edges and turns at its corners, and approximating that with an arc around the centre\n\t/// bows the path out into the road at the middle of each side and cuts the corners off. A circle really is\n\t/// an arc, so it gets one.\n\t///\n\t/// The loop runs all the way round, arm mouths included \u2014 whoever consumes it is expected to split it at\n\t/// the kerbs, because where the pavement is interrupted is the same question as where the crossings go.\n\t/// </summary>\n\tpublic List<Vector3> GetSidewalkOutline(float _Spacing)\n\t{\n\t\tvar points = new List<Vector3>();\n\n\t\tif (!HasSidewalks)\n\t\t\treturn points;\n\n\t\tfloat spacing = Math.Max(1.0f, _Spacing);\n\n\t\t// Centre of the slab, so it sits where someone would actually walk rather than on either kerb.\n\t\tfloat outset = SidewalkWidth * 0.5f;\n\t\tVector3 lift = Vector3.Up * SidewalkHeight;\n\n\t\tif (Shape == IntersectionShape.Circle)\n\t\t{\n\t\t\tfloat radius = Radius + outset;\n\t\t\tint steps = Math.Max(8, (int)MathF.Ceiling(MathF.Tau * radius / spacing));\n\n\t\t\tfor (int i = 0; i < steps; i++)\n\t\t\t{\n\t\t\t\tfloat angle = MathF.Tau * i / steps;\n\n\t\t\t\tpoints.Add(new Vector3(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius, 0.0f) + lift);\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Written with the same right/forward vectors BuildRectangleRoad uses, rather than as raw x/y\n\t\t\t// components. Width runs along Right and Length along Forward, which in s&box axes is Y and X \u2014\n\t\t\t// spelling that out by hand gets them the wrong way round, and a junction outline rotated 90\u00b0\n\t\t\t// looks almost plausible until nothing connects to it.\n\t\t\tVector3 right = Vector3.Right;\n\t\t\tVector3 forward = Vector3.Forward;\n\n\t\t\tfloat hw = Width * 0.5f + outset;\n\t\t\tfloat hl = Length * 0.5f + outset;\n\n\t\t\t// Round the rectangle in order, so the loop has a consistent winding for anything that walks it.\n\t\t\tVector3[] corners =\n\t\t\t[\n\t\t\t\t-right * hw - forward * hl,\n\t\t\t\t right * hw - forward * hl,\n\t\t\t\t right * hw + forward * hl,\n\t\t\t\t-right * hw + forward * hl,\n\t\t\t];\n\n\t\t\tfor (int i = 0; i < corners.Length; i++)\n\t\t\t{\n\t\t\t\tVector3 from = corners[i];\n\t\t\t\tVector3 to = corners[(i + 1) % corners.Length];\n\n\t\t\t\tint steps = Math.Max(1, (int)MathF.Ceiling(Vector3.DistanceBetween(from, to) / spacing));\n\n\t\t\t\t// Last point of each edge is skipped \u2014 it's the first of the next one, and a closed loop\n\t\t\t\t// mustn't repeat its corners.\n\t\t\t\tfor (int s = 0; s < steps; s++)\n\t\t\t\t\tpoints.Add(Vector3.Lerp(from, to, (float)s / steps) + lift);\n\t\t\t}\n\t\t}\n\n\t\tfor (int i = 0; i < points.Count; i++)\n\t\t\tpoints[i] = WorldTransform.PointToWorld(points[i]);\n\n\t\treturn points;\n\t}\n\n\n\n\t/// <summary>Whether this junction has pavement to walk on at all.</summary>\n\tpublic bool HasSidewalks => SidewalkWidth > 0.0f;\n}\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "RoadIntersectionComponent/RoadIntersectionComponent.Terrain.cs",
            "FileName": "RoadIntersectionComponent.Terrain.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadIntersectionComponent\r\n{\r\n\t[Property, Feature(\"Terrain\", Icon = \"landscape\", Tint = EditorTint.Green), Hide]\r\n\tprivate Terrain TerrainTarget { get; set; }\r\n\r\n\t[Property, Feature(\"Terrain\"), Range(0f, 2000f)]\r\n\tpublic float TerrainFalloffRadius { get; set; } = 500f;\r\n\r\n\t[Property, Feature(\"Terrain\"), Range(-10f, 10f)]\r\n\tpublic float TerrainHeightOffset { get; set; } = 0f;\r\n\r\n\t[Property, Feature(\"Terrain\"), Range(0f, 100f)]\r\n\tpublic float TerrainRoadInset { get; set; } = 10f;\r\n\r\n\t[Property, Feature(\"Terrain\"), Group(\"Texture\"), Range(100f, 1000f)]\r\n\tpublic float TerrainEdgeRadius { get; set; } = 500f;\r\n\r\n\t[Property, Feature(\"Terrain\"), Group(\"Texture\")]\r\n\tpublic TerrainTextureLayer TerrainTargetLayer { get; set; } = TerrainTextureLayer.Overlay;\r\n\r\n\t[Property, Feature(\"Terrain\"), Group(\"Texture\"), Range(0f, 1f)]\r\n\tpublic float TerrainTextureNoise { get; set; } = 0.2f;\r\n\r\n\t[Property, Feature(\"Terrain\"), Group(\"Texture\")]\r\n\tpublic TerrainMaterial[] TerrainEdgeMaterials { get; set; } = Array.Empty<TerrainMaterial>();\r\n\r\n\t[Property, Feature(\"Terrain\"), Group(\"Texture\")]\r\n\tpublic Gradient TerrainEdgeBlendGradient = new Gradient(\r\n\t\tnew Gradient.ColorFrame(0, Color.White),\r\n\t\tnew Gradient.ColorFrame(1, Color.White.WithAlpha(0f))\r\n\t);\r\n\r\n\r\n\r\n\t[Button(\"Apply to the Ground\"), Feature(\"Terrain\")]\r\n\tprivate void ApplyTerrainToGround()\r\n\t{\r\n\t\tif (!Scene.IsEditor)\r\n\t\t\treturn;\r\n\r\n\t\tAdaptTerrainToIntersection();\r\n\t}\r\n\r\n\r\n\r\n\tpublic void AdaptTerrainToIntersection()\r\n\t{\r\n\t\tif (!TerrainTarget.IsValid())\r\n\t\t{\r\n\t\t\t// Always take the closest terrain\r\n\t\t\tTerrainTarget = Scene.GetAllComponents<Terrain>().OrderBy(x => x.WorldPosition.DistanceSquared(WorldPosition)).FirstOrDefault();\r\n\t\t}\r\n\r\n\t\tif (!TerrainTarget.IsValid())\r\n\t\t{\r\n\t\t\tLog.Warning(\"RoadTool: No Terrain found in scene.\");\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar storage = TerrainTarget.Storage;\r\n\t\tif (storage == null || storage.HeightMap == null) return;\r\n\r\n\t\t// 1. Setup Parameters \r\n\t\tint resolution = storage.Resolution;\r\n\t\tfloat terrainSize = storage.TerrainSize;\r\n\t\tfloat terrainMaxHeight = storage.TerrainHeight;\r\n\t\tfloat halfSize = terrainSize * 0.5f;\r\n\r\n\t\t// Calculate bounds including falloff\r\n\t\tfloat boundSize = (Shape == IntersectionShape.Rectangle ? Math.Max(Width, Length) * 0.5f : Radius) + TerrainFalloffRadius;\r\n\t\tBBox worldBounds = new BBox(WorldPosition - new Vector3(boundSize), WorldPosition + new Vector3(boundSize));\r\n\r\n\t\tvar heightMap = storage.HeightMap;\r\n\r\n\t\t// Capture initial state for Undo\r\n\t\tbool hasModified = false;\r\n\r\n\t\t// Initialize buffers for height calculation \r\n\t\tvar updatedHeights = new float[heightMap.Length];\r\n\t\tvar bestDistance = new float[heightMap.Length];\r\n\r\n\t\tfor (int i = 0; i < heightMap.Length; i++)\r\n\t\t{\r\n\t\t\t// Decode: Map [0..1] ushort to [0 .. MaxHeight] to match RoadComponent\r\n\t\t\tupdatedHeights[i] = (heightMap[i] / (float)ushort.MaxValue) * terrainMaxHeight;\r\n\t\t\tbestDistance[i] = float.MaxValue;\r\n\t\t}\r\n\r\n\t\tBuildRectangleExitCorridors();\r\n\r\n\t\t// 2. Grid Traversal\r\n\t\tfor (int ix = 0; ix < resolution; ix++)\r\n\t\t{\r\n\t\t\tfor (int iy = 0; iy < resolution; iy++)\r\n\t\t\t{\r\n\t\t\t\t// 1. Adaptive coordinate detection (Center vs Corner) matching RoadComponent\r\n\t\t\t\tfloat nodeLocalX_corner = (ix / (float)(resolution - 1)) * terrainSize;\r\n\t\t\t\tfloat nodeLocalY_corner = (iy / (float)(resolution - 1)) * terrainSize;\r\n\r\n\t\t\t\tfloat nodeLocalX = nodeLocalX_corner;\r\n\t\t\t\tfloat nodeLocalY = nodeLocalY_corner;\r\n\r\n\t\t\t\t// Check if the intersection is in the centered range\r\n\t\t\t\tvar checkPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);\r\n\t\t\t\tif (checkPos.x < 0f || checkPos.x > terrainSize || checkPos.y < 0f || checkPos.y > terrainSize)\r\n\t\t\t\t{\r\n\t\t\t\t\tnodeLocalX = nodeLocalX_corner - halfSize;\r\n\t\t\t\t\tnodeLocalY = nodeLocalY_corner - halfSize;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tVector3 pixelWorldPos = TerrainTarget.Transform.World.PointToWorld(new Vector3(nodeLocalX, nodeLocalY, 0));\r\n\r\n\t\t\t\tif (!worldBounds.Contains(pixelWorldPos)) continue;\r\n\r\n\t\t\t\tint index = iy * resolution + ix;\r\n\r\n\t\t\t\t// 2. Distance to intersection shape \r\n\t\t\t\tVector3 relativePos = WorldTransform.PointToLocal(pixelWorldPos);\r\n\t\t\t\tfloat distance = GetDistanceToIntersectionShape(relativePos.WithZ(0));\r\n\r\n\t\t\t\tif (distance > TerrainFalloffRadius) continue;\r\n\r\n\t\t\t\t// 3. Target height matching RoadComponent (0 to MaxHeight range)\r\n\t\t\t\tVector3 intersectionLocalPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);\r\n\t\t\t\tfloat roadSurfaceHeight = Math.Clamp(intersectionLocalPos.z + TerrainHeightOffset, 0f, terrainMaxHeight);\r\n\t\t\t\tfloat roadInsetHeight = Math.Clamp(roadSurfaceHeight - TerrainRoadInset, 0f, terrainMaxHeight);\r\n\t\t\t\tfloat currentPixelHeight = (heightMap[index] / (float)ushort.MaxValue) * terrainMaxHeight;\r\n\r\n\t\t\t\tfloat candidateHeight;\r\n\t\t\t\tif (distance <= 0) // Inside the intersection \u2014 sink terrain below road to prevent Z-fighting\r\n\t\t\t\t{\r\n\t\t\t\t\tcandidateHeight = roadInsetHeight;\r\n\t\t\t\t}\r\n\t\t\t\telse if (SidewalkWidth > 0f && distance <= SidewalkWidth) // Sidewalk ring \u2014 flush with road surface\r\n\t\t\t\t{\r\n\t\t\t\t\tcandidateHeight = roadSurfaceHeight;\r\n\t\t\t\t}\r\n\t\t\t\telse // Falloff \u2014 blend from surface/inset back to original terrain\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat transitionStart = SidewalkWidth > 0f ? SidewalkWidth : 0f;\r\n\t\t\t\t\tfloat transitionBaseHeight = SidewalkWidth > 0f ? roadSurfaceHeight : roadInsetHeight;\r\n\t\t\t\t\tfloat t = Math.Clamp((distance - transitionStart) / TerrainFalloffRadius, 0f, 1f);\r\n\t\t\t\t\tfloat smoothT = t * t * (3f - 2f * t);\r\n\t\t\t\t\tcandidateHeight = MathX.Lerp(transitionBaseHeight, currentPixelHeight, smoothT);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (distance < bestDistance[index])\r\n\t\t\t\t{\r\n\t\t\t\t\tbestDistance[index] = distance;\r\n\t\t\t\t\tupdatedHeights[index] = candidateHeight;\r\n\t\t\t\t\thasModified = true;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (hasModified)\r\n\t\t{\r\n\t\t\t// 4. Final encoding to ushort (Mapping back to 0..1 without the 0.5 offset)\r\n\t\t\tfor (int i = 0; i < heightMap.Length; i++)\r\n\t\t\t{\r\n\t\t\t\theightMap[i] = (ushort)MathF.Round(Math.Clamp(updatedHeights[i], 0f, terrainMaxHeight) / terrainMaxHeight * ushort.MaxValue);\r\n\t\t\t}\r\n\r\n\t\t\tstorage.HeightMap = heightMap;\r\n\t\t\tstorage.StateHasChanged();\r\n\t\t\tTerrainTarget.Create();\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void PaintTerrainToIntersection()\r\n\t{\r\n\t\tif (!TerrainTarget.IsValid() || TerrainEdgeMaterials == null || TerrainEdgeMaterials.Length == 0) return;\r\n\r\n\t\tvar storage = TerrainTarget.Storage;\r\n\t\tif (storage == null || storage.ControlMap == null) return;\r\n\r\n\t\tint resolution = storage.Resolution;\r\n\t\tfloat terrainSize = storage.TerrainSize;\r\n\t\tfloat halfSize = terrainSize * 0.5f;\r\n\r\n\t\t// Identify all material indices in the terrain storage \r\n\t\tbool materialsAdded = false;\r\n\t\tvar materialIndices = new int[TerrainEdgeMaterials.Length];\r\n\t\tfor (int m = 0; m < TerrainEdgeMaterials.Length; m++)\r\n\t\t{\r\n\t\t\tif (TerrainEdgeMaterials[m] == null) continue;\r\n\r\n\t\t\tint idx = storage.Materials.IndexOf(TerrainEdgeMaterials[m]);\r\n\t\t\tif (idx == -1)\r\n\t\t\t{\r\n\t\t\t\tstorage.Materials.Add(TerrainEdgeMaterials[m]);\r\n\t\t\t\tidx = storage.Materials.Count - 1;\r\n\t\t\t\tmaterialsAdded = true;\r\n\t\t\t}\r\n\r\n\t\t\tif (idx > 31)\r\n\t\t\t{\r\n\t\t\t\tLog.Error($\"RoadTool: Terrain has too many materials ({idx}). Material '{TerrainEdgeMaterials[m].ResourceName}' cannot be painted.\");\r\n\t\t\t\tidx = 0;\r\n\t\t\t}\r\n\r\n\t\t\tmaterialIndices[m] = idx;\r\n\t\t}\r\n\r\n\t\tif (materialsAdded)\r\n\t\t{\r\n\t\t\tstorage.StateHasChanged();\r\n\t\t\tTerrainTarget.Create();\r\n\t\t}\r\n\r\n\t\tfloat boundSize = (Shape == IntersectionShape.Rectangle ? Math.Max(Width, Length) * 0.5f : Radius) + TerrainEdgeRadius; // This line is unchanged \r\n\t\tBBox worldBounds = new BBox(WorldPosition - new Vector3(boundSize), WorldPosition + new Vector3(boundSize)); // This line is unchanged\r\n\r\n\t\tBuildRectangleExitCorridors();\r\n\r\n\t\tvar controlMap = storage.ControlMap;\r\n\t\tbool hasModified = false;\r\n\r\n\t\tfor (int ix = 0; ix < resolution; ix++)\r\n\t\t{\r\n\t\t\tfor (int iy = 0; iy < resolution; iy++)\r\n\t\t\t{\r\n\t\t\t\tfloat nodeLocalX = (ix / (float)(resolution - 1)) * terrainSize;\r\n\t\t\t\tfloat nodeLocalY = (iy / (float)(resolution - 1)) * terrainSize;\r\n\r\n\t\t\t\tvar checkPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);\r\n\t\t\t\tif (checkPos.x < 0f || checkPos.x > terrainSize || checkPos.y < 0f || checkPos.y > terrainSize)\r\n\t\t\t\t{\r\n\t\t\t\t\tnodeLocalX -= halfSize;\r\n\t\t\t\t\tnodeLocalY -= halfSize;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tVector3 pixelWorldPos = TerrainTarget.Transform.World.PointToWorld(new Vector3(nodeLocalX, nodeLocalY, 0));\r\n\t\t\t\tif (!worldBounds.Contains(pixelWorldPos)) continue;\r\n\r\n\t\t\t\tVector3 relativePos = WorldTransform.PointToLocal(pixelWorldPos);\r\n\t\t\t\tfloat distance = GetDistanceToIntersectionShape(relativePos.WithZ(0));\r\n\r\n\t\t\t\tif (distance > TerrainEdgeRadius) continue;\r\n\r\n\t\t\t\tint index = iy * resolution + ix;\r\n\t\t\t\tfloat t = Math.Clamp(distance / TerrainEdgeRadius, 0f, 1f);\r\n\t\t\t\tfloat blendStrength = TerrainEdgeBlendGradient.Evaluate(t).a;\r\n\r\n\t\t\t\tif (blendStrength > 0.01f)\r\n\t\t\t\t{\r\n\t\t\t\t\t// Add deterministic noise to blend textures together (Dithering)\r\n\t\t\t\t\tfloat pixelNoise = ((float)((index * 1103515245 + 12345) & 0x7FFFFFFF) / 0x7FFFFFFF) * TerrainTextureNoise - (TerrainTextureNoise * 0.5f);\r\n\t\t\t\t\tfloat noisyT = Math.Clamp(t + pixelNoise, 0f, 1f);\r\n\t\t\t\t\tfloat noisyDistance = distance + (pixelNoise * TerrainEdgeRadius);\r\n\r\n\t\t\t\t\tint materialIndex;\r\n\t\t\t\t\tif (noisyDistance <= 0)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tmaterialIndex = materialIndices[0];\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tint edgeMatCount = materialIndices.Length - 1;\r\n\t\t\t\t\t\t// Using noisyT for index selection \r\n\t\t\t\t\t\tint edgeIdx = edgeMatCount > 0 ? Math.Clamp((int)(noisyT * edgeMatCount), 0, edgeMatCount - 1) + 1 : 0;\r\n\t\t\t\t\t\tmaterialIndex = materialIndices[edgeIdx];\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tuint packed = controlMap[index];\r\n\t\t\t\t\tvar mat = new CompactTerrainMaterial(packed);\r\n\r\n\t\t\t\t\tif (TerrainTargetLayer == TerrainTextureLayer.Base)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tmat.BaseTextureId = (byte)materialIndex;\r\n\t\t\t\t\t\tmat.BlendFactor = (byte)MathX.Lerp(mat.BlendFactor, 0, blendStrength);\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t// Otherwise, we place it in Overlay and increase the BlendFactor to display it\r\n\t\t\t\t\t\tmat.OverlayTextureId = (byte)materialIndex;\r\n\t\t\t\t\t\tmat.BlendFactor = (byte)MathX.Lerp(mat.BlendFactor, 255, blendStrength);\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tcontrolMap[index] = mat.Packed;\r\n\t\t\t\t\thasModified = true;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (hasModified)\r\n\t\t{\r\n\t\t\tstorage.ControlMap = controlMap;\r\n\t\t\tstorage.StateHasChanged();\r\n\t\t\tTerrainTarget.SyncGPUTexture();\r\n\t\t}\r\n\t}\r\n\r\n\t// Per-opening exit corridors in local space, rebuilt once per flatten so the per-pixel distance test stays cheap.\r\n\t// Each entry is an opening's road-edge centre, its outward direction, its lateral direction, and half its width.\r\n\tprivate (Vector3 Center, Vector3 Outward, Vector3 Lateral, float Half)[] m_RectangleExitCorridors = Array.Empty<(Vector3, Vector3, Vector3, float)>();\r\n\r\n\tprivate void BuildRectangleExitCorridors()\r\n\t{\r\n\t\tif (Shape != IntersectionShape.Rectangle)\r\n\t\t{\r\n\t\t\tm_RectangleExitCorridors = Array.Empty<(Vector3, Vector3, Vector3, float)>();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tEnsureRectangleExits();\r\n\r\n\t\tm_RectangleExitCorridors = Exits\r\n\t\t\t.Where(exit => exit != null)\r\n\t\t\t.Select(exit =>\r\n\t\t\t{\r\n\t\t\t\tTransform t = GetRectangleExitLocalTransform(exit.Side, false, exit.Offset);\r\n\t\t\t\treturn (t.Position, t.Rotation.Forward, t.Rotation.Right, exit.Width * 0.5f);\r\n\t\t\t})\r\n\t\t\t.ToArray();\r\n\t}\r\n\r\n\r\n\r\n\tprivate float GetDistanceToIntersectionShape(Vector3 localPixelPos)\r\n\t{\r\n\t\tif (Shape == IntersectionShape.Rectangle)\r\n\t\t{\r\n\t\t\tfloat hl = Length * 0.5f;\r\n\t\t\tfloat hw = Width * 0.5f;\r\n\t\t\tfloat dx = MathF.Max(MathF.Abs(localPixelPos.x) - hl, 0);\r\n\t\t\tfloat dy = MathF.Max(MathF.Abs(localPixelPos.y) - hw, 0);\r\n\t\t\tfloat dist = MathF.Sqrt(dx * dx + dy * dy);\r\n\r\n\t\t\t// Treat each open exit's corridor as inside, so terrain doesn't poke up through a road opening. A corridor is\r\n\t\t\t// the band beyond an opening's road edge (outward) and within that opening's width (lateral) \u2014 one per opening.\r\n\t\t\tif (dist > 0)\r\n\t\t\t{\r\n\t\t\t\tforeach (var corridor in m_RectangleExitCorridors)\r\n\t\t\t\t{\r\n\t\t\t\t\tVector3 toPixel = localPixelPos - corridor.Center;\r\n\r\n\t\t\t\t\tif (Vector3.Dot(toPixel, corridor.Outward) >= 0.0f && MathF.Abs(Vector3.Dot(toPixel, corridor.Lateral)) <= corridor.Half)\r\n\t\t\t\t\t\treturn 0;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\treturn dist;\r\n\t\t}\r\n\r\n\t\t// Circle\r\n\t\tfloat radDist = MathF.Max(localPixelPos.WithZ(0).Length - Radius, 0);\r\n\r\n\t\tif (radDist > 0 && CircleExits != null && CircleExits.Length > 0)\r\n\t\t{\r\n\t\t\tVector3 pixelDir = localPixelPos.WithZ(0);\r\n\t\t\tif (pixelDir.LengthSquared > 0.0001f)\r\n\t\t\t\tpixelDir = pixelDir.Normal;\r\n\r\n\t\t\tforeach (var exit in CircleExits)\r\n\t\t\t{\r\n\t\t\t\t// Use dot product to stay independent of angle conventions\r\n\t\t\t\tVector3 exitDir = Rotation.FromYaw(exit.AngleDegrees).Forward;\r\n\t\t\t\tfloat cosHalfAngle = MathF.Cos(MathF.Atan(exit.RoadWidth / Radius));\r\n\t\t\t\tif (Vector3.Dot(pixelDir, exitDir) >= cosHalfAngle)\r\n\t\t\t\t\treturn 0;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn radDist;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "RoadIntersectionComponent/RoadIntersectionComponent.Traffic.cs",
            "FileName": "RoadIntersectionComponent.Traffic.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\npublic partial class RoadIntersectionComponent\n{\n\t/// <summary>\n\t/// A single drivable exit of an intersection, expressed in world space.\n\t/// <see cref=\"Transform\"/>.Forward points outward (away from the intersection), matching the snap targets.\n\t/// </summary>\n\tpublic readonly struct TrafficExit\n\t{\n\t\tpublic Transform Transform { get; init; }\n\t\tpublic float RoadWidth { get; init; }\n\n\t\t/// <summary>Pedestrians get no crossing over this arm's mouth. Vehicles are unaffected.</summary>\n\t\tpublic bool NoCrossing { get; init; }\n\t}\n\n\t/// <summary>\n\t/// When enabled, this intersection is ignored by the traffic system: vehicles will not route through it.\n\t/// </summary>\n\t[Property, Feature(\"General\"), Category(\"Traffic\"), Order(2)] public bool ExcludeTraffic { get; set; } = false;\n\n\t/// <summary>Speed limit for traffic crossing this intersection, in km/h.</summary>\n\t[Property, Feature(\"General\"), Category(\"Traffic\"), Order(2), Range(5.0f, 130.0f)] public float SpeedLimit { get; set; } = 30.0f;\n\n\n\n\t/// <summary>\n\t/// Enumerates every active exit of this intersection (rectangle or circle) as a world transform plus road width.\n\t/// These are the same outer-edge positions that <see cref=\"SnapNearbyRoads\"/> snaps roads to, so the traffic\n\t/// graph can match road endpoints against them by proximity.\n\t/// </summary>\n\tpublic List<TrafficExit> GetTrafficExits()\n\t{\n\t\tvar exits = new List<TrafficExit>();\n\n\t\tif (Shape == IntersectionShape.Rectangle)\n\t\t{\n\t\t\tEnsureRectangleExits();\n\n\t\t\tforeach (var exit in Exits)\n\t\t\t{\n\t\t\t\tif (exit is null)\n\t\t\t\t\tcontinue;\n\n\t\t\t\texits.Add(new TrafficExit\n\t\t\t\t{\n\t\t\t\t\tTransform = GetRectangleExitTransform(exit.Side, true, exit.Offset),\n\t\t\t\t\tRoadWidth = exit.Width,\n\t\t\t\t\tNoCrossing = exit.NoCrossing\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\tvar circleExits = CircleExits ?? Array.Empty<CircleExit>();\n\n\t\t\tfor (int i = 0; i < circleExits.Length; i++)\n\t\t\t{\n\t\t\t\texits.Add(new TrafficExit\n\t\t\t\t{\n\t\t\t\t\tTransform = GetCircleExitTransform(i, true),\n\t\t\t\t\tRoadWidth = circleExits[i].RoadWidth,\n\t\t\t\t\tNoCrossing = circleExits[i].NoCrossing\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn exits;\n\t}\n}\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "RoadIntersectionComponent/RoadIntersectionComponent.Utility.cs",
            "FileName": "RoadIntersectionComponent.Utility.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadIntersectionComponent\r\n{\r\n\t// Computes a quadratic Bezier control point at the intersection of the two tangent lines.\r\n\t// Returns true if the lines intersect, false if parallel (in which case the midpoint is used as a fallback).\r\n\t// The control distance along _StartTan is clamped to the chord length so asymmetric tangents (e.g. an\r\n\t// off-grid exit angle whose disc tangent points well past the outer corner) don't drive the bezier past\r\n\t// the outer endpoint \u2014 overshoot produces samples beyond the endpoint and flips downstream triangle winding.\r\n\tprivate static bool TryBezierControl(Vector3 _Start, Vector3 _StartTan, Vector3 _End, Vector3 _EndTan, out Vector3 _Control)\r\n\t{\r\n\t\tfloat det = _StartTan.x * _EndTan.y - _EndTan.x * _StartTan.y;\r\n\r\n\t\tif (MathF.Abs(det) < 0.0001f)\r\n\t\t{\r\n\t\t\t_Control = (_Start + _End) * 0.5f;\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tVector3 d = _End - _Start;\r\n\t\tfloat r = (d.x * _EndTan.y - _EndTan.x * d.y) / det;\r\n\t\tfloat rMax = d.Length;\r\n\t\tfloat rClamped = Math.Clamp(r, 0.0f, rMax);\r\n\t\t_Control = _Start + rClamped * _StartTan;\r\n\t\treturn true;\r\n\t}\r\n\r\n\tprivate static Vector3 SampleQuadBezier(Vector3 _B0, Vector3 _B1, Vector3 _B2, float _T)\r\n\t{\r\n\t\tfloat u = 1.0f - _T;\r\n\t\treturn u * u * _B0 + 2.0f * u * _T * _B1 + _T * _T * _B2;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "RoadManager/RoadTrafficGraph.Routing.cs",
            "FileName": "RoadTrafficGraph.Routing.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// <summary>\n/// Routing queries over the lane graph \u2014 how far it actually is to drive from one place to another.\n///\n/// \"Actually\" is the point: straight-line distance is useless for anything that has to obey roads. A delivery\n/// two blocks away as the crow flies can be a mile of one-way streets, and a job that times you on crow-flight\n/// distance is unwinnable in exactly the places that are most interesting to drive.\n/// </summary>\npublic sealed partial class RoadTrafficGraph\n{\n\t/// <summary>\n\t/// How far either end of a route may be from a lane and still count as being on it. A road carries a lane\n\t/// per direction, so a single point sits near several \u2014 see <see cref=\"FindNearbyLanes\"/> for why taking\n\t/// them all matters.\n\t/// </summary>\n\tpublic const float DefaultRouteSnapRadius = 1000.0f;\n\n\n\n\t/// <summary>\n\t/// Driving distance from <paramref name=\"_From\"/> to <paramref name=\"_To\"/>, following lanes in their legal\n\t/// direction. False when neither end is anywhere near a road, or when no route exists at all \u2014 a one-way\n\t/// system can genuinely have no way round.\n\t///\n\t/// Dijkstra over whole lanes rather than individual waypoints: lanes are the unit the graph is linked in,\n\t/// and a city's worth of them is a few thousand nodes. Costs are measured to the START of each lane, with\n\t/// the partial lengths at both ends added on, so the answer is measured between the actual points.\n\t/// </summary>\n\tpublic bool TryGetDrivingDistance(Vector3 _From, Vector3 _To, out float _Distance)\n\t{\n\t\t_Distance = 0.0f;\n\n\t\tList<(TrafficLane Lane, int Index)> starts = FindNearbyLanes(_From);\n\t\tList<(TrafficLane Lane, int Index)> goals = FindNearbyLanes(_To);\n\n\t\tif (starts.Count == 0 || goals.Count == 0)\n\t\t\treturn false;\n\n\t\tvar goalIndices = new Dictionary<TrafficLane, int>();\n\n\t\tforeach ((TrafficLane lane, int index) in goals)\n\t\t\tgoalIndices[lane] = index;\n\n\t\tvar best = new Dictionary<TrafficLane, float>();\n\t\tvar queue = new PriorityQueue<TrafficLane, float>();\n\n\t\tfloat bestTotal = float.MaxValue;\n\n\t\tforeach ((TrafficLane lane, int index) in starts)\n\t\t{\n\t\t\t// Goal on the same lane and ahead of us: straight down the road, no junction involved. Behind us\n\t\t\t// doesn't count \u2014 a lane is one-way, so that really does mean driving round and coming back, which\n\t\t\t// the search below works out properly.\n\t\t\tif (goalIndices.TryGetValue(lane, out int goalIndex) && goalIndex >= index)\n\t\t\t\tbestTotal = Math.Min(bestTotal, lane.DistanceFromStart(goalIndex) - lane.DistanceFromStart(index));\n\n\t\t\t// We start partway along, so what's reachable is the successors, at the cost of finishing this lane.\n\t\t\tfloat toEnd = lane.DistanceToEnd(index);\n\n\t\t\tforeach (TrafficLane next in lane.Successors)\n\t\t\t\tRelax(next, toEnd, best, queue);\n\t\t}\n\n\t\twhile (queue.TryDequeue(out TrafficLane lane, out float cost))\n\t\t{\n\t\t\t// Min-ordered, so once the cheapest thing left already costs more than an answer we have, nothing\n\t\t\t// better can come out of it.\n\t\t\tif (cost >= bestTotal)\n\t\t\t\tbreak;\n\n\t\t\tif (best.TryGetValue(lane, out float known) && cost > known)\n\t\t\t\tcontinue;\n\n\t\t\t// Reaching a goal lane doesn't end the search: another route might arrive at a different goal\n\t\t\t// candidate \u2014 the other side of the same street, say \u2014 for less.\n\t\t\tif (goalIndices.TryGetValue(lane, out int goalIndex))\n\t\t\t\tbestTotal = Math.Min(bestTotal, cost + lane.DistanceFromStart(goalIndex));\n\n\t\t\tfloat exit = cost + lane.Length;\n\n\t\t\tforeach (TrafficLane next in lane.Successors)\n\t\t\t\tRelax(next, exit, best, queue);\n\t\t}\n\n\t\tif (bestTotal >= float.MaxValue)\n\t\t\treturn false;\n\n\t\t_Distance = Math.Max(0.0f, bestTotal);\n\n\t\treturn true;\n\t}\n\n\n\n\t/// <summary>\n\t/// The actual waypoints to DRIVE from one point to another, following lanes in their legal direction. False\n\t/// when there's no route, same as <see cref=\"TryGetDrivingDistance\"/>.\n\t///\n\t/// This is the vehicle counterpart of a navmesh path, and it has to be a separate structure rather than the\n\t/// navmesh itself. A navmesh is baked around a person \u2014 it runs over pavements, through doorways and up\n\t/// stairs, and its corridors are person-wide. A route down one is a perfectly valid walk and an impossible\n\t/// drive, and unlike a pedestrian scraping a wall, a car routed somewhere it doesn't fit is stuck for good.\n\t/// Lanes are the drivable surface by construction, and they carry direction, which a navmesh has no concept\n\t/// of at all.\n\t///\n\t/// The list is reused rather than returned, because anything chasing anything re-routes constantly.\n\t/// </summary>\n\tpublic bool TryGetDrivingRoute(Vector3 _From, Vector3 _To, List<Vector3> _Route)\n\t{\n\t\t_Route.Clear();\n\n\t\tif (!TrySearchRoute(_From, _To, out RouteSearch search))\n\t\t\treturn false;\n\n\t\tBuildRoute(search.Starts, search.GoalIndices, search.CameFrom, search.Goal, search.Start, search.StartIndex, _Route);\n\n\t\treturn _Route.Count > 0;\n\t}\n\n\n\n\t/// <summary>\n\t/// The same route as <see cref=\"TryGetDrivingRoute\"/>, but as the LANES to drive rather than the waypoints\n\t/// along them \u2014 including the one we're starting on.\n\t///\n\t/// This is the form anything that already knows how to drive a lane wants. <see cref=\"TrafficVehicle\"/>\n\t/// follows lanes and picks a successor at every junction; handing it a lane list turns \"wander\" into \"go\n\t/// here\" without touching a single line of how it actually steers, brakes or corners.\n\t/// </summary>\n\tpublic bool TryGetDrivingLaneRoute(Vector3 _From, Vector3 _To, List<TrafficLane> _Route)\n\t{\n\t\treturn TryGetDrivingLaneRoute(FindNearbyLanes(_From), _To, _Route);\n\t}\n\n\n\n\t/// <summary>\n\t/// The same, but starting from the lane a vehicle is ALREADY DRIVING rather than from its position.\n\t///\n\t/// This is the overload anything mid-journey wants, and the difference is not subtle. Asking by position\n\t/// seeds the search with every lane in range \u2014 including the one going the other way down the same road \u2014\n\t/// and Dijkstra will happily return the shortest route from whichever of those is cheapest. That route\n\t/// starts on a lane the vehicle is not on, so it never matches, and a driver that can't find itself in its\n\t/// own route falls back to picking turns at random. The symptom is a car that mostly goes the right way and\n\t/// occasionally sets off round the block for no visible reason.\n\t/// </summary>\n\tpublic bool TryGetDrivingLaneRoute(TrafficLane _FromLane, Vector3 _FromPosition, Vector3 _To, List<TrafficLane> _Route)\n\t{\n\t\t_Route.Clear();\n\n\t\tif (_FromLane is null || _FromLane.Waypoints.Count == 0)\n\t\t\treturn false;\n\n\t\treturn TryGetDrivingLaneRoute([(_FromLane, NearestWaypointIndex(_FromLane, _FromPosition))], _To, _Route);\n\t}\n\n\n\n\t/// <summary>Which waypoint of a lane is closest to a point.</summary>\n\tprivate static int NearestWaypointIndex(TrafficLane _Lane, Vector3 _Point)\n\t{\n\t\tint best = 0;\n\t\tfloat bestDistance = float.MaxValue;\n\n\t\tfor (int i = 0; i < _Lane.Waypoints.Count; i++)\n\t\t{\n\t\t\tfloat distance = _Lane.Waypoints[i].DistanceSquared(_Point);\n\n\t\t\tif (distance >= bestDistance)\n\t\t\t\tcontinue;\n\n\t\t\tbestDistance = distance;\n\t\t\tbest = i;\n\t\t}\n\n\t\treturn best;\n\t}\n\n\n\n\tprivate bool TryGetDrivingLaneRoute(List<(TrafficLane Lane, int Index)> _Starts, Vector3 _To, List<TrafficLane> _Route)\n\t{\n\t\t_Route.Clear();\n\n\t\tif (!TrySearchRoute(_Starts, _To, out RouteSearch search))\n\t\t\treturn false;\n\n\t\t// Never left the start lane \u2014 the route is just that one.\n\t\tif (search.Goal is null)\n\t\t{\n\t\t\tif (search.Start is null)\n\t\t\t\treturn false;\n\n\t\t\t_Route.Add(search.Start);\n\n\t\t\treturn true;\n\t\t}\n\n\t\tList<TrafficLane> chain = BuildLaneChain(search.CameFrom, search.Goal);\n\n\t\tif (chain.Count == 0)\n\t\t\treturn false;\n\n\t\t// The lane we're ON isn't in the chain (the chain begins at one of its successors), and a driver already\n\t\t// travelling it needs to see it in the list or its very first junction is an unplanned one.\n\t\tforeach ((TrafficLane lane, int _) in search.Starts)\n\t\t{\n\t\t\tif (!lane.Successors.Contains(chain[0]))\n\t\t\t\tcontinue;\n\n\t\t\t_Route.Add(lane);\n\n\t\t\tbreak;\n\t\t}\n\n\t\t_Route.AddRange(chain);\n\n\t\treturn true;\n\t}\n\n\n\n\t/// <summary>What a completed search found: the winning route's ends, and the map to walk it back with.</summary>\n\tprivate struct RouteSearch\n\t{\n\t\tpublic List<(TrafficLane Lane, int Index)> Starts;\n\t\tpublic Dictionary<TrafficLane, int> GoalIndices;\n\t\tpublic Dictionary<TrafficLane, TrafficLane> CameFrom;\n\n\t\t/// <summary>The lane the route ends on, or null when it never left the lane it started on.</summary>\n\t\tpublic TrafficLane Goal;\n\n\t\tpublic TrafficLane Start;\n\t\tpublic int StartIndex;\n\t}\n\n\n\n\t/// <summary>\n\t/// The Dijkstra itself, shared by both route shapes so there's one search to be correct rather than two to\n\t/// keep in step.\n\t/// </summary>\n\tprivate bool TrySearchRoute(Vector3 _From, Vector3 _To, out RouteSearch _Result)\n\t{\n\t\treturn TrySearchRoute(FindNearbyLanes(_From), _To, out _Result);\n\t}\n\n\n\n\t/// <inheritdoc cref=\"TrySearchRoute(Vector3, Vector3, out RouteSearch)\"/>\n\tprivate bool TrySearchRoute(List<(TrafficLane Lane, int Index)> _Starts, Vector3 _To, out RouteSearch _Result)\n\t{\n\t\t_Result = default;\n\n\t\tList<(TrafficLane Lane, int Index)> starts = _Starts;\n\t\tList<(TrafficLane Lane, int Index)> goals = FindNearbyLanes(_To);\n\n\t\tif (starts is null || starts.Count == 0 || goals.Count == 0)\n\t\t\treturn false;\n\n\t\tvar goalIndices = new Dictionary<TrafficLane, int>();\n\n\t\tforeach ((TrafficLane lane, int index) in goals)\n\t\t\tgoalIndices[lane] = index;\n\n\t\tvar best = new Dictionary<TrafficLane, float>();\n\t\tvar cameFrom = new Dictionary<TrafficLane, TrafficLane>();\n\t\tvar queue = new PriorityQueue<TrafficLane, float>();\n\n\t\t// The winning route so far: where it ends, and which of the several starts it began at.\n\t\tfloat bestTotal = float.MaxValue;\n\t\tTrafficLane bestGoal = null;\n\t\tTrafficLane bestStart = null;\n\t\tint bestStartIndex = 0;\n\n\t\tforeach ((TrafficLane lane, int index) in starts)\n\t\t{\n\t\t\t// Goal on the same lane and ahead of us \u2014 no junction involved, so the route is just this stretch.\n\t\t\tif (goalIndices.TryGetValue(lane, out int sameLaneGoal) && sameLaneGoal >= index)\n\t\t\t{\n\t\t\t\tfloat direct = lane.DistanceFromStart(sameLaneGoal) - lane.DistanceFromStart(index);\n\n\t\t\t\tif (direct < bestTotal)\n\t\t\t\t{\n\t\t\t\t\tbestTotal = direct;\n\t\t\t\t\tbestGoal = null;      // null goal marks \"never left the start lane\"\n\t\t\t\t\tbestStart = lane;\n\t\t\t\t\tbestStartIndex = index;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfloat toEnd = lane.DistanceToEnd(index);\n\n\t\t\tforeach (TrafficLane next in lane.Successors)\n\t\t\t{\n\t\t\t\tif (Relax(next, toEnd, best, queue))\n\t\t\t\t\tcameFrom[next] = lane;\n\t\t\t}\n\t\t}\n\n\t\twhile (queue.TryDequeue(out TrafficLane lane, out float cost))\n\t\t{\n\t\t\tif (cost >= bestTotal)\n\t\t\t\tbreak;\n\n\t\t\tif (best.TryGetValue(lane, out float known) && cost > known)\n\t\t\t\tcontinue;\n\n\t\t\tif (goalIndices.TryGetValue(lane, out int goalIndex))\n\t\t\t{\n\t\t\t\tfloat total = cost + lane.DistanceFromStart(goalIndex);\n\n\t\t\t\tif (total < bestTotal)\n\t\t\t\t{\n\t\t\t\t\tbestTotal = total;\n\t\t\t\t\tbestGoal = lane;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfloat exit = cost + lane.Length;\n\n\t\t\tforeach (TrafficLane next in lane.Successors)\n\t\t\t{\n\t\t\t\tif (Relax(next, exit, best, queue))\n\t\t\t\t\tcameFrom[next] = lane;\n\t\t\t}\n\t\t}\n\n\t\tif (bestTotal >= float.MaxValue)\n\t\t\treturn false;\n\n\t\t_Result = new RouteSearch\n\t\t{\n\t\t\tStarts = starts,\n\t\t\tGoalIndices = goalIndices,\n\t\t\tCameFrom = cameFrom,\n\t\t\tGoal = bestGoal,\n\t\t\tStart = bestStart,\n\t\t\tStartIndex = bestStartIndex\n\t\t};\n\n\t\treturn true;\n\t}\n\n\n\n\t/// <summary>\n\t/// Walks the predecessor chain back from the winning goal lane to whichever start it came from, then lays\n\t/// the waypoints down in travel order.\n\t///\n\t/// The two ends are partial lanes \u2014 we join partway along the first and stop partway along the last \u2014 which\n\t/// is why they're handled separately from the whole lanes in between.\n\t/// </summary>\n\tprivate void BuildRoute(List<(TrafficLane Lane, int Index)> _Starts, Dictionary<TrafficLane, int> _GoalIndices,\n\t                        Dictionary<TrafficLane, TrafficLane> _CameFrom, TrafficLane _Goal,\n\t                        TrafficLane _Start, int _StartIndex, List<Vector3> _Route)\n\t{\n\t\t// Never left the start lane: one straight run down it.\n\t\tif (_Goal is null)\n\t\t{\n\t\t\tif (_Start is null || !_GoalIndices.TryGetValue(_Start, out int stop))\n\t\t\t\treturn;\n\n\t\t\tfor (int i = _StartIndex; i <= stop; i++)\n\t\t\t\t_Route.Add(_Start.Waypoints[i]);\n\n\t\t\treturn;\n\t\t}\n\n\t\tList<TrafficLane> chain = BuildLaneChain(_CameFrom, _Goal);\n\n\t\tif (chain.Count == 0)\n\t\t\treturn;\n\n\t\t// The first lane in the chain is a successor of the start lane, so the start lane itself isn't in it \u2014\n\t\t// find which of the candidates fed it and lay down the tail of that one first.\n\t\tTrafficLane head = chain[0];\n\n\t\tforeach ((TrafficLane lane, int index) in _Starts)\n\t\t{\n\t\t\tif (!lane.Successors.Contains(head))\n\t\t\t\tcontinue;\n\n\t\t\tfor (int i = index; i < lane.Waypoints.Count; i++)\n\t\t\t\t_Route.Add(lane.Waypoints[i]);\n\n\t\t\tbreak;\n\t\t}\n\n\t\tfor (int c = 0; c < chain.Count; c++)\n\t\t{\n\t\t\tTrafficLane lane = chain[c];\n\n\t\t\t// The last one stops at the goal waypoint rather than running to the end of the road.\n\t\t\tint stop = c == chain.Count - 1 && _GoalIndices.TryGetValue(lane, out int goalIndex)\n\t\t\t\t? goalIndex\n\t\t\t\t: lane.Waypoints.Count - 1;\n\n\t\t\tfor (int i = 0; i <= stop; i++)\n\t\t\t\t_Route.Add(lane.Waypoints[i]);\n\t\t}\n\t}\n\n\n\n\t/// <summary>Walks the predecessor map back from a goal lane and returns the chain in travel order.</summary>\n\tprivate List<TrafficLane> BuildLaneChain(Dictionary<TrafficLane, TrafficLane> _CameFrom, TrafficLane _Goal)\n\t{\n\t\tvar chain = new List<TrafficLane>();\n\t\tTrafficLane current = _Goal;\n\n\t\t// Bounded by the lane count so a cycle in the map can't spin forever.\n\t\tfor (int step = 0; step <= Lanes.Count && current is not null; step++)\n\t\t{\n\t\t\tchain.Add(current);\n\n\t\t\tif (!_CameFrom.TryGetValue(current, out TrafficLane previous))\n\t\t\t\tbreak;\n\n\t\t\tcurrent = previous;\n\t\t}\n\n\t\tchain.Reverse();\n\n\t\treturn chain;\n\t}\n\n\n\n\t/// <summary>True when this was an improvement, so the caller knows whether to record the predecessor.</summary>\n\tprivate static bool Relax(TrafficLane _Lane, float _Cost, Dictionary<TrafficLane, float> _Best, PriorityQueue<TrafficLane, float> _Queue)\n\t{\n\t\tif (_Best.TryGetValue(_Lane, out float existing) && existing <= _Cost)\n\t\t\treturn false;\n\n\t\t_Best[_Lane] = _Cost;\n\n\t\t_Queue.Enqueue(_Lane, _Cost);\n\n\t\treturn true;\n\t}\n\n\n\n\t/// <summary>\n\t/// Every drivable lane with a waypoint within <paramref name=\"_Radius\"/> of the point, and which waypoint\n\t/// that was \u2014 at most one entry per lane.\n\t///\n\t/// Taking ALL of them, rather than just the closest, is what makes routing reliable. A road carries a lane\n\t/// per direction, so any point on it is near at least two; picking only the nearest is a coin flip that can\n\t/// land on the one pointing away from where you're going, or on one nothing feeds into. The route then comes\n\t/// back as impossible even though the lane a few metres over is trivially routable. Seeding the search with\n\t/// every candidate \u2014 and accepting any of them at the far end \u2014 also gets the natural answer for free:\n\t/// either side of the street will do, whichever is closer to drive.\n\t///\n\t/// Road lanes only. Snapping an endpoint onto an intersection cross-lane would measure from the middle of\n\t/// a junction.\n\t/// </summary>\n\tpublic List<(TrafficLane Lane, int Index)> FindNearbyLanes(Vector3 _Point, float _Radius = DefaultRouteSnapRadius)\n\t{\n\t\tvar results = new List<(TrafficLane, int)>();\n\n\t\tTrafficLane nearestLane = null;\n\t\tint nearestIndex = 0;\n\t\tfloat nearestDistance = float.MaxValue;\n\n\t\tfloat radiusSquared = _Radius * _Radius;\n\n\t\tforeach (TrafficLane lane in Lanes)\n\t\t{\n\t\t\tif (!lane.IsRoadLane)\n\t\t\t\tcontinue;\n\n\t\t\tint laneIndex = -1;\n\t\t\tfloat laneDistance = float.MaxValue;\n\n\t\t\tfor (int i = 0; i < lane.Waypoints.Count; i++)\n\t\t\t{\n\t\t\t\tfloat distance = lane.Waypoints[i].DistanceSquared(_Point);\n\n\t\t\t\tif (distance >= laneDistance)\n\t\t\t\t\tcontinue;\n\n\t\t\t\tlaneDistance = distance;\n\t\t\t\tlaneIndex = i;\n\t\t\t}\n\n\t\t\tif (laneIndex < 0)\n\t\t\t\tcontinue;\n\n\t\t\tif (laneDistance < nearestDistance)\n\t\t\t{\n\t\t\t\tnearestDistance = laneDistance;\n\t\t\t\tnearestLane = lane;\n\t\t\t\tnearestIndex = laneIndex;\n\t\t\t}\n\n\t\t\tif (laneDistance <= radiusSquared)\n\t\t\t\tresults.Add((lane, laneIndex));\n\t\t}\n\n\t\t// Off-road entirely (a car park, a field) \u2014 the closest lane is still the honest answer, so don't come\n\t\t// back empty and turn a long route into \"no route\".\n\t\tif (results.Count == 0 && nearestLane is not null)\n\t\t\tresults.Add((nearestLane, nearestIndex));\n\n\t\treturn results;\n\t}\n\n\n\n\t/// <summary>\n\t/// The single drivable lane closest to a point, and which waypoint that was. Prefer\n\t/// <see cref=\"FindNearbyLanes\"/> for routing \u2014 one lane is rarely the whole answer for a two-way road.\n\t/// </summary>\n\tpublic TrafficLane FindNearestLane(Vector3 _Point, out int _Index)\n\t{\n\t\tTrafficLane bestLane = null;\n\t\tfloat bestDistance = float.MaxValue;\n\n\t\t_Index = 0;\n\n\t\tforeach (TrafficLane lane in Lanes)\n\t\t{\n\t\t\tif (!lane.IsRoadLane)\n\t\t\t\tcontinue;\n\n\t\t\tfor (int i = 0; i < lane.Waypoints.Count; i++)\n\t\t\t{\n\t\t\t\tfloat distance = lane.Waypoints[i].DistanceSquared(_Point);\n\n\t\t\t\tif (distance >= bestDistance)\n\t\t\t\t\tcontinue;\n\n\t\t\t\tbestDistance = distance;\n\t\t\t\tbestLane = lane;\n\n\t\t\t\t_Index = i;\n\t\t\t}\n\t\t}\n\n\t\treturn bestLane;\n\t}\n}\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "RoadManager/RoadVehicleDriver.cs",
            "FileName": "RoadVehicleDriver.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// <summary>\n/// A primitive control surface the traffic AI (<see cref=\"TrafficVehicle\"/>) uses to drive ONE vehicle. It's a plain\n/// bag of delegates \u2014 there is no interface for a vehicle controller to implement, and your vehicle code never needs\n/// to reference this library.\n///\n/// The seam is filled in by whoever uses both this tool AND a vehicle controller \u2014 i.e. your GAME \u2014 via\n/// <see cref=\"RoadManager.ResolveVehicleDriver\"/>. The game maps whatever its controller looks like onto these few\n/// delegates. Any field left null is simply skipped. The demo wires <see cref=\"DemoCarController\"/> automatically.\n/// </summary>\npublic sealed class RoadVehicleDriver\n{\n\t/// <summary>True while a player is at the wheel \u2014 the AI then hands this car over for good and never reclaims it.</summary>\n\tpublic Func<bool> IsPlayerDriving;\n\n\t/// <summary>The vehicle body's world velocity. The brain reads it to chase a target speed and to detect being jammed.</summary>\n\tpublic Func<Vector3> Velocity;\n\n\t/// <summary>Tell the controller whether the AI is currently driving this vehicle (vs parked / player-driven). Pushed every frame.</summary>\n\tpublic Action<bool> SetAiControlled;\n\n\t/// <summary>Push the AI's per-frame inputs: throttle and steer in [-1, 1] (steer + = left), plus handbrake.</summary>\n\tpublic Action<float, float, bool> Drive;\n\n\t/// <summary>Optional: max steering angle in degrees, used to widen the entity look-ahead toward where the car is turning.</summary>\n\tpublic Func<float> MaxSteering;\n}\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Editor/RoadToolWindow/RoadToolWindow.Gizmo.cs",
            "FileName": "RoadToolWindow.Gizmo.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool.Editor;\r\n\r\npublic partial class RoadToolWindow\r\n{\r\n\tprivate const float GIZMO_BOX_SIZE = 2.0f;\r\n\tprivate const float LINE_THICKNESS = 2.0f;\r\n\tprivate const float TANGENT_LINE_THICKNESS = 0.8f;\r\n\r\n\r\n\r\n\tprivate void DrawGizmos()\r\n\t{\r\n\t\tusing (Gizmo.Scope(\"road_editor\", _targetComponent.WorldTransform))\r\n\t\t{\r\n\t\t\tDrawSplineSegments();\r\n\t\t\tDrawPositionGizmo();\r\n\t\t\tDrawPointControls();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawSplineSegments()\r\n\t{\r\n\t\t_targetComponent.Spline.ConvertToPolyline(ref _polyLine);\r\n\r\n\t\tfor (var i = 0; i < _polyLine.Count - 1; i++)\r\n\t\t{\r\n\t\t\tDrawSegment(i, _polyLine[i], _polyLine[i + 1]);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawSegment(int index, Vector3 start, Vector3 end)\r\n\t{\r\n\t\tusing (Gizmo.Scope(\"segment\" + index))\r\n\t\tusing (Gizmo.Hitbox.LineScope())\r\n\t\t{\r\n\t\t\tGizmo.Draw.LineThickness = LINE_THICKNESS;\r\n\t\t\tGizmo.Hitbox.AddPotentialLine(start, end, LINE_THICKNESS * 2f);\r\n\t\t\tGizmo.Draw.Line(start, end);\r\n\r\n\t\t\tif (Gizmo.IsHovered && Gizmo.HasMouseFocus)\r\n\t\t\t{\r\n\t\t\t\tHandleSegmentHover(start, end);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandleSegmentHover(Vector3 start, Vector3 end)\r\n\t{\r\n\t\tGizmo.Draw.Color = Color.Cyan;\r\n\r\n\t\tif (!new Line(start, end).ClosestPoint(Gizmo.CurrentRay.ToLocal(Gizmo.Transform),\r\n\t\t\tout Vector3 pointOnLine, out _))\r\n\t\t\treturn;\r\n\r\n\t\tvar hoverSample = _targetComponent.Spline.SampleAtClosestPosition(pointOnLine);\r\n\t\tDrawHoverHandle(pointOnLine, hoverSample.Tangent);\r\n\r\n\t\tif (Gizmo.HasClicked && Gizmo.Pressed.This)\r\n\t\t{\r\n\t\t\tInsertPointAtHover(hoverSample.Distance);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawHoverHandle(Vector3 position, Vector3 tangent)\r\n\t{\r\n\t\tusing (Gizmo.Scope(\"hover_handle\", new Transform(position, Rotation.LookAt(tangent))))\r\n\t\tusing (Gizmo.GizmoControls.PushFixedScale())\r\n\t\t{\r\n\t\t\tGizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void InsertPointAtHover(float distance)\r\n\t{\r\n\t\tusing (CreateUndoScope(\"Added spline point\"))\r\n\t\t{\r\n\t\t\tvar newPointIndex = _targetComponent.Spline.AddPointAtDistance(distance, true);\r\n\t\t\tSelectedPointIndex = newPointIndex;\r\n\t\t\t_inTangentSelected = false;\r\n\t\t\t_outTangentSelected = false;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawPositionGizmo()\r\n\t{\r\n\t\t// The first point sits on the component origin, so its move handle lands right on top of the GameObject's\r\n\t\t// own transform gizmo \u2014 two move gizmos fighting over the same spot. Hide the point-body handle there and\r\n\t\t// let the object transform gizmo move the whole track instead. Tangent editing still gets its own gizmo.\r\n\t\tif (SelectedPointIndex == 0 && !_inTangentSelected && !_outTangentSelected)\r\n\t\t\treturn;\r\n\r\n\t\tvar gizmoPosition = CalculateGizmoPosition();\r\n\r\n\t\tif (!Gizmo.IsShiftPressed)\r\n\t\t{\r\n\t\t\t_draggingOutNewPoint = false;\r\n\t\t}\r\n\r\n\t\tusing (Gizmo.Scope(\"position\", new Transform(gizmoPosition)))\r\n\t\t{\r\n\t\t\tHandlePositionControl();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate Vector3 CalculateGizmoPosition()\r\n\t{\r\n\t\tvar position = _selectedPoint.Position;\r\n\r\n\t\tif (_inTangentSelected)\r\n\t\t\tposition += _selectedPoint.In;\r\n\t\telse if (_outTangentSelected)\r\n\t\t\tposition += _selectedPoint.Out;\r\n\r\n\t\treturn position;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandlePositionControl()\r\n\t{\r\n\t\t_moveInProgress = false;\r\n\r\n\t\tif (Gizmo.Control.Position(\"spline_control_\", Vector3.Zero, out var delta, GetHandleRotation()))\r\n\t\t{\r\n\t\t\t_moveInProgress = true;\r\n\t\t\t_movementUndoScope ??= CreateUndoScope(\"Moved spline point\");\r\n\r\n\t\t\tif (_inTangentSelected)\r\n\t\t\t\tMoveSelectedPointInTangent(delta);\r\n\t\t\telse if (_outTangentSelected)\r\n\t\t\t\tMoveSelectedPointOutTangent(delta);\r\n\t\t\telse\r\n\t\t\t\tHandlePointMove(delta);\r\n\t\t}\r\n\r\n\t\tif (!_moveInProgress && Gizmo.WasLeftMouseReleased)\r\n\t\t{\r\n\t\t\t_movementUndoScope?.Dispose();\r\n\t\t\t_movementUndoScope = null;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Orientation for the move gizmo's arrows, honouring the editor's Global/Local space toggle\r\n\t/// (the same <see cref=\"Gizmo.Settings.GlobalSpace\"/> button the object move tool uses).\r\n\t/// Global keeps the arrows world-aligned; Local aligns them to the selected point's tangent frame\r\n\t/// so a point can be dragged straight along the track. We are already inside the component-transform\r\n\t/// scope, so returning the point's <em>local</em> frame is what places the arrows on the world tangent \u2014\r\n\t/// and <see cref=\"Gizmo.Control.Position\"/> hands the delta back in component-local space, matching how\r\n\t/// <see cref=\"MoveSelectedPoint\"/> applies it.\r\n\t/// </summary>\r\n\tprivate Rotation GetHandleRotation()\r\n\t{\r\n\t\tif (Gizmo.Settings.GlobalSpace)\r\n\t\t\treturn Rotation.Identity;\r\n\r\n\t\tif (!IsSelectedPointValid())\r\n\t\t\treturn Rotation.Identity;\r\n\r\n\t\tvar spline = _targetComponent.Spline;\r\n\t\tvar sample = spline.SampleAtDistance(spline.GetDistanceAtPoint(SelectedPointIndex));\r\n\r\n\t\tif (sample.Tangent.IsNearlyZero())\r\n\t\t\treturn Rotation.Identity;\r\n\r\n\t\tvar up = Rotation.FromAxis(sample.Tangent, sample.Roll) * sample.Up;\r\n\r\n\t\treturn Rotation.LookAt(sample.Tangent, up);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandlePointMove(Vector3 delta)\r\n\t{\r\n\t\tif (Gizmo.IsShiftPressed && !_draggingOutNewPoint)\r\n\t\t{\r\n\t\t\t_draggingOutNewPoint = true;\r\n\t\t\tvar currentPoint = _targetComponent.Spline.GetPoint(SelectedPointIndex);\r\n\t\t\t_targetComponent.Spline.InsertPoint(SelectedPointIndex + 1, currentPoint);\r\n\t\t\tSelectedPointIndex++;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tMoveSelectedPoint(delta);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawPointControls()\r\n\t{\r\n\t\tvar spline = _targetComponent.Spline;\r\n\r\n\t\tfor (var i = 0; i < spline.PointCount; i++)\r\n\t\t{\r\n\t\t\tif (spline.IsLoop && i == spline.SegmentCount)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar point = spline.GetPoint(i);\r\n\t\t\tDrawPointControl(i, point);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawPointControl(int index, Spline.Point point)\r\n\t{\r\n\t\tusing (Gizmo.Scope(\"point_controls\" + index, new Transform(point.Position)))\r\n\t\t{\r\n\t\t\tGizmo.Draw.IgnoreDepth = true;\r\n\t\t\tDrawPointPositionHandle(index);\r\n\r\n\t\t\tif (SelectedPointIndex == index)\r\n\t\t\t{\r\n\t\t\t\tDrawTangentHandles(point);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawPointPositionHandle(int index)\r\n\t{\r\n\t\tusing (Gizmo.Scope(\"position\"))\r\n\t\tusing (Gizmo.GizmoControls.PushFixedScale())\r\n\t\t{\r\n\t\t\tGizmo.Hitbox.DepthBias = 0.1f;\r\n\t\t\tGizmo.Hitbox.BBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\r\n\t\t\tbool isSelected = index == SelectedPointIndex && !_inTangentSelected && !_outTangentSelected;\r\n\r\n\t\t\tif (Gizmo.IsHovered || isSelected)\r\n\t\t\t{\r\n\t\t\t\tGizmo.Draw.Color = Color.Cyan;\r\n\t\t\t}\r\n\r\n\t\t\tGizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\r\n\t\t\tif (Gizmo.HasClicked && Gizmo.Pressed.This)\r\n\t\t\t{\r\n\t\t\t\tSelectPoint(index);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawTangentHandles(Spline.Point point)\r\n\t{\r\n\t\tGizmo.Draw.Color = Color.White;\r\n\t\tGizmo.Draw.LineThickness = TANGENT_LINE_THICKNESS;\r\n\r\n\t\tDrawTangentHandle(\"in_tangent\", point.In, -point.In, ref _inTangentSelected, ref _outTangentSelected);\r\n\t\tDrawTangentHandle(\"out_tangent\", point.Out, -point.Out, ref _outTangentSelected, ref _inTangentSelected);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawTangentHandle(string name, Vector3 offset, Vector3 lineStart, ref bool thisSelected, ref bool otherSelected)\r\n\t{\r\n\t\tusing (Gizmo.Scope(name, new Transform(offset)))\r\n\t\t{\r\n\t\t\tbool isMirroredOrAuto = _selectedPointTangentMode is HandleModeTemp.Mirrored or HandleModeTemp.Auto;\r\n\t\t\tif (isMirroredOrAuto && (thisSelected || otherSelected))\r\n\t\t\t{\r\n\t\t\t\tGizmo.Draw.Color = Color.Cyan;\r\n\t\t\t}\r\n\r\n\t\t\tGizmo.Draw.Line(lineStart, Vector3.Zero);\r\n\r\n\t\t\tif (_selectedPointTangentMode != HandleModeTemp.Linear)\r\n\t\t\t{\r\n\t\t\t\tDrawTangentBox(ref thisSelected, ref otherSelected);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawTangentBox(ref bool thisSelected, ref bool otherSelected)\r\n\t{\r\n\t\tusing (Gizmo.GizmoControls.PushFixedScale())\r\n\t\t{\r\n\t\t\tGizmo.Hitbox.DepthBias = 0.1f;\r\n\t\t\tGizmo.Hitbox.BBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\r\n\t\t\tif (Gizmo.IsHovered || thisSelected)\r\n\t\t\t{\r\n\t\t\t\tGizmo.Draw.Color = Color.Cyan;\r\n\t\t\t}\r\n\r\n\t\t\tGizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\r\n\t\t\tif (Gizmo.HasClicked && Gizmo.Pressed.This)\r\n\t\t\t{\r\n\t\t\t\tthisSelected = true;\r\n\t\t\t\totherSelected = false;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Editor/RoadToolWindow/RoadToolWindow.UI.cs",
            "FileName": "RoadToolWindow.UI.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\nusing Editor;\r\n\r\nnamespace RedSnail.RoadTool.Editor;\r\n\r\npublic partial class RoadToolWindow\r\n{\r\n\tprivate const int HEADER_HEIGHT = 32;\r\n\r\n\r\n\r\n\tprivate void Rebuild()\r\n\t{\r\n\t\tLayout.Clear(true);\r\n\t\tLayout.Margin = 0;\r\n\t\tIcon = _isClosed ? \"\" : \"route\";\r\n\t\tUpdateWindowTitle();\r\n\t\tIsGrabbable = !_isClosed;\r\n\r\n\t\tif (_isClosed)\r\n\t\t{\r\n\t\t\tBuildClosedState();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tMinimumWidth = 400;\r\n\t\tBuildHeader();\r\n\r\n\t\tif (_targetComponent.IsValid())\r\n\t\t{\r\n\t\t\tBuildControlSheet();\r\n\t\t}\r\n\r\n\t\tLayout.Margin = 4;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildClosedState()\r\n\t{\r\n\t\tvar closedRow = Layout.AddRow();\r\n\r\n\t\tclosedRow.Add(new IconButton(\"route\", () => { _isClosed = false; Rebuild(); })\r\n\t\t{\r\n\t\t\tToolTip = \"Open Spline Point Editor\",\r\n\t\t\tFixedHeight = HEADER_HEIGHT,\r\n\t\t\tFixedWidth = HEADER_HEIGHT,\r\n\t\t\tBackground = Color.Transparent\r\n\t\t});\r\n\r\n\t\tMinimumWidth = 0;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildHeader()\r\n\t{\r\n\t\tvar headerRow = Layout.AddRow();\r\n\r\n\t\theaderRow.AddStretchCell();\r\n\r\n\t\theaderRow.Add(new IconButton(\"info\")\r\n\t\t{\r\n\t\t\tToolTip = GetInfoTooltip(),\r\n\t\t\tFixedHeight = HEADER_HEIGHT,\r\n\t\t\tFixedWidth = HEADER_HEIGHT,\r\n\t\t\tBackground = Color.Transparent\r\n\t\t});\r\n\r\n\t\theaderRow.Add(new IconButton(\"close\", CloseWindow)\r\n\t\t{\r\n\t\t\tToolTip = \"Close Editor\",\r\n\t\t\tFixedHeight = HEADER_HEIGHT,\r\n\t\t\tFixedWidth = HEADER_HEIGHT,\r\n\t\t\tBackground = Color.Transparent\r\n\t\t});\r\n\t}\r\n\r\n\r\n\r\n\tprivate string GetInfoTooltip()\r\n\t{\r\n\t\treturn \"Controls to edit the spline points.\\n\" +\r\n\t\t\t   \"In addition to modifying the properties in the control sheet, you can also use the 3D Gizmos.\\n\" +\r\n\t\t\t   \"Clicking on the spline between points will split the spline at that position.\\n\" +\r\n\t\t\t   \"Holding shift while dragging a point's position will drag out a new point.\";\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildControlSheet()\r\n\t{\r\n\t\tvar serialized = this.GetSerialized();\r\n\t\tvar controlSheet = new ControlSheet();\r\n\r\n\t\t// Add property rows\r\n\t\tcontrolSheet.AddRow(serialized.GetProperty(nameof(_selectedPointTangentMode)));\r\n\t\tcontrolSheet.AddRow(serialized.GetProperty(nameof(_selectedPointPosition)));\r\n\t\t_inTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointIn)));\r\n\t\t_outTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointOut)));\r\n\r\n\t\t// Add advanced group\r\n\t\tvar roll = serialized.GetProperty(nameof(_selectedPointRoll));\r\n\t\tvar scale = serialized.GetProperty(nameof(_selectedPointScale));\r\n\t\tvar up = serialized.GetProperty(nameof(_selectedPointUp));\r\n\t\tcontrolSheet.AddGroup(\"Advanced\", [roll, scale, up]);\r\n\r\n\t\t// Add control buttons\r\n\t\tcontrolSheet.AddLayout(BuildControlButtons());\r\n\r\n\t\tLayout.Add(controlSheet);\r\n\t\tToggleTangentInput();\r\n\t}\r\n\r\n\r\n\r\n\tprivate Layout BuildControlButtons()\r\n\t{\r\n\t\tvar row = Layout.Row();\r\n\t\trow.Spacing = 16;\r\n\t\trow.Margin = 8;\r\n\r\n\t\trow.Add(CreateNavigationButton(\"skip_previous\", -1, \"Go to previous point\"));\r\n\t\trow.Add(CreateNavigationButton(\"skip_next\", 1, \"Go to next point\"));\r\n\t\trow.Add(CreateDeleteButton());\r\n\t\trow.Add(CreateAddButton());\r\n\r\n\t\treturn row;\r\n\t}\r\n\r\n\r\n\r\n\tprivate IconButton CreateNavigationButton(string icon, int direction, string tooltip)\r\n\t{\r\n\t\treturn new IconButton(icon, () =>\r\n\t\t{\r\n\t\t\tif (direction < 0)\r\n\t\t\t\tSelectedPointIndex = int.Max(0, SelectedPointIndex - 1);\r\n\t\t\telse\r\n\t\t\t\tSelectedPointIndex = int.Min(_targetComponent.Spline.PointCount - 1, SelectedPointIndex + 1);\r\n\r\n\t\t\tUpdateWindowTitle();\r\n\t\t\tFocus();\r\n\t\t})\r\n\t\t{ ToolTip = tooltip };\r\n\t}\r\n\r\n\r\n\r\n\tprivate IconButton CreateDeleteButton()\r\n\t{\r\n\t\treturn new IconButton(\"delete\", () =>\r\n\t\t{\r\n\t\t\tusing (CreateUndoScope(\"Delete Spline Point\"))\r\n\t\t\t{\r\n\t\t\t\t_targetComponent.Spline.RemovePoint(SelectedPointIndex);\r\n\t\t\t\tSelectedPointIndex = int.Max(0, SelectedPointIndex - 1);\r\n\t\t\t}\r\n\t\t\tUpdateWindowTitle();\r\n\t\t\tFocus();\r\n\t\t})\r\n\t\t{ ToolTip = \"Delete point\" };\r\n\t}\r\n\r\n\r\n\r\n\tprivate IconButton CreateAddButton()\r\n\t{\r\n\t\treturn new IconButton(\"add\", () =>\r\n\t\t{\r\n\t\t\tusing (CreateUndoScope(\"Added Spline Point\"))\r\n\t\t\t{\r\n\t\t\t\tInsertNewPoint();\r\n\t\t\t}\r\n\t\t\tSelectedPointIndex++;\r\n\t\t\tUpdateWindowTitle();\r\n\t\t\tFocus();\r\n\t\t})\r\n\t\t{\r\n\t\t\tToolTip = \"Insert point after current point.\\n\" +\r\n\t\t\t\t\t  \"You can also hold shift while dragging a point to create a new point.\"\r\n\t\t};\r\n\t}\r\n\r\n\r\n\r\n\tprivate void InsertNewPoint()\r\n\t{\r\n\t\tvar spline = _targetComponent.Spline;\r\n\r\n\t\tif (SelectedPointIndex == spline.PointCount - 1)\r\n\t\t{\r\n\t\t\tvar distance = spline.GetDistanceAtPoint(SelectedPointIndex);\r\n\t\t\tvar tangent = spline.SampleAtDistance(distance).Tangent;\r\n\t\t\tvar newPosition = _selectedPoint.Position + tangent * 200;\r\n\r\n\t\t\tspline.InsertPoint(SelectedPointIndex + 1, _selectedPoint with { Position = newPosition });\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar currentDist = spline.GetDistanceAtPoint(SelectedPointIndex);\r\n\t\t\tvar nextDist = spline.GetDistanceAtPoint(SelectedPointIndex + 1);\r\n\t\t\tvar midDist = (currentDist + nextDist) / 2;\r\n\r\n\t\t\tspline.AddPointAtDistance(midDist, true);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateWindowTitle()\r\n\t{\r\n\t\tWindowTitle = _isClosed ? \"\" : $\"Spline Point [{SelectedPointIndex}] Editor - {_targetComponent?.GameObject?.Name ?? \"\"}\";\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CloseWindow()\r\n\t{\r\n\t\t_isClosed = true;\r\n\t\tRebuild();\r\n\t\tPosition = Parent.Size - 32;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Code/RoadComponent/RoadComponent.Lampposts.cs",
            "FileName": "RoadComponent.Lampposts.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Linq;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadComponent\r\n{\r\n\tprivate bool m_DoesLamppostsNeedRebuild = false;\r\n\r\n\t[Property, FeatureEnabled(\"Lampposts\", Icon = \"light_mode\", Tint = EditorTint.Red), Change] private bool HasLampposts { get; set; } = false;\r\n\t[Property, Feature(\"Lampposts\")] public GameObject LamppostPrefab { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } }\r\n\t[Property, Feature(\"Lampposts\"), Range(50.0f, 2000.0f)] private float LamppostSpacing { get; set { field = value.Clamp(10.0f, 100000.0f); m_DoesLamppostsNeedRebuild = true; } } = 50.0f;\r\n\t[Property, Feature(\"Lampposts\"), Range(-200.0f, 200.0f)] private float LamppostOffsetFromSidewalk { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 10.0f;\r\n\t[Property, Feature(\"Lampposts\"), Range(0.0f, 10.0f)] private float LamppostHeightOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;\r\n\t[Property, Feature(\"Lampposts\")] private LamppostSide LamppostPlacement { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = LamppostSide.Both;\r\n\t[Property, Feature(\"Lampposts\")] private bool AlignToSplineRotation { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = true;\r\n\t[Property(Title = \"Keep Vertical (World Up)\"), Feature(\"Lampposts\")] private bool KeepVertical { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = true;\r\n\t[Property, Feature(\"Lampposts\"), Range(0.0f, 360.0f)] private float LamppostRotationOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;\r\n\t[Property, Feature(\"Lampposts\"), Range(0.0f, 100.0f)] private float StartOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;\r\n\t[Property, Feature(\"Lampposts\"), Range(0.0f, 100.0f)] private float EndOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;\r\n\r\n\tpublic enum LamppostSide\r\n\t{\r\n\t\tLeft,\r\n\t\tRight,\r\n\t\tBoth,\r\n\t\tAlternating\r\n\t}\r\n\r\n\r\n\r\n\tprivate void OnHasLamppostsChanged(bool _OldValue, bool _NewValue)\r\n\t{\r\n\t\tm_DoesLamppostsNeedRebuild = true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateLampposts()\r\n\t{\r\n\t\tRemoveLampposts();\r\n\r\n\t\tif (!HasLampposts || !LamppostPrefab.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tBuildLampposts();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveLampposts()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tGameObject containerObject = GameObject.Children.FirstOrDefault(x => x.Name == \"Lampposts\");\r\n\r\n\t\tif (containerObject.IsValid())\r\n\t\t{\r\n\t\t\tcontainerObject.Destroy();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateLampposts()\r\n\t{\r\n\t\tif (m_DoesLamppostsNeedRebuild)\r\n\t\t{\r\n\t\t\tCreateLampposts();\r\n\r\n\t\t\tm_DoesLamppostsNeedRebuild = false;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildLampposts()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tGameObject containerObject = new GameObject(GameObject, true, \"Lampposts\");\r\n\t\tcontainerObject.Tags.Add(\"road_props\");\r\n\r\n\t\tfloat splineLength = Spline.Length;\r\n\t\tfloat effectiveLength = splineLength - StartOffset - EndOffset;\r\n\r\n\t\tif (effectiveLength <= 0)\r\n\t\t\treturn;\r\n\r\n\t\tGetSplineFrameData(out var frames, out var segmentsToKeep);\r\n\r\n\t\tvar simplifiedPositions = new List<(Transform _Frame, float _Distance)>();\r\n\r\n\t\tforeach (int index in segmentsToKeep)\r\n\t\t{\r\n\t\t\tfloat t = (float)index / (frames.Length - 1);\r\n\t\t\tfloat distance = t * splineLength;\r\n\r\n\t\t\tsimplifiedPositions.Add((frames[index], distance));\r\n\t\t}\r\n\r\n\t\tint lamppostCount = Math.Max(1, (int)MathF.Ceiling(effectiveLength / LamppostSpacing));\r\n\t\tint frameCount = lamppostCount + 1;\r\n\r\n\t\tfloat roadEdgeOffset = RoadWidth * 0.5f;\r\n\t\tfloat sidewalkOffset = HasSidewalk ? SidewalkWidth : 0.0f;\r\n\t\tfloat totalOffset = roadEdgeOffset + sidewalkOffset + LamppostOffsetFromSidewalk;\r\n\r\n\t\tfor (int i = 0; i < frameCount; i++)\r\n\t\t{\r\n\t\t\tfloat t = (float)i / (frameCount - 1);\r\n\t\t\tfloat distance = t * splineLength;\r\n\r\n\t\t\t// Skip if outside the start/end offset range\r\n\t\t\tif (distance < StartOffset || distance > splineLength - EndOffset)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Interpolate frame at this distance along the simplified spline\r\n\t\t\tTransform frame = InterpolateFrameAtDistance(simplifiedPositions, distance);\r\n\r\n\t\t\tVector3 basePosition = frame.Position;\r\n\t\t\tVector3 forward = frame.Rotation.Forward;\r\n\t\t\tVector3 up = frame.Rotation.Up;\r\n\t\t\tVector3 right = frame.Rotation.Right;\r\n\r\n\t\t\tbool placeLeft = false;\r\n\t\t\tbool placeRight = false;\r\n\r\n\t\t\tswitch (LamppostPlacement)\r\n\t\t\t{\r\n\t\t\t\tcase LamppostSide.Left:\r\n\t\t\t\t\tplaceLeft = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase LamppostSide.Right:\r\n\t\t\t\t\tplaceRight = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase LamppostSide.Both:\r\n\t\t\t\t\tplaceLeft = true;\r\n\t\t\t\t\tplaceRight = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase LamppostSide.Alternating:\r\n\t\t\t\t\tif (i % 2 == 0)\r\n\t\t\t\t\t\tplaceLeft = true;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tplaceRight = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tdefault:\r\n\t\t\t\t\tplaceLeft = true;\r\n\t\t\t\t\tplaceRight = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\r\n\t\t\tif (placeLeft)\r\n\t\t\t{\r\n\t\t\t\tVector3 leftPosition = basePosition - right * totalOffset + up * (LamppostHeightOffset + (HasSidewalk ? SidewalkHeight : 0.0f));\r\n\t\t\t\tRotation leftRotation = CalculateLamppostRotation(forward, up, LamppostRotationOffset);\r\n\r\n\t\t\t\tCreateLamppost(containerObject, leftPosition, leftRotation);\r\n\t\t\t}\r\n\r\n\t\t\tif (placeRight)\r\n\t\t\t{\r\n\t\t\t\tVector3 rightPosition = basePosition + right * totalOffset + up * (LamppostHeightOffset + (HasSidewalk ? SidewalkHeight : 0.0f));\r\n\t\t\t\tRotation rightRotation = CalculateLamppostRotation(forward, up, LamppostRotationOffset + 180.0f);\r\n\r\n\t\t\t\tCreateLamppost(containerObject, rightPosition, rightRotation);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateLamppost(GameObject _Parent, Vector3 _Position, Rotation _Rotation)\r\n\t{\r\n\t\tif (!LamppostPrefab.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tGameObject lamppostObject = LamppostPrefab.Clone(_Parent, _Position, _Rotation, Vector3.One);\r\n\r\n\t\tif (!lamppostObject.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tlamppostObject.LocalPosition = _Position;\r\n\t\tlamppostObject.LocalRotation = _Rotation;\r\n\t}\r\n\r\n\r\n\r\n\tprivate Rotation CalculateLamppostRotation(Vector3 _Forward, Vector3 _SplineUp, float _YawOffset)\r\n\t{\r\n\t\tif (!AlignToSplineRotation)\r\n\t\t\treturn Rotation.FromYaw(_YawOffset);\r\n\r\n\t\tRotation finalRotation;\r\n\r\n\t\tif (KeepVertical)\r\n\t\t{\r\n\t\t\tVector3 flatForward = _Forward.WithZ(0).Normal;\r\n\r\n\t\t\tif (flatForward.Length > 0.001f)\r\n\t\t\t{\r\n\t\t\t\tfinalRotation = Rotation.LookAt(flatForward, Vector3.Up);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tfinalRotation = Rotation.FromYaw(_YawOffset);\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tfinalRotation = Rotation.LookAt(_Forward, _SplineUp);\r\n\t\t}\r\n\r\n\t\treturn finalRotation * Rotation.FromYaw(_YawOffset);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Code/RoadComponent/RoadComponent.Lines.cs",
            "FileName": "RoadComponent.Lines.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadComponent\r\n{\r\n\t[Property, FeatureEnabled(\"Lines\", Icon = \"show_chart\", Tint = EditorTint.Yellow), Change] private bool HasLines { get; set; } = false;\r\n\t[Property(Title = \"Lines\"), Feature(\"Lines\")] public RoadLineDefinition[] LineDefinitions { get; set { field = value; IsDirty = true; } }\r\n\t[Property(Title = \"Offset\"), Feature(\"Lines\"), Range(0.01f, 1.0f)] private float LinesOffset { get; set { field = value; IsDirty = true; } } = 0.1f;\r\n\t[Property(Title = \"Width\"), Feature(\"Lines\"), Range(1.0f, 50.0f)] private float LinesWidth { get; set { field = value; IsDirty = true; } } = 5.0f;\r\n\t[Property(Title = \"Extra Spacing\"), Feature(\"Lines\"), Range(0.0f, 1000.0f)] private float LinesExtraSpacing { get; set { field = value; IsDirty = true; } } = 0.0f;\r\n\t[Property(Title = \"Texture Repeat\"), Feature(\"Lines\")] private float LinesTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 10.0f;\r\n\r\n\r\n\r\n\tprivate void OnHasLinesChanged(bool _OldValue, bool _NewValue)\r\n\t{\r\n\t\tIsDirty = true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateLines()\r\n\t{\r\n\t\tEnsureLinesMeshExist();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateLines()\r\n\t{\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveLines()\r\n\t{\r\n\t\tRemoveGeneratedMeshChildren(LineSurfaceTag);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnsureLinesMeshExist()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tif (HasGeneratedMeshChildren(LineSurfaceTag))\r\n\t\t\treturn;\r\n\r\n\t\tBuildLinesMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RebuildLinesMesh()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(LineSurfaceTag);\r\n\t\tBuildLinesMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildLinesMesh()\r\n\t{\r\n\t\tif (!HasLines || LineDefinitions == null || LineDefinitions.Length == 0)\r\n\t\t\treturn;\r\n\r\n\t\tGetSplineFrameData(out var frames, out var segmentsToKeep);\r\n\r\n\t\tint finalSegmentCount = segmentsToKeep.Count - 1;\r\n\r\n\t\tif (finalSegmentCount <= 0)\r\n\t\t\treturn;\r\n\r\n\t\tfloat roadWidth = RoadWidth + LinesExtraSpacing;\r\n\t\tfloat lineSpacing = roadWidth / (LineDefinitions.Length + 1);\r\n\r\n\t\tvar polygonMeshes = new PolygonMesh[LineDefinitions.Length];\r\n\t\tfor (int i = 0; i < LineDefinitions.Length; i++)\r\n\t\t\tpolygonMeshes[i] = new PolygonMesh();\r\n\r\n\t\tfloat[] lineDistances = new float[LineDefinitions.Length];\r\n\r\n\t\tfor (int i = 0; i < finalSegmentCount; i++)\r\n\t\t{\r\n\t\t\tint idx0 = segmentsToKeep[i];\r\n\t\t\tint idx1 = segmentsToKeep[i + 1];\r\n\r\n\t\t\tTransform f0 = frames[idx0];\r\n\t\t\tTransform f1 = frames[idx1];\r\n\r\n\t\t\tVector3 p0 = f0.Position;\r\n\t\t\tVector3 p1 = f1.Position;\r\n\r\n\t\t\tVector3 right0 = f0.Rotation.Right;\r\n\r\n\t\t\tfor (int line = 0; line < LineDefinitions.Length; line++)\r\n\t\t\t{\r\n\t\t\t\tfloat offsetFromCenter = ((line + 1) * lineSpacing) - (roadWidth * 0.5f);\r\n\r\n\t\t\t\tVector3 center0 =\r\n\t\t\t\t\tp0 +\r\n\t\t\t\t\tf0.Rotation.Right * offsetFromCenter +\r\n\t\t\t\t\tf0.Rotation.Up * LinesOffset;\r\n\r\n\t\t\t\tVector3 center1 =\r\n\t\t\t\t\tp1 +\r\n\t\t\t\t\tf1.Rotation.Right * offsetFromCenter +\r\n\t\t\t\t\tf1.Rotation.Up * LinesOffset;\r\n\r\n\t\t\t\tfloat segmentLength = Vector3.DistanceBetween(center0, center1);\r\n\t\t\t\tVector3 dir = (center1 - center0).Normal;\r\n\r\n\t\t\t\tfloat remaining = segmentLength;\r\n\t\t\t\tVector3 curCenter = center0;\r\n\r\n\t\t\t\tfloat dashSpacing = LineDefinitions[line]?.DashSpacing ?? 0.0f;\r\n\t\t\t\tfloat dashFillRatio = LineDefinitions[line]?.DashFillRatio ?? 1.0f;\r\n\t\t\t\tfloat dashLength = dashSpacing * dashFillRatio;\r\n\t\t\t\tfloat halfWidth = LinesWidth * 0.5f;\r\n\r\n\t\t\t\tvar polygonMesh = polygonMeshes[line];\r\n\t\t\t\tvar material = LineDefinitions[line]?.Material ?? Material.Load(\"materials/default.vmat\");\r\n\r\n\t\t\t\twhile (remaining > 0.001f)\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat linePos = lineDistances[line];\r\n\t\t\t\t\tfloat cyclePos = dashSpacing > 0 ? linePos % dashSpacing : 0;\r\n\r\n\t\t\t\t\tif (cyclePos < 0.0001f)\r\n\t\t\t\t\t\tcyclePos = 0.0f;\r\n\r\n\t\t\t\t\tif (dashSpacing > 0 && dashSpacing - cyclePos < 0.0001f)\r\n\t\t\t\t\t\tcyclePos = dashSpacing;\r\n\r\n\t\t\t\t\tbool inDash = dashSpacing <= 0 || cyclePos <= dashLength - 0.0001f;\r\n\r\n\t\t\t\t\tfloat step;\r\n\r\n\t\t\t\t\tif (dashSpacing <= 0)\r\n\t\t\t\t\t\tstep = remaining;\r\n\t\t\t\t\telse if (inDash)\r\n\t\t\t\t\t\tstep = dashLength - cyclePos;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tstep = dashSpacing - cyclePos;\r\n\r\n\t\t\t\t\tstep = Math.Max(step, 0.01f);\r\n\t\t\t\t\tstep = Math.Min(step, remaining);\r\n\r\n\t\t\t\t\tVector3 nextCenter = curCenter + dir * step;\r\n\r\n\t\t\t\t\tif (inDash)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tVector3 l0 = curCenter - right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 r0 = curCenter + right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 l1 = nextCenter - right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 r1 = nextCenter + right0 * halfWidth;\r\n\r\n\t\t\t\t\t\tfloat v0 = linePos / LinesTextureRepeat;\r\n\t\t\t\t\t\tfloat v1 = (linePos + step) / LinesTextureRepeat;\r\n\r\n\t\t\t\t\t\tvar verts = polygonMesh.AddVertices(l0, r0, r1, l1);\r\n\t\t\t\t\t\tMeshUtility.AddTexturedQuad(polygonMesh, material, verts[0], verts[1], verts[2], verts[3],\r\n\t\t\t\t\t\t\tnew Vector2(0, v0), new Vector2(1, v0),\r\n\t\t\t\t\t\t\tnew Vector2(1, v1), new Vector2(0, v1));\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tlineDistances[line] += step;\r\n\t\t\t\t\tcurCenter = nextCenter;\r\n\t\t\t\t\tremaining -= step;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfor (int line = 0; line < LineDefinitions.Length; line++)\r\n\t\t{\r\n\t\t\tvar child = new GameObject(GameObject, true, $\"Line_{line}\");\r\n\t\t\tchild.Tags.Add(LineSurfaceTag);\r\n\r\n\t\t\tvar meshComponent = child.AddComponent<MeshComponent>();\r\n\t\t\tmeshComponent.Mesh = polygonMeshes[line];\r\n\t\t\tmeshComponent.Collision = MeshComponent.CollisionType.None;\r\n\t\t\tmeshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;\r\n\t\t\tmeshComponent.SmoothingAngle = 40.0f;\r\n\t\t\tmeshComponent.Static = true;\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Code/RoadParkingLotComponent/RoadParkingLotComponent.cs",
            "FileName": "RoadParkingLotComponent.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\n/// <summary>\r\n/// Generates parking lot lines for parking spaces\r\n/// </summary>\r\n[Icon(\"local_parking\")]\r\npublic partial class RoadParkingLotComponent : Component, Component.ExecuteInEditor\r\n{\r\n\tprivate bool m_IsDirty;\r\n\r\n\tprivate const string LinesTag = \"parking_lines\";\r\n\tprivate const string CurbsTag = \"parking_curbs\";\r\n\r\n\t/// <summary>\r\n\t/// An optional prefab, if non-empty the parking lot will generate a bunch of child gameobjects positioned at each parking spots center.\r\n\t/// (e.g. this allows you to use a gameobject prefab with a car spawner system component attached to it)\r\n\t/// </summary>\r\n\t[Property, Feature(\"General\", Icon = \"public\", Tint = EditorTint.White)] private GameObject SpotPrefab { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// The amount of parking spots you want to generate.\r\n\t/// </summary>\r\n\t[Property, Feature(\"General\"), Range(1, 50)] private int SpotCount { get; set { field = value; m_IsDirty = true; } } = 10;\r\n\r\n\t/// <summary>\r\n\t/// Well that's the parking spot length\r\n\t/// </summary>\r\n\t[Property, Feature(\"General\"), Range(10.0f, 1000.0f)] private float SpotLength { get; set { field = value; m_IsDirty = true; } } = 250.0f;\r\n\r\n\t/// <summary>\r\n\t/// and width...\r\n\t/// </summary>\r\n\t[Property, Feature(\"General\"), Range(10.0f, 1000.0f)] private float SpotWidth { get; set { field = value; m_IsDirty = true; } } = 150.0f;\r\n\r\n\t/// <summary>\r\n\t/// The angle of the parking spots in degrees (0 = perpendicular, 45 = angled, 90 = parallel)\r\n\t/// </summary>\r\n\t[Property, Feature(\"General\"), Range(-90.0f, 90.0f), Step(1.0f)] private float SpotAngle { get; set { field = value; m_IsDirty = true; } } = 0.0f;\r\n\t[Property, Feature(\"General\"), Range(0.5f, 1.0f)] private float SpotAngleThreshold { get; set { field = value; m_IsDirty = true; } } = 0.5f;\r\n\r\n\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tBuildAllMeshes();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tDestroyMeshChildren();\r\n\t\tRemoveParkingSpots();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif (m_IsDirty)\r\n\t\t{\r\n\t\t\tif (!SandboxUtility.IsInPlayMode)\r\n\t\t\t{\r\n\t\t\t\tDestroyMeshChildren();\r\n\t\t\t\tBuildAllMeshes();\r\n\t\t\t}\r\n\r\n\t\t\tm_IsDirty = false;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DestroyMeshChildren()\r\n\t{\r\n\t\tvar toRemove = GameObject.Children\r\n\t\t\t.Where(c => c.Tags.Has(LinesTag) || c.Tags.Has(CurbsTag))\r\n\t\t\t.ToList();\r\n\r\n\t\tforeach (var child in toRemove)\r\n\t\t\tchild.Destroy();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildAllMeshes()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tBuildParkingLines();\r\n\t\tBuildCurbs();\r\n\r\n\t\tRemoveParkingSpots();\r\n\t\tCreateParkingSpots();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tif (!Gizmo.IsSelected)\r\n\t\t\treturn;\r\n\r\n\t\tGizmo.Draw.LineThickness = 2.0f;\r\n\t\tGizmo.Draw.Color = Color.Green.WithAlpha(0.5f);\r\n\r\n\t\tfloat angleRad = SpotAngle.DegreeToRadian();\r\n\t\tfloat sinAngle = float.Sin(angleRad);\r\n\t\tfloat cosAngle = float.Cos(angleRad);\r\n\r\n\t\tfloat spacing = CalculateSpacing();\r\n\r\n\t\t// Draw parking spot outlines\r\n\t\tfor (int i = 0; i < SpotCount; i++)\r\n\t\t{\r\n\t\t\tfloat xPos = i * spacing;\r\n\r\n\t\t\tVector3 frontLeft = new Vector3(xPos, 0, LinesOffset);\r\n\t\t\tVector3 frontRight = new Vector3(xPos + SpotWidth * cosAngle, SpotWidth * sinAngle, LinesOffset);\r\n\t\t\tVector3 backLeft = new Vector3(xPos - SpotLength * sinAngle, SpotLength * cosAngle, LinesOffset);\r\n\t\t\tVector3 backRight = new Vector3(xPos + SpotWidth * cosAngle - SpotLength * sinAngle, SpotWidth * sinAngle + SpotLength * cosAngle, LinesOffset);\r\n\r\n\t\t\tGizmo.Draw.Line(frontLeft, frontRight);\r\n\t\t\tGizmo.Draw.Line(frontRight, backRight);\r\n\t\t\tGizmo.Draw.Line(backRight, backLeft);\r\n\t\t\tGizmo.Draw.Line(backLeft, frontLeft);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateParkingSpots()\r\n\t{\r\n\t\t// If we're in play mode, do not build (Since they're already saved in the scene file)\r\n\t\tif (LoadingScreen.IsVisible || Game.IsPlaying)\r\n\t\t\treturn;\r\n\r\n\t\tif (!SpotPrefab.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tGameObject containerObject = GameObject.Children.FirstOrDefault(x => x.Name == \"ParkingSpots\");\r\n\r\n\t\tif (!containerObject.IsValid())\r\n\t\t\tcontainerObject = new GameObject(GameObject, true, \"ParkingSpots\");\r\n\r\n\t\tfloat angleRad = SpotAngle.DegreeToRadian();\r\n\t\tfloat sinAngle = float.Sin(angleRad);\r\n\t\tfloat cosAngle = float.Cos(angleRad);\r\n\r\n\t\tfloat spacing = CalculateSpacing();\r\n\r\n\t\tfor (int i = 0; i < SpotCount; i++)\r\n\t\t{\r\n\t\t\tfloat xPos = i * spacing;\r\n\r\n\t\t\tfloat centerX = xPos + (SpotWidth * 0.5f * cosAngle) - (SpotLength * 0.5f * sinAngle);\r\n\t\t\tfloat centerY = (SpotWidth * 0.5f * sinAngle) + (SpotLength * 0.5f * cosAngle);\r\n\r\n\t\t\tVector3 position = new Vector3(centerX, centerY, 0);\r\n\r\n\t\t\tGameObject gameObject = SpotPrefab.Clone(new Transform(), containerObject);\r\n\t\t\tgameObject.LocalPosition = position;\r\n\t\t\tgameObject.LocalRotation = Rotation.FromYaw(SpotAngle);\r\n\t\t\tgameObject.NetworkMode = NetworkMode.Object;\r\n\t\t\tgameObject.Network.SetOrphanedMode(NetworkOrphaned.Host);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveParkingSpots()\r\n\t{\r\n\t\t// If we're in play mode, do not remove (Since they're already saved in the scene file)\r\n\t\tif (LoadingScreen.IsVisible || Game.IsPlaying)\r\n\t\t\treturn;\r\n\r\n\t\tGameObject containerObject = GameObject.Children.FirstOrDefault(x => x.Name == \"ParkingSpots\");\r\n\r\n\t\tif (!containerObject.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tforeach (var gameObject in containerObject.Children.Where(x => x.IsValid()))\r\n\t\t{\r\n\t\t\tgameObject.Destroy();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Utility button to directly snap the parking lot to the nearest solid ground\r\n\t/// </summary>\r\n\t[Button(\"Snap to Ground\"), Feature(\"General\"), Order(100)]\r\n\tpublic void SnapToGround()\r\n\t{\r\n\t\tSceneTraceResult trace = Scene.Trace.Ray(WorldPosition, WorldPosition + Vector3.Down * 10000.0f).Run();\r\n\r\n\t\tif (trace.Distance < 0.1f) // Ignore really close hits, bcs that mean the parking lot is already properly grounded\r\n\t\t\treturn;\r\n\r\n\t\tif (!trace.Hit)\r\n\t\t\treturn;\r\n\r\n\t\tWorldPosition = trace.HitPosition;\r\n\t\tWorldRotation = Rotation.LookAt(trace.Normal, Vector3.Up) * Rotation.FromPitch(90.0f);\r\n\t}\r\n\r\n\r\n\r\n\tprivate float CalculateSpacing()\r\n\t{\r\n\t\tfloat angleRad = SpotAngle.DegreeToRadian();\r\n\t\tfloat cosAngle = float.Cos(angleRad);\r\n\r\n\t\treturn SpotWidth / float.Max(cosAngle, SpotAngleThreshold);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Code/Utility/VehicleSpawnResource.cs",
            "FileName": "VehicleSpawnResource.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n[AssetType(Name = \"Vehicle Spawn\", Extension = \"vspawn\", Category = \"Road Tool\")]\npublic sealed class VehicleSpawnResource : GameResource\n{\n\t[Property] public GameObject Prefab { get; set; }\n\n\t[Property, Range(0.0f, 10.0f)] public float Probability { get; set; } = 1.0f;\n\t\n\t\n\t\n\tprotected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)\n\t{\n\t\treturn CreateSimpleAssetTypeIcon(\"car_rental\", _Width, _Height, \"#00ccff\", \"black\");\n\t}\n}\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "ISplineComponent.cs",
            "FileName": "ISplineComponent.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// <summary>\n/// A component that owns an editable <see cref=\"Sandbox.Spline\"/>. Lets the spline editor tool/window drive any of\n/// them (roads, rails, \u2026) without being tied to a single component type.\n/// </summary>\npublic interface ISplineComponent : IValid\n{\n\tSpline Spline { get; }\n\tTransform WorldTransform { get; }\n\tGameObject GameObject { get; }\n}\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "RoadComponent/RoadComponent.cs",
            "FileName": "RoadComponent.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\n/// <summary>\r\n/// Represents a road component that can be manipulated within the editor and at runtime.\r\n/// </summary>\r\n[Icon(\"signpost\")]\r\npublic partial class RoadComponent : Component, Component.ExecuteInEditor, Component.IHasBounds, ISplineComponent\r\n{\r\n\t/// <summary>\r\n\t/// Undo/redo and prefab reloads deserialize a brand new <see cref=\"Sandbox.Spline\"/> into this component\r\n\t/// instead of mutating the existing one, so the setter has to move our change listener over. Otherwise the\r\n\t/// subscription stays on the discarded instance and later edits never mark the mesh dirty again.\r\n\t/// </summary>\r\n\t[Property, Feature(\"General\"), Hide]\r\n\tpublic Spline Spline\r\n\t{\r\n\t\tget;\r\n\t\tset\r\n\t\t{\r\n\t\t\tfield = value;\r\n\r\n\t\t\tSubscribeToSpline();\r\n\t\t\tUpdateData();\r\n\t\t}\r\n\t} = new();\r\n\r\n\tprivate Spline m_SubscribedSpline;\r\n\r\n\tprivate bool m_DoesRoadMeshNeedRebuild;\r\n\r\n\tprivate const string RoadMeshTag = \"road_mesh\";\r\n\tprivate const string RoadSurfaceTag = \"road_surface\";\r\n\tprivate const string SidewalkSurfaceTag = \"road_sidewalk\";\r\n\tprivate const string LineSurfaceTag = \"road_lines\";\r\n\r\n\t[Property, Feature(\"General\", Icon = \"public\", Tint = EditorTint.White), Category(\"Optimization\")] private bool AutoSimplify { get; set { field = value; IsDirty = true; } } = false;\r\n\t[Property, Feature(\"General\"), Category(\"Optimization\"), Range(0.1f, 10.0f)] private float StraightThreshold { get; set { field = value; IsDirty = true; } } = 1.0f; // Degrees - how straight before merging\r\n\t[Property, Feature(\"General\"), Category(\"Optimization\"), Range(2, 50)] private int MinSegmentsToMerge { get; set { field = value; IsDirty = true; } } = 3; // Minimum consecutive straight segments before merging\r\n\r\n\t[Property, Feature(\"General\"), Category(\"Miscellaneous\")] public bool UseRotationMinimizingFrames { get; set { field = value; IsDirty = true; } }\r\n\r\n\t[Property, FeatureEnabled(\"Bridge\", Icon = \"architecture\", Tint = EditorTint.Blue)]\r\n\tprivate bool HasBridge { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = false;\r\n\r\n\t/// <summary>\r\n\t/// This will prevent the bridge from being rebuilt if any property is edited or if the road component get disable and re-enabled.\r\n\t/// Really useful if you plan to edit the mesh with the mapping tool so you don't accidently erase/rebuild the bridge.\r\n\t/// </summary>\r\n\t[Property(Title = \"\ud83d\udd12 Locked\"), Feature(\"Bridge\")]\r\n\tprivate bool IsLocked { get; set; } = false;\r\n\r\n\t/// <summary>\r\n\t/// This is your bridge material you wanna use.\r\n\t/// I recommend using a tileable texture for better result.\r\n\t/// </summary>\r\n\t[Property(Title = \"Material\"), Feature(\"Bridge\"), Group(\"Texturing\"), Order(1)]\r\n\tprivate Material BridgeMaterial { get; set { field = value; m_DoesBridgeNeedRebuild = true; } }\r\n\r\n\t[Property(Title = \"Texture Repeat\"), Feature(\"Bridge\"), Group(\"Texturing\"), Order(1), Step(1)]\r\n\tprivate float BridgeTextureRepeat { get; set { field = value.Clamp(10.0f, 10000.0f); m_DoesBridgeNeedRebuild = true; } } = 500.0f;\r\n\r\n\t[Property(Title = \"Border Width\"), Feature(\"Bridge\"), Group(\"Shape\"), Order(0), Range(10.0f, 500.0f)]\r\n\tprivate float BridgeBorderWidth { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;\r\n\r\n\t[Property(Title = \"Border Height\"), Feature(\"Bridge\"), Group(\"Shape\"), Range(10.0f, 500.0f)]\r\n\tprivate float BridgeBorderHeight { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;\r\n\r\n\t[Property(Title = \"Bottom Depth\"), Feature(\"Bridge\"), Group(\"Shape\"), Range(10.0f, 500.0f)]\r\n\tprivate float BridgeBottomDepth { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;\r\n\r\n\t[Property(Title = \"Close Caps\"), Feature(\"Bridge\"), Group(\"Shape\")]\r\n\tprivate bool BridgeCloseCaps { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;\r\n\r\n\t[Property(Title = \"Pillars\"), Feature(\"Bridge\"), ToggleGroup(\"Pillars\"), Order(2)]\r\n\tprivate bool Pillars { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;\r\n\r\n\t[Property(Title = \"Shape\"), Feature(\"Bridge\"), ToggleGroup(\"Pillars\")]\r\n\tprivate BridgePillarShape BridgePillarType { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = BridgePillarShape.Square;\r\n\r\n\t[Property(Title = \"Size\"), Feature(\"Bridge\"), ToggleGroup(\"Pillars\"), Range(10.0f, 1000.0f)]\r\n\tprivate float BridgePillarSize { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 200.0f;\r\n\r\n\t[Property(Title = \"Height\"), Feature(\"Bridge\"), ToggleGroup(\"Pillars\"), Range(10.0f, 5000.0f)]\r\n\tprivate float BridgePillarHeight { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 600.0f;\r\n\r\n\t[Property(Title = \"Spacing\"), Feature(\"Bridge\"), ToggleGroup(\"Pillars\"), Range(100.0f, 10000.0f)]\r\n\tprivate float BridgePillarSpacing { get; set { field = value.Clamp(100.0f, 100000.0f); m_DoesBridgeNeedRebuild = true; } } = 1200.0f;\r\n\r\n\t[Property(Title = \"Inset\"), Feature(\"Bridge\"), ToggleGroup(\"Pillars\"), Range(0.0f, 200.0f)]\r\n\tprivate float BridgePillarInset { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 20.0f;\r\n\r\n\t[Property(Title = \"Segments\"), Feature(\"Bridge\"), ToggleGroup(\"Pillars\"), Range(3, 24), ShowIf(nameof(BridgePillarType), BridgePillarShape.Cylinder)]\r\n\tprivate int BridgePillarRoundSegments { get; set { field = value.Clamp(3, 64); m_DoesBridgeNeedRebuild = true; } } = 12;\r\n\r\n\t/// <summary>\r\n\t/// Does the pillars follow world up vector or follow the road shape ?\r\n\t/// </summary>\r\n\t[Property(Title = \"Keep Vertical (World Up)\"), Feature(\"Bridge\"), ToggleGroup(\"Pillars\")]\r\n\tprivate bool BridgePillarsKeepVertical { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;\r\n\t\r\n\t[Property, FeatureEnabled(\"Crosswalks\", Icon = \"menu\", Tint = EditorTint.Pink), Change] private bool HasCrosswalks { get; set; } = false;\r\n\t[Property(Title = \"Config\"), Feature(\"Crosswalks\")] public CrosswalkConfig CrosswalkConfig { get; set { field = value; m_DoesCrosswalksNeedsRebuild = true; } } = CrosswalkConfig.Both;\r\n\t[Property(Title = \"Decal Definition\"), Feature(\"Crosswalks\")] public DecalDefinition CrosswalkDefinition { get; set { field = value; m_DoesCrosswalksNeedsRebuild = true; } }\r\n\t[Property(Title = \"Decal Size\"), Feature(\"Crosswalks\"), Range(0.1f, 10.0f)] private Vector2 CrosswalkSize { get; set { field = value; m_DoesCrosswalksNeedsRebuild = true; } } = Vector2.One;\r\n\t\r\n\tprivate bool IsDirty\r\n\t{\r\n\t\tget;\r\n\t\tset\r\n\t\t{\r\n\t\t\tfield = value;\r\n\r\n\t\t\tm_DoesRoadMeshNeedRebuild = value;\r\n\t\t\tm_DoesLamppostsNeedRebuild = value;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic BBox LocalBounds => Spline.Bounds;\r\n\r\n\r\n\r\n\tpublic RoadComponent()\r\n\t{\r\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(0, 0, 0) });\r\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1000, 0, 0) });\r\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1600, 1000, 0) });\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tSubscribeToSpline();\r\n\r\n\t\tEnsureLanes(); // migrate/seed the lane layout before anything that reads it (lines mesh, traffic graph) runs\r\n\r\n\t\tEnsureRoadMeshExist();\r\n\t\tEnsureSidewalkMeshExist();\r\n\t\tEnsureBridgeMeshExist();\r\n\r\n\t\tCreateLines();\r\n\t\tCreateDecals();\r\n\t\tCreateLampposts();\r\n\t\tCreateCrosswalks();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tUnsubscribeFromSpline();\r\n\r\n\t\tRemoveRoadMesh();\r\n\t\tRemoveSidewalkMesh();\r\n\t\tRemoveLines();\r\n\t\tRemoveDecals();\r\n\t\tRemoveLampposts();\r\n\t\tRemoveCrosswalks();\r\n\t\tRemoveBridge();\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Undo/redo restores the serialized spline data in place, which does not raise\r\n\t/// <see cref=\"Sandbox.Spline.SplineChanged\"/>. Rebuilding from here is what makes the mesh follow an undo.\r\n\t/// </summary>\r\n\tprotected override void OnValidate()\r\n\t{\r\n\t\tSubscribeToSpline();\r\n\t\tUpdateData();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tSyncSplineSubscription();\r\n\r\n\t\tUpdateRoadMeshes();\r\n\t\tUpdateLines();\r\n\t\tUpdateDecals();\r\n\t\tUpdateLampposts();\r\n\t\tUpdateCrosswalks();\r\n\t\tUpdateBridge();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateRoadMeshes()\r\n\t{\r\n\t\tif (!m_DoesRoadMeshNeedRebuild)\r\n\t\t\treturn;\r\n\r\n\t\tRebuildRoadMesh();\r\n\t\tRebuildSidewalkMesh();\r\n\t\tRebuildLinesMesh();\r\n\r\n\t\tm_DoesRoadMeshNeedRebuild = false;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RebuildRoadMesh()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tif (IsRoadLocked)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(RoadSurfaceTag);\r\n\t\tBuildRoadMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RebuildSidewalkMesh()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tif (IsSidewalkLocked)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(SidewalkSurfaceTag);\r\n\t\tBuildSidewalkMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveRoadMesh()\r\n\t{\r\n\t\tif (IsRoadLocked)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(RoadSurfaceTag);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveSidewalkMesh()\r\n\t{\r\n\t\tif (IsSidewalkLocked)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(SidewalkSurfaceTag);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveGeneratedMeshChildren(string _Tag)\r\n\t{\r\n\t\tvar toRemove = GameObject.Children.Where(child => child.Tags.Has(_Tag)).ToList();\r\n\r\n\t\tforeach (var child in toRemove)\r\n\t\t\tchild.Destroy();\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool HasGeneratedMeshChildren(string _Tag)\r\n\t{\r\n\t\treturn GameObject.Children.Any(child => child.Tags.Has(_Tag));\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Safety net for editor state changes that swap the spline instance without ever touching the\r\n\t/// property setter or <see cref=\"OnValidate\"/>. A reference compare per frame is cheap enough to\r\n\t/// be worth never silently losing the subscription again.\r\n\t/// </summary>\r\n\tprivate void SyncSplineSubscription()\r\n\t{\r\n\t\tif (ReferenceEquals(m_SubscribedSpline, Spline))\r\n\t\t\treturn;\r\n\r\n\t\tSubscribeToSpline();\r\n\t\tUpdateData();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void SubscribeToSpline()\r\n\t{\r\n\t\tif (ReferenceEquals(m_SubscribedSpline, Spline))\r\n\t\t\treturn;\r\n\r\n\t\tUnsubscribeFromSpline();\r\n\r\n\t\tm_SubscribedSpline = Spline;\r\n\r\n\t\tif (m_SubscribedSpline is not null)\r\n\t\t\tm_SubscribedSpline.SplineChanged += UpdateData;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UnsubscribeFromSpline()\r\n\t{\r\n\t\tif (m_SubscribedSpline is null)\r\n\t\t\treturn;\r\n\r\n\t\tm_SubscribedSpline.SplineChanged -= UpdateData;\r\n\t\tm_SubscribedSpline = null;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateData()\r\n\t{\r\n\t\tif (!GameObject.IsValid() || !Scene.IsEditor)\r\n\t\t\treturn;\r\n\r\n\t\tIsDirty = true;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "RoadComponent/RoadComponent.Lines.cs",
            "FileName": "RoadComponent.Lines.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadComponent\r\n{\r\n\t[Property, FeatureEnabled(\"Lines\", Icon = \"show_chart\", Tint = EditorTint.Yellow), Change] private bool HasLines { get; set; } = false;\r\n\t[Property(Title = \"Lines\"), Feature(\"Lines\")] public RoadLineDefinition[] LineDefinitions { get; set { field = value; IsDirty = true; } }\r\n\t[Property(Title = \"Offset\"), Feature(\"Lines\"), Range(0.01f, 1.0f)] private float LinesOffset { get; set { field = value; IsDirty = true; } } = 0.1f;\r\n\t[Property(Title = \"Width\"), Feature(\"Lines\"), Range(1.0f, 50.0f)] private float LinesWidth { get; set { field = value; IsDirty = true; } } = 5.0f;\r\n\t[Property(Title = \"Extra Spacing\"), Feature(\"Lines\"), Range(0.0f, 1000.0f)] private float LinesExtraSpacing { get; set { field = value; IsDirty = true; } } = 0.0f;\r\n\t[Property(Title = \"Texture Repeat\"), Feature(\"Lines\")] private float LinesTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 10.0f;\r\n\r\n\r\n\r\n\tprivate void OnHasLinesChanged(bool _OldValue, bool _NewValue)\r\n\t{\r\n\t\tIsDirty = true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateLines()\r\n\t{\r\n\t\tEnsureLinesMeshExist();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateLines()\r\n\t{\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveLines()\r\n\t{\r\n\t\tRemoveGeneratedMeshChildren(LineSurfaceTag);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnsureLinesMeshExist()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tif (HasGeneratedMeshChildren(LineSurfaceTag))\r\n\t\t\treturn;\r\n\r\n\t\tBuildLinesMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RebuildLinesMesh()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(LineSurfaceTag);\r\n\t\tBuildLinesMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildLinesMesh()\r\n\t{\r\n\t\tif (!HasLines || LineDefinitions == null || LineDefinitions.Length == 0)\r\n\t\t\treturn;\r\n\r\n\t\tGetSplineFrameData(out var frames, out var segmentsToKeep);\r\n\r\n\t\tint finalSegmentCount = segmentsToKeep.Count - 1;\r\n\r\n\t\tif (finalSegmentCount <= 0)\r\n\t\t\treturn;\r\n\r\n\t\tfloat roadWidth = RoadWidth + LinesExtraSpacing;\r\n\t\tfloat lineSpacing = roadWidth / (LineDefinitions.Length + 1);\r\n\r\n\t\tvar polygonMeshes = new PolygonMesh[LineDefinitions.Length];\r\n\t\tfor (int i = 0; i < LineDefinitions.Length; i++)\r\n\t\t\tpolygonMeshes[i] = new PolygonMesh();\r\n\r\n\t\tfloat[] lineDistances = new float[LineDefinitions.Length];\r\n\r\n\t\tfor (int i = 0; i < finalSegmentCount; i++)\r\n\t\t{\r\n\t\t\tint idx0 = segmentsToKeep[i];\r\n\t\t\tint idx1 = segmentsToKeep[i + 1];\r\n\r\n\t\t\tTransform f0 = frames[idx0];\r\n\t\t\tTransform f1 = frames[idx1];\r\n\r\n\t\t\tVector3 p0 = f0.Position;\r\n\t\t\tVector3 p1 = f1.Position;\r\n\r\n\t\t\tVector3 right0 = f0.Rotation.Right;\r\n\r\n\t\t\tfor (int line = 0; line < LineDefinitions.Length; line++)\r\n\t\t\t{\r\n\t\t\t\tfloat offsetFromCenter = ((line + 1) * lineSpacing) - (roadWidth * 0.5f);\r\n\r\n\t\t\t\tVector3 center0 =\r\n\t\t\t\t\tp0 +\r\n\t\t\t\t\tf0.Rotation.Right * offsetFromCenter +\r\n\t\t\t\t\tf0.Rotation.Up * LinesOffset;\r\n\r\n\t\t\t\tVector3 center1 =\r\n\t\t\t\t\tp1 +\r\n\t\t\t\t\tf1.Rotation.Right * offsetFromCenter +\r\n\t\t\t\t\tf1.Rotation.Up * LinesOffset;\r\n\r\n\t\t\t\tfloat segmentLength = Vector3.DistanceBetween(center0, center1);\r\n\t\t\t\tVector3 dir = (center1 - center0).Normal;\r\n\r\n\t\t\t\tfloat remaining = segmentLength;\r\n\t\t\t\tVector3 curCenter = center0;\r\n\r\n\t\t\t\tfloat dashSpacing = LineDefinitions[line]?.DashSpacing ?? 0.0f;\r\n\t\t\t\tfloat dashFillRatio = LineDefinitions[line]?.DashFillRatio ?? 1.0f;\r\n\t\t\t\tfloat dashLength = dashSpacing * dashFillRatio;\r\n\t\t\t\tfloat halfWidth = LinesWidth * 0.5f;\r\n\r\n\t\t\t\tvar polygonMesh = polygonMeshes[line];\r\n\t\t\t\tvar material = LineDefinitions[line]?.Material ?? Material.Load(\"materials/default.vmat\");\r\n\r\n\t\t\t\twhile (remaining > 0.001f)\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat linePos = lineDistances[line];\r\n\t\t\t\t\tfloat cyclePos = dashSpacing > 0 ? linePos % dashSpacing : 0;\r\n\r\n\t\t\t\t\tif (cyclePos < 0.0001f)\r\n\t\t\t\t\t\tcyclePos = 0.0f;\r\n\r\n\t\t\t\t\tif (dashSpacing > 0 && dashSpacing - cyclePos < 0.0001f)\r\n\t\t\t\t\t\tcyclePos = dashSpacing;\r\n\r\n\t\t\t\t\tbool inDash = dashSpacing <= 0 || cyclePos <= dashLength - 0.0001f;\r\n\r\n\t\t\t\t\tfloat step;\r\n\r\n\t\t\t\t\tif (dashSpacing <= 0)\r\n\t\t\t\t\t\tstep = remaining;\r\n\t\t\t\t\telse if (inDash)\r\n\t\t\t\t\t\tstep = dashLength - cyclePos;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tstep = dashSpacing - cyclePos;\r\n\r\n\t\t\t\t\tstep = Math.Max(step, 0.01f);\r\n\t\t\t\t\tstep = Math.Min(step, remaining);\r\n\r\n\t\t\t\t\tVector3 nextCenter = curCenter + dir * step;\r\n\r\n\t\t\t\t\tif (inDash)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tVector3 l0 = curCenter - right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 r0 = curCenter + right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 l1 = nextCenter - right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 r1 = nextCenter + right0 * halfWidth;\r\n\r\n\t\t\t\t\t\tfloat v0 = linePos / LinesTextureRepeat;\r\n\t\t\t\t\t\tfloat v1 = (linePos + step) / LinesTextureRepeat;\r\n\r\n\t\t\t\t\t\tvar verts = polygonMesh.AddVertices(l0, r0, r1, l1);\r\n\t\t\t\t\t\tMeshUtility.AddTexturedQuad(polygonMesh, material, verts[0], verts[1], verts[2], verts[3],\r\n\t\t\t\t\t\t\tnew Vector2(0, v0), new Vector2(1, v0),\r\n\t\t\t\t\t\t\tnew Vector2(1, v1), new Vector2(0, v1));\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tlineDistances[line] += step;\r\n\t\t\t\t\tcurCenter = nextCenter;\r\n\t\t\t\t\tremaining -= step;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfor (int line = 0; line < LineDefinitions.Length; line++)\r\n\t\t{\r\n\t\t\tvar child = new GameObject(GameObject, true, $\"Line_{line}\");\r\n\t\t\tchild.Tags.Add(LineSurfaceTag);\r\n\r\n\t\t\tvar meshComponent = child.AddComponent<MeshComponent>();\r\n\t\t\tmeshComponent.Mesh = polygonMeshes[line];\r\n\t\t\tmeshComponent.Collision = MeshComponent.CollisionType.None;\r\n\t\t\tmeshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;\r\n\t\t\tmeshComponent.SmoothingAngle = 40.0f;\r\n\t\t\tmeshComponent.Static = true;\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Utility/MeshUtility.cs",
            "FileName": "MeshUtility.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic static class MeshUtility\r\n{\r\n\tpublic static HalfEdgeMesh.VertexHandle GetOrAddVertex(PolygonMesh _Mesh, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, Vector3 _Pos)\r\n\t{\r\n\t\tif (!_Cache.TryGetValue(_Pos, out var handle))\r\n\t\t{\r\n\t\t\thandle = _Mesh.AddVertices(_Pos)[0];\r\n\t\t\t_Cache[_Pos] = handle;\r\n\t\t}\r\n\r\n\t\treturn handle;\r\n\t}\r\n\r\n\r\n\r\n\tpublic static void AddTexturedQuad(PolygonMesh _Mesh, Material _Material,\n\t\tHalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,\n\t\tHalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D,\n\t\tVector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)\n\t{\n\t\tif (HasDuplicateVertex(_A, _B, _C, _D))\n\t\t\treturn;\n\n\t\tvar face = _Mesh.AddFace(_A, _B, _C, _D);\n\r\n\t\tif (!face.IsValid)\r\n\t\t\treturn;\r\n\r\n\t\t_Mesh.SetFaceMaterial(face, _Material);\r\n\t\t_Mesh.SetFaceTextureCoords(face, new List<Vector2> { _UvA, _UvB, _UvC, _UvD });\r\n\t}\r\n\r\n\r\n\r\n\tpublic static void AddTexturedTriangle(PolygonMesh _Mesh, Material _Material,\n\t\tHalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,\n\t\tHalfEdgeMesh.VertexHandle _C,\n\t\tVector2 _UvA, Vector2 _UvB, Vector2 _UvC)\n\t{\n\t\tif (HasDuplicateVertex(_A, _B, _C))\n\t\t\treturn;\n\n\t\tvar face = _Mesh.AddFace(_A, _B, _C);\n\r\n\t\tif (!face.IsValid)\r\n\t\t\treturn;\r\n\r\n\t\t_Mesh.SetFaceMaterial(face, _Material);\n\t\t_Mesh.SetFaceTextureCoords(face, new List<Vector2> { _UvA, _UvB, _UvC });\n\t}\n\n\n\n\tprivate static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C)\n\t{\n\t\treturn _A.Equals(_B) || _A.Equals(_C) || _B.Equals(_C);\n\t}\n\n\n\n\tprivate static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D)\n\t{\n\t\treturn _A.Equals(_B) || _A.Equals(_C) || _A.Equals(_D) ||\n\t\t\t_B.Equals(_C) || _B.Equals(_D) ||\n\t\t\t_C.Equals(_D);\n\t}\n}\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Code/RailComponent/RailComponent.Fishplates.cs",
            "FileName": "RailComponent.Fishplates.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\npublic partial class RailComponent\n{\n\t[Property, FeatureEnabled(\"Fishplates\", Icon = \"link\", Tint = EditorTint.Green)] private bool HasFishplates { get; set { field = value; IsDirty = true; } } = true;\n\t[Property(Title = \"Material\"), Feature(\"Fishplates\")] private Material FishplateMaterial { get; set { field = value; IsDirty = true; } }\n\n\t/// <summary>Distance along the track between joints. Real rail comes in long sections, so this is much larger than the sleeper spacing.</summary>\n\t[Property(Title = \"Spacing\"), Feature(\"Fishplates\"), Range(100.0f, 5000.0f)] private float FishplateSpacing { get; set { field = value; IsDirty = true; } } = 1200.0f;\n\n\t/// <summary>Plate length along the track (it straddles the joint between two rail sections).</summary>\n\t[Property(Title = \"Length\"), Feature(\"Fishplates\"), Range(10.0f, 500.0f)] private float FishplateLength { get; set { field = value; IsDirty = true; } } = 80.0f;\n\n\t/// <summary>Plate height \u2014 sized to sit against the rail web, between the foot and the head.</summary>\n\t[Property(Title = \"Height\"), Feature(\"Fishplates\"), Range(2.0f, 100.0f)] private float FishplateHeight { get; set { field = value; IsDirty = true; } } = 14.0f;\n\n\t/// <summary>How far the plate stands out from the rail web.</summary>\n\t[Property(Title = \"Thickness\"), Feature(\"Fishplates\"), Range(1.0f, 50.0f)] private float FishplateThickness { get; set { field = value; IsDirty = true; } } = 6.0f;\n\n\t/// <summary>Radius of the hexagonal bolt heads.</summary>\n\t[Property(Title = \"Bolt Radius\"), Feature(\"Fishplates\"), Range(0.5f, 20.0f)] private float FishplateBoltRadius { get; set { field = value; IsDirty = true; } } = 3.5f;\n\n\t/// <summary>How far the bolt heads stand out from the plate.</summary>\n\t[Property(Title = \"Bolt Depth\"), Feature(\"Fishplates\"), Range(0.5f, 20.0f)] private float FishplateBoltDepth { get; set { field = value; IsDirty = true; } } = 3.0f;\n\n\t[Property(Title = \"Texture Repeat\"), Feature(\"Fishplates\")] private float FishplateTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 100.0f;\n\n\n\n\tprivate void BuildFishplates(Transform[] _Frames)\n\t{\n\t\tfloat length = Spline.Length;\n\n\t\tif (length <= 0.0f)\n\t\t\treturn;\n\n\t\tfloat spacing = Math.Max(1.0f, FishplateSpacing);\n\n\t\tint count = (int)MathF.Floor(length / spacing);\n\n\t\tif (count < 1)\n\t\t\treturn; // track is shorter than one rail section \u2014 no joints\n\n\t\tvar material = FishplateMaterial ?? Material.Load(\"materials/dev/reflectivity_50.vmat\");\n\t\tvar polygonMesh = new PolygonMesh();\n\n\t\tfloat textureRepeat = Math.Max(1.0f, FishplateTextureRepeat);\n\t\tfloat baseHeight = HasSleepers ? SleeperHeight : 0.0f;\n\t\tfloat halfGauge = RailGauge * 0.5f;\n\n\t\tint placed = 0;\n\n\t\tfor (int s = 1; s <= count; s++)\n\t\t{\n\t\t\tfloat distance = s * spacing;\n\n\t\t\tif (distance >= length)\n\t\t\t\tbreak; // don't drop a joint right on the end of the track\n\n\t\t\tvar frame = SampleFrameAtDistance(_Frames, distance, length);\n\n\t\t\t// One plate on the OUTER side of each rail (the outward sign flips the side for the left rail).\n\t\t\tAddFishplate(polygonMesh, material, frame, halfGauge, 1.0f, baseHeight, textureRepeat);\n\t\t\tAddFishplate(polygonMesh, material, frame, halfGauge, -1.0f, baseHeight, textureRepeat);\n\n\t\t\tplaced++;\n\t\t}\n\n\t\tif (placed == 0)\n\t\t\treturn;\n\n\t\tCreateFishplateChild(\"Fishplates\", polygonMesh);\n\t}\n\n\n\n\t/// <summary>Adds one bolted joint plate against the outer face of a single rail's web.</summary>\n\tprivate void AddFishplate(PolygonMesh _Mesh, Material _Material, Transform _Frame, float _HalfGauge, float _OutwardSign, float _BaseHeight, float _TextureRepeat)\n\t{\n\t\tVector3 origin = _Frame.Position;\n\t\tVector3 right = _Frame.Rotation.Right;\n\t\tVector3 forward = _Frame.Rotation.Forward;\n\t\tVector3 up = _Frame.Rotation.Up;\n\n\t\tVector3 outward = right * _OutwardSign; // points away from the track centre, toward the outer side of this rail\n\n\t\tfloat halfWeb = RailWidth * RailWebScale * 0.5f;\n\t\tfloat webMid = _BaseHeight + RailHeight * 0.5f;\n\n\t\tfloat embed = MathF.Min(1.0f, halfWeb * 0.5f); // bite into the web so the hidden inner face never z-fights it\n\t\tfloat innerDist = halfWeb - embed;\n\t\tfloat outerDist = halfWeb + FishplateThickness;\n\t\tfloat centreDist = (innerDist + outerDist) * 0.5f;\n\t\tfloat halfOut = (outerDist - innerDist) * 0.5f;\n\n\t\tfloat halfLength = FishplateLength * 0.5f;\n\t\tfloat halfHeight = FishplateHeight * 0.5f;\n\n\t\tVector3 railCentre = origin + right * (_HalfGauge * _OutwardSign);\n\t\tVector3 plateCentre = railCentre + outward * centreDist + up * webMid;\n\n\t\tAddBox(_Mesh, _Material, plateCentre, outward, forward, up, halfOut, halfLength, halfHeight, _TextureRepeat);\n\n\t\t// Two hex bolts standing out of the plate's outer face, spread along the joint.\n\t\tVector3 outerFace = plateCentre + outward * halfOut;\n\t\tfloat boltOffset = FishplateLength * 0.25f;\n\n\t\tAddHexBolt(_Mesh, _Material, outerFace + forward * boltOffset, outward, forward, FishplateBoltRadius, FishplateBoltDepth, _TextureRepeat);\n\t\tAddHexBolt(_Mesh, _Material, outerFace - forward * boltOffset, outward, forward, FishplateBoltRadius, FishplateBoltDepth, _TextureRepeat);\n\t}\n\n\n\n\t/// <summary>A closed box built from three frame axes, textured with a uniform box (cube) projection.</summary>\n\tprivate static void AddBox(PolygonMesh _Mesh, Material _Material, Vector3 _Centre, Vector3 _AxisR, Vector3 _AxisF, Vector3 _AxisU, float _HalfR, float _HalfF, float _HalfU, float _TextureRepeat)\n\t{\n\t\t// The faces below wind outward only for a right-handed basis. The outward axis flips for the left rail, which\n\t\t// would make it left-handed and flip every face to a backface \u2014 mirror one axis to keep it right-handed (the\n\t\t// box is symmetric, so this changes nothing but the winding).\n\t\tif (Vector3.Dot(Vector3.Cross(_AxisR, _AxisF), _AxisU) < 0.0f)\n\t\t\t_AxisF = -_AxisF;\n\n\t\tVector3 Corner(float r, float f, float u) => _Centre + _AxisR * r + _AxisF * f + _AxisU * u;\n\n\t\tvar c000 = Corner(-_HalfR, -_HalfF, -_HalfU);\n\t\tvar c100 = Corner(_HalfR, -_HalfF, -_HalfU);\n\t\tvar c110 = Corner(_HalfR, _HalfF, -_HalfU);\n\t\tvar c010 = Corner(-_HalfR, _HalfF, -_HalfU);\n\t\tvar c001 = Corner(-_HalfR, -_HalfF, _HalfU);\n\t\tvar c101 = Corner(_HalfR, -_HalfF, _HalfU);\n\t\tvar c111 = Corner(_HalfR, _HalfF, _HalfU);\n\t\tvar c011 = Corner(-_HalfR, _HalfF, _HalfU);\n\n\t\tfloat r0 = -_HalfR / _TextureRepeat, r1 = _HalfR / _TextureRepeat;\n\t\tfloat f0 = -_HalfF / _TextureRepeat, f1 = _HalfF / _TextureRepeat;\n\t\tfloat u0 = -_HalfU / _TextureRepeat, u1 = _HalfU / _TextureRepeat;\n\n\t\tAddQuad(_Mesh, _Material, c100, c110, c111, c101, new Vector2(f0, u0), new Vector2(f1, u0), new Vector2(f1, u1), new Vector2(f0, u1)); // +R\n\t\tAddQuad(_Mesh, _Material, c010, c000, c001, c011, new Vector2(f1, u0), new Vector2(f0, u0), new Vector2(f0, u1), new Vector2(f1, u1)); // -R\n\t\tAddQuad(_Mesh, _Material, c110, c010, c011, c111, new Vector2(r1, u0), new Vector2(r0, u0), new Vector2(r0, u1), new Vector2(r1, u1)); // +F\n\t\tAddQuad(_Mesh, _Material, c000, c100, c101, c001, new Vector2(r0, u0), new Vector2(r1, u0), new Vector2(r1, u1), new Vector2(r0, u1)); // -F\n\t\tAddQuad(_Mesh, _Material, c001, c101, c111, c011, new Vector2(r0, f0), new Vector2(r1, f0), new Vector2(r1, f1), new Vector2(r0, f1)); // +U\n\t\tAddQuad(_Mesh, _Material, c000, c010, c110, c100, new Vector2(r0, f0), new Vector2(r0, f1), new Vector2(r1, f1), new Vector2(r1, f0)); // -U\n\t}\n\n\n\n\t/// <summary>A hexagonal bolt head \u2014 a 6-sided prism standing out of the plate along <paramref name=\"_AxisOut\"/>, capped on the outer end.</summary>\n\tprivate static void AddHexBolt(PolygonMesh _Mesh, Material _Material, Vector3 _Base, Vector3 _AxisOut, Vector3 _Forward, float _Radius, float _Depth, float _TextureRepeat)\n\t{\n\t\tconst int sides = 6;\n\n\t\t// t2 completes a right-handed (out, t1, t2) basis so the winding below stays outward-facing on either rail.\n\t\tVector3 t1 = _Forward;\n\t\tVector3 t2 = Vector3.Cross(_AxisOut, t1);\n\n\t\tvar baseRing = new Vector3[sides];\n\t\tvar topRing = new Vector3[sides];\n\n\t\tfor (int k = 0; k < sides; k++)\n\t\t{\n\t\t\tfloat angle = MathF.PI * 2.0f * k / sides;\n\t\t\tVector3 radial = t1 * MathF.Cos(angle) + t2 * MathF.Sin(angle);\n\n\t\t\tbaseRing[k] = _Base + radial * _Radius;\n\t\t\ttopRing[k] = baseRing[k] + _AxisOut * _Depth;\n\t\t}\n\n\t\tfloat edge = _Radius; // a regular hexagon's side length equals its radius\n\t\tfloat v1 = _Depth / _TextureRepeat;\n\n\t\tfor (int k = 0; k < sides; k++)\n\t\t{\n\t\t\tint next = (k + 1) % sides;\n\t\t\tfloat u0 = edge * k / _TextureRepeat;\n\t\t\tfloat u1 = edge * (k + 1) / _TextureRepeat;\n\n\t\t\tAddQuad(_Mesh, _Material, baseRing[k], baseRing[next], topRing[next], topRing[k],\n\t\t\t\tnew Vector2(u0, 0.0f), new Vector2(u1, 0.0f), new Vector2(u1, v1), new Vector2(u0, v1));\n\t\t}\n\n\t\tVector2 CapUv(Vector3 _P) => new Vector2(Vector3.Dot(_P - _Base, t1), Vector3.Dot(_P - _Base, t2)) / _TextureRepeat;\n\n\t\tfor (int k = 1; k < sides - 1; k++)\n\t\t{\n\t\t\tAddTri(_Mesh, _Material, topRing[0], topRing[k], topRing[k + 1],\n\t\t\t\tCapUv(topRing[0]), CapUv(topRing[k]), CapUv(topRing[k + 1]));\n\t\t}\n\t}\n\n\n\n\t// Own vertices per face so every plate edge and bolt facet stays hard.\n\tprivate static void AddQuad(PolygonMesh _Mesh, Material _Material, Vector3 _A, Vector3 _B, Vector3 _C, Vector3 _D, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)\n\t{\n\t\tvar v = _Mesh.AddVertices(_A, _B, _C, _D);\n\n\t\tMeshUtility.AddTexturedQuad(_Mesh, _Material, v[0], v[1], v[2], v[3], _UvA, _UvB, _UvC, _UvD);\n\t}\n\n\n\n\tprivate static void AddTri(PolygonMesh _Mesh, Material _Material, Vector3 _A, Vector3 _B, Vector3 _C, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC)\n\t{\n\t\tvar v = _Mesh.AddVertices(_A, _B, _C);\n\n\t\tMeshUtility.AddTexturedTriangle(_Mesh, _Material, v[0], v[1], v[2], _UvA, _UvB, _UvC);\n\t}\n\n\n\n\tprivate void CreateFishplateChild(string _Name, PolygonMesh _PolygonMesh)\n\t{\n\t\tvar child = new GameObject(GameObject, true, _Name);\n\t\tchild.Tags.Add(RailMeshTag);\n\t\tchild.Tags.Add(FishplateSurfaceTag);\n\n\t\tvar meshComponent = child.AddComponent<MeshComponent>();\n\t\tmeshComponent.Mesh = _PolygonMesh;\n\t}\n}\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Code/RailComponent/RailComponent.Utility.cs",
            "FileName": "RailComponent.Utility.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\npublic partial class RailComponent\n{\n\tprivate static Transform[] CalculateTangentFramesUsingUpDir(Spline _Spline, int _FrameCount)\n\t{\n\t\tvar frames = new Transform[_FrameCount];\n\t\tfloat totalSplineLength = _Spline.Length;\n\n\t\tvar sample = _Spline.SampleAtDistance(0.0f);\n\t\tsample.Up = Vector3.Up;\n\n\t\t// Fall back to a different up if the tangent runs (nearly) straight up.\n\t\tif (MathF.Abs(Vector3.Dot(sample.Tangent, sample.Up)) > 0.999f)\n\t\t\tsample.Up = Vector3.Right;\n\n\t\tfor (int i = 0; i < _FrameCount; i++)\n\t\t{\n\t\t\tfloat t = _FrameCount > 1 ? (float)i / (_FrameCount - 1) : 0.0f;\n\t\t\tfloat distance = t * totalSplineLength;\n\n\t\t\tsample = _Spline.SampleAtDistance(distance);\n\n\t\t\tvar up = Rotation.FromAxis(sample.Tangent, sample.Roll) * sample.Up;\n\t\t\tRotation rotation = Rotation.LookAt(sample.Tangent, up);\n\n\t\t\tframes[i] = new Transform(sample.Position, rotation, sample.Scale);\n\t\t}\n\n\t\treturn frames;\n\t}\n\n\n\n\tprivate static Transform[] CalculateRotationMinimizingTangentFrames(Spline _Spline, int _FrameCount)\n\t{\n\t\tvar frames = new Transform[_FrameCount];\n\t\tfloat totalSplineLength = _Spline.Length;\n\n\t\tvar previousSample = _Spline.SampleAtDistance(0.0f);\n\t\tVector3 up = Vector3.Up;\n\n\t\tif (MathF.Abs(Vector3.Dot(previousSample.Tangent, up)) > 0.999f)\n\t\t\tup = Vector3.Right;\n\n\t\tup = Rotation.FromAxis(previousSample.Tangent, previousSample.Roll) * up;\n\n\t\tframes[0] = new Transform(previousSample.Position, Rotation.LookAt(previousSample.Tangent, up), previousSample.Scale);\n\n\t\tfor (int i = 1; i < _FrameCount; i++)\n\t\t{\n\t\t\tfloat t = _FrameCount > 1 ? (float)i / (_FrameCount - 1) : 0.0f;\n\t\t\tfloat distance = t * totalSplineLength;\n\n\t\t\tvar sample = _Spline.SampleAtDistance(distance);\n\n\t\t\t// Parallel-transport the up vector so the profile doesn't twist through 3D curves.\n\t\t\tup = GetRotationMinimizingNormal(previousSample.Position, previousSample.Tangent, up, sample.Position, sample.Tangent);\n\n\t\t\tfloat deltaRoll = sample.Roll - previousSample.Roll;\n\t\t\tup = Rotation.FromAxis(sample.Tangent, deltaRoll) * up;\n\n\t\t\tRotation rotation = Rotation.LookAt(sample.Tangent, up);\n\t\t\tframes[i] = new Transform(sample.Position, rotation, sample.Scale);\n\n\t\t\tpreviousSample = sample;\n\t\t}\n\n\t\treturn frames;\n\t}\n\n\n\n\tprivate static Vector3 GetRotationMinimizingNormal(Vector3 _PosA, Vector3 _TangentA, Vector3 _NormalA, Vector3 _PosB, Vector3 _TangentB)\n\t{\n\t\t// Source: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf\n\t\tVector3 v1 = _PosB - _PosA;\n\n\t\tfloat v1DotV1Half = Vector3.Dot(v1, v1) / 2.0f;\n\n\t\tif (v1DotV1Half <= 0.0001f)\n\t\t\treturn _NormalA;\n\n\t\tfloat r1 = Vector3.Dot(v1, _NormalA) / v1DotV1Half;\n\t\tfloat r2 = Vector3.Dot(v1, _TangentA) / v1DotV1Half;\n\n\t\tVector3 nL = _NormalA - r1 * v1;\n\t\tVector3 tL = _TangentA - r2 * v1;\n\t\tVector3 v2 = _TangentB - tL;\n\n\t\tfloat r3 = Vector3.Dot(v2, nL) / Vector3.Dot(v2, v2);\n\n\t\treturn (nL - 2.0f * r3 * v2).Normal;\n\t}\n}\n"
        },
        {
            "Ident": "redsnail.roadtool",
            "Path": "Code/RoadIntersectionComponent/RoadIntersectionComponent.Sidewalk.cs",
            "FileName": "RoadIntersectionComponent.Sidewalk.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 342326,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// <summary>\n/// How much of a junction reaches the pedestrian graph. Geometry is never affected \u2014 the pavement mesh builds\n/// the same either way; this only decides what pedestrians are allowed to route over.\n///\n/// Ordered most-to-least deliberately, so the zero value is the harmless one: a component that somehow arrives\n/// without this field set behaves like every other junction rather than silently vanishing from the graph.\n/// </summary>\npublic enum SidewalkGraphMode\n{\n\t/// <summary>Pavement round the corners and a crossing at every arm.</summary>\n\tAll,\n\n\t/// <summary>\n\t/// Corners only. The pavement stays joined all the way round and connects to every road that meets here,\n\t/// but there's nothing to step off the kerb onto \u2014 pedestrians walk round the junction instead of over it.\n\t/// </summary>\n\tNoCrossing,\n\n\t/// <summary>\n\t/// Nothing at all. The roads that meet here keep their own pavements; they just stop being connected\n\t/// THROUGH this junction, so expect their ends to show as dead ends.\n\t/// </summary>\n\tNone\n}\n\n\n\npublic partial class RoadIntersectionComponent\n{\n\t/// <summary>\n\t/// What this junction contributes to the pedestrian graph \u2014 the pedestrian counterpart of\n\t/// <see cref=\"ExcludeTraffic\"/>.\n\t///\n\t/// <see cref=\"SidewalkGraphMode.None\"/> for somewhere nobody should be walking round at all: a slip road, a\n\t/// service yard, a junction whose pavement exists only because the mesh needs an edge.\n\t/// <see cref=\"SidewalkGraphMode.NoCrossing\"/> for somewhere they may walk past but not across \u2014 a forecourt\n\t/// or car park entrance, where the kerb should stay continuous and stepping into the vehicle route is the\n\t/// thing you're trying to prevent.\n\t/// </summary>\n\t[Property, Feature(\"General\"), Category(\"Sidewalk\"), Order(3)] public SidewalkGraphMode SidewalkGraph { get; set; } = SidewalkGraphMode.All;\n\n\n\n\t/// <summary>\n\t/// The walking line around this junction's pavement, in world space \u2014 a closed loop running down the\n\t/// middle of the sidewalk slab.\n\t///\n\t/// Built from the junction's OWN outline, which is the whole point: a rectangular intersection's pavement\n\t/// runs along its edges and turns at its corners, and approximating that with an arc around the centre\n\t/// bows the path out into the road at the middle of each side and cuts the corners off. A circle really is\n\t/// an arc, so it gets one.\n\t///\n\t/// The loop runs all the way round, arm mouths included \u2014 whoever consumes it is expected to split it at\n\t/// the kerbs, because where the pavement is interrupted is the same question as where the crossings go.\n\t/// </summary>\n\tpublic List<Vector3> GetSidewalkOutline(float _Spacing)\n\t{\n\t\tvar points = new List<Vector3>();\n\n\t\tif (!HasSidewalks)\n\t\t\treturn points;\n\n\t\tfloat spacing = Math.Max(1.0f, _Spacing);\n\n\t\t// Centre of the slab, so it sits where someone would actually walk rather than on either kerb.\n\t\tfloat outset = SidewalkWidth * 0.5f;\n\t\tVector3 lift = Vector3.Up * SidewalkHeight;\n\n\t\tif (Shape == IntersectionShape.Circle)\n\t\t{\n\t\t\tfloat radius = Radius + outset;\n\t\t\tint steps = Math.Max(8, (int)MathF.Ceiling(MathF.Tau * radius / spacing));\n\n\t\t\tfor (int i = 0; i < steps; i++)\n\t\t\t{\n\t\t\t\tfloat angle = MathF.Tau * i / steps;\n\n\t\t\t\tpoints.Add(new Vector3(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius, 0.0f) + lift);\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Written with the same right/forward vectors BuildRectangleRoad uses, rather than as raw x/y\n\t\t\t// components. Width runs along Right and Length along Forward, which in s&box axes is Y and X \u2014\n\t\t\t// spelling that out by hand gets them the wrong way round, and a junction outline rotated 90\u00b0\n\t\t\t// looks almost plausible until nothing connects to it.\n\t\t\tVector3 right = Vector3.Right;\n\t\t\tVector3 forward = Vector3.Forward;\n\n\t\t\tfloat hw = Width * 0.5f + outset;\n\t\t\tfloat hl = Length * 0.5f + outset;\n\n\t\t\t// Round the rectangle in order, so the loop has a consistent winding for anything that walks it.\n\t\t\tVector3[] corners =\n\t\t\t[\n\t\t\t\t-right * hw - forward * hl,\n\t\t\t\t right * hw - forward * hl,\n\t\t\t\t right * hw + forward * hl,\n\t\t\t\t-right * hw + forward * hl,\n\t\t\t];\n\n\t\t\tfor (int i = 0; i < corners.Length; i++)\n\t\t\t{\n\t\t\t\tVector3 from = corners[i];\n\t\t\t\tVector3 to = corners[(i + 1) % corners.Length];\n\n\t\t\t\tint steps = Math.Max(1, (int)MathF.Ceiling(Vector3.DistanceBetween(from, to) / spacing));\n\n\t\t\t\t// Last point of each edge is skipped \u2014 it's the first of the next one, and a closed loop\n\t\t\t\t// mustn't repeat its corners.\n\t\t\t\tfor (int s = 0; s < steps; s++)\n\t\t\t\t\tpoints.Add(Vector3.Lerp(from, to, (float)s / steps) + lift);\n\t\t\t}\n\t\t}\n\n\t\tfor (int i = 0; i < points.Count; i++)\n\t\t\tpoints[i] = WorldTransform.PointToWorld(points[i]);\n\n\t\treturn points;\n\t}\n\n\n\n\t/// <summary>Whether this junction has pavement to walk on at all.</summary>\n\tpublic bool HasSidewalks => SidewalkWidth > 0.0f;\n}\n"
        }
    ]
}