s&box Package Code Search

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

Showing code results for query: * (71 total matches found)
redsnail.roadtool2 / Network/RoadBorderTaper.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// How one side's border tapers at either end of a segment: over the given length from the node its relief (kerb
/// height, verge drop) runs to the share of itself given for that end — nothing when the other border is of another
/// kind, the height the two meet at when it is of the same kind — and its width runs to the width the two meet at,
/// so the two borders meet flush at the node. A zero length is no taper. An end marked as a spill has a country road
/// on the other side, whose gravel is blended onto the border there. A gore end is the nose where this road and
/// another diverge from one edge: the border there grows from nothing as the square of the distance, the way the gap
/// between two roads parting from a common tangent opens up, so the two borders fill the gore between them without
/// crossing.
/// </summary>
/// <param name="StartRelief">
/// The share of the profile's own relief the border has at the start node: 0 flattens it into the road, 1 leaves
/// it as it is, and anything else brings it to a height it shares with the border on the other side (above 1 for
/// the lower of the two).
/// </param>
/// <param name="StartOffset">
/// How far from the start node the taper ends: 0 at a plain pass-through, the arm's trim at a junction, where the
/// corner starts. Everything nearer the node than that — the corner itself — is built in the shape the taper runs
/// to, so the two roads round a corner meet in one cross-section instead of turning into each other on the arc.
/// </param>
public readonly record struct RoadBorderTaper(float StartLength, float StartWidth, float StartRelief, bool StartSpill, float EndLength, float EndWidth, float EndRelief, bool EndSpill, bool StartGore = false, bool EndGore = false, float StartOffset = 0.0f, float EndOffset = 0.0f)
{
	/// <summary>
	/// The share of the profile's relief in force at a curve distance (1 away from any taper, that end's relief at
	/// its node, and beyond its offset), how far along a taper the point is (0 outside one), the width the border is
	/// that far towards, and whether that taper's neighbour spills onto it. The two ends' tapers, offsets included,
	/// are each held to half the segment, so no point is ever inside both.
	/// </summary>
	public float ReliefAt(float _Along, float _Length, out float _Towards, out float _TargetWidth, out bool _Spill)
	{
		float relief = 1.0f;

		_Towards = 0.0f;
		_TargetWidth = 0.0f;
		_Spill = false;

		if (StartLength > 0.0f && _Along < StartOffset + StartLength)
		{
			float progress = Math.Clamp((_Along - StartOffset) / StartLength, 0.0f, 1.0f);

			_Towards = Towards(progress, StartGore);
			_TargetWidth = StartWidth;
			_Spill = StartSpill;
			relief = MathX.Lerp(StartRelief, 1.0f, progress);
		}

		if (EndLength > 0.0f && _Length - _Along < EndOffset + EndLength)
		{
			float progress = Math.Clamp((_Length - _Along - EndOffset) / EndLength, 0.0f, 1.0f);
			float towards = Towards(progress, EndGore);

			if (towards > _Towards)
			{
				_Towards = towards;
				_TargetWidth = EndWidth;
				_Spill = EndSpill;
				relief = MathX.Lerp(EndRelief, 1.0f, progress);
			}
		}

		return relief;
	}



	/// <summary>
	/// How far towards the node's width the border is at a progress along the taper (0 at the node, 1 at its far
	/// end): straight along a plain taper, and along a gore the square, so the width is the square of the progress.
	/// </summary>
	private static float Towards(float _Progress, bool _Gore)
	{
		return _Gore ? 1.0f - _Progress * _Progress : 1.0f - _Progress;
	}



	/// <summary>
	/// The factor the border's nominal width is scaled by at a curve distance, on its way to the neighbour's.
	/// </summary>
	public float WidthScaleAt(float _Along, float _Length, float _NominalWidth)
	{
		ReliefAt(_Along, _Length, out float towards, out float targetWidth, out _);

		if (towards <= 0.0f || _NominalWidth <= 0.001f)
			return 1.0f;

		return (_NominalWidth + (targetWidth - _NominalWidth) * towards) / _NominalWidth;
	}



	public void AddToHash(ref StableHash _Hash)
	{
		_Hash.Add(StartLength);
		_Hash.Add(StartWidth);
		_Hash.Add(StartRelief);
		_Hash.Add(StartSpill ? 1 : 0);
		_Hash.Add(StartGore ? 1 : 0);
		_Hash.Add(StartOffset);
		_Hash.Add(EndLength);
		_Hash.Add(EndWidth);
		_Hash.Add(EndRelief);
		_Hash.Add(EndSpill ? 1 : 0);
		_Hash.Add(EndGore ? 1 : 0);
		_Hash.Add(EndOffset);
	}

	/// <summary>
	/// Whether the border tapers at the end a node is at.
	/// </summary>
	public bool TapersAt(bool _AtStart)
	{
		return _AtStart ? StartLength > 0.0f : EndLength > 0.0f;
	}
}

/// <summary>
/// The tapers of a segment's two borders, looking along the segment.
/// </summary>
public readonly record struct SegmentBorderTapers(RoadBorderTaper Left, RoadBorderTaper Right)
{
	public RoadBorderTaper Side(bool _Left)
	{
		return _Left ? Left : Right;
	}

	public void AddToHash(ref StableHash _Hash)
	{
		Left.AddToHash(ref _Hash);
		Right.AddToHash(ref _Hash);
	}
}
redsnail.roadtool2 / Network/RoadSurfaceBuilder.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// Generates the road surface of one chunk: a strip of quads between cross-section frames at the rows the chunk is
/// built on (even distances along the segment's curve, thinned where the road type allows it).
/// </summary>
internal static class RoadSurfaceBuilder
{
	/// <summary>
	/// The two edge points of the road at a distance along the curve, each its own distance out from the centreline
	/// there. Junctions use this for the rows they share with the arm chunks, so both sides of the seam are computed
	/// the same way.
	/// </summary>
	public static void CrossSection(RoadCurveTable _Table, float _Distance, in RoadWidth _Width, float _Lift, out Vector3 _Left, out Vector3 _Right)
	{
		_Table.Frame(_Distance, out Vector3 position, out Vector3 tangent);

		position += Vector3.Up * _Lift;

		Vector3 right = RoadCurve.RightOf(tangent);

		_Left = position - right * _Width.LeftAt(_Distance);
		_Right = position + right * _Width.RightAt(_Distance);
	}



	public static PolygonMesh Build(RoadCurveTable _Table, float _StartDistance, float _EndDistance, in RoadWidth _Width, float _Spacing, in RoadSimplify _Simplify, float _TextureRepeat, float _SurfaceOffset, Material _Material)
	{
		var mesh = new PolygonMesh();

		// The rows the borders and the bridge are built on too, so the pieces meet exactly.
		var rows = RoadRows.Build(_Table, _StartDistance, _EndDistance, _Spacing, _Width, _Simplify);
		float repeat = MathF.Max(1.0f, _TextureRepeat);

		var left = new HalfEdgeMesh.VertexHandle[rows.Count];
		var right = new HalfEdgeMesh.VertexHandle[rows.Count];
		var uvLeft = new Vector2[rows.Count];
		var uvRight = new Vector2[rows.Count];

		for (int i = 0; i < rows.Count; i++)
		{
			float distance = rows[i];

			CrossSection(_Table, distance, _Width, _SurfaceOffset, out Vector3 leftPos, out Vector3 rightPos);

			var handles = mesh.AddVertices(leftPos, rightPos);
			left[i] = handles[0];
			right[i] = handles[1];

			// U runs across the road from the centreline, V along it, so a tiling material follows the road instead
			// of the world, and an edge that moves out takes its texture with it.
			float v = distance / repeat;
			uvLeft[i] = new Vector2(-_Width.LeftAt(distance) / repeat, v);
			uvRight[i] = new Vector2(_Width.RightAt(distance) / repeat, v);
		}

		// Two triangles rather than a quad: a road that turns as it climbs has cells whose four corners are not
		// coplanar, and then it matters exactly how the cell is split, both here and for the paint laid on top.
		for (int i = 0; i < rows.Count - 1; i++)
		{
			MeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i], right[i + 1], uvLeft[i], uvRight[i], uvRight[i + 1]);
			MeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i + 1], left[i + 1], uvLeft[i], uvRight[i + 1], uvLeft[i + 1]);
		}

		return mesh;
	}



	/// <summary>
	/// The height of one cell of road surface, as it is built, over a point in plan: the cell is split from its
	/// near left corner to its far right one, so which of the two triangles the point falls on is which side of
	/// that diagonal it lies. The triangle's plane is used even for a point just off the cell (paint hanging over
	/// the road's edge), which keeps that paint on the surface's own plane.
	/// </summary>
	public static float CellHeight(Vector3 _NearLeft, Vector3 _NearRight, Vector3 _FarRight, Vector3 _FarLeft, Vector2 _Point)
	{
		Vector2 diagonal = Flat(_FarRight) - Flat(_NearLeft);
		float side = Cross(diagonal, _Point - Flat(_NearLeft));
		float nearRightSide = Cross(diagonal, Flat(_NearRight) - Flat(_NearLeft));

		return side * nearRightSide >= 0.0f
			? PlaneHeight(_NearLeft, _NearRight, _FarRight, _Point)
			: PlaneHeight(_NearLeft, _FarRight, _FarLeft, _Point);
	}



	/// <summary>
	/// The height of the plane through three points over a point in plan.
	/// </summary>
	private static float PlaneHeight(Vector3 _A, Vector3 _B, Vector3 _C, Vector2 _Point)
	{
		Vector2 a = Flat(_A);
		Vector2 b = Flat(_B);
		Vector2 c = Flat(_C);
		float area = Cross(b - a, c - a);

		if (MathF.Abs(area) < 0.0001f)
			return _A.z;

		float weightA = Cross(b - _Point, c - _Point) / area;
		float weightB = Cross(c - _Point, a - _Point) / area;

		return weightA * _A.z + weightB * _B.z + (1.0f - weightA - weightB) * _C.z;
	}



	private static Vector2 Flat(Vector3 _Vector)
	{
		return new Vector2(_Vector.x, _Vector.y);
	}



	private static float Cross(Vector2 _A, Vector2 _B)
	{
		return _A.x * _B.y - _A.y * _B.x;
	}
}
redsnail.roadtool2 / Editor/RoadBorderDebugDump.cs
Editor library
using System;
using System.Collections.Generic;
using System.IO;
using Editor;
using Sandbox;

namespace RedSnail.RoadTool2.Editor;

/// <summary>
/// Writes each network's border debug record to Libraries/RoadTool2/debug/borders.txt whenever its border pieces
/// have been rebuilt with the overlay on, so the numbers can be read outside the editor.
/// </summary>
public static class RoadBorderDebugDump
{
	private static readonly Dictionary<Guid, int> s_Written = new();



	[EditorEvent.Frame]
	public static void OnFrame()
	{
		var scene = SceneEditorSession.Active?.Scene;

		if (scene is null)
			return;

		var network = scene.Get<RoadNetwork>();

		if (!network.IsValid() || !network.DebugBorders || network.IsRebuilding)
			return;

		if (s_Written.TryGetValue(network.Id, out int revision) && revision == network.DebugRevision)
			return;

		s_Written[network.Id] = network.DebugRevision;

		try
		{
			string path = GetPath(network);

			Directory.CreateDirectory(Path.GetDirectoryName(path));
			File.WriteAllText(path, network.BuildDebugRecord());

			Log.Info($"Road Tool 2: border debug written to {path}");
		}
		catch (Exception exception)
		{
			Log.Warning($"Road Tool 2: could not write the border debug record: {exception.Message}");
		}
	}



	private static string GetPath(RoadNetwork _Network)
	{
		string root = Project.Current.GetRootPath();
		string library = Path.Combine(root, "Libraries", "RoadTool2");

		// The library folder may have been renamed; find it by its project file.
		if (!Directory.Exists(library))
		{
			foreach (string directory in Directory.GetDirectories(Path.Combine(root, "Libraries")))
			{
				if (File.Exists(Path.Combine(directory, "roadtool2.sbproj")))
				{
					library = directory;
					break;
				}
			}
		}

		string name = string.Concat(_Network.GameObject.Name.Split(Path.GetInvalidFileNameChars())).Replace(' ', '_');

		return Path.Combine(library, "debug", $"borders_{name}.txt");
	}
}
redsnail.roadtool2 / Code/Profiles/BridgeStyle.cs
Game library
using System.Text.Json.Serialization;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// What stands along the two edges of a bridge deck.
/// </summary>
public enum BridgeSideStyle
{
	/// <summary>
	/// Nothing above the deck: its side face is all there is.
	/// </summary>
	None,

	/// <summary>
	/// A solid parapet wall.
	/// </summary>
	Wall,

	/// <summary>
	/// Posts at intervals with horizontal rails running between them.
	/// </summary>
	Railing,

	/// <summary>
	/// A prefab of your own repeated along the edge (a fence panel, a lamp, a cable anchor...).
	/// </summary>
	Prefab
}

/// <summary>
/// What holds a bridge deck up.
/// </summary>
public enum BridgeSupportStyle
{
	None,

	/// <summary>
	/// Pillars at intervals, from the underside of the deck down into the ground.
	/// </summary>
	Pillars,

	/// <summary>
	/// A prefab of your own at intervals under the deck (a pier, a tower...).
	/// </summary>
	Prefab
}

public enum BridgePillarShape
{
	Square,
	Round
}

/// <summary>
/// One pillar under the middle of the deck, or one under each edge.
/// </summary>
public enum BridgePillarLayout
{
	Single,
	Pair
}

/// <summary>
/// How a stretch of road becomes a bridge: the deck under it, what stands along the deck's edges, and what holds it
/// up. Each part is picked from a set of built-in designs or handed to a prefab of your own, so new kinds of bridge
/// (cable-stayed, trusses...) are new designs of a part rather than a new kind of road. Roads reference a style, so
/// every bridge built from it changes together when the asset is edited.
/// </summary>
[AssetType(Name = "Bridge Style", Extension = "bridg", Category = "Road Tool 2")]
public sealed class BridgeStyle : GameResource
{
	private const string DefaultMaterial = "roadtool2/materials/concrete.vmat";

	/// <summary>
	/// Material of the deck's sides and underside (and of the walls and pillars when they have none of their own).
	/// </summary>
	[Group("Deck")] public Material DeckMaterial { get; set; }
	[Group("Deck"), Range(10.0f, 10000.0f)] public float DeckTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 500.0f;

	/// <summary>
	/// The deck's thickness below the road surface.
	/// </summary>
	[Group("Deck"), Range(5.0f, 500.0f)] public float DeckDepth { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 60.0f;

	/// <summary>
	/// A bare ledge of deck outside the side (wall, railing) on each edge. 0 ends the deck at the side's outer face.
	/// </summary>
	[Group("Deck"), Range(0.0f, 300.0f)] public float DeckOverhang { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 0.0f;

	[Group("Side")] public BridgeSideStyle Side { get; set; } = BridgeSideStyle.Wall;

	/// <summary>
	/// Material of the wall or the railing. Empty uses the deck's.
	/// </summary>
	[Group("Side"), ShowIf(nameof(HasBuiltSide), true)] public Material SideMaterial { get; set; }
	[Group("Side"), ShowIf(nameof(HasBuiltSide), true), Range(10.0f, 10000.0f)] public float SideTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 100.0f;

	/// <summary>
	/// The wall's thickness, or the railing's post size; also the width of the strip a prefab side stands on.
	/// </summary>
	[Group("Side"), HideIf(nameof(Side), BridgeSideStyle.None), Range(1.0f, 200.0f)] public float SideWidth { get; set { field = value.Clamp(0.1f, 10000.0f); } } = 20.0f;

	/// <summary>
	/// Height of the wall or railing above the road's border (the pavement top when there is one).
	/// </summary>
	[Group("Side"), ShowIf(nameof(HasBuiltSide), true), Range(5.0f, 300.0f)] public float SideHeight { get; set { field = value.Clamp(0.1f, 10000.0f); } } = 45.0f;

	[Group("Railing"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(50.0f, 1000.0f)] public float PostSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 200.0f;
	[Group("Railing"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(1, 6)] public int RailCount { get; set { field = value.Clamp(1, 32); } } = 2;
	[Group("Railing"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(1.0f, 50.0f)] public float RailSize { get; set { field = value.Clamp(0.1f, 1000.0f); } } = 6.0f;

	/// <summary>
	/// Placed on the deck's edge strip with its forward along the road and its right pointing away from the road.
	/// </summary>
	[Group("Side"), ShowIf(nameof(Side), BridgeSideStyle.Prefab)] public GameObject SidePrefab { get; set; }
	[Group("Side"), ShowIf(nameof(Side), BridgeSideStyle.Prefab), Range(10.0f, 2000.0f)] public float SideSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 200.0f;

	[Group("Supports")] public BridgeSupportStyle Supports { get; set; } = BridgeSupportStyle.Pillars;
	[Group("Supports"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(200.0f, 10000.0f)] public float SupportSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 1500.0f;

	/// <summary>
	/// The pillars' material. Empty uses the deck's.
	/// </summary>
	[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public Material SupportMaterial { get; set; }
	[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarShape PillarShape { get; set; } = BridgePillarShape.Square;
	[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarLayout PillarLayout { get; set; } = BridgePillarLayout.Single;
	[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(10.0f, 1000.0f)] public float PillarSize { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 100.0f;

	/// <summary>
	/// For a pair of pillars: how far in from the deck's edges they stand.
	/// </summary>
	[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(0.0f, 500.0f)] public float PillarInset { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 20.0f;
	[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(3, 32)] public int PillarSegments { get; set { field = value.Clamp(3, 64); } } = 12;

	/// <summary>
	/// How far a pillar carries on below the ground it lands on, so it never floats off a slope.
	/// </summary>
	[Group("Supports"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(0.0f, 500.0f)] public float SupportSink { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 50.0f;

	/// <summary>
	/// How far below the deck a support reaches when there is no ground under it to land on.
	/// </summary>
	[Group("Supports"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(100.0f, 10000.0f)] public float SupportFallbackHeight { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 1000.0f;

	/// <summary>
	/// Placed on the ground under the middle of the deck (or the fallback height below it), with its forward along
	/// the road. It is up to the prefab to reach the deck.
	/// </summary>
	[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Prefab)] public GameObject SupportPrefab { get; set; }

	[Hide, JsonIgnore] public bool HasBuiltSide => Side is BridgeSideStyle.Wall or BridgeSideStyle.Railing;

	[Hide, JsonIgnore] public Material ResolvedDeckMaterial => DeckMaterial ?? Material.Load(DefaultMaterial);

	[Hide, JsonIgnore] public Material ResolvedSideMaterial => SideMaterial ?? ResolvedDeckMaterial;

	[Hide, JsonIgnore] public Material ResolvedSupportMaterial => SupportMaterial ?? ResolvedDeckMaterial;

	/// <summary>
	/// How wide the strip along each deck edge is that the side occupies (nothing for a bare deck).
	/// </summary>
	[Hide, JsonIgnore] public float SideStripWidth => Side == BridgeSideStyle.None ? 0.0f : SideWidth;



	/// <summary>
	/// Folds everything the bridge geometry is built from into a hash, so pieces built with an older version of
	/// the style are noticed.
	/// </summary>
	public void AddToHash(ref StableHash _Hash)
	{
		_Hash.Add(DeckMaterial?.ResourcePath);
		_Hash.Add(DeckTextureRepeat);
		_Hash.Add(DeckDepth);
		_Hash.Add(DeckOverhang);
		_Hash.Add((int)Side);
		_Hash.Add(SideMaterial?.ResourcePath);
		_Hash.Add(SideTextureRepeat);
		_Hash.Add(SideWidth);
		_Hash.Add(SideHeight);
		_Hash.Add(PostSpacing);
		_Hash.Add(RailCount);
		_Hash.Add(RailSize);
		_Hash.Add(RoadPropSet.PrefabKey(SidePrefab));
		_Hash.Add(SideSpacing);
		_Hash.Add((int)Supports);
		_Hash.Add(SupportSpacing);
		_Hash.Add(SupportMaterial?.ResourcePath);
		_Hash.Add((int)PillarShape);
		_Hash.Add((int)PillarLayout);
		_Hash.Add(PillarSize);
		_Hash.Add(PillarInset);
		_Hash.Add(PillarSegments);
		_Hash.Add(SupportSink);
		_Hash.Add(SupportFallbackHeight);
		_Hash.Add(RoadPropSet.PrefabKey(SupportPrefab));
	}



	/// <summary>
	/// The asset was saved: every network with a bridge in this style has stale pieces now.
	/// </summary>
	protected override void PostReload()
	{
		base.PostReload();

		RoadNetwork.Editor_MarkDirty();
	}



	protected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)
	{
		return CreateSimpleAssetTypeIcon("architecture", _Width, _Height, "#7fb2ff", "black");
	}
}
redsnail.roadtool2 / Code/Utility/MeshUtility.cs
Game library
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool2;

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;
	}



	/// <summary>
	/// Adds a quad face; returns an invalid handle when the mesh refused it (a duplicate vertex, a topology clash).
	/// </summary>
	public static HalfEdgeMesh.FaceHandle 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 default;

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

		if (!face.IsValid)
			return default;

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

		return face;
	}



	/// <summary>
	/// Adds a triangle face; returns an invalid handle when the mesh refused it.
	/// </summary>
	public static HalfEdgeMesh.FaceHandle 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 default;

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

		if (!face.IsValid)
			return default;

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

		return face;
	}



	/// <summary>
	/// Paints a face's vertices with blend weights (what a blendable material mixes its layers by), in the order
	/// the face's vertices were given.
	/// </summary>
	public static void SetFaceBlends(PolygonMesh _Mesh, HalfEdgeMesh.FaceHandle _Face, IReadOnlyList<Color32> _Blends)
	{
		if (!_Face.IsValid || !_Mesh.GetFaceVerticesConnectedToFace(_Face, out var edges))
			return;

		int count = System.Math.Min(edges.Length, _Blends.Count);

		for (int i = 0; i < count; i++)
			_Mesh.SetVertexBlend(edges[i], _Blends[i]);
	}



	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.roadtool2 / Geometry/RoadCurveTable.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// Arc-length table over a <see cref="RoadCurve"/>, so the road can be sampled at even distances (mesh rows, chunk
/// boundaries) instead of at even parameter values, which bunch up around tight bends. The samples double as a
/// polyline for picking.
/// </summary>
public sealed class RoadCurveTable
{
	private readonly Vector3[] m_Positions;
	private readonly float[] m_Distances;

	public RoadCurve Curve { get; }

	public float Length => m_Distances[^1];

	public int SampleCount => m_Positions.Length;

	/// <summary>
	/// The box the curve itself runs through. Picking widens it by the road's own reach and skips the road
	/// altogether when the cursor's ray misses it, which is most of them in a city.
	/// </summary>
	public BBox Bounds { get; }



	public RoadCurveTable(RoadCurve _Curve, int _Samples)
	{
		Curve = _Curve;

		int count = Math.Max(1, _Samples);
		m_Positions = new Vector3[count + 1];
		m_Distances = new float[count + 1];
		m_Positions[0] = _Curve.P0;

		var bounds = new BBox(_Curve.P0, _Curve.P0);

		for (int i = 1; i <= count; i++)
		{
			m_Positions[i] = _Curve.Evaluate((float)i / count);
			m_Distances[i] = m_Distances[i - 1] + Vector3.DistanceBetween(m_Positions[i - 1], m_Positions[i]);
			bounds = bounds.AddPoint(m_Positions[i]);
		}

		Bounds = bounds;
	}



	/// <summary>
	/// Builds a table with a sample density that suits the curve's length.
	/// </summary>
	public static RoadCurveTable Create(RoadCurve _Curve)
	{
		int samples = Math.Clamp((int)(_Curve.ApproximateLength() / 20.0f), 16, 1024);

		return new RoadCurveTable(_Curve, samples);
	}



	public Vector3 SamplePosition(int _Index)
	{
		return m_Positions[_Index];
	}



	public float SampleDistance(int _Index)
	{
		return m_Distances[_Index];
	}



	public float DistanceToT(float _Distance)
	{
		if (_Distance <= 0.0f)
			return 0.0f;

		if (_Distance >= Length)
			return 1.0f;

		int low = 0;
		int high = m_Distances.Length - 1;

		while (high - low > 1)
		{
			int mid = (low + high) / 2;

			if (m_Distances[mid] <= _Distance)
				low = mid;
			else
				high = mid;
		}

		float span = m_Distances[high] - m_Distances[low];
		float fraction = span > 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;

		return (low + fraction) / (m_Distances.Length - 1);
	}



	public Vector3 PositionAt(float _Distance)
	{
		return Curve.Evaluate(DistanceToT(_Distance));
	}



	public Vector3 TangentAt(float _Distance)
	{
		return Curve.Tangent(DistanceToT(_Distance));
	}



	/// <summary>
	/// Where the road is at a distance and which way it runs there, in one go: everything that lays something
	/// across the road wants both, and asking for them separately searches the table twice.
	/// </summary>
	public void Frame(float _Distance, out Vector3 _Position, out Vector3 _Tangent)
	{
		float t = DistanceToT(_Distance);

		_Position = Curve.Evaluate(t);
		_Tangent = Curve.Tangent(t);
	}
}
redsnail.roadtool2 / Network/RoadJunctionBuilder.cs
Game library
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// Generates the surface of a junction: the layout's polygon fanned out from the node. Every triangle shares the
/// centre, so each one is planar and none can be dropped by the mesh builder, whatever the arms' heights do.
/// </summary>
internal static class RoadJunctionBuilder
{
	public static PolygonMesh Build(RoadJunctionLayout _Layout, float _TextureRepeat, Material _Material)
	{
		var mesh = new PolygonMesh();
		var polygon = _Layout.Polygon;

		if (polygon.Count < 3)
			return mesh;

		var centre = mesh.AddVertices(_Layout.Centre)[0];
		var ring = mesh.AddVertices(polygon.ToArray());
		Vector2 centreUv = PlanarUv(_Layout.Centre, _TextureRepeat);

		for (int i = 0; i < polygon.Count; i++)
		{
			int next = (i + 1) % polygon.Count;

			MeshUtility.AddTexturedTriangle(mesh, _Material, centre, ring[i], ring[next],
				centreUv, PlanarUv(polygon[i], _TextureRepeat), PlanarUv(polygon[next], _TextureRepeat));
		}

		return mesh;
	}



	/// <summary>
	/// Junctions have no single direction to run a texture along, so they use a top-down projection.
	/// </summary>
	private static Vector2 PlanarUv(Vector3 _Position, float _TextureRepeat)
	{
		return new Vector2(_Position.x, _Position.y) / _TextureRepeat;
	}
}
redsnail.roadtool2 / .obj/__compiler_extra.cs
Game library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Road Tool" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "roadtool2" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "redsnail" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "redsnail.roadtool2" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "30" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-10-18T00:54:30.0847204Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.166.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.166.0")]
redsnail.roadtool2 / Code/Geometry/RoadCurveTable.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// Arc-length table over a <see cref="RoadCurve"/>, so the road can be sampled at even distances (mesh rows, chunk
/// boundaries) instead of at even parameter values, which bunch up around tight bends. The samples double as a
/// polyline for picking.
/// </summary>
public sealed class RoadCurveTable
{
	private readonly Vector3[] m_Positions;
	private readonly float[] m_Distances;

	public RoadCurve Curve { get; }

	public float Length => m_Distances[^1];

	public int SampleCount => m_Positions.Length;

	/// <summary>
	/// The box the curve itself runs through. Picking widens it by the road's own reach and skips the road
	/// altogether when the cursor's ray misses it, which is most of them in a city.
	/// </summary>
	public BBox Bounds { get; }



	public RoadCurveTable(RoadCurve _Curve, int _Samples)
	{
		Curve = _Curve;

		int count = Math.Max(1, _Samples);
		m_Positions = new Vector3[count + 1];
		m_Distances = new float[count + 1];
		m_Positions[0] = _Curve.P0;

		var bounds = new BBox(_Curve.P0, _Curve.P0);

		for (int i = 1; i <= count; i++)
		{
			m_Positions[i] = _Curve.Evaluate((float)i / count);
			m_Distances[i] = m_Distances[i - 1] + Vector3.DistanceBetween(m_Positions[i - 1], m_Positions[i]);
			bounds = bounds.AddPoint(m_Positions[i]);
		}

		Bounds = bounds;
	}



	/// <summary>
	/// Builds a table with a sample density that suits the curve's length.
	/// </summary>
	public static RoadCurveTable Create(RoadCurve _Curve)
	{
		int samples = Math.Clamp((int)(_Curve.ApproximateLength() / 20.0f), 16, 1024);

		return new RoadCurveTable(_Curve, samples);
	}



	public Vector3 SamplePosition(int _Index)
	{
		return m_Positions[_Index];
	}



	public float SampleDistance(int _Index)
	{
		return m_Distances[_Index];
	}



	public float DistanceToT(float _Distance)
	{
		if (_Distance <= 0.0f)
			return 0.0f;

		if (_Distance >= Length)
			return 1.0f;

		int low = 0;
		int high = m_Distances.Length - 1;

		while (high - low > 1)
		{
			int mid = (low + high) / 2;

			if (m_Distances[mid] <= _Distance)
				low = mid;
			else
				high = mid;
		}

		float span = m_Distances[high] - m_Distances[low];
		float fraction = span > 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;

		return (low + fraction) / (m_Distances.Length - 1);
	}



	public Vector3 PositionAt(float _Distance)
	{
		return Curve.Evaluate(DistanceToT(_Distance));
	}



	public Vector3 TangentAt(float _Distance)
	{
		return Curve.Tangent(DistanceToT(_Distance));
	}



	/// <summary>
	/// Where the road is at a distance and which way it runs there, in one go: everything that lays something
	/// across the road wants both, and asking for them separately searches the table twice.
	/// </summary>
	public void Frame(float _Distance, out Vector3 _Position, out Vector3 _Tangent)
	{
		float t = DistanceToT(_Distance);

		_Position = Curve.Evaluate(t);
		_Tangent = Curve.Tangent(t);
	}
}
redsnail.roadtool2 / Code/Geometry/RoadWidth.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// How far a road's two edges sit from its centreline along a segment: half the road's width on each side, plus
/// whatever extra each side has at the segment's start and end, blended smoothly between the two. That is what lets
/// a lane appear on one side over a taper (a highway exit or entry), a ramp start as a point and grow, or a road
/// flare out to a wider one.
/// </summary>
public readonly struct RoadWidth
{
	/// <summary>
	/// No road is built narrower than this: two edges on one point make faces with no area.
	/// </summary>
	private const float MinWidth = 2.0f;

	public readonly float Base;
	public readonly float LeftStart;
	public readonly float LeftEnd;
	public readonly float RightStart;
	public readonly float RightEnd;
	public readonly float Length;

	public RoadWidth(float _Base, float _LeftStart, float _LeftEnd, float _RightStart, float _RightEnd, float _Length)
	{
		Base = _Base;
		LeftStart = _LeftStart;
		LeftEnd = _LeftEnd;
		RightStart = _RightStart;
		RightEnd = _RightEnd;
		Length = MathF.Max(0.001f, _Length);
	}

	public RoadWidth(RoadSegment _Segment, float _Length)
		: this(_Segment.Width, _Segment.LeftExtraStart, _Segment.LeftExtraEnd, _Segment.RightExtraStart, _Segment.RightExtraEnd, _Length)
	{
	}

	/// <summary>
	/// A plain road of one width all along.
	/// </summary>
	public static RoadWidth Uniform(float _Width)
	{
		return new RoadWidth(_Width, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f);
	}

	/// <summary>
	/// True when both edges sit at half the width all along: the road every piece of the tool was first built for.
	/// </summary>
	public bool IsUniform => LeftStart == 0.0f && LeftEnd == 0.0f && RightStart == 0.0f && RightEnd == 0.0f;

	/// <summary>
	/// The furthest either edge gets from the centreline anywhere along the segment.
	/// </summary>
	public float MaxHalf => MathF.Max(MathF.Max(Base * 0.5f + LeftStart, Base * 0.5f + LeftEnd), MathF.Max(Base * 0.5f + RightStart, Base * 0.5f + RightEnd));

	/// <summary>
	/// Distance of the left edge from the centreline at a curve distance, looking along the segment.
	/// </summary>
	public float LeftAt(float _Distance)
	{
		float left = Base * 0.5f + Blend(LeftStart, LeftEnd, _Distance);
		float right = Base * 0.5f + Blend(RightStart, RightEnd, _Distance);

		return left + right < MinWidth ? left + (MinWidth - left - right) * 0.5f : left;
	}

	public float RightAt(float _Distance)
	{
		float left = Base * 0.5f + Blend(LeftStart, LeftEnd, _Distance);
		float right = Base * 0.5f + Blend(RightStart, RightEnd, _Distance);

		return left + right < MinWidth ? right + (MinWidth - left - right) * 0.5f : right;
	}

	/// <summary>
	/// The road's full width at a curve distance.
	/// </summary>
	public float WidthAt(float _Distance)
	{
		return LeftAt(_Distance) + RightAt(_Distance);
	}

	/// <summary>
	/// How much wider than the base half width the left edge is at a curve distance (negative when narrower).
	/// </summary>
	public float LeftExtraAt(float _Distance)
	{
		return LeftAt(_Distance) - Base * 0.5f;
	}

	public float RightExtraAt(float _Distance)
	{
		return RightAt(_Distance) - Base * 0.5f;
	}



	private float Blend(float _Start, float _End, float _Distance)
	{
		if (_Start == _End)
			return _Start;

		float t = Math.Clamp(_Distance / Length, 0.0f, 1.0f);

		// Eased, so the edge leaves and arrives without a kink.
		t = t * t * (3.0f - 2.0f * t);

		return _Start + (_End - _Start) * t;
	}



	public void AddToHash(ref StableHash _Hash)
	{
		_Hash.Add(Base);
		_Hash.Add(LeftStart);
		_Hash.Add(LeftEnd);
		_Hash.Add(RightStart);
		_Hash.Add(RightEnd);
	}
}
redsnail.roadtool2 / Code/Utility/StableHash.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// 64-bit FNV-1a hash that is stable across editor sessions, unlike <see cref="HashCode"/> which is seeded per
/// process. Used to stamp generated meshes with the inputs they were built from, so a scene reopened days later
/// still knows which chunks are current.
/// </summary>
public struct StableHash
{
	private const ulong OffsetBasis = 14695981039346656037UL;
	private const ulong Prime = 1099511628211UL;

	private ulong m_Hash;

	public long Value => unchecked((long)m_Hash);



	public static StableHash Create()
	{
		return new StableHash { m_Hash = OffsetBasis };
	}



	public void Add(int _Value)
	{
		unchecked
		{
			for (int i = 0; i < 4; i++)
			{
				m_Hash ^= (byte)(_Value >> (i * 8));
				m_Hash *= Prime;
			}
		}
	}



	public void Add(long _Value)
	{
		Add(unchecked((int)_Value));
		Add(unchecked((int)(_Value >> 32)));
	}



	public void Add(float _Value)
	{
		Add(BitConverter.SingleToInt32Bits(_Value));
	}



	public void Add(Vector3 _Value)
	{
		Add(_Value.x);
		Add(_Value.y);
		Add(_Value.z);
	}



	public void Add(string _Value)
	{
		if (_Value is not null)
		{
			foreach (char c in _Value)
				Add((int)c);
		}

		// Terminator, so "ab" + "c" never collides with "a" + "bc".
		Add(-1);
	}
}
redsnail.roadtool2 / Network/RoadChunk.cs
Game library
using Sandbox;

namespace RedSnail.RoadTool2;

public enum RoadChunkKind
{
	/// <summary>
	/// The road surface itself.
	/// </summary>
	Surface,

	/// <summary>
	/// What runs along the road's sides: sidewalks, verges.
	/// </summary>
	Border,

	/// <summary>
	/// The painted lines on the surface: lane lines, centre line, edge lines.
	/// </summary>
	Marking,

	/// <summary>
	/// The bridge under a road: deck, sides, supports.
	/// </summary>
	Bridge,

	/// <summary>
	/// The prefabs standing along a road and before its junctions.
	/// </summary>
	Props,

	/// <summary>
	/// The rounded end of a road that stops: surface and border together, in one piece per end.
	/// </summary>
	Cap
}

/// <summary>
/// Marks a generated mesh child of a <see cref="RoadNetwork"/> and remembers what it covers: a stretch of a segment,
/// or the junction at a node, and whether it is the surface or the border of it. The network uses it to replace
/// exactly the pieces that went stale and leave the rest alone.
/// </summary>
[Icon("view_week")]
public sealed class RoadChunk : Component
{
	/// <summary>
	/// The segment this chunk is a stretch of, or -1 for a junction piece.
	/// </summary>
	[Property, ReadOnly] public int SegmentId { get; set; } = -1;

	/// <summary>
	/// The node this chunk is the junction of, or -1 for a road stretch.
	/// </summary>
	[Property, ReadOnly] public int NodeId { get; set; } = -1;

	/// <summary>
	/// The parking lot this chunk is a piece of, or -1 for anything else.
	/// </summary>
	[Property, ReadOnly] public int LotId { get; set; } = -1;

	[Property, ReadOnly] public RoadChunkKind Kind { get; set; } = RoadChunkKind.Surface;

	[Property, ReadOnly] public int ChunkIndex { get; set; }

	[Property, ReadOnly] public int ChunkCount { get; set; }

	/// <summary>
	/// A locked chunk is never regenerated, so its mesh can be edited by hand. It keeps its geometry even if the road
	/// under it changes; unlock it to have it rebuilt.
	/// </summary>
	[Property]
	public bool Locked
	{
		get;
		set
		{
			field = value;

			// Unlocking is a request to catch up with the graph.
			GameObject?.Parent?.Components.Get<RoadNetwork>()?.MarkDirty();
		}
	}

	/// <summary>
	/// Hash of everything this chunk was generated from. When it no longer matches what the graph says it should be
	/// the chunk is stale.
	/// </summary>
	[Property, Hide] public long Signature { get; set; }

	/// <summary>
	/// The rows a border piece was built from, when the network's debug overlay is on. Not saved.
	/// </summary>
	public BorderDebug Debug { get; set; }

	public bool IsJunction => NodeId >= 0;

	public bool IsLot => LotId >= 0;



	protected override void DrawGizmos()
	{
		if (Debug is null)
			return;

		var network = GameObject?.Parent?.Components.Get<RoadNetwork>();

		if (network is null || !network.DebugBorders)
			return;

		Gizmo.Draw.IgnoreDepth = true;
		Gizmo.Draw.LineThickness = 2.0f;

		foreach (var line in Debug.Lines)
		{
			Gizmo.Draw.Color = line.Color;
			Gizmo.Draw.Line(line.From, line.To);
		}

		foreach (var marker in Debug.Markers)
		{
			Gizmo.Draw.Color = marker.Color;
			Gizmo.Draw.SolidSphere(marker.Position, 2.0f);
		}
	}
}
redsnail.roadtool2 / Network/RoadNetwork.Rebuild.cs
Game library
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace RedSnail.RoadTool2;

public sealed partial class RoadNetwork
{
	/// <summary>
	/// Bump whenever the generated geometry changes (UV layout, sampling, profile...), so pieces built by an older
	/// generator are replaced on the next reconcile instead of lingering until their road is edited.
	/// </summary>
	private const int GeneratorVersion = 57;

	/// <summary>
	/// A stretch of road shorter than this is not worth a chunk (it happens when a junction swallows a whole chunk).
	/// </summary>
	private const float MinChunkLength = 1.0f;

	/// <summary>
	/// Where a segment's painted lines may run: between <see cref="Start"/> and <see cref="End"/>, skipping the
	/// gap between <see cref="GapStart"/> and <see cref="GapEnd"/> when there is one (a crossing in the middle of
	/// a short road between two junctions).
	/// </summary>
	private readonly record struct LineRange(float Start, float End, float GapStart, float GapEnd)
	{
		public bool HasGap => GapEnd > GapStart;

		public void AddToHash(ref StableHash _Hash)
		{
			_Hash.Add(Start);
			_Hash.Add(End);
			_Hash.Add(GapStart);
			_Hash.Add(GapEnd);
		}
	}

	/// <summary>
	/// A segment's pedestrian crossings: at either end, or one shared crossing halfway along a short road between
	/// two junctions (recorded on the start end). Centres are curve distances from the segment's start.
	/// </summary>
	private readonly record struct SegmentCrossings(bool AtStart, float StartCentre, bool AtEnd, float EndCentre);

	/// <summary>
	/// One piece of a segment to build. <see cref="Lines"/> bounds the painted lines; <see cref="Taper"/> is the
	/// border's run-out into a different road type at either end; <see cref="Bridge"/> is the whole built range
	/// and the ends of a bridge piece; the other kinds ignore them.
	/// </summary>
	private readonly record struct ChunkPlan(int Index, int Count, float Start, float End, RoadChunkKind Kind, BorderCaps Caps, RoadBorderBuilder.SegmentBorderBlends Blends, long Signature, LineRange Lines = default, SegmentBorderTapers Taper = default, RoadBridgeBuilder.BridgeSpan Bridge = default, RoadPropBuilder.PropSpan Props = default, RoadMarkingBuilder.DashPhase Dash = default, SegmentCrossings Crossings = default);

	private readonly record struct PendingGroup(long Signature, int Remaining);

	/// <summary>
	/// One piece to (re)build: a chunk of a segment, or, when <see cref="NodeId"/> is set, a piece of a junction.
	/// <see cref="ArmBlends"/> and <see cref="ArmTapers"/> carry, for a junction's border piece, what each arm's
	/// chunks were built with, keyed by segment.
	/// </summary>
	private readonly record struct RebuildJob(int SegmentId, int NodeId, RoadChunkKind Kind, long GroupSignature, ChunkPlan Plan, RoadCurveTable Table, RoadWidth Width, RoadProfile Profile, RoadJunctionLayout Junction, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> ArmBlends, Dictionary<int, SegmentBorderTapers> ArmTapers, RoadLot Lot);

	private bool m_IsDirty;
	private readonly Queue<RebuildJob> m_Jobs = new();
	private readonly Dictionary<int, PendingGroup> m_PendingSegments = new();
	private readonly Dictionary<(int NodeId, RoadChunkKind Kind), PendingGroup> m_PendingJunctions = new();

	/// <summary>
	/// Junction layouts keyed by node, each with a hash of what it was computed from. Finding corners on curved
	/// edges is the one expensive step of a reconcile, and while a node is dragged only its own junction and its
	/// neighbours' change.
	/// </summary>
	private readonly Dictionary<int, (long Key, RoadJunctionLayout Layout)> m_JunctionCache = new();

	/// <summary>
	/// True while pieces are still queued for (re)generation.
	/// </summary>
	public bool IsRebuilding => m_Jobs.Count > 0;



	/// <summary>
	/// Asks for a reconcile pass on the next editor frame: the graph is compared against the pieces that exist and
	/// only what differs gets queued for regeneration.
	/// </summary>
	public void MarkDirty()
	{
		m_IsDirty = true;

		// Undo, reloads and the inspector all land here with a graph nobody told us about piece by piece.
		InvalidateIndex();
	}



	/// <summary>
	/// Asks the network in the editor's scene for a reconcile. Editing a road type, a bridge, a prop set or a
	/// parking layout changes roads that hold no copy of it, so the asset tells the network rather than the other
	/// way round.
	/// </summary>
	public static void Editor_MarkDirty()
	{
		var scene = Application.Editor?.Scene;

		if (!scene.IsValid())
			return;

		var network = scene.Get<RoadNetwork>();

		if (!network.IsValid())
			return;

		network.MarkDirty();
	}



	/// <summary>
	/// One reconcile pass, then as much of the queued work as this frame's budget allows. Driven from the
	/// component's own update in RoadNetwork.cs.
	/// </summary>
	private void Tick()
	{
		if (m_IsDirty)
		{
			m_IsDirty = false;
			Reconcile();
		}

		ProcessJobs();
	}



	public IEnumerable<RoadChunk> GetChunks()
	{
		foreach (var child in GameObject.Children)
		{
			if (!child.IsValid() || child.IsDestroyed)
				continue;

			var chunk = child.Components.Get<RoadChunk>();

			if (chunk is not null)
				yield return chunk;
		}
	}



	private RoadChunk FindChunk(int _SegmentId, int _ChunkIndex, RoadChunkKind _Kind)
	{
		return GetChunks().FirstOrDefault(chunk => !chunk.IsJunction && chunk.SegmentId == _SegmentId && chunk.ChunkIndex == _ChunkIndex && chunk.Kind == _Kind);
	}



	private RoadChunk FindJunctionChunk(int _NodeId, RoadChunkKind _Kind)
	{
		return GetChunks().FirstOrDefault(chunk => chunk.IsJunction && chunk.NodeId == _NodeId && chunk.Kind == _Kind);
	}



	/// <summary>
	/// Compares the graph with the pieces under this GameObject and queues work for everything that is missing,
	/// stale or built from different settings. Stateless on purpose: it copes with undo, reloads, hand deleted
	/// pieces and hot reloads alike, because the pieces themselves carry what they were built from.
	/// </summary>
	private void Reconcile()
	{
		PruneCurveTables();

		// A lot's entrances belong to its edge, wherever the lot has been moved to since.
		SnapAllOpenings();

		var segmentChunks = new Dictionary<int, List<RoadChunk>>();
		var junctionChunks = new Dictionary<(int NodeId, RoadChunkKind Kind), RoadChunk>();
		var lotChunks = new Dictionary<(int LotId, RoadChunkKind Kind), RoadChunk>();

		foreach (var chunk in GetChunks())
		{
			if (chunk.IsLot)
			{
				if (lotChunks.TryGetValue((chunk.LotId, chunk.Kind), out var duplicate))
					duplicate.GameObject.Destroy();

				lotChunks[(chunk.LotId, chunk.Kind)] = chunk;
				continue;
			}

			if (chunk.IsJunction)
			{
				// Two pieces for one junction can only be leftovers; keep one, the reconcile sorts out whether it is current.
				if (junctionChunks.TryGetValue((chunk.NodeId, chunk.Kind), out var duplicate))
					duplicate.GameObject.Destroy();

				junctionChunks[(chunk.NodeId, chunk.Kind)] = chunk;
				continue;
			}

			if (!segmentChunks.TryGetValue(chunk.SegmentId, out var list))
			{
				list = new List<RoadChunk>();
				segmentChunks[chunk.SegmentId] = list;
			}

			list.Add(chunk);
		}

		var layouts = ComputeJunctionLayouts();
		var dashPhases = ComputeDashPhases();
		var liveSegments = new HashSet<int>();
		var segmentBlends = new Dictionary<int, RoadBorderBuilder.SegmentBorderBlends>();
		var segmentTapers = new Dictionary<int, SegmentBorderTapers>();

		foreach (var segment in Segments)
		{
			var table = GetCurveTable(segment);

			if (table is null)
				continue;

			liveSegments.Add(segment.Id);

			var profile = ProfileOf(segment);
			// A road opening into a parking lot starts where the lot's surface stops covering it.
			float trimStart = MathF.Max(layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id)?.Trim ?? 0.0f, LotTrimAt(segment.StartNodeId));
			float trimEnd = MathF.Max(layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id)?.Trim ?? 0.0f, LotTrimAt(segment.EndNodeId));
			var caps = ComputeBorderCaps(segment, layouts);
			var blends = ComputeBorderBlends(segment, table, layouts);
			var taper = ComputeBorderTaper(segment, table, layouts, trimStart, trimEnd);
			var crossings = ComputeCrossings(segment, table, layouts, trimStart, trimEnd);
			var lines = ComputeLineRange(table, profile, trimStart, trimEnd, crossings);
			var dash = dashPhases.GetValueOrDefault(segment.Id);

			var plans = PlanChunks(segment, table, trimStart, trimEnd, profile, caps, blends, taper, ComputeSurfaceSignature(segment, table.Curve, profile), lines, dash, crossings);
			long groupSignature = CombineSignatures(plans);

			segmentBlends[segment.Id] = blends;
			segmentTapers[segment.Id] = taper;
			segmentChunks.TryGetValue(segment.Id, out var existing);

			if (IsSegmentUpToDate(existing, plans))
				continue;

			// Already queued with these exact inputs: let those jobs finish rather than doubling them.
			if (m_PendingSegments.TryGetValue(segment.Id, out var pending) && pending.Signature == groupSignature)
				continue;

			EnqueueSegment(segment, table, profile, plans, groupSignature, existing);
		}

		foreach (var (nodeId, layout) in layouts)
		{
			ReconcileJunctionPiece(nodeId, layout, RoadChunkKind.Surface, ComputeJunctionSurfaceSignature(layout), junctionChunks, null);

			bool anyBorder = layout.Arms.Any(arm => ProfileOf(arm.Segment).HasBorder);

			if (anyBorder)
			{
				// The junction's border runs continue the arms' chunks, so they take the same lifts and tapers those
				// were built with.
				var armBlends = new Dictionary<int, RoadBorderBuilder.SegmentBorderBlends>();
				var armTapers = new Dictionary<int, SegmentBorderTapers>();

				foreach (var arm in layout.Arms)
				{
					armBlends[arm.Segment.Id] = segmentBlends.GetValueOrDefault(arm.Segment.Id);
					armTapers[arm.Segment.Id] = segmentTapers.GetValueOrDefault(arm.Segment.Id);
				}

				ReconcileJunctionPiece(nodeId, layout, RoadChunkKind.Border, ComputeJunctionBorderSignature(layout, armBlends, armTapers), junctionChunks, armBlends, armTapers);
			}
			else if (junctionChunks.TryGetValue((nodeId, RoadChunkKind.Border), out var border))
			{
				border.GameObject.Destroy();
			}

			// Crossings used to be decals hung on the junction; the roads paint their own now, so anything left
			// here from then goes.
			if (junctionChunks.TryGetValue((nodeId, RoadChunkKind.Marking), out var marking))
				marking.GameObject.Destroy();
		}

		ReconcileLots(lotChunks);

		// Whatever no longer belongs to anything in the graph goes, locked or not.
		foreach (var (segmentId, chunks) in segmentChunks)
		{
			if (liveSegments.Contains(segmentId))
				continue;

			foreach (var chunk in chunks)
				chunk.GameObject.Destroy();

			m_PendingSegments.Remove(segmentId);
		}

		foreach (var (key, chunk) in junctionChunks)
		{
			if (layouts.ContainsKey(key.NodeId))
				continue;

			chunk.GameObject.Destroy();
			m_PendingJunctions.Remove(key);
		}
	}



	private void ReconcileJunctionPiece(int _NodeId, RoadJunctionLayout _Layout, RoadChunkKind _Kind, long _Signature, Dictionary<(int NodeId, RoadChunkKind Kind), RoadChunk> _Existing, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> _ArmBlends, Dictionary<int, SegmentBorderTapers> _ArmTapers = null)
	{
		var key = (_NodeId, _Kind);

		if (_Existing.TryGetValue(key, out var existing) && (existing.Locked || (existing.Signature == _Signature && !LacksDebugRecord(existing))))
			return;

		if (m_PendingJunctions.TryGetValue(key, out var pending) && pending.Signature == _Signature)
			return;

		m_Jobs.Enqueue(new RebuildJob(-1, _NodeId, _Kind, _Signature, default, null, RoadWidth.Uniform(0.0f), null, _Layout, _ArmBlends, _ArmTapers, null));
		m_PendingJunctions[key] = new PendingGroup(_Signature, 1);
	}



	/// <summary>
	/// Where every segment sits in its run: the stretch of road carried on through pass-through nodes until a
	/// junction, a road end or a loop. Each run is walked once, from the lowest-numbered segment in it backwards
	/// to its head and then forwards to its tail, in that segment's direction, so every segment of the run agrees
	/// on where the run's distance starts and the dashes carry across the nodes.
	/// </summary>
	private Dictionary<int, RoadMarkingBuilder.DashPhase> ComputeDashPhases()
	{
		var phases = new Dictionary<int, RoadMarkingBuilder.DashPhase>();

		foreach (var seed in Segments.OrderBy(segment => segment.Id))
		{
			if (phases.ContainsKey(seed.Id))
				continue;

			var seen = new HashSet<int> { seed.Id };
			var run = new List<(RoadSegment Segment, bool Forward)>();

			// Back to the head: a road carrying on through the node runs with the run when it ends at that node.
			var segment = seed;
			int nodeId = seed.StartNodeId;

			while (RunContinuation(segment, nodeId) is { } previous && seen.Add(previous.Id))
			{
				bool forward = previous.EndNodeId == nodeId;

				run.Add((previous, forward));
				segment = previous;
				nodeId = forward ? previous.StartNodeId : previous.EndNodeId;
			}

			run.Reverse();
			run.Add((seed, true));

			// On to the tail.
			segment = seed;
			nodeId = seed.EndNodeId;

			while (RunContinuation(segment, nodeId) is { } next && seen.Add(next.Id))
			{
				bool forward = next.StartNodeId == nodeId;

				run.Add((next, forward));
				segment = next;
				nodeId = forward ? next.EndNodeId : next.StartNodeId;
			}

			float offset = 0.0f;

			foreach (var (member, forward) in run)
			{
				phases[member.Id] = new RoadMarkingBuilder.DashPhase(offset, !forward);
				offset += GetCurveTable(member)?.Length ?? 0.0f;
			}
		}

		return phases;
	}



	/// <summary>
	/// The road a segment carries straight on into at a pass-through node, or null at anything else.
	/// </summary>
	private RoadSegment RunContinuation(RoadSegment _Segment, int _NodeId)
	{
		if (GetDegree(_NodeId) != 2)
			return null;

		return GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);
	}



	/// <summary>
	/// How a segment's border runs out at each end where it meets another road type's: over the profile's transition
	/// length, towards the cross-section the two share. Without the junction layouts only pass-through nodes are
	/// known; the reconcile passes them, so junction corners taper too.
	/// </summary>
	public SegmentBorderTapers GetBorderTaper(RoadSegment _Segment)
	{
		var table = GetCurveTable(_Segment);

		return table is null ? default : ComputeBorderTaper(_Segment, table);
	}



	/// <summary>
	/// One end of a border's taper: how long it is, the width and share of relief it runs to, whether a country road's
	/// gravel spills over it, and how far from the node it ends.
	/// </summary>
	private readonly record struct EndTaper(float Length, float Width, float Relief, bool Spill, float Offset);



	private SegmentBorderTapers ComputeBorderTaper(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts = null, float _TrimStart = 0.0f, float _TrimEnd = 0.0f)
	{
		var profile = ProfileOf(_Segment);

		if (!profile.HasBorder)
			return default;

		float limit = _Table.Length * 0.5f;
		RoadBorderTaper left = default;
		RoadBorderTaper right = default;

		// A transition ends at its trim, so the two ends share the stretch BETWEEN the trims and each gets half of
		// it; however far a junction reaches along the road (a ramp meeting a highway can trim most of it), the two
		// never run into each other and neither is squeezed out.
		float transitionLimit = MathF.Max(0.0f, _Table.Length - _TrimStart - _TrimEnd) * 0.5f;

		if (profile.TransitionLength > 0.0f)
		{
			SideTapers(_Segment, profile, _Segment.StartNodeId, true, transitionLimit, _Layouts, ref left, ref right);
			SideTapers(_Segment, profile, _Segment.EndNodeId, false, transitionLimit, _Layouts, ref left, ref right);
		}

		// At a nose, the border on the side facing the road this one parts from grows out of nothing instead.
		if (GoreLength(_Segment, _Table, _Segment.StartNodeId, true, limit) is { } startLeft)
			left = left with { StartLength = startLeft, StartWidth = 0.0f, StartRelief = 0.0f, StartSpill = false, StartGore = true, StartOffset = 0.0f };

		if (GoreLength(_Segment, _Table, _Segment.StartNodeId, false, limit) is { } startRight)
			right = right with { StartLength = startRight, StartWidth = 0.0f, StartRelief = 0.0f, StartSpill = false, StartGore = true, StartOffset = 0.0f };

		if (GoreLength(_Segment, _Table, _Segment.EndNodeId, true, limit) is { } endLeft)
			left = left with { EndLength = endLeft, EndWidth = 0.0f, EndRelief = 0.0f, EndSpill = false, EndGore = true, EndOffset = 0.0f };

		if (GoreLength(_Segment, _Table, _Segment.EndNodeId, false, limit) is { } endRight)
			right = right with { EndLength = endRight, EndWidth = 0.0f, EndRelief = 0.0f, EndSpill = false, EndGore = true, EndOffset = 0.0f };

		return new SegmentBorderTapers(left, right);
	}



	/// <summary>
	/// The tapers at one end of a segment, one per side, each towards the border across from it there. At a plain
	/// pass-through node that is the other road on both sides. At a junction it is the road on the far side of each
	/// corner, and the taper ends at the arm's trim, where the corner starts: the corner itself is then built in the
	/// one cross-section both its roads have run to, so it is a clean curve rather than one border turning into
	/// the other round the arc.
	/// </summary>
	private void SideTapers(RoadSegment _Segment, RoadProfile _Profile, int _NodeId, bool _AtStart, float _Limit, Dictionary<int, RoadJunctionLayout> _Layouts, ref RoadBorderTaper _Left, ref RoadBorderTaper _Right)
	{
		int degree = GetDegree(_NodeId);

		if (degree < 2)
			return;

		RoadSegment leftNeighbour = null;
		RoadSegment rightNeighbour = null;
		float trim = 0.0f;

		if (_Layouts is not null && _Layouts.TryGetValue(_NodeId, out var layout) && layout.FindArm(_Segment.Id) is { } arm)
		{
			trim = arm.Trim;

			// A corner starting on this arm runs on its left looking out of the node, one ending on it on its right;
			// looking along a segment that ends at the node swaps the two.
			foreach (var path in layout.CornerPaths)
			{
				if (path.ArmA.Segment.Id == _Segment.Id)
				{
					if (_AtStart)
						leftNeighbour = path.ArmB.Segment;
					else
						rightNeighbour = path.ArmB.Segment;
				}
				else if (path.ArmB.Segment.Id == _Segment.Id)
				{
					if (_AtStart)
						rightNeighbour = path.ArmA.Segment;
					else
						leftNeighbour = path.ArmA.Segment;
				}
			}
		}
		else if (degree == 2)
		{
			leftNeighbour = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);
			rightNeighbour = leftNeighbour;
		}

		if (SideTaper(_Profile, leftNeighbour, degree, trim, _Limit, out var leftEnd))
			_Left = WithEnd(_Left, _AtStart, leftEnd);

		if (SideTaper(_Profile, rightNeighbour, degree, trim, _Limit, out var rightEnd))
			_Right = WithEnd(_Right, _AtStart, rightEnd);
	}



	/// <summary>
	/// The taper one side of a border takes towards the border across from it at a node, or false when it takes none:
	/// nothing across, a border that is already the same size, or — at a junction — a border of another kind, which
	/// a junction still closes off against this one.
	/// </summary>
	private bool SideTaper(RoadProfile _Profile, RoadSegment _Neighbour, int _Degree, float _Trim, float _Limit, out EndTaper _Taper)
	{
		_Taper = default;

		if (_Neighbour is null)
			return false;

		var other = ProfileOf(_Neighbour);
		bool sameKind = other.Border == _Profile.Border;

		if (_Degree > 2 && !sameKind)
			return false;

		if (sameKind && MathF.Abs(other.FlatBorderWidth - _Profile.FlatBorderWidth) < 0.01f && MathF.Abs(other.ReliefHeight - _Profile.ReliefHeight) < 0.01f)
			return false;

		// Held to its half of the stretch between the trims, so it never runs into the taper at the far end.
		float length = MathF.Min(_Profile.TransitionLength, _Limit);

		if (length <= 0.01f)
			return false;

		_Taper = new EndTaper(length, RoadProfile.MeetingWidth(_Profile, other), RoadProfile.MeetingRelief(_Profile, other), !sameKind && other.Border == RoadBorderStyle.CountrySide, _Trim);

		return true;
	}



	private static RoadBorderTaper WithEnd(in RoadBorderTaper _Taper, bool _AtStart, in EndTaper _End)
	{
		return _AtStart
			? _Taper with { StartLength = _End.Length, StartWidth = _End.Width, StartRelief = _End.Relief, StartSpill = _End.Spill, StartOffset = _End.Offset }
			: _Taper with { EndLength = _End.Length, EndWidth = _End.Width, EndRelief = _End.Relief, EndSpill = _End.Spill, EndOffset = _End.Offset };
	}



	/// <summary>
	/// How far from a node the gap between this segment's edge on one side (its own left or right) and the edge of
	/// another road parting from the same point has opened enough for both roads' borders to fit side by side; null
	/// when no road parts from that edge there. Sampled along both roads at equal distances from the node, which
	/// is how two roads leaving a common tangent separate.
	/// </summary>
	private float? GoreLength(RoadSegment _Segment, RoadCurveTable _Table, int _NodeId, bool _Left, float _Limit)
	{
		const float step = 25.0f;

		if (GetDegree(_NodeId) < 3)
			return null;

		bool atStart = _Segment.StartNodeId == _NodeId;
		float own = ProfileOf(_Segment).FlatBorderWidth;
		var width = new RoadWidth(_Segment, _Table.Length);

		// This segment's edge point at a distance from the node, on the side asked for.
		Vector3 OwnEdge(float _FromNode)
		{
			float distance = atStart ? _FromNode : _Table.Length - _FromNode;

			RoadSurfaceBuilder.CrossSection(_Table, distance, width, 0.0f, out Vector3 leftEdge, out Vector3 rightEdge);

			return _Left ? leftEdge : rightEdge;
		}

		Vector3 start = OwnEdge(0.0f);

		foreach (var other in GetSegmentsAt(_NodeId))
		{
			if (other.Id == _Segment.Id)
				continue;

			var otherTable = GetCurveTable(other);

			if (otherTable is null)
				continue;

			bool otherAtStart = other.StartNodeId == _NodeId;
			var otherWidth = new RoadWidth(other, otherTable.Length);

			Vector3 OtherEdge(float _FromNode, bool _OtherLeft)
			{
				float distance = otherAtStart ? _FromNode : otherTable.Length - _FromNode;

				RoadSurfaceBuilder.CrossSection(otherTable, distance, otherWidth, 0.0f, out Vector3 leftEdge, out Vector3 rightEdge);

				return _OtherLeft ? leftEdge : rightEdge;
			}

			// The other road's edge that starts on ours, if either does.
			bool otherLeft;

			if (OtherEdge(0.0f, true).Distance(start) < 1.0f)
				otherLeft = true;
			else if (OtherEdge(0.0f, false).Distance(start) < 1.0f)
				otherLeft = false;
			else
				continue;

			// Parting means leaving the node the same way; a road turning off at an angle is a corner, not a gore.
			Vector3 ownWay = OwnEdge(step) - start;
			Vector3 otherWay = OtherEdge(step, otherLeft) - start;

			if (ownWay.IsNearZeroLength || otherWay.IsNearZeroLength || Vector3.Dot(ownWay.Normal, otherWay.Normal) < 0.7f)
				continue;

			float needed = own + ProfileOf(other).FlatBorderWidth;
			float reach = MathF.Min(_Limit, MathF.Min(_Table.Length, otherTable.Length));

			for (float fromNode = step; fromNode <= reach; fromNode += step)
			{
				if (OwnEdge(fromNode).Distance(OtherEdge(fromNode, otherLeft)) >= needed)
					return MathF.Max(step, fromNode);
			}

			return MathF.Max(step, reach);
		}

		return null;
	}



	/// <summary>
	/// Whether two road types' borders come to one cross-section where they meet, so nothing needs closing off
	/// between them: of a kind, and both tapering into the width and height they share.
	/// </summary>
	private static bool MeetsFlush(RoadProfile _A, RoadProfile _B)
	{
		return _A.HasBorder && _A.Border == _B.Border && _A.TransitionLength > 0.0f && _B.TransitionLength > 0.0f;
	}



	/// <summary>
	/// The filled area of every junction, in network space (lifted to the road surface): its centre and its
	/// outline, which the junction surface fans between, plus how far a pavement reaches out from the outline
	/// round it and how far above the outline that pavement's top sits (nothing when no arm has a sidewalk). For
	/// tools that need to know where road is.
	/// </summary>
	public IEnumerable<(Vector3 Centre, IReadOnlyList<Vector3> Polygon, float PavementReach, float PavementTop)> GetJunctionPolygons()
	{
		// A parking lot is a junction as far as the ground under it is concerned.
		foreach (var lot in Lots)
		{
			var profile = ProfileOf(lot);
			bool pavement = profile.Border == RoadBorderStyle.Sidewalk;

			yield return (lot.Centre, RoadLotOutline.Build(lot, OpeningsOf(lot)).Points, pavement ? profile.BorderWidth : 0.0f, pavement ? profile.CurbHeight : 0.0f);
		}

		// A rounded dead end reaches past its node, where no road piece covers the ground.
		foreach (var segment in Segments)
		{
			var table = GetCurveTable(segment);

			if (table is null || segment.Bridge is not null)
				continue;

			var profile = ProfileOf(segment);
			bool pavement = profile.Border == RoadBorderStyle.Sidewalk;
			var width = new RoadWidth(segment, table.Length);

			foreach (bool atStart in new[] { true, false })
			{
				int nodeId = atStart ? segment.StartNodeId : segment.EndNodeId;

				if (GetDegree(nodeId) != 1 || LotAtOpening(nodeId) is not null)
					continue;

				var outline = RoadCapBuilder.Outline(table, atStart ? 0.0f : table.Length, atStart, width, profile, SurfaceOffset, out Vector3 centre);

				if (outline.Count >= 3)
					yield return (centre, outline, pavement ? profile.BorderWidth : 0.0f, pavement ? profile.CurbHeight : 0.0f);
			}
		}

		foreach (var layout in ComputeJunctionLayouts().Values)
		{
			if (layout.Polygon.Count < 3)
				continue;

			float reach = 0.0f;
			float top = 0.0f;

			foreach (var arm in layout.Arms)
			{
				var profile = ProfileOf(arm.Segment);

				if (profile.Border != RoadBorderStyle.Sidewalk)
					continue;

				reach = MathF.Max(reach, profile.BorderWidth);
				top = MathF.Max(top, profile.CurbHeight);
			}

			yield return (layout.Centre, layout.Polygon, reach, top);
		}
	}



	/// <summary>
	/// Lays out every junction. A road too short for the fillets at both its ends gets those fillets shrunk until
	/// their tangent points fit on it (a trim past the tangent point would send the corner geometry back over the
	/// road); only corners that cannot fit even sharp are clamped. Then the junction polygons are built.
	/// </summary>
	private Dictionary<int, RoadJunctionLayout> ComputeJunctionLayouts()
	{
		var radii = Nodes.ToDictionary(node => node.Id, node => MathF.Max(0.0f, node.CornerRadius));
		var layouts = BuildJunctionLayouts(radii);
		var shrink = new Dictionary<int, float>();

		foreach (var segment in Segments)
		{
			var table = GetCurveTable(segment);

			if (table is null)
				continue;

			var startArm = layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id);
			var endArm = layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id);
			float sharp = (startArm?.SharpTrim ?? 0.0f) + (endArm?.SharpTrim ?? 0.0f);
			float fillet = (startArm is null ? 0.0f : startArm.RawTrim - startArm.SharpTrim) + (endArm is null ? 0.0f : endArm.RawTrim - endArm.SharpTrim);
			float available = MathF.Max(0.0f, table.Length - MinChunkLength);

			if (fillet <= 0.0f || sharp + fillet <= available)
				continue;

			// The fillet's share of a trim is proportional to the radius, so this factor makes it fit exactly.
			float factor = Math.Clamp((available - sharp) / fillet, 0.0f, 1.0f);

			foreach (int nodeId in new[] { segment.StartNodeId, segment.EndNodeId })
			{
				if (layouts.ContainsKey(nodeId))
					shrink[nodeId] = MathF.Min(shrink.GetValueOrDefault(nodeId, 1.0f), factor);
			}
		}

		if (shrink.Count > 0)
		{
			foreach (var (nodeId, factor) in shrink)
				radii[nodeId] *= factor;

			layouts = BuildJunctionLayouts(radii);
		}

		// Last resort, for corners that do not fit even when sharp: share what road there is.
		foreach (var segment in Segments)
		{
			var table = GetCurveTable(segment);

			if (table is null)
				continue;

			var startArm = layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id);
			var endArm = layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id);
			float total = (startArm?.Trim ?? 0.0f) + (endArm?.Trim ?? 0.0f);
			float available = MathF.Max(0.0f, table.Length - MinChunkLength);

			if (total <= available || total <= 0.0f)
				continue;

			float scale = available / total;

			if (startArm is not null)
				startArm.Trim *= scale;

			if (endArm is not null)
				endArm.Trim *= scale;
		}

		foreach (var layout in layouts.Values)
			layout.BuildPolygon();

		return layouts;
	}



	/// <summary>
	/// The corner fold on one side of a segment at one of its nodes, and whether the segment is that fold's first
	/// arm (its left edge) or second (its right edge). Looking out of the node, a segment starting there has its
	/// left on the arm's left; one ending there has it on the arm's right.
	/// </summary>
	private static RoadJunctionLayout.CornerFold FindFold(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts, out bool _SideA)
	{
		_SideA = _AtStart == _Left;

		if (!_Layouts.TryGetValue(_NodeId, out var layout))
			return null;

		var path = _SideA
			? layout.CornerPaths.FirstOrDefault(candidate => candidate.ArmA.Segment.Id == _Segment.Id)
			: layout.CornerPaths.FirstOrDefault(candidate => candidate.ArmB.Segment.Id == _Segment.Id);

		return path?.Fold;
	}



	/// <summary>
	/// The junction layout of every node for the given fillet radii, from the cache where the inputs match.
	/// </summary>
	private Dictionary<int, RoadJunctionLayout> BuildJunctionLayouts(Dictionary<int, float> _Radii)
	{
		var layouts = new Dictionary<int, RoadJunctionLayout>();
		var liveNodes = new HashSet<int>();

		foreach (var node in Nodes)
		{
			liveNodes.Add(node.Id);

			float radius = _Radii[node.Id];
			long key = ComputeJunctionKey(node, radius);
			RoadJunctionLayout layout;

			// A cached layout is only as good as its references: undo and reloads swap the graph's objects.
			if (m_JunctionCache.TryGetValue(node.Id, out var cached) && cached.Key == key && (cached.Layout is null || cached.Layout.Refresh(this)))
			{
				layout = cached.Layout;
			}
			else
			{
				layout = RoadJunctionLayout.Create(this, node, SurfaceOffset, radius);
				m_JunctionCache[node.Id] = (key, layout);
			}

			if (layout is null)
				continue;

			layout.ResetTrims();
			layouts[node.Id] = layout;
		}

		foreach (int nodeId in m_JunctionCache.Keys.Where(id => !liveNodes.Contains(id)).ToList())
			m_JunctionCache.Remove(nodeId);

		return layouts;
	}



	/// <summary>
	/// Whether an end of a road asks for a pedestrian crossing: the road type has them, the road has not turned
	/// this end's off, and the end is a real junction (three roads or more; a bend has a layout too, but nobody
	/// crosses at a bend) that trims the road.
	/// </summary>
	private bool WantsCrossing(RoadSegment _Segment, int _NodeId, Dictionary<int, RoadJunctionLayout> _Layouts, float _Trim)
	{
		if (!ProfileOf(_Segment).Crossings || !_Segment.CrossingAt(_NodeId))
			return false;

		return _Layouts.TryGetValue(_NodeId, out var layout) && layout.Arms.Count >= 3 && _Trim > 0.01f;
	}



	/// <summary>
	/// Places a road's crossings on the stretch left between its junctions. Two crossings need room for both and
	/// a gap between; a shorter road between two junctions gets one crossing shared by both, halfway along; a road
	/// shorter than a crossing gets none.
	/// </summary>
	private SegmentCrossings ComputeCrossings(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts, float _TrimStart, float _TrimEnd)
	{
		var profile = ProfileOf(_Segment);
		float depth = profile.CrossingDepth;
		float clear = _Table.Length - _TrimStart - _TrimEnd;
		bool wantStart = WantsCrossing(_Segment, _Segment.StartNodeId, _Layouts, _TrimStart);
		bool wantEnd = WantsCrossing(_Segment, _Segment.EndNodeId, _Layouts, _TrimEnd);

		if (clear < depth || (!wantStart && !wantEnd))
			return default;

		// A ladder of stripes is laid on the road outside the junction; two lines mark the walk across the
		// junction's own mouth, so that kind is pulled in far enough to put its outer line on the pavement's
		// corner instead of a crossing's depth short of it.
		bool inside = profile.CrossingStyle == RoadCrossingStyle.Lines;
		float startCentre = inside
			? MathF.Max(depth * 0.5f, _TrimStart - depth * 0.5f)
			: _TrimStart + depth * 0.5f;

		float endCentre = inside
			? MathF.Min(_Table.Length - depth * 0.5f, _Table.Length - _TrimEnd + depth * 0.5f)
			: _Table.Length - _TrimEnd - depth * 0.5f;

		if (wantStart && wantEnd)
		{
			if (clear >= 2.0f * depth + profile.CrossingClearance)
				return new SegmentCrossings(true, startCentre, true, endCentre);

			return new SegmentCrossings(true, _TrimStart + clear * 0.5f, false, 0.0f);
		}

		return wantStart
			? new SegmentCrossings(true, startCentre, false, 0.0f)
			: new SegmentCrossings(false, 0.0f, true, endCentre);
	}



	/// <summary>
	/// Where the painted lines run: the stretch between the junctions, kept clear of the crossings.
	/// </summary>
	private static LineRange ComputeLineRange(RoadCurveTable _Table, RoadProfile _Profile, float _TrimStart, float _TrimEnd, in SegmentCrossings _Crossings)
	{
		float reach = _Profile.CrossingDepth * 0.5f + _Profile.CrossingClearance;
		float start = _TrimStart;
		float end = _Table.Length - _TrimEnd;
		float gapStart = 0.0f;
		float gapEnd = 0.0f;

		if (_Crossings.AtEnd)
			end = MathF.Min(end, _Crossings.EndCentre - reach);

		if (_Crossings.AtStart)
		{
			float before = _Crossings.StartCentre - reach;
			float after = _Crossings.StartCentre + reach;

			// A crossing at the very start just pushes the lines back; one further along cuts a gap in them.
			if (before <= start + 1.0f)
			{
				start = MathF.Max(start, after);
			}
			else
			{
				gapStart = before;
				gapEnd = after;
			}
		}

		return new LineRange(start, end, gapStart, gapEnd);
	}



	/// <summary>
	/// Which ends of a segment's borders nothing continues: a road end, a side of a junction with no corner on it,
	/// or a pass-through node where the road changes type or width.
	/// </summary>
	private BorderCaps ComputeBorderCaps(RoadSegment _Segment, Dictionary<int, RoadJunctionLayout> _Layouts)
	{
		var caps = BorderCaps.None;

		if (IsBorderOpen(_Segment, _Segment.StartNodeId, true, _Layouts, out bool startLeft, out bool startRight))
		{
			caps |= startLeft ? BorderCaps.StartLeft : BorderCaps.None;
			caps |= startRight ? BorderCaps.StartRight : BorderCaps.None;
		}

		if (IsBorderOpen(_Segment, _Segment.EndNodeId, false, _Layouts, out bool endLeft, out bool endRight))
		{
			caps |= endLeft ? BorderCaps.EndLeft : BorderCaps.None;
			caps |= endRight ? BorderCaps.EndRight : BorderCaps.None;
		}

		return caps;
	}



	/// <summary>
	/// A bridge segment's built range and ends. An end is closed off unless it is a pass-through node onto
	/// another bridge of the same style, width and border, where the two decks meet flush; there, only one of the
	/// two segments provides the node's railing post: the one leaving the node, or the lower id when both leave
	/// or both arrive.
	/// </summary>
	private RoadBridgeBuilder.BridgeSpan ComputeBridgeSpan(RoadSegment _Segment, float _RangeStart, float _RangeEnd)
	{
		var startNeighbour = DeckContinuation(_Segment, _Segment.StartNodeId);
		var endNeighbour = DeckContinuation(_Segment, _Segment.EndNodeId);

		bool postStart = startNeighbour is null || startNeighbour.EndNodeId == _Segment.StartNodeId || _Segment.Id < startNeighbour.Id;
		bool postEnd = endNeighbour is null || (endNeighbour.EndNodeId == _Segment.EndNodeId && _Segment.Id < endNeighbour.Id);

		return new RoadBridgeBuilder.BridgeSpan(_RangeStart, _RangeEnd, startNeighbour is null, postStart, endNeighbour is null, postEnd);
	}



	/// <summary>
	/// The bridge segment the deck carries straight on to at a node, or null where the deck ends.
	/// </summary>
	private RoadSegment DeckContinuation(RoadSegment _Segment, int _NodeId)
	{
		if (GetDegree(_NodeId) != 2)
			return null;

		var other = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);

		if (other?.Bridge is null || other.Bridge.ResourcePath != _Segment.Bridge.ResourcePath || !SameEdgesAt(_Segment, other, _NodeId))
			return null;

		bool sameExtent = MathF.Abs(RoadBridgeBuilder.DeckExtent(ProfileOf(other), other.Bridge) - RoadBridgeBuilder.DeckExtent(ProfileOf(_Segment), _Segment.Bridge)) < 0.01f;

		return sameExtent ? other : null;
	}



	/// <summary>
	/// The lift lines of a segment's two borders: at each end, the corner's height difference on that side (or
	/// nothing), run linearly along the road between the two.
	/// </summary>
	private RoadBorderBuilder.SegmentBorderBlends ComputeBorderBlends(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts)
	{
		return new RoadBorderBuilder.SegmentBorderBlends(SideBlendFor(_Segment, _Table, true, _Layouts), SideBlendFor(_Segment, _Table, false, _Layouts));
	}



	private RoadBorderBuilder.SideBlend SideBlendFor(RoadSegment _Segment, RoadCurveTable _Table, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts)
	{
		bool hasStart = CornerLift(_Segment, _Segment.StartNodeId, true, _Left, _Layouts, out float startDistance, out float startDelta);
		bool hasEnd = CornerLift(_Segment, _Segment.EndNodeId, false, _Left, _Layouts, out float endDistance, out float endDelta);

		if (!hasStart && !hasEnd)
			return default;

		// An end with no corner on this side holds the border at road height from where the border actually starts,
		// which is past the trim: a road opening into a lot stops at the lot's flares, and a blend still running
		// there hands the lot a pavement tilted by whatever it had left to shed while the lot's own is flat.
		if (!hasStart)
		{
			startDistance = LotTrimAt(_Segment.StartNodeId);
			startDelta = 0.0f;
		}

		if (!hasEnd)
		{
			endDistance = _Table.Length - LotTrimAt(_Segment.EndNodeId);
			endDelta = 0.0f;
		}

		return new RoadBorderBuilder.SideBlend(true, startDistance, startDelta, endDistance, endDelta);
	}



	/// <summary>
	/// Where along the segment the corner's tangent point on this side is, and how much higher the corner sits
	/// than the road edge there; false when there is no inside corner on that side at that end.
	/// </summary>
	private static bool CornerLift(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts, out float _Distance, out float _Delta)
	{
		_Distance = 0.0f;
		_Delta = 0.0f;

		var fold = FindFold(_Segment, _NodeId, _AtStart, _Left, _Layouts, out bool sideA);

		if (fold is null || !_Layouts.TryGetValue(_NodeId, out var layout))
			return false;

		var arm = layout.FindArm(_Segment.Id);

		if (arm is null)
			return false;

		_Distance = arm.CurveDistance(sideA ? fold.TangentParamA : fold.TangentParamB);
		_Delta = fold.ApexHeight - (sideA ? fold.TangentHeightA : fold.TangentHeightB);

		return true;
	}



	private bool IsBorderOpen(RoadSegment _Segment, int _NodeId, bool _AtStart, Dictionary<int, RoadJunctionLayout> _Layouts, out bool _Left, out bool _Right)
	{
		int degree = GetDegree(_NodeId);

		// A road opening into a lot runs into the lot's own kerb, which closes it off; two caps in one place
		// would only fight each other.
		if (LotAtOpening(_NodeId) is { } lot && ProfileOf(lot).Border == ProfileOf(_Segment).Border)
		{
			_Left = false;
			_Right = false;
			return false;
		}

		if (degree <= 1)
		{
			// A road rounded off where it stops carries its border round the end; a cap across the road as well
			// would sit inside that.
			bool rounded = _Segment.Bridge is null && ProfileOf(_Segment).RoundedEnds;

			_Left = !rounded;
			_Right = !rounded;
			return !rounded;
		}

		if (_Layouts.TryGetValue(_NodeId, out var layout))
		{
			// A corner path starting on this arm continues its left border (looking out of the node); one ending on
			// it continues its right. Looking out of the node along a segment that ends there swaps the sides.
			bool armLeftContinued = layout.CornerPaths.Any(path => path.ArmA.Segment.Id == _Segment.Id);
			bool armRightContinued = layout.CornerPaths.Any(path => path.ArmB.Segment.Id == _Segment.Id);

			_Left = !(_AtStart ? armLeftContinued : armRightContinued);
			_Right = !(_AtStart ? armRightContinued : armLeftContinued);
			return _Left || _Right;
		}

		if (degree == 2)
		{
			var other = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);
			bool continuous = other is not null && SameEdgesAt(_Segment, other, _NodeId)
				&& (RoadBorderBuilder.SameProfile(ProfileOf(other), ProfileOf(_Segment)) || MeetsFlush(ProfileOf(other), ProfileOf(_Segment)));

			_Left = !continuous;
			_Right = !continuous;
			return !continuous;
		}

		_Left = true;
		_Right = true;
		return true;
	}



	/// <summary>
	/// Splits a segment into chunks at fixed distances from its start, clipped by the junction trims at each end,
	/// with a border chunk over each stretch when the road type has borders. Anchoring the boundaries to the
	/// segment rather than to the trimmed length means a junction change at one end only touches the chunk next
	/// to it.
	/// </summary>
	private List<ChunkPlan> PlanChunks(RoadSegment _Segment, RoadCurveTable _Table, float _TrimStart, float _TrimEnd, RoadProfile _Profile, BorderCaps _Caps, in RoadBorderBuilder.SegmentBorderBlends _Blends, in SegmentBorderTapers _Taper, long _SurfaceSignature, in LineRange _Lines, in RoadMarkingBuilder.DashPhase _Dash, in SegmentCrossings _Crossings)
	{
		float length = _Table.Length;
		int count = Math.Max(1, (int)MathF.Ceiling(length / ChunkLength));
		float usableEnd = length - _TrimEnd;
		var ranges = new List<(int Index, float Start, float End)>(count);

		for (int i = 0; i < count; i++)
		{
			float start = MathF.Max(length * i / count, _TrimStart);
			float end = MathF.Min(length * (i + 1) / count, usableEnd);

			if (end - start >= MinChunkLength)
				ranges.Add((i, start, end));
		}

		var plans = new List<ChunkPlan>(ranges.Count * 4);
		var borderHash = StableHash.Create();
		borderHash.Add(_SurfaceSignature);
		_Profile.AddBorderToHash(ref borderHash);
		_Blends.AddToHash(ref borderHash);
		_Taper.AddToHash(ref borderHash);
		borderHash.Add(DebugBorders ? 1 : 0);
		long borderSignature = borderHash.Value;

		// The deck follows the road and stands off its border, so it is stale when either changes; its posts and
		// supports are spread over the whole built range, so every piece is stale when an end moves.
		var bridge = _Segment.Bridge;
		var bridgeSpan = bridge is null ? default : ComputeBridgeSpan(_Segment, _TrimStart, usableEnd);
		long bridgeSignature = 0;

		if (bridge is not null)
		{
			var bridgeHash = StableHash.Create();
			bridgeHash.Add(_SurfaceSignature);
			bridgeHash.Add(bridge.ResourcePath);
			_Profile.AddBorderToHash(ref bridgeHash);
			bridge.AddToHash(ref bridgeHash);
			bridgeSpan.AddToHash(ref bridgeHash);
			bridgeSignature = bridgeHash.Value;
		}

		// Props are spread over the whole built range too, and stand on the border, so they follow its shape.
		var props = _Segment.Props;
		var propSpan = props is null ? default : new RoadPropBuilder.PropSpan(_TrimStart, usableEnd, GetDegree(_Segment.StartNodeId) >= 3, GetDegree(_Segment.EndNodeId) >= 3);
		long propSignature = 0;

		if (props is not null && !props.IsEmpty)
		{
			var propHash = StableHash.Create();
			propHash.Add(_SurfaceSignature);
			propHash.Add(props.ResourcePath);
			propHash.Add(bridge is null ? 0 : 1);
			_Profile.AddBorderToHash(ref propHash);
			props.AddToHash(ref propHash);
			propSpan.AddToHash(ref propHash);
			propSignature = propHash.Value;
		}

		// A road with no lines of its own still needs the piece when it has a crossing to paint.
		bool markings = RoadMarkingBuilder.HasMarkings(_Segment, _Profile) || _Crossings.AtStart || _Crossings.AtEnd;
		var markingHash = StableHash.Create();
		markingHash.Add(_SurfaceSignature);
		markingHash.Add(_Segment.LaneCount);
		markingHash.Add(_Segment.ForwardLanes);
		_Lines.AddToHash(ref markingHash);
		_Dash.AddToHash(ref markingHash);
		_Profile.AddMarkingsToHash(ref markingHash);
		_Profile.AddCrossingsToHash(ref markingHash);
		markingHash.Add(_Crossings.AtStart ? _Crossings.StartCentre : -1.0f);
		markingHash.Add(_Crossings.AtEnd ? _Crossings.EndCentre : -1.0f);
		long markingSignature = markingHash.Value;

		for (int r = 0; r < ranges.Count; r++)
		{
			var (index, start, end) = ranges[r];

			var hash = StableHash.Create();
			hash.Add(_SurfaceSignature);
			hash.Add(index);
			hash.Add(count);
			hash.Add(start);
			hash.Add(end);

			plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Surface, BorderCaps.None, default, hash.Value));

			if (markings)
			{
				hash = StableHash.Create();
				// A crossing belongs to one piece: the outermost, since its band may lie clear of every piece's
				// own stretch of road.
				var owned = new SegmentCrossings(_Crossings.AtStart && r == 0, _Crossings.StartCentre,
					_Crossings.AtEnd && r == ranges.Count - 1, _Crossings.EndCentre);

				hash.Add(markingSignature);
				hash.Add(index);
				hash.Add(count);
				hash.Add(start);
				hash.Add(end);
				hash.Add(owned.AtStart ? 1 : 0);
				hash.Add(owned.AtEnd ? 1 : 0);

				plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Marking, BorderCaps.None, default, hash.Value, _Lines, Dash: _Dash, Crossings: owned));
			}

			if (bridge is not null)
			{
				// The piece works out for itself whether it is an outermost one, from its stretch against the span.
				hash = StableHash.Create();
				hash.Add(bridgeSignature);
				hash.Add(index);
				hash.Add(count);
				hash.Add(start);
				hash.Add(end);

				plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Bridge, BorderCaps.None, default, hash.Value, Bridge: bridgeSpan));
			}

			if (propSignature != 0)
			{
				hash = StableHash.Create();
				hash.Add(propSignature);
				hash.Add(index);
				hash.Add(count);
				hash.Add(start);
				hash.Add(end);

				plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Props, BorderCaps.None, default, hash.Value, Props: propSpan));
			}

			if (!_Profile.HasBorder)
				continue;

			// Only the outermost stretches have open ends to close.
			var caps = BorderCaps.None;

			if (r == 0)
				caps |= _Caps & (BorderCaps.StartLeft | BorderCaps.StartRight);

			if (r == ranges.Count - 1)
				caps |= _Caps & (BorderCaps.EndLeft | BorderCaps.EndRight);

			hash = StableHash.Create();
			hash.Add(borderSignature);
			hash.Add(index);
			hash.Add(count);
			hash.Add(start);
			hash.Add(end);
			hash.Add((int)caps);

			plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Border, caps, _Blends, hash.Value, default, _Taper));
		}

		// A road that simply stops is rounded off there, in a piece of its own beyond the node.
		AddCapPlan(plans, _Segment, _Table, _Profile, _SurfaceSignature, true);
		AddCapPlan(plans, _Segment, _Table, _Profile, _SurfaceSignature, false);

		return plans;
	}



	/// <summary>
	/// The rounded end of a road, where it has one: at a node with nothing else at it, which is not a way into a
	/// parking lot (the lot's own kerb closes that), and on a road no bridge carries (a deck ends where it ends).
	/// The plan's start is the end's own distance along the curve and its end the depth the cap reaches past it.
	/// </summary>
	private void AddCapPlan(List<ChunkPlan> _Plans, RoadSegment _Segment, RoadCurveTable _Table, RoadProfile _Profile, long _SurfaceSignature, bool _AtStart)
	{
		int nodeId = _AtStart ? _Segment.StartNodeId : _Segment.EndNodeId;

		if (_Segment.Bridge is not null || GetDegree(nodeId) != 1 || LotAtOpening(nodeId) is not null)
			return;

		float distance = _AtStart ? 0.0f : _Table.Length;
		float depth = RoadCapBuilder.Depth(_Profile, new RoadWidth(_Segment, _Table.Length), distance);

		if (depth <= 0.01f)
			return;

		var hash = StableHash.Create();

		hash.Add(_SurfaceSignature);
		hash.Add(_AtStart ? 1 : 0);
		hash.Add(depth);
		_Profile.AddBorderToHash(ref hash);

		_Plans.Add(new ChunkPlan(_AtStart ? 0 : 1, 2, distance, depth, RoadChunkKind.Cap, BorderCaps.None, default, hash.Value));
	}



	private static long CombineSignatures(List<ChunkPlan> _Plans)
	{
		var hash = StableHash.Create();
		hash.Add(_Plans.Count);

		foreach (var plan in _Plans)
			hash.Add(plan.Signature);

		return hash.Value;
	}



	private bool IsSegmentUpToDate(List<RoadChunk> _Existing, List<ChunkPlan> _Plans)
	{
		int have = _Existing?.Count ?? 0;

		if (have != _Plans.Count)
			return false;

		foreach (var plan in _Plans)
		{
			var chunk = _Existing.FirstOrDefault(candidate => candidate.ChunkIndex == plan.Index && candidate.Kind == plan.Kind);

			if (chunk is null)
				return false;

			// A locked chunk is exempt: it is deliberately stale.
			if (!chunk.Locked && (chunk.Signature != plan.Signature || LacksDebugRecord(chunk)))
				return false;
		}

		return true;
	}



	/// <summary>
	/// While the border debug overlay is on, a border piece without its record (built before the flag went on, or
	/// before a hot reload, which drops the records) is rebuilt to get one.
	/// </summary>
	private bool LacksDebugRecord(RoadChunk _Chunk)
	{
		return DebugBorders && _Chunk.Kind == RoadChunkKind.Border && (_Chunk.Debug is null || _Chunk.Debug.Text.Length == 0);
	}



	/// <summary>
	/// Throws away every unlocked piece's signature so the next reconcile rebuilds all of them, junction layouts
	/// included. Toggling the GameObject does not do this: pieces whose inputs have not changed are kept as they
	/// are, so after a generator change this is the way to see it everywhere.
	/// </summary>
	[Button("Regenerate All", "refresh")]
	public void RegenerateAll()
	{
		foreach (var chunk in GetChunks())
		{
			if (!chunk.Locked)
				chunk.Signature = 0;
		}

		m_JunctionCache.Clear();
		MarkDirty();
	}



	private long ComputeSurfaceSignature(RoadSegment _Segment, in RoadCurve _Curve, RoadProfile _Profile)
	{
		var hash = StableHash.Create();

		hash.Add(GeneratorVersion);
		hash.Add(_Curve.P0);
		hash.Add(_Curve.P1);
		hash.Add(_Curve.P2);
		hash.Add(_Curve.P3);
		new RoadWidth(_Segment, 1.0f).AddToHash(ref hash);
		hash.Add(SampleSpacing);
		hash.Add(ChunkLength);
		hash.Add(SurfaceOffset);
		_Profile.AddSurfaceToHash(ref hash);

		return hash.Value;
	}



	/// <summary>
	/// Everything a node's junction layout depends on: the node itself and the curve and width of each road at it.
	/// </summary>
	private long ComputeJunctionKey(RoadNode _Node, float _Radius)
	{
		var hash = StableHash.Create();

		hash.Add(GeneratorVersion);
		hash.Add(_Node.Position);
		hash.Add(_Radius);
		hash.Add(SurfaceOffset);

		foreach (var segment in GetSegmentsAt(_Node.Id).OrderBy(segment => segment.Id))
		{
			hash.Add(segment.Id);
			new RoadWidth(segment, 1.0f).AddToHash(ref hash);
			hash.Add(segment.Ramp ? 1 : 0);
			hash.Add(segment.StartNodeId == _Node.Id ? 1 : 0);

			if (!TryGetCurve(segment, out var curve))
				continue;

			hash.Add(curve.P0);
			hash.Add(curve.P1);
			hash.Add(curve.P2);
			hash.Add(curve.P3);
		}

		return hash.Value;
	}



	private static void AddPolygonToHash(RoadJunctionLayout _Layout, ref StableHash _Hash)
	{
		_Hash.Add(GeneratorVersion);
		_Hash.Add(_Layout.Centre);
		_Hash.Add(_Layout.Polygon.Count);

		foreach (var vertex in _Layout.Polygon)
			_Hash.Add(vertex);
	}



	private long ComputeJunctionSurfaceSignature(RoadJunctionLayout _Layout)
	{
		var hash = StableHash.Create();

		AddPolygonToHash(_Layout, ref hash);
		JunctionSurfaceProfile(_Layout).AddSurfaceToHash(ref hash);

		return hash.Value;
	}



	private long ComputeJunctionBorderSignature(RoadJunctionLayout _Layout, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> _ArmBlends, Dictionary<int, SegmentBorderTapers> _ArmTapers)
	{
		var hash = StableHash.Create();

		AddPolygonToHash(_Layout, ref hash);
		hash.Add(DebugBorders ? 1 : 0);

		foreach (var arm in _Layout.Arms)
		{
			var profile = ProfileOf(arm.Segment);

			hash.Add(arm.Segment.Id);
			hash.Add(arm.AtStart ? 1 : 0);
			hash.Add(arm.Length);
			profile.AddSurfaceToHash(ref hash);
			profile.AddBorderToHash(ref hash);
			_ArmBlends.GetValueOrDefault(arm.Segment.Id).AddToHash(ref hash);
			_ArmTapers.GetValueOrDefault(arm.Segment.Id).AddToHash(ref hash);
		}

		return hash.Value;
	}



	/// <summary>
	/// A junction's surface takes after its main road: the widest arm.
	/// </summary>
	private RoadProfile JunctionSurfaceProfile(RoadJunctionLayout _Layout)
	{
		var main = _Layout.Arms.OrderByDescending(arm => arm.Widths.Base).ThenBy(arm => arm.Segment.Id).FirstOrDefault();

		return ProfileOf(main?.Segment);
	}



	private void EnqueueSegment(RoadSegment _Segment, RoadCurveTable _Table, RoadProfile _Profile, List<ChunkPlan> _Plans, long _GroupSignature, List<RoadChunk> _Existing)
	{
		// Chunks with no plan (swallowed by a junction, past a shorter road's end, or a border the road no longer
		// has) have no job coming to replace them.
		if (_Existing is not null)
		{
			foreach (var chunk in _Existing)
			{
				if (!chunk.Locked && _Plans.All(plan => plan.Index != chunk.ChunkIndex || plan.Kind != chunk.Kind))
					chunk.GameObject.Destroy();
			}
		}

		if (_Plans.Count == 0)
		{
			m_PendingSegments.Remove(_Segment.Id);
			return;
		}

		foreach (var plan in _Plans)
			m_Jobs.Enqueue(new RebuildJob(_Segment.Id, -1, plan.Kind, _GroupSignature, plan, _Table, new RoadWidth(_Segment, _Table.Length), _Profile, null, null, null, null));

		m_PendingSegments[_Segment.Id] = new PendingGroup(_GroupSignature, _Plans.Count);
	}



	private void ProcessJobs()
	{
		int budget = Math.Max(1, RebuildBudget);

		while (budget > 0 && m_Jobs.Count > 0)
		{
			var job = m_Jobs.Dequeue();

			if (job.Lot is not null)
			{
				var lotKey = (job.Lot.Id, job.Kind);

				// Superseded by a newer version of the same lot, or the lot is gone.
				if (!m_PendingLots.TryGetValue(lotKey, out var pendingLot) || pendingLot.Signature != job.GroupSignature)
					continue;

				BuildLotPiece(job);
				budget--;

				m_PendingLots.Remove(lotKey);
				continue;
			}

			if (job.NodeId >= 0)
			{
				var key = (job.NodeId, job.Kind);

				// Superseded by a newer layout of the same junction, or the junction is gone.
				if (!m_PendingJunctions.TryGetValue(key, out var pending) || pending.Signature != job.GroupSignature)
					continue;

				BuildJunctionPiece(job);
				budget--;

				m_PendingJunctions.Remove(key);
				continue;
			}

			// Superseded by a newer rebuild of the same segment, or the segment is gone.
			if (!m_PendingSegments.TryGetValue(job.SegmentId, out var pendingSegment) || pendingSegment.Signature != job.GroupSignature)
				continue;

			if (BuildChunk(job))
				budget--;

			if (pendingSegment.Remaining <= 1)
				m_PendingSegments.Remove(job.SegmentId);
			else
				m_PendingSegments[job.SegmentId] = pendingSegment with { Remaining = pendingSegment.Remaining - 1 };
		}
	}



	/// <summary>
	/// Returns false when there was nothing to do: the chunk in place was already built from these inputs (a
	/// junction change at the far end of the road requeues the whole road, but only the chunk next to it differs).
	/// </summary>
	private bool BuildChunk(in RebuildJob _Job)
	{
		var plan = _Job.Plan;
		var existing = FindChunk(_Job.SegmentId, plan.Index, plan.Kind);

		if (existing is not null && (existing.Locked || (existing.Signature == plan.Signature && !LacksDebugRecord(existing))))
			return false;

		PolygonMesh mesh;
		string name;
		BorderDebug debug = null;
		var placements = new List<(GameObject Prefab, Transform Transform)>();

		if (plan.Kind == RoadChunkKind.Border)
		{
			debug = DebugBorders ? new BorderDebug() : null;
			mesh = RoadBorderBuilder.BuildSegment(_Job.Table, plan.Start, plan.End, _Job.Width, _Job.Profile, SampleSpacing, SurfaceOffset, plan.Caps, plan.Blends, plan.Taper, debug);
			name = $"Road {_Job.SegmentId} border [{plan.Index + 1}/{plan.Count}]";
		}
		else if (plan.Kind == RoadChunkKind.Bridge)
		{
			var segment = FindSegment(_Job.SegmentId);

			if (segment?.Bridge is null)
				return false;

			var parts = new List<RoadBridgeBuilder.Placement>();
			mesh = RoadBridgeBuilder.Build(_Job.Table, plan.Start, plan.End, plan.Bridge, _Job.Width, _Job.Profile, segment.Bridge, SampleSpacing, SurfaceOffset, GroundBelow, parts);
			name = $"Road {_Job.SegmentId} bridge [{plan.Index + 1}/{plan.Count}]";

			foreach (var part in parts)
				placements.Add((part.Prefab, new Transform(part.Position, part.Rotation)));
		}
		else if (plan.Kind == RoadChunkKind.Props)
		{
			var segment = FindSegment(_Job.SegmentId);

			if (segment?.Props is null)
				return false;

			// Nothing to mesh: the piece is only the parent of its prefabs.
			var props = new List<RoadPropBuilder.Placement>();
			var context = new RoadPropBuilder.Context(_Job.Table, plan.Start, plan.End, plan.Props, _Job.SegmentId, _Job.Width, _Job.Profile, segment.Bridge is not null, SurfaceOffset, GroundAround);

			RoadPropBuilder.Build(context, segment.Props, props);
			mesh = null;
			name = $"Road {_Job.SegmentId} props [{plan.Index + 1}/{plan.Count}]";

			foreach (var prop in props)
				placements.Add((prop.Prefab, new Transform(prop.Position, prop.Rotation, prop.Scale)));
		}
		else if (plan.Kind == RoadChunkKind.Cap)
		{
			// Surface and border in one piece: the whole end is small, and nothing else shares it.
			mesh = RoadCapBuilder.Build(_Job.Table, plan.Start, plan.Index == 0, _Job.Width, _Job.Profile, SurfaceOffset, plan.End);
			name = $"Road {_Job.SegmentId} {(plan.Index == 0 ? "start" : "end")} cap";
		}
		else if (plan.Kind == RoadChunkKind.Marking)
		{
			var segment = FindSegment(_Job.SegmentId);

			if (segment is null)
				return false;

			// Lines only where the chunk overlaps the stretches left clear by the crossings, laid on the rows the
			// road surface itself was built on.
			mesh = new PolygonMesh();

			var roadRows = RoadRows.Build(_Job.Table, plan.Start, plan.End, SampleSpacing, _Job.Width, _Job.Profile.Simplification);

			if (plan.Lines.HasGap)
			{
				RoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.Start), MathF.Min(plan.End, plan.Lines.GapStart), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);
				RoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.GapEnd), MathF.Min(plan.End, plan.Lines.End), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);
			}
			else
			{
				RoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.Start), MathF.Min(plan.End, plan.Lines.End), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);
			}

			// The crossings this piece owns, laid on the same rows as the lines and the road itself.
			if (plan.Crossings.AtStart)
				RoadMarkingBuilder.BuildCrossing(mesh, _Job.Table, roadRows, plan.Crossings.StartCentre, _Job.Width, _Job.Profile, SurfaceOffset);

			if (plan.Crossings.AtEnd)
				RoadMarkingBuilder.BuildCrossing(mesh, _Job.Table, roadRows, plan.Crossings.EndCentre, _Job.Width, _Job.Profile, SurfaceOffset);

			name = $"Road {_Job.SegmentId} markings [{plan.Index + 1}/{plan.Count}]";
		}
		else
		{
			mesh = RoadSurfaceBuilder.Build(_Job.Table, plan.Start, plan.End, _Job.Width, SampleSpacing, _Job.Profile.Simplification, _Job.Profile.RoadTextureRepeat, SurfaceOffset, _Job.Profile.ResolvedRoadMaterial);
			name = $"Road {_Job.SegmentId} [{plan.Index + 1}/{plan.Count}]";
		}

		var piece = CreateChunkObject(name, _Job.SegmentId, -1, plan.Kind, plan.Index, plan.Count, plan.Signature, mesh, debug);

		foreach (var (prefab, transform) in placements)
			prefab.Clone(transform, piece);

		// The new piece is in place before the old one goes, so the road never flickers.
		existing?.GameObject.Destroy();

		return true;
	}



	/// <summary>
	/// The height of whatever lies under a point of the network (its own pieces excepted), in network space, for
	/// bridge supports to land on: the search starts just below the point, so the deck itself is never it. Null
	/// over nothing.
	/// </summary>
	private float? GroundBelow(Vector3 _Local)
	{
		return TraceGround(_Local, -1.0f);
	}



	/// <summary>
	/// The height of the ground at a point of the network, for props to stand on: the search starts well above
	/// the point, so ground rising beside a road is found as well as ground falling away from it.
	/// </summary>
	private float? GroundAround(Vector3 _Local)
	{
		return TraceGround(_Local, 3000.0f);
	}



	private float? TraceGround(Vector3 _Local, float _StartAbove)
	{
		const float reach = 50000.0f;

		var transform = WorldTransform;
		Vector3 from = transform.PointToWorld(_Local) + Vector3.Up * _StartAbove;
		var trace = Scene.Trace.Ray(from, from + Vector3.Down * reach).WithoutTags(ChunkTag).Run();

		if (!trace.Hit)
			return null;

		return transform.PointToLocal(trace.EndPosition).z;
	}



	private void BuildJunctionPiece(in RebuildJob _Job)
	{
		var existing = FindJunctionChunk(_Job.NodeId, _Job.Kind);

		if (existing is not null && existing.Locked)
			return;

		PolygonMesh mesh;
		string name;
		BorderDebug debug = null;

		if (_Job.Kind == RoadChunkKind.Border)
		{
			var armBlends = _Job.ArmBlends;
			var armTapers = _Job.ArmTapers;

			debug = DebugBorders ? new BorderDebug() : null;
			mesh = RoadBorderBuilder.BuildJunction(_Job.Junction, ProfileOf,
				segment => armBlends is not null ? armBlends.GetValueOrDefault(segment.Id) : default,
				segment => armTapers is not null ? armTapers.GetValueOrDefault(segment.Id) : default,
				debug);
			name = $"Junction {_Job.NodeId} border";
		}
		else
		{
			var profile = JunctionSurfaceProfile(_Job.Junction);

			mesh = RoadJunctionBuilder.Build(_Job.Junction, profile.RoadTextureRepeat, profile.ResolvedRoadMaterial);
			name = $"Junction {_Job.NodeId}";
		}

		CreateChunkObject(name, -1, _Job.NodeId, _Job.Kind, 0, 1, _Job.GroupSignature, mesh, debug);

		existing?.GameObject.Destroy();
	}



	private GameObject CreateChunkObject(string _Name, int _SegmentId, int _NodeId, RoadChunkKind _Kind, int _Index, int _Count, long _Signature, PolygonMesh _Mesh, BorderDebug _Debug, int _LotId = -1)
	{
		var child = new GameObject(GameObject, true, _Name);
		child.Tags.Add(ChunkTag);

		var chunk = child.AddComponent<RoadChunk>();
		chunk.SegmentId = _SegmentId;
		chunk.NodeId = _NodeId;
		chunk.LotId = _LotId;
		chunk.Kind = _Kind;
		chunk.ChunkIndex = _Index;
		chunk.ChunkCount = _Count;
		chunk.Signature = _Signature;
		chunk.Debug = _Debug;

		if (_Debug is not null)
			DebugRevision++;

		// A piece of props has no geometry of its own; it is the parent of its prefabs.
		if (_Mesh is null)
			return child;

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

		// Paint lies flat on the road: nothing to collide with, and its shadow would be a sliver hidden under it,
		// at the cost of a shadow pass.
		if (_Kind == RoadChunkKind.Marking)
		{
			meshComponent.Collision = MeshComponent.CollisionType.None;
			meshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;
		}

		return child;
	}
}
redsnail.roadtool2 / Editor/RoadGizmos.cs
Editor library
using Sandbox;

namespace RedSnail.RoadTool2.Editor;

/// <summary>
/// Gizmo drawing shared by the road tools. Curves are in network-local space and drawn through the network's
/// transform, in whatever colour and thickness the caller set up.
/// </summary>
internal static class RoadGizmos
{
	public static void DrawCurve(in RoadCurve _Curve, in Transform _Transform, int _Steps = 24)
	{
		Vector3 previous = _Transform.PointToWorld(_Curve.P0);

		for (int i = 1; i <= _Steps; i++)
		{
			Vector3 point = _Transform.PointToWorld(_Curve.Evaluate((float)i / _Steps));

			Gizmo.Draw.Line(previous, point);
			previous = point;
		}
	}



	/// <summary>
	/// Draws the two edges of a road of the given width along the curve, capped at both ends.
	/// </summary>
	public static void DrawRoadOutline(in RoadCurve _Curve, float _Width, in Transform _Transform, int _Steps = 24)
	{
		DrawRoadOutline(_Curve, RoadWidth.Uniform(_Width), _Transform, _Steps);
	}



	/// <summary>
	/// Draws the two edges of a road along the curve, each its own distance out (the curve's parameter standing in
	/// for its distance), capped at both ends.
	/// </summary>
	public static void DrawRoadOutline(in RoadCurve _Curve, in RoadWidth _Width, in Transform _Transform, int _Steps = 24)
	{
		Vector3 previousLeft = default;
		Vector3 previousRight = default;

		for (int i = 0; i <= _Steps; i++)
		{
			float t = (float)i / _Steps;
			float distance = t * _Width.Length;
			Vector3 point = _Curve.Evaluate(t);
			Vector3 right = RoadCurve.RightOf(_Curve.Tangent(t));

			Vector3 left = _Transform.PointToWorld(point - right * _Width.LeftAt(distance));
			Vector3 rightEdge = _Transform.PointToWorld(point + right * _Width.RightAt(distance));

			if (i == 0 || i == _Steps)
				Gizmo.Draw.Line(left, rightEdge);

			if (i > 0)
			{
				Gizmo.Draw.Line(previousLeft, left);
				Gizmo.Draw.Line(previousRight, rightEdge);
			}

			previousLeft = left;
			previousRight = rightEdge;
		}
	}



	/// <summary>
	/// A node marker: a flat ring on the ground, filled when it is the one being worked on.
	/// </summary>
	public static void DrawNode(Vector3 _WorldPosition, bool _Filled)
	{
		float radius = RoadPicker.NodeRadius(_WorldPosition);

		if (_Filled)
			Gizmo.Draw.SolidSphere(_WorldPosition, radius * 0.6f);

		Gizmo.Draw.LineCircle(_WorldPosition, Vector3.Up, radius);
	}
}
redsnail.roadtool2 / Code/Geometry/RoadCurve.cs
Game library
using System;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// The cubic Bezier of one road segment, in network-local space. P1 and P2 are the tangent handles hanging off the
/// two end nodes.
/// </summary>
public readonly struct RoadCurve : IEquatable<RoadCurve>
{
	public readonly Vector3 P0;
	public readonly Vector3 P1;
	public readonly Vector3 P2;
	public readonly Vector3 P3;



	public RoadCurve(Vector3 _P0, Vector3 _P1, Vector3 _P2, Vector3 _P3)
	{
		P0 = _P0;
		P1 = _P1;
		P2 = _P2;
		P3 = _P3;
	}



	public Vector3 Evaluate(float _T)
	{
		float u = 1.0f - _T;
		float uu = u * u;
		float tt = _T * _T;

		return P0 * (uu * u) + P1 * (3.0f * uu * _T) + P2 * (3.0f * u * tt) + P3 * (tt * _T);
	}



	/// <summary>
	/// Unit direction of travel at <paramref name="_T"/>. Falls back to the chord when the derivative vanishes (a
	/// handle sitting on its node) so callers always get a usable frame.
	/// </summary>
	public Vector3 Tangent(float _T)
	{
		float u = 1.0f - _T;
		Vector3 derivative = (P1 - P0) * (3.0f * u * u) + (P2 - P1) * (6.0f * u * _T) + (P3 - P2) * (3.0f * _T * _T);

		if (derivative.IsNearZeroLength)
			derivative = P3 - P0;

		if (derivative.IsNearZeroLength)
			return Vector3.Forward;

		return derivative.Normal;
	}



	/// <summary>
	/// Chord-summed length, good enough to size sampling tables and count chunks.
	/// </summary>
	public float ApproximateLength(int _Steps = 32)
	{
		float length = 0.0f;
		Vector3 previous = P0;

		for (int i = 1; i <= _Steps; i++)
		{
			Vector3 point = Evaluate((float)i / _Steps);
			length += Vector3.DistanceBetween(previous, point);
			previous = point;
		}

		return length;
	}



	/// <summary>
	/// De Casteljau subdivision: the two halves trace exactly the same path as the whole, so a road can be split
	/// without moving.
	/// </summary>
	public void Split(float _T, out RoadCurve _Left, out RoadCurve _Right)
	{
		Vector3 q0 = Vector3.Lerp(P0, P1, _T);
		Vector3 q1 = Vector3.Lerp(P1, P2, _T);
		Vector3 q2 = Vector3.Lerp(P2, P3, _T);
		Vector3 r0 = Vector3.Lerp(q0, q1, _T);
		Vector3 r1 = Vector3.Lerp(q1, q2, _T);
		Vector3 s = Vector3.Lerp(r0, r1, _T);

		_Left = new RoadCurve(P0, q0, r0, s);
		_Right = new RoadCurve(s, r1, q2, P3);
	}



	/// <summary>
	/// The road's right-hand direction for a direction of travel: flat, perpendicular to the tangent, and
	/// consistent with <see cref="Rotation.Right"/> for a rotation looking down the tangent with world up.
	/// </summary>
	public static Vector3 RightOf(Vector3 _Tangent)
	{
		Vector3 right = Vector3.Cross(_Tangent, Vector3.Up);

		// Vertical tangent (a ramp seen from nowhere useful): any horizontal axis will do.
		if (right.IsNearZeroLength)
			return Vector3.Right;

		return right.Normal;
	}



	public bool Equals(RoadCurve _Other)
	{
		return P0 == _Other.P0 && P1 == _Other.P1 && P2 == _Other.P2 && P3 == _Other.P3;
	}



	public override bool Equals(object _Obj)
	{
		return _Obj is RoadCurve other && Equals(other);
	}



	public override int GetHashCode()
	{
		return HashCode.Combine(P0, P1, P2, P3);
	}
}
redsnail.roadtool2 / Code/Network/RoadCapBuilder.cs
Game library
using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// Generates the rounded end of a road that stops: a half disc of surface carried on past the last row, with the
/// border swept round it, so a dead end reads as a turning head rather than a road sliced through. The piece is
/// built beyond the road's own end, on the end row itself, so its seam with the last chunk is exact and nothing
/// about the road's length changes.
/// </summary>
internal static class RoadCapBuilder
{
	/// <summary>
	/// The arc is drawn in steps of about this many degrees.
	/// </summary>
	private const float ArcStepDegrees = 9.0f;

	/// <summary>
	/// How deep a road's rounded end reaches past it, as a share of the road's half width. 1 is a half circle.
	/// </summary>
	public const float DepthFactor = 1.0f;



	/// <summary>
	/// How far the rounded end of a road reaches past the node, or 0 where the road type does not round its ends.
	/// </summary>
	public static float Depth(RoadProfile _Profile, in RoadWidth _Width, float _Distance)
	{
		if (_Profile is null || !_Profile.RoundedEnds)
			return 0.0f;

		return (_Width.LeftAt(_Distance) + _Width.RightAt(_Distance)) * 0.5f * DepthFactor;
	}



	/// <summary>
	/// The rounded end at one end of a road. <paramref name="_Distance"/> is the end's own distance along the curve
	/// (0 or the table's length) and <paramref name="_AtStart"/> which end it is; the cap reaches out from there.
	/// </summary>
	public static PolygonMesh Build(RoadCurveTable _Table, float _Distance, bool _AtStart, in RoadWidth _Width, RoadProfile _Profile, float _Lift, float _Depth)
	{
		var mesh = new PolygonMesh();

		if (_Depth <= 0.01f)
			return mesh;

		_Table.Frame(_Distance, out Vector3 centre, out Vector3 tangent);

		centre += Vector3.Up * _Lift;

		// Out of the road, and the road's own right at that end. The arc keeps the road's grade because the
		// direction it reaches along carries the curve's climb with it.
		Vector3 forward = (_AtStart ? -tangent : tangent).Normal;
		Vector3 right = RoadCurve.RightOf(tangent);
		float leftHalf = _Width.LeftAt(_Distance);
		float rightHalf = _Width.RightAt(_Distance);

		if (leftHalf + rightHalf <= 0.01f)
			return mesh;

		int steps = Math.Max(4, (int)MathF.Ceiling(180.0f / ArcStepDegrees));
		var outline = new List<Vector3>(steps + 1);
		var outwards = new List<Vector3>(steps + 1);
		var offsets = new List<float>(steps + 1);
		var aheads = new List<float>(steps + 1);

		Arc(centre, forward, right, leftHalf, rightHalf, _Depth, steps, outline, outwards, offsets, aheads);

		AddSurface(mesh, centre, outline, offsets, aheads, _Distance, _AtStart, _Profile);
		AddBorder(mesh, outline, outwards, _Profile);

		return mesh;
	}



	/// <summary>
	/// The outline of a road's rounded end in network space, lifted onto the road surface, for the tools that need
	/// to know where road is (the ground under it is held down and brought up to its kerb like a junction's).
	/// Empty where the road does not round that end.
	/// </summary>
	public static List<Vector3> Outline(RoadCurveTable _Table, float _Distance, bool _AtStart, in RoadWidth _Width, RoadProfile _Profile, float _Lift, out Vector3 _Centre)
	{
		var outline = new List<Vector3>();
		float depth = Depth(_Profile, _Width, _Distance);

		_Table.Frame(_Distance, out _Centre, out Vector3 tangent);

		_Centre += Vector3.Up * _Lift;

		float leftHalf = _Width.LeftAt(_Distance);
		float rightHalf = _Width.RightAt(_Distance);

		if (depth <= 0.01f || leftHalf + rightHalf <= 0.01f)
			return outline;

		int steps = Math.Max(4, (int)MathF.Ceiling(180.0f / ArcStepDegrees));

		Arc(_Centre, (_AtStart ? -tangent : tangent).Normal, RoadCurve.RightOf(tangent), leftHalf, rightHalf, depth, steps, outline, null, null, null);

		return outline;
	}



	/// <summary>
	/// The half ellipse from the road's left edge, round the tip, to its right edge: where each point is, which way
	/// is out of the road there, and how far across and ahead of the end row it sits.
	/// </summary>
	private static void Arc(Vector3 _Centre, Vector3 _Forward, Vector3 _Right, float _LeftHalf, float _RightHalf, float _Depth, int _Steps,
		List<Vector3> _Points, List<Vector3> _Outwards, List<float> _Offsets, List<float> _Aheads)
	{
		for (int i = 0; i <= _Steps; i++)
		{
			float angle = MathF.PI * (i / (float)_Steps - 0.5f);
			float sin = MathF.Sin(angle);
			float cos = MathF.Cos(angle);
			float offset = sin * (sin >= 0.0f ? _RightHalf : _LeftHalf);
			float ahead = _Depth * cos;

			_Points.Add(_Centre + _Right * offset + _Forward * ahead);
			_Outwards?.Add((_Right * sin + _Forward * cos).Normal);
			_Offsets?.Add(offset);
			_Aheads?.Add(ahead);
		}
	}



	/// <summary>
	/// The half disc of road surface: a fan from the middle of the end row out to the arc, so every triangle is
	/// planar and the row itself is the fan's own base.
	/// </summary>
	private static void AddSurface(PolygonMesh _Mesh, Vector3 _Centre, List<Vector3> _Outline, List<float> _Offsets, List<float> _Aheads,
		float _Distance, bool _AtStart, RoadProfile _Profile)
	{
		float repeat = MathF.Max(1.0f, _Profile.RoadTextureRepeat);
		var material = _Profile.ResolvedRoadMaterial;
		var centre = _Mesh.AddVertices(_Centre)[0];
		var ring = new HalfEdgeMesh.VertexHandle[_Outline.Count];

		for (int i = 0; i < _Outline.Count; i++)
			ring[i] = _Mesh.AddVertices(_Outline[i])[0];

		// U runs across the road from its centreline and V along it, the same as the road's own surface, so the
		// asphalt carries on through the seam instead of restarting at it.
		Vector2 centreUv = new(0.0f, _Distance / repeat);

		// Which way round the fan closes, so it faces up like the road it carries on from.
		Vector3 first = _Outline[0] - _Centre;
		Vector3 second = _Outline[1] - _Centre;
		bool flip = Vector3.Cross(first, second).z < 0.0f;

		for (int i = 0; i < _Outline.Count - 1; i++)
		{
			var uvA = new Vector2(_Offsets[i] / repeat, (_Distance + (_AtStart ? -_Aheads[i] : _Aheads[i])) / repeat);
			var uvB = new Vector2(_Offsets[i + 1] / repeat, (_Distance + (_AtStart ? -_Aheads[i + 1] : _Aheads[i + 1])) / repeat);

			if (flip)
				MeshUtility.AddTexturedTriangle(_Mesh, material, centre, ring[i + 1], ring[i], centreUv, uvB, uvA);
			else
				MeshUtility.AddTexturedTriangle(_Mesh, material, centre, ring[i], ring[i + 1], centreUv, uvA, uvB);
		}
	}



	/// <summary>
	/// The border round the arc, open at both ends: what closes it off is the road's own border, which runs into
	/// it at the end row.
	/// </summary>
	private static void AddBorder(PolygonMesh _Mesh, List<Vector3> _Outline, List<Vector3> _Outwards, RoadProfile _Profile)
	{
		if (!_Profile.HasBorder)
			return;

		var path = new List<RoadBorderBuilder.PathPoint>(_Outline.Count);
		float along = 0.0f;

		for (int i = 0; i < _Outline.Count; i++)
		{
			if (i > 0)
				along += _Outline[i].Distance(_Outline[i - 1]);

			path.Add(new RoadBorderBuilder.PathPoint(_Outline[i], _Outwards[i], along));
		}

		RoadBorderBuilder.BuildPath(_Mesh, _Profile, path, false, false);
	}
}
redsnail.roadtool2 / Code/Network/RoadJunctionBuilder.cs
Game library
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// Generates the surface of a junction: the layout's polygon fanned out from the node. Every triangle shares the
/// centre, so each one is planar and none can be dropped by the mesh builder, whatever the arms' heights do.
/// </summary>
internal static class RoadJunctionBuilder
{
	public static PolygonMesh Build(RoadJunctionLayout _Layout, float _TextureRepeat, Material _Material)
	{
		var mesh = new PolygonMesh();
		var polygon = _Layout.Polygon;

		if (polygon.Count < 3)
			return mesh;

		var centre = mesh.AddVertices(_Layout.Centre)[0];
		var ring = mesh.AddVertices(polygon.ToArray());
		Vector2 centreUv = PlanarUv(_Layout.Centre, _TextureRepeat);

		for (int i = 0; i < polygon.Count; i++)
		{
			int next = (i + 1) % polygon.Count;

			MeshUtility.AddTexturedTriangle(mesh, _Material, centre, ring[i], ring[next],
				centreUv, PlanarUv(polygon[i], _TextureRepeat), PlanarUv(polygon[next], _TextureRepeat));
		}

		return mesh;
	}



	/// <summary>
	/// Junctions have no single direction to run a texture along, so they use a top-down projection.
	/// </summary>
	private static Vector2 PlanarUv(Vector3 _Position, float _TextureRepeat)
	{
		return new Vector2(_Position.x, _Position.y) / _TextureRepeat;
	}
}
redsnail.roadtool2 / Code/Profiles/ParkingStyle.cs
Game library
using System.Text.Json.Serialization;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// How a parking lot is laid out inside: the bays round its edges and, on a lot with room for them, the rows down
/// its middle. Lots reference a style, so every car park built from it changes together when the asset is edited.
/// </summary>
[AssetType(Name = "Parking Style", Extension = "park", Category = "Road Tool 2")]
public sealed class ParkingStyle : GameResource
{
	/// <summary>
	/// How wide one bay is, measured along the edge it stands on.
	/// </summary>
	[Group("Bays"), Range(80.0f, 400.0f)] public float BayWidth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 150.0f;

	/// <summary>
	/// How far a bay reaches in from the edge.
	/// </summary>
	[Group("Bays"), Range(100.0f, 800.0f)] public float BayDepth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 260.0f;

	/// <summary>
	/// Bays turned this far from square to the edge they stand on, so cars drive into them at an angle. The bays
	/// on the far side of an aisle lean the other way, as they do in a real car park.
	/// </summary>
	[Group("Bays"), Range(0.0f, 60.0f)] public float BayAngle { get; set { field = value.Clamp(0.0f, 80.0f); } } = 0.0f;

	/// <summary>
	/// Bays round the inside of the lot's edges.
	/// </summary>
	[Group("Bays")] public bool EdgeBays { get; set; } = true;

	/// <summary>
	/// Back to back rows of bays down the middle of the lot, as many as fit with an aisle between each of them.
	/// </summary>
	[Group("Rows")] public bool CentreRows { get; set; } = true;

	/// <summary>
	/// How much room is left for driving between one row of bays and the next.
	/// </summary>
	[Group("Rows"), Range(300.0f, 1500.0f)] public float AisleWidth { get; set { field = value.Clamp(50.0f, 100000.0f); } } = 600.0f;

	/// <summary>
	/// How the bay lines are painted. Empty paints them like a road's lane lines.
	/// </summary>
	[Group("Markings")] public RoadLineStyle Line { get; set; } = new() { Width = 8.0f };

	/// <summary>
	/// A line along the back of each row, closing its bays off.
	/// </summary>
	[Group("Markings")] public bool BackLine { get; set; } = false;

	[Hide, JsonIgnore] public Material ResolvedLineMaterial => Line?.Material ?? Material.Load("roadtool2/materials/linewhite.vmat");

	/// <summary>
	/// How far apart two bay lines sit along the edge, which is wider than a bay once the bays lean.
	/// </summary>
	[Hide, JsonIgnore] public float Pitch => BayWidth / float.Cos(BayAngle.DegreeToRadian());



	public void AddToHash(ref StableHash _Hash)
	{
		_Hash.Add(BayWidth);
		_Hash.Add(BayDepth);
		_Hash.Add(BayAngle);
		_Hash.Add(EdgeBays ? 1 : 0);
		_Hash.Add(CentreRows ? 1 : 0);
		_Hash.Add(AisleWidth);
		_Hash.Add(BackLine ? 1 : 0);
		_Hash.Add(Line is null ? 0 : 1);
		Line?.AddToHash(ref _Hash);
	}



	/// <summary>
	/// The asset was saved: every network with a lot in this style has stale pieces now.
	/// </summary>
	protected override void PostReload()
	{
		base.PostReload();

		RoadNetwork.Editor_MarkDirty();
	}



	protected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)
	{
		return CreateSimpleAssetTypeIcon("local_parking", _Width, _Height, "#8fd3ff", "black");
	}
}
redsnail.roadtool2 / Network/BorderDebug.cs
Game library
using System.Collections.Generic;
using System.Text;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// What a border piece was built from, kept on the piece while the network's debug overlay is on: one line per row
/// from the kerb to the outer edge, coloured by the run it belongs to, plus a plain-text record of every number
/// that went into the rows, corners and patches (the editor writes those out to a file). Diagnostic only.
/// </summary>
public sealed class BorderDebug
{
	public readonly record struct DebugLine(Vector3 From, Vector3 To, Color Color);

	public readonly record struct DebugMarker(Vector3 Position, Color Color);

	public static readonly Color ArmColor = Color.Green;
	public static readonly Color FirstHalfColor = Color.Red;
	public static readonly Color SecondHalfColor = Color.Cyan;
	public static readonly Color PatchColor = Color.Yellow;
	public static readonly Color CapColor = Color.Magenta;

	public List<DebugLine> Lines { get; } = new();

	public List<DebugMarker> Markers { get; } = new();

	/// <summary>
	/// The record, one entry per line.
	/// </summary>
	public StringBuilder Text { get; } = new();



	public void Line(Vector3 _From, Vector3 _To, Color _Color)
	{
		Lines.Add(new DebugLine(_From, _To, _Color));
	}



	public void Marker(Vector3 _Position, Color _Color)
	{
		Markers.Add(new DebugMarker(_Position, _Color));
	}



	public void Note(string _Text)
	{
		Text.AppendLine(_Text);
	}



	public static string Format(Vector3 _Vector)
	{
		return $"({_Vector.x:0.###} {_Vector.y:0.###} {_Vector.z:0.###})";
	}



	public static string Format(Vector2 _Vector)
	{
		return $"({_Vector.x:0.###} {_Vector.y:0.###})";
	}
}
redsnail.roadtool2 / Network/RoadNetwork.Debug.cs
Game library
using System;
using System.Linq;
using System.Text;
using Sandbox;

namespace RedSnail.RoadTool2;

public sealed partial class RoadNetwork
{
	/// <summary>
	/// The whole border debug record as text: the graph and generation settings, then what every border piece was
	/// built from. Only pieces built while <see cref="DebugBorders"/> was on carry a record.
	/// </summary>
	public string BuildDebugRecord()
	{
		var text = new StringBuilder();

		text.AppendLine($"ROADTOOL2 BORDER DEBUG generator={GeneratorVersion} revision={DebugRevision} time={DateTime.Now:HH:mm:ss}");
		text.AppendLine($"NETWORK name=\"{GameObject.Name}\" surfaceOffset={SurfaceOffset} spacing={SampleSpacing} chunkLength={ChunkLength} defaultProfile={DefaultProfile?.ResourceName ?? "(none)"}");

		foreach (var node in Nodes)
			text.AppendLine($"NODE id={node.Id} position={BorderDebug.Format(node.Position)} cornerRadius={node.CornerRadius} autoTangents={(node.AutoTangents ? 1 : 0)} degree={GetDegree(node.Id)}");

		foreach (var segment in Segments)
		{
			text.AppendLine($"SEGMENT id={segment.Id} start={segment.StartNodeId} end={segment.EndNodeId} startTangent={BorderDebug.Format(segment.StartTangent)} endTangent={BorderDebug.Format(segment.EndTangent)} width={segment.Width} profile={ProfileOf(segment).ResourceName}");

			if (TryGetCurve(segment, out var curve))
				text.AppendLine($"CURVE segment={segment.Id} p0={BorderDebug.Format(curve.P0)} p1={BorderDebug.Format(curve.P1)} p2={BorderDebug.Format(curve.P2)} p3={BorderDebug.Format(curve.P3)} length={GetCurveTable(segment)?.Length ?? 0.0f:0.###}");
		}

		foreach (var chunk in GetChunks().OrderBy(chunk => chunk.IsJunction ? 1 : 0).ThenBy(chunk => chunk.IsJunction ? chunk.NodeId : chunk.SegmentId).ThenBy(chunk => chunk.ChunkIndex))
		{
			if (chunk.Kind != RoadChunkKind.Border)
				continue;

			text.AppendLine();
			text.AppendLine($"PIECE name=\"{chunk.GameObject.Name}\" segment={chunk.SegmentId} node={chunk.NodeId} index={chunk.ChunkIndex} count={chunk.ChunkCount} record={(chunk.Debug is not null ? 1 : 0)}");

			if (chunk.Debug is not null)
				text.Append(chunk.Debug.Text);
		}

		return text.ToString();
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 71,
    "Files": [
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Network/RoadBorderTaper.cs",
            "FileName": "RoadBorderTaper.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// How one side's border tapers at either end of a segment: over the given length from the node its relief (kerb\r\n/// height, verge drop) runs to the share of itself given for that end \u2014 nothing when the other border is of another\r\n/// kind, the height the two meet at when it is of the same kind \u2014 and its width runs to the width the two meet at,\r\n/// so the two borders meet flush at the node. A zero length is no taper. An end marked as a spill has a country road\r\n/// on the other side, whose gravel is blended onto the border there. A gore end is the nose where this road and\r\n/// another diverge from one edge: the border there grows from nothing as the square of the distance, the way the gap\r\n/// between two roads parting from a common tangent opens up, so the two borders fill the gore between them without\r\n/// crossing.\r\n/// </summary>\r\n/// <param name=\"StartRelief\">\r\n/// The share of the profile's own relief the border has at the start node: 0 flattens it into the road, 1 leaves\r\n/// it as it is, and anything else brings it to a height it shares with the border on the other side (above 1 for\r\n/// the lower of the two).\r\n/// </param>\r\n/// <param name=\"StartOffset\">\r\n/// How far from the start node the taper ends: 0 at a plain pass-through, the arm's trim at a junction, where the\r\n/// corner starts. Everything nearer the node than that \u2014 the corner itself \u2014 is built in the shape the taper runs\r\n/// to, so the two roads round a corner meet in one cross-section instead of turning into each other on the arc.\r\n/// </param>\r\npublic readonly record struct RoadBorderTaper(float StartLength, float StartWidth, float StartRelief, bool StartSpill, float EndLength, float EndWidth, float EndRelief, bool EndSpill, bool StartGore = false, bool EndGore = false, float StartOffset = 0.0f, float EndOffset = 0.0f)\r\n{\r\n\t/// <summary>\r\n\t/// The share of the profile's relief in force at a curve distance (1 away from any taper, that end's relief at\r\n\t/// its node, and beyond its offset), how far along a taper the point is (0 outside one), the width the border is\r\n\t/// that far towards, and whether that taper's neighbour spills onto it. The two ends' tapers, offsets included,\r\n\t/// are each held to half the segment, so no point is ever inside both.\r\n\t/// </summary>\r\n\tpublic float ReliefAt(float _Along, float _Length, out float _Towards, out float _TargetWidth, out bool _Spill)\r\n\t{\r\n\t\tfloat relief = 1.0f;\r\n\r\n\t\t_Towards = 0.0f;\r\n\t\t_TargetWidth = 0.0f;\r\n\t\t_Spill = false;\r\n\r\n\t\tif (StartLength > 0.0f && _Along < StartOffset + StartLength)\r\n\t\t{\r\n\t\t\tfloat progress = Math.Clamp((_Along - StartOffset) / StartLength, 0.0f, 1.0f);\r\n\r\n\t\t\t_Towards = Towards(progress, StartGore);\r\n\t\t\t_TargetWidth = StartWidth;\r\n\t\t\t_Spill = StartSpill;\r\n\t\t\trelief = MathX.Lerp(StartRelief, 1.0f, progress);\r\n\t\t}\r\n\r\n\t\tif (EndLength > 0.0f && _Length - _Along < EndOffset + EndLength)\r\n\t\t{\r\n\t\t\tfloat progress = Math.Clamp((_Length - _Along - EndOffset) / EndLength, 0.0f, 1.0f);\r\n\t\t\tfloat towards = Towards(progress, EndGore);\r\n\r\n\t\t\tif (towards > _Towards)\r\n\t\t\t{\r\n\t\t\t\t_Towards = towards;\r\n\t\t\t\t_TargetWidth = EndWidth;\r\n\t\t\t\t_Spill = EndSpill;\r\n\t\t\t\trelief = MathX.Lerp(EndRelief, 1.0f, progress);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn relief;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// How far towards the node's width the border is at a progress along the taper (0 at the node, 1 at its far\r\n\t/// end): straight along a plain taper, and along a gore the square, so the width is the square of the progress.\r\n\t/// </summary>\r\n\tprivate static float Towards(float _Progress, bool _Gore)\r\n\t{\r\n\t\treturn _Gore ? 1.0f - _Progress * _Progress : 1.0f - _Progress;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The factor the border's nominal width is scaled by at a curve distance, on its way to the neighbour's.\r\n\t/// </summary>\r\n\tpublic float WidthScaleAt(float _Along, float _Length, float _NominalWidth)\r\n\t{\r\n\t\tReliefAt(_Along, _Length, out float towards, out float targetWidth, out _);\r\n\r\n\t\tif (towards <= 0.0f || _NominalWidth <= 0.001f)\r\n\t\t\treturn 1.0f;\r\n\r\n\t\treturn (_NominalWidth + (targetWidth - _NominalWidth) * towards) / _NominalWidth;\r\n\t}\r\n\r\n\r\n\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(StartLength);\r\n\t\t_Hash.Add(StartWidth);\r\n\t\t_Hash.Add(StartRelief);\r\n\t\t_Hash.Add(StartSpill ? 1 : 0);\r\n\t\t_Hash.Add(StartGore ? 1 : 0);\r\n\t\t_Hash.Add(StartOffset);\r\n\t\t_Hash.Add(EndLength);\r\n\t\t_Hash.Add(EndWidth);\r\n\t\t_Hash.Add(EndRelief);\r\n\t\t_Hash.Add(EndSpill ? 1 : 0);\r\n\t\t_Hash.Add(EndGore ? 1 : 0);\r\n\t\t_Hash.Add(EndOffset);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Whether the border tapers at the end a node is at.\r\n\t/// </summary>\r\n\tpublic bool TapersAt(bool _AtStart)\r\n\t{\r\n\t\treturn _AtStart ? StartLength > 0.0f : EndLength > 0.0f;\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// The tapers of a segment's two borders, looking along the segment.\r\n/// </summary>\r\npublic readonly record struct SegmentBorderTapers(RoadBorderTaper Left, RoadBorderTaper Right)\r\n{\r\n\tpublic RoadBorderTaper Side(bool _Left)\r\n\t{\r\n\t\treturn _Left ? Left : Right;\r\n\t}\r\n\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\tLeft.AddToHash(ref _Hash);\r\n\t\tRight.AddToHash(ref _Hash);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Network/RoadSurfaceBuilder.cs",
            "FileName": "RoadSurfaceBuilder.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Generates the road surface of one chunk: a strip of quads between cross-section frames at the rows the chunk is\r\n/// built on (even distances along the segment's curve, thinned where the road type allows it).\r\n/// </summary>\r\ninternal static class RoadSurfaceBuilder\r\n{\r\n\t/// <summary>\r\n\t/// The two edge points of the road at a distance along the curve, each its own distance out from the centreline\r\n\t/// there. Junctions use this for the rows they share with the arm chunks, so both sides of the seam are computed\r\n\t/// the same way.\r\n\t/// </summary>\r\n\tpublic static void CrossSection(RoadCurveTable _Table, float _Distance, in RoadWidth _Width, float _Lift, out Vector3 _Left, out Vector3 _Right)\r\n\t{\r\n\t\t_Table.Frame(_Distance, out Vector3 position, out Vector3 tangent);\r\n\r\n\t\tposition += Vector3.Up * _Lift;\r\n\r\n\t\tVector3 right = RoadCurve.RightOf(tangent);\r\n\r\n\t\t_Left = position - right * _Width.LeftAt(_Distance);\r\n\t\t_Right = position + right * _Width.RightAt(_Distance);\r\n\t}\r\n\r\n\r\n\r\n\tpublic static PolygonMesh Build(RoadCurveTable _Table, float _StartDistance, float _EndDistance, in RoadWidth _Width, float _Spacing, in RoadSimplify _Simplify, float _TextureRepeat, float _SurfaceOffset, Material _Material)\r\n\t{\r\n\t\tvar mesh = new PolygonMesh();\r\n\r\n\t\t// The rows the borders and the bridge are built on too, so the pieces meet exactly.\r\n\t\tvar rows = RoadRows.Build(_Table, _StartDistance, _EndDistance, _Spacing, _Width, _Simplify);\r\n\t\tfloat repeat = MathF.Max(1.0f, _TextureRepeat);\r\n\r\n\t\tvar left = new HalfEdgeMesh.VertexHandle[rows.Count];\r\n\t\tvar right = new HalfEdgeMesh.VertexHandle[rows.Count];\r\n\t\tvar uvLeft = new Vector2[rows.Count];\r\n\t\tvar uvRight = new Vector2[rows.Count];\r\n\r\n\t\tfor (int i = 0; i < rows.Count; i++)\r\n\t\t{\r\n\t\t\tfloat distance = rows[i];\r\n\r\n\t\t\tCrossSection(_Table, distance, _Width, _SurfaceOffset, out Vector3 leftPos, out Vector3 rightPos);\r\n\r\n\t\t\tvar handles = mesh.AddVertices(leftPos, rightPos);\r\n\t\t\tleft[i] = handles[0];\r\n\t\t\tright[i] = handles[1];\r\n\r\n\t\t\t// U runs across the road from the centreline, V along it, so a tiling material follows the road instead\r\n\t\t\t// of the world, and an edge that moves out takes its texture with it.\r\n\t\t\tfloat v = distance / repeat;\r\n\t\t\tuvLeft[i] = new Vector2(-_Width.LeftAt(distance) / repeat, v);\r\n\t\t\tuvRight[i] = new Vector2(_Width.RightAt(distance) / repeat, v);\r\n\t\t}\r\n\r\n\t\t// Two triangles rather than a quad: a road that turns as it climbs has cells whose four corners are not\r\n\t\t// coplanar, and then it matters exactly how the cell is split, both here and for the paint laid on top.\r\n\t\tfor (int i = 0; i < rows.Count - 1; i++)\r\n\t\t{\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i], right[i + 1], uvLeft[i], uvRight[i], uvRight[i + 1]);\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i + 1], left[i + 1], uvLeft[i], uvRight[i + 1], uvLeft[i + 1]);\r\n\t\t}\r\n\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The height of one cell of road surface, as it is built, over a point in plan: the cell is split from its\r\n\t/// near left corner to its far right one, so which of the two triangles the point falls on is which side of\r\n\t/// that diagonal it lies. The triangle's plane is used even for a point just off the cell (paint hanging over\r\n\t/// the road's edge), which keeps that paint on the surface's own plane.\r\n\t/// </summary>\r\n\tpublic static float CellHeight(Vector3 _NearLeft, Vector3 _NearRight, Vector3 _FarRight, Vector3 _FarLeft, Vector2 _Point)\r\n\t{\r\n\t\tVector2 diagonal = Flat(_FarRight) - Flat(_NearLeft);\r\n\t\tfloat side = Cross(diagonal, _Point - Flat(_NearLeft));\r\n\t\tfloat nearRightSide = Cross(diagonal, Flat(_NearRight) - Flat(_NearLeft));\r\n\r\n\t\treturn side * nearRightSide >= 0.0f\r\n\t\t\t? PlaneHeight(_NearLeft, _NearRight, _FarRight, _Point)\r\n\t\t\t: PlaneHeight(_NearLeft, _FarRight, _FarLeft, _Point);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The height of the plane through three points over a point in plan.\r\n\t/// </summary>\r\n\tprivate static float PlaneHeight(Vector3 _A, Vector3 _B, Vector3 _C, Vector2 _Point)\r\n\t{\r\n\t\tVector2 a = Flat(_A);\r\n\t\tVector2 b = Flat(_B);\r\n\t\tVector2 c = Flat(_C);\r\n\t\tfloat area = Cross(b - a, c - a);\r\n\r\n\t\tif (MathF.Abs(area) < 0.0001f)\r\n\t\t\treturn _A.z;\r\n\r\n\t\tfloat weightA = Cross(b - _Point, c - _Point) / area;\r\n\t\tfloat weightB = Cross(c - _Point, a - _Point) / area;\r\n\r\n\t\treturn weightA * _A.z + weightB * _B.z + (1.0f - weightA - weightB) * _C.z;\r\n\t}\r\n\r\n\r\n\r\n\tprivate static Vector2 Flat(Vector3 _Vector)\r\n\t{\r\n\t\treturn new Vector2(_Vector.x, _Vector.y);\r\n\t}\r\n\r\n\r\n\r\n\tprivate static float Cross(Vector2 _A, Vector2 _B)\r\n\t{\r\n\t\treturn _A.x * _B.y - _A.y * _B.x;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Editor/RoadBorderDebugDump.cs",
            "FileName": "RoadBorderDebugDump.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing Editor;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2.Editor;\r\n\r\n/// <summary>\r\n/// Writes each network's border debug record to Libraries/RoadTool2/debug/borders.txt whenever its border pieces\r\n/// have been rebuilt with the overlay on, so the numbers can be read outside the editor.\r\n/// </summary>\r\npublic static class RoadBorderDebugDump\r\n{\r\n\tprivate static readonly Dictionary<Guid, int> s_Written = new();\r\n\r\n\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic static void OnFrame()\r\n\t{\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\r\n\t\tif (scene is null)\r\n\t\t\treturn;\r\n\r\n\t\tvar network = scene.Get<RoadNetwork>();\r\n\r\n\t\tif (!network.IsValid() || !network.DebugBorders || network.IsRebuilding)\r\n\t\t\treturn;\r\n\r\n\t\tif (s_Written.TryGetValue(network.Id, out int revision) && revision == network.DebugRevision)\r\n\t\t\treturn;\r\n\r\n\t\ts_Written[network.Id] = network.DebugRevision;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tstring path = GetPath(network);\r\n\r\n\t\t\tDirectory.CreateDirectory(Path.GetDirectoryName(path));\r\n\t\t\tFile.WriteAllText(path, network.BuildDebugRecord());\r\n\r\n\t\t\tLog.Info($\"Road Tool 2: border debug written to {path}\");\r\n\t\t}\r\n\t\tcatch (Exception exception)\r\n\t\t{\r\n\t\t\tLog.Warning($\"Road Tool 2: could not write the border debug record: {exception.Message}\");\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate static string GetPath(RoadNetwork _Network)\r\n\t{\r\n\t\tstring root = Project.Current.GetRootPath();\r\n\t\tstring library = Path.Combine(root, \"Libraries\", \"RoadTool2\");\r\n\r\n\t\t// The library folder may have been renamed; find it by its project file.\r\n\t\tif (!Directory.Exists(library))\r\n\t\t{\r\n\t\t\tforeach (string directory in Directory.GetDirectories(Path.Combine(root, \"Libraries\")))\r\n\t\t\t{\r\n\t\t\t\tif (File.Exists(Path.Combine(directory, \"roadtool2.sbproj\")))\r\n\t\t\t\t{\r\n\t\t\t\t\tlibrary = directory;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tstring name = string.Concat(_Network.GameObject.Name.Split(Path.GetInvalidFileNameChars())).Replace(' ', '_');\r\n\r\n\t\treturn Path.Combine(library, \"debug\", $\"borders_{name}.txt\");\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Code/Profiles/BridgeStyle.cs",
            "FileName": "BridgeStyle.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System.Text.Json.Serialization;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// What stands along the two edges of a bridge deck.\r\n/// </summary>\r\npublic enum BridgeSideStyle\r\n{\r\n\t/// <summary>\r\n\t/// Nothing above the deck: its side face is all there is.\r\n\t/// </summary>\r\n\tNone,\r\n\r\n\t/// <summary>\r\n\t/// A solid parapet wall.\r\n\t/// </summary>\r\n\tWall,\r\n\r\n\t/// <summary>\r\n\t/// Posts at intervals with horizontal rails running between them.\r\n\t/// </summary>\r\n\tRailing,\r\n\r\n\t/// <summary>\r\n\t/// A prefab of your own repeated along the edge (a fence panel, a lamp, a cable anchor...).\r\n\t/// </summary>\r\n\tPrefab\r\n}\r\n\r\n/// <summary>\r\n/// What holds a bridge deck up.\r\n/// </summary>\r\npublic enum BridgeSupportStyle\r\n{\r\n\tNone,\r\n\r\n\t/// <summary>\r\n\t/// Pillars at intervals, from the underside of the deck down into the ground.\r\n\t/// </summary>\r\n\tPillars,\r\n\r\n\t/// <summary>\r\n\t/// A prefab of your own at intervals under the deck (a pier, a tower...).\r\n\t/// </summary>\r\n\tPrefab\r\n}\r\n\r\npublic enum BridgePillarShape\r\n{\r\n\tSquare,\r\n\tRound\r\n}\r\n\r\n/// <summary>\r\n/// One pillar under the middle of the deck, or one under each edge.\r\n/// </summary>\r\npublic enum BridgePillarLayout\r\n{\r\n\tSingle,\r\n\tPair\r\n}\r\n\r\n/// <summary>\r\n/// How a stretch of road becomes a bridge: the deck under it, what stands along the deck's edges, and what holds it\r\n/// up. Each part is picked from a set of built-in designs or handed to a prefab of your own, so new kinds of bridge\r\n/// (cable-stayed, trusses...) are new designs of a part rather than a new kind of road. Roads reference a style, so\r\n/// every bridge built from it changes together when the asset is edited.\r\n/// </summary>\r\n[AssetType(Name = \"Bridge Style\", Extension = \"bridg\", Category = \"Road Tool 2\")]\r\npublic sealed class BridgeStyle : GameResource\r\n{\r\n\tprivate const string DefaultMaterial = \"roadtool2/materials/concrete.vmat\";\r\n\r\n\t/// <summary>\r\n\t/// Material of the deck's sides and underside (and of the walls and pillars when they have none of their own).\r\n\t/// </summary>\r\n\t[Group(\"Deck\")] public Material DeckMaterial { get; set; }\r\n\t[Group(\"Deck\"), Range(10.0f, 10000.0f)] public float DeckTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 500.0f;\r\n\r\n\t/// <summary>\r\n\t/// The deck's thickness below the road surface.\r\n\t/// </summary>\r\n\t[Group(\"Deck\"), Range(5.0f, 500.0f)] public float DeckDepth { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 60.0f;\r\n\r\n\t/// <summary>\r\n\t/// A bare ledge of deck outside the side (wall, railing) on each edge. 0 ends the deck at the side's outer face.\r\n\t/// </summary>\r\n\t[Group(\"Deck\"), Range(0.0f, 300.0f)] public float DeckOverhang { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 0.0f;\r\n\r\n\t[Group(\"Side\")] public BridgeSideStyle Side { get; set; } = BridgeSideStyle.Wall;\r\n\r\n\t/// <summary>\r\n\t/// Material of the wall or the railing. Empty uses the deck's.\r\n\t/// </summary>\r\n\t[Group(\"Side\"), ShowIf(nameof(HasBuiltSide), true)] public Material SideMaterial { get; set; }\r\n\t[Group(\"Side\"), ShowIf(nameof(HasBuiltSide), true), Range(10.0f, 10000.0f)] public float SideTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 100.0f;\r\n\r\n\t/// <summary>\r\n\t/// The wall's thickness, or the railing's post size; also the width of the strip a prefab side stands on.\r\n\t/// </summary>\r\n\t[Group(\"Side\"), HideIf(nameof(Side), BridgeSideStyle.None), Range(1.0f, 200.0f)] public float SideWidth { get; set { field = value.Clamp(0.1f, 10000.0f); } } = 20.0f;\r\n\r\n\t/// <summary>\r\n\t/// Height of the wall or railing above the road's border (the pavement top when there is one).\r\n\t/// </summary>\r\n\t[Group(\"Side\"), ShowIf(nameof(HasBuiltSide), true), Range(5.0f, 300.0f)] public float SideHeight { get; set { field = value.Clamp(0.1f, 10000.0f); } } = 45.0f;\r\n\r\n\t[Group(\"Railing\"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(50.0f, 1000.0f)] public float PostSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 200.0f;\r\n\t[Group(\"Railing\"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(1, 6)] public int RailCount { get; set { field = value.Clamp(1, 32); } } = 2;\r\n\t[Group(\"Railing\"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(1.0f, 50.0f)] public float RailSize { get; set { field = value.Clamp(0.1f, 1000.0f); } } = 6.0f;\r\n\r\n\t/// <summary>\r\n\t/// Placed on the deck's edge strip with its forward along the road and its right pointing away from the road.\r\n\t/// </summary>\r\n\t[Group(\"Side\"), ShowIf(nameof(Side), BridgeSideStyle.Prefab)] public GameObject SidePrefab { get; set; }\r\n\t[Group(\"Side\"), ShowIf(nameof(Side), BridgeSideStyle.Prefab), Range(10.0f, 2000.0f)] public float SideSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 200.0f;\r\n\r\n\t[Group(\"Supports\")] public BridgeSupportStyle Supports { get; set; } = BridgeSupportStyle.Pillars;\r\n\t[Group(\"Supports\"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(200.0f, 10000.0f)] public float SupportSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 1500.0f;\r\n\r\n\t/// <summary>\r\n\t/// The pillars' material. Empty uses the deck's.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public Material SupportMaterial { get; set; }\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarShape PillarShape { get; set; } = BridgePillarShape.Square;\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarLayout PillarLayout { get; set; } = BridgePillarLayout.Single;\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(10.0f, 1000.0f)] public float PillarSize { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 100.0f;\r\n\r\n\t/// <summary>\r\n\t/// For a pair of pillars: how far in from the deck's edges they stand.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(0.0f, 500.0f)] public float PillarInset { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 20.0f;\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(3, 32)] public int PillarSegments { get; set { field = value.Clamp(3, 64); } } = 12;\r\n\r\n\t/// <summary>\r\n\t/// How far a pillar carries on below the ground it lands on, so it never floats off a slope.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(0.0f, 500.0f)] public float SupportSink { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 50.0f;\r\n\r\n\t/// <summary>\r\n\t/// How far below the deck a support reaches when there is no ground under it to land on.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(100.0f, 10000.0f)] public float SupportFallbackHeight { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 1000.0f;\r\n\r\n\t/// <summary>\r\n\t/// Placed on the ground under the middle of the deck (or the fallback height below it), with its forward along\r\n\t/// the road. It is up to the prefab to reach the deck.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Prefab)] public GameObject SupportPrefab { get; set; }\r\n\r\n\t[Hide, JsonIgnore] public bool HasBuiltSide => Side is BridgeSideStyle.Wall or BridgeSideStyle.Railing;\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedDeckMaterial => DeckMaterial ?? Material.Load(DefaultMaterial);\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedSideMaterial => SideMaterial ?? ResolvedDeckMaterial;\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedSupportMaterial => SupportMaterial ?? ResolvedDeckMaterial;\r\n\r\n\t/// <summary>\r\n\t/// How wide the strip along each deck edge is that the side occupies (nothing for a bare deck).\r\n\t/// </summary>\r\n\t[Hide, JsonIgnore] public float SideStripWidth => Side == BridgeSideStyle.None ? 0.0f : SideWidth;\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Folds everything the bridge geometry is built from into a hash, so pieces built with an older version of\r\n\t/// the style are noticed.\r\n\t/// </summary>\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(DeckMaterial?.ResourcePath);\r\n\t\t_Hash.Add(DeckTextureRepeat);\r\n\t\t_Hash.Add(DeckDepth);\r\n\t\t_Hash.Add(DeckOverhang);\r\n\t\t_Hash.Add((int)Side);\r\n\t\t_Hash.Add(SideMaterial?.ResourcePath);\r\n\t\t_Hash.Add(SideTextureRepeat);\r\n\t\t_Hash.Add(SideWidth);\r\n\t\t_Hash.Add(SideHeight);\r\n\t\t_Hash.Add(PostSpacing);\r\n\t\t_Hash.Add(RailCount);\r\n\t\t_Hash.Add(RailSize);\r\n\t\t_Hash.Add(RoadPropSet.PrefabKey(SidePrefab));\r\n\t\t_Hash.Add(SideSpacing);\r\n\t\t_Hash.Add((int)Supports);\r\n\t\t_Hash.Add(SupportSpacing);\r\n\t\t_Hash.Add(SupportMaterial?.ResourcePath);\r\n\t\t_Hash.Add((int)PillarShape);\r\n\t\t_Hash.Add((int)PillarLayout);\r\n\t\t_Hash.Add(PillarSize);\r\n\t\t_Hash.Add(PillarInset);\r\n\t\t_Hash.Add(PillarSegments);\r\n\t\t_Hash.Add(SupportSink);\r\n\t\t_Hash.Add(SupportFallbackHeight);\r\n\t\t_Hash.Add(RoadPropSet.PrefabKey(SupportPrefab));\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The asset was saved: every network with a bridge in this style has stale pieces now.\r\n\t/// </summary>\r\n\tprotected override void PostReload()\r\n\t{\r\n\t\tbase.PostReload();\r\n\r\n\t\tRoadNetwork.Editor_MarkDirty();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)\r\n\t{\r\n\t\treturn CreateSimpleAssetTypeIcon(\"architecture\", _Width, _Height, \"#7fb2ff\", \"black\");\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Code/Utility/MeshUtility.cs",
            "FileName": "MeshUtility.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\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\t/// <summary>\r\n\t/// Adds a quad face; returns an invalid handle when the mesh refused it (a duplicate vertex, a topology clash).\r\n\t/// </summary>\r\n\tpublic static HalfEdgeMesh.FaceHandle AddTexturedQuad(PolygonMesh _Mesh, Material _Material,\r\n\t\tHalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,\r\n\t\tHalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D,\r\n\t\tVector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)\r\n\t{\r\n\t\tif (HasDuplicateVertex(_A, _B, _C, _D))\r\n\t\t\treturn default;\r\n\r\n\t\tvar face = _Mesh.AddFace(_A, _B, _C, _D);\r\n\r\n\t\tif (!face.IsValid)\r\n\t\t\treturn default;\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\r\n\t\treturn face;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Adds a triangle face; returns an invalid handle when the mesh refused it.\r\n\t/// </summary>\r\n\tpublic static HalfEdgeMesh.FaceHandle AddTexturedTriangle(PolygonMesh _Mesh, Material _Material,\r\n\t\tHalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,\r\n\t\tHalfEdgeMesh.VertexHandle _C,\r\n\t\tVector2 _UvA, Vector2 _UvB, Vector2 _UvC)\r\n\t{\r\n\t\tif (HasDuplicateVertex(_A, _B, _C))\r\n\t\t\treturn default;\r\n\r\n\t\tvar face = _Mesh.AddFace(_A, _B, _C);\r\n\r\n\t\tif (!face.IsValid)\r\n\t\t\treturn default;\r\n\r\n\t\t_Mesh.SetFaceMaterial(face, _Material);\r\n\t\t_Mesh.SetFaceTextureCoords(face, new List<Vector2> { _UvA, _UvB, _UvC });\r\n\r\n\t\treturn face;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Paints a face's vertices with blend weights (what a blendable material mixes its layers by), in the order\r\n\t/// the face's vertices were given.\r\n\t/// </summary>\r\n\tpublic static void SetFaceBlends(PolygonMesh _Mesh, HalfEdgeMesh.FaceHandle _Face, IReadOnlyList<Color32> _Blends)\r\n\t{\r\n\t\tif (!_Face.IsValid || !_Mesh.GetFaceVerticesConnectedToFace(_Face, out var edges))\r\n\t\t\treturn;\r\n\r\n\t\tint count = System.Math.Min(edges.Length, _Blends.Count);\r\n\r\n\t\tfor (int i = 0; i < count; i++)\r\n\t\t\t_Mesh.SetVertexBlend(edges[i], _Blends[i]);\r\n\t}\r\n\r\n\r\n\r\n\tprivate static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C)\r\n\t{\r\n\t\treturn _A.Equals(_B) || _A.Equals(_C) || _B.Equals(_C);\r\n\t}\r\n\r\n\r\n\r\n\tprivate static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D)\r\n\t{\r\n\t\treturn _A.Equals(_B) || _A.Equals(_C) || _A.Equals(_D) ||\r\n\t\t\t_B.Equals(_C) || _B.Equals(_D) ||\r\n\t\t\t_C.Equals(_D);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Geometry/RoadCurveTable.cs",
            "FileName": "RoadCurveTable.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Arc-length table over a <see cref=\"RoadCurve\"/>, so the road can be sampled at even distances (mesh rows, chunk\r\n/// boundaries) instead of at even parameter values, which bunch up around tight bends. The samples double as a\r\n/// polyline for picking.\r\n/// </summary>\r\npublic sealed class RoadCurveTable\r\n{\r\n\tprivate readonly Vector3[] m_Positions;\r\n\tprivate readonly float[] m_Distances;\r\n\r\n\tpublic RoadCurve Curve { get; }\r\n\r\n\tpublic float Length => m_Distances[^1];\r\n\r\n\tpublic int SampleCount => m_Positions.Length;\r\n\r\n\t/// <summary>\r\n\t/// The box the curve itself runs through. Picking widens it by the road's own reach and skips the road\r\n\t/// altogether when the cursor's ray misses it, which is most of them in a city.\r\n\t/// </summary>\r\n\tpublic BBox Bounds { get; }\r\n\r\n\r\n\r\n\tpublic RoadCurveTable(RoadCurve _Curve, int _Samples)\r\n\t{\r\n\t\tCurve = _Curve;\r\n\r\n\t\tint count = Math.Max(1, _Samples);\r\n\t\tm_Positions = new Vector3[count + 1];\r\n\t\tm_Distances = new float[count + 1];\r\n\t\tm_Positions[0] = _Curve.P0;\r\n\r\n\t\tvar bounds = new BBox(_Curve.P0, _Curve.P0);\r\n\r\n\t\tfor (int i = 1; i <= count; i++)\r\n\t\t{\r\n\t\t\tm_Positions[i] = _Curve.Evaluate((float)i / count);\r\n\t\t\tm_Distances[i] = m_Distances[i - 1] + Vector3.DistanceBetween(m_Positions[i - 1], m_Positions[i]);\r\n\t\t\tbounds = bounds.AddPoint(m_Positions[i]);\r\n\t\t}\r\n\r\n\t\tBounds = bounds;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Builds a table with a sample density that suits the curve's length.\r\n\t/// </summary>\r\n\tpublic static RoadCurveTable Create(RoadCurve _Curve)\r\n\t{\r\n\t\tint samples = Math.Clamp((int)(_Curve.ApproximateLength() / 20.0f), 16, 1024);\r\n\r\n\t\treturn new RoadCurveTable(_Curve, samples);\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 SamplePosition(int _Index)\r\n\t{\r\n\t\treturn m_Positions[_Index];\r\n\t}\r\n\r\n\r\n\r\n\tpublic float SampleDistance(int _Index)\r\n\t{\r\n\t\treturn m_Distances[_Index];\r\n\t}\r\n\r\n\r\n\r\n\tpublic float DistanceToT(float _Distance)\r\n\t{\r\n\t\tif (_Distance <= 0.0f)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\tif (_Distance >= Length)\r\n\t\t\treturn 1.0f;\r\n\r\n\t\tint low = 0;\r\n\t\tint high = m_Distances.Length - 1;\r\n\r\n\t\twhile (high - low > 1)\r\n\t\t{\r\n\t\t\tint mid = (low + high) / 2;\r\n\r\n\t\t\tif (m_Distances[mid] <= _Distance)\r\n\t\t\t\tlow = mid;\r\n\t\t\telse\r\n\t\t\t\thigh = mid;\r\n\t\t}\r\n\r\n\t\tfloat span = m_Distances[high] - m_Distances[low];\r\n\t\tfloat fraction = span > 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;\r\n\r\n\t\treturn (low + fraction) / (m_Distances.Length - 1);\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 PositionAt(float _Distance)\r\n\t{\r\n\t\treturn Curve.Evaluate(DistanceToT(_Distance));\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 TangentAt(float _Distance)\r\n\t{\r\n\t\treturn Curve.Tangent(DistanceToT(_Distance));\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where the road is at a distance and which way it runs there, in one go: everything that lays something\r\n\t/// across the road wants both, and asking for them separately searches the table twice.\r\n\t/// </summary>\r\n\tpublic void Frame(float _Distance, out Vector3 _Position, out Vector3 _Tangent)\r\n\t{\r\n\t\tfloat t = DistanceToT(_Distance);\r\n\r\n\t\t_Position = Curve.Evaluate(t);\r\n\t\t_Tangent = Curve.Tangent(t);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Network/RoadJunctionBuilder.cs",
            "FileName": "RoadJunctionBuilder.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Generates the surface of a junction: the layout's polygon fanned out from the node. Every triangle shares the\r\n/// centre, so each one is planar and none can be dropped by the mesh builder, whatever the arms' heights do.\r\n/// </summary>\r\ninternal static class RoadJunctionBuilder\r\n{\r\n\tpublic static PolygonMesh Build(RoadJunctionLayout _Layout, float _TextureRepeat, Material _Material)\r\n\t{\r\n\t\tvar mesh = new PolygonMesh();\r\n\t\tvar polygon = _Layout.Polygon;\r\n\r\n\t\tif (polygon.Count < 3)\r\n\t\t\treturn mesh;\r\n\r\n\t\tvar centre = mesh.AddVertices(_Layout.Centre)[0];\r\n\t\tvar ring = mesh.AddVertices(polygon.ToArray());\r\n\t\tVector2 centreUv = PlanarUv(_Layout.Centre, _TextureRepeat);\r\n\r\n\t\tfor (int i = 0; i < polygon.Count; i++)\r\n\t\t{\r\n\t\t\tint next = (i + 1) % polygon.Count;\r\n\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, centre, ring[i], ring[next],\r\n\t\t\t\tcentreUv, PlanarUv(polygon[i], _TextureRepeat), PlanarUv(polygon[next], _TextureRepeat));\r\n\t\t}\r\n\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Junctions have no single direction to run a texture along, so they use a top-down projection.\r\n\t/// </summary>\r\n\tprivate static Vector2 PlanarUv(Vector3 _Position, float _TextureRepeat)\r\n\t{\r\n\t\treturn new Vector2(_Position.x, _Position.y) / _TextureRepeat;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Road Tool\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"roadtool2\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"redsnail\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"redsnail.roadtool2\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"30\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-10-18T00:54:30.0847204Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.166.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.166.0\")]"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Code/Geometry/RoadCurveTable.cs",
            "FileName": "RoadCurveTable.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Arc-length table over a <see cref=\"RoadCurve\"/>, so the road can be sampled at even distances (mesh rows, chunk\r\n/// boundaries) instead of at even parameter values, which bunch up around tight bends. The samples double as a\r\n/// polyline for picking.\r\n/// </summary>\r\npublic sealed class RoadCurveTable\r\n{\r\n\tprivate readonly Vector3[] m_Positions;\r\n\tprivate readonly float[] m_Distances;\r\n\r\n\tpublic RoadCurve Curve { get; }\r\n\r\n\tpublic float Length => m_Distances[^1];\r\n\r\n\tpublic int SampleCount => m_Positions.Length;\r\n\r\n\t/// <summary>\r\n\t/// The box the curve itself runs through. Picking widens it by the road's own reach and skips the road\r\n\t/// altogether when the cursor's ray misses it, which is most of them in a city.\r\n\t/// </summary>\r\n\tpublic BBox Bounds { get; }\r\n\r\n\r\n\r\n\tpublic RoadCurveTable(RoadCurve _Curve, int _Samples)\r\n\t{\r\n\t\tCurve = _Curve;\r\n\r\n\t\tint count = Math.Max(1, _Samples);\r\n\t\tm_Positions = new Vector3[count + 1];\r\n\t\tm_Distances = new float[count + 1];\r\n\t\tm_Positions[0] = _Curve.P0;\r\n\r\n\t\tvar bounds = new BBox(_Curve.P0, _Curve.P0);\r\n\r\n\t\tfor (int i = 1; i <= count; i++)\r\n\t\t{\r\n\t\t\tm_Positions[i] = _Curve.Evaluate((float)i / count);\r\n\t\t\tm_Distances[i] = m_Distances[i - 1] + Vector3.DistanceBetween(m_Positions[i - 1], m_Positions[i]);\r\n\t\t\tbounds = bounds.AddPoint(m_Positions[i]);\r\n\t\t}\r\n\r\n\t\tBounds = bounds;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Builds a table with a sample density that suits the curve's length.\r\n\t/// </summary>\r\n\tpublic static RoadCurveTable Create(RoadCurve _Curve)\r\n\t{\r\n\t\tint samples = Math.Clamp((int)(_Curve.ApproximateLength() / 20.0f), 16, 1024);\r\n\r\n\t\treturn new RoadCurveTable(_Curve, samples);\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 SamplePosition(int _Index)\r\n\t{\r\n\t\treturn m_Positions[_Index];\r\n\t}\r\n\r\n\r\n\r\n\tpublic float SampleDistance(int _Index)\r\n\t{\r\n\t\treturn m_Distances[_Index];\r\n\t}\r\n\r\n\r\n\r\n\tpublic float DistanceToT(float _Distance)\r\n\t{\r\n\t\tif (_Distance <= 0.0f)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\tif (_Distance >= Length)\r\n\t\t\treturn 1.0f;\r\n\r\n\t\tint low = 0;\r\n\t\tint high = m_Distances.Length - 1;\r\n\r\n\t\twhile (high - low > 1)\r\n\t\t{\r\n\t\t\tint mid = (low + high) / 2;\r\n\r\n\t\t\tif (m_Distances[mid] <= _Distance)\r\n\t\t\t\tlow = mid;\r\n\t\t\telse\r\n\t\t\t\thigh = mid;\r\n\t\t}\r\n\r\n\t\tfloat span = m_Distances[high] - m_Distances[low];\r\n\t\tfloat fraction = span > 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;\r\n\r\n\t\treturn (low + fraction) / (m_Distances.Length - 1);\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 PositionAt(float _Distance)\r\n\t{\r\n\t\treturn Curve.Evaluate(DistanceToT(_Distance));\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 TangentAt(float _Distance)\r\n\t{\r\n\t\treturn Curve.Tangent(DistanceToT(_Distance));\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where the road is at a distance and which way it runs there, in one go: everything that lays something\r\n\t/// across the road wants both, and asking for them separately searches the table twice.\r\n\t/// </summary>\r\n\tpublic void Frame(float _Distance, out Vector3 _Position, out Vector3 _Tangent)\r\n\t{\r\n\t\tfloat t = DistanceToT(_Distance);\r\n\r\n\t\t_Position = Curve.Evaluate(t);\r\n\t\t_Tangent = Curve.Tangent(t);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Code/Geometry/RoadWidth.cs",
            "FileName": "RoadWidth.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// How far a road's two edges sit from its centreline along a segment: half the road's width on each side, plus\r\n/// whatever extra each side has at the segment's start and end, blended smoothly between the two. That is what lets\r\n/// a lane appear on one side over a taper (a highway exit or entry), a ramp start as a point and grow, or a road\r\n/// flare out to a wider one.\r\n/// </summary>\r\npublic readonly struct RoadWidth\r\n{\r\n\t/// <summary>\r\n\t/// No road is built narrower than this: two edges on one point make faces with no area.\r\n\t/// </summary>\r\n\tprivate const float MinWidth = 2.0f;\r\n\r\n\tpublic readonly float Base;\r\n\tpublic readonly float LeftStart;\r\n\tpublic readonly float LeftEnd;\r\n\tpublic readonly float RightStart;\r\n\tpublic readonly float RightEnd;\r\n\tpublic readonly float Length;\r\n\r\n\tpublic RoadWidth(float _Base, float _LeftStart, float _LeftEnd, float _RightStart, float _RightEnd, float _Length)\r\n\t{\r\n\t\tBase = _Base;\r\n\t\tLeftStart = _LeftStart;\r\n\t\tLeftEnd = _LeftEnd;\r\n\t\tRightStart = _RightStart;\r\n\t\tRightEnd = _RightEnd;\r\n\t\tLength = MathF.Max(0.001f, _Length);\r\n\t}\r\n\r\n\tpublic RoadWidth(RoadSegment _Segment, float _Length)\r\n\t\t: this(_Segment.Width, _Segment.LeftExtraStart, _Segment.LeftExtraEnd, _Segment.RightExtraStart, _Segment.RightExtraEnd, _Length)\r\n\t{\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// A plain road of one width all along.\r\n\t/// </summary>\r\n\tpublic static RoadWidth Uniform(float _Width)\r\n\t{\r\n\t\treturn new RoadWidth(_Width, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// True when both edges sit at half the width all along: the road every piece of the tool was first built for.\r\n\t/// </summary>\r\n\tpublic bool IsUniform => LeftStart == 0.0f && LeftEnd == 0.0f && RightStart == 0.0f && RightEnd == 0.0f;\r\n\r\n\t/// <summary>\r\n\t/// The furthest either edge gets from the centreline anywhere along the segment.\r\n\t/// </summary>\r\n\tpublic float MaxHalf => MathF.Max(MathF.Max(Base * 0.5f + LeftStart, Base * 0.5f + LeftEnd), MathF.Max(Base * 0.5f + RightStart, Base * 0.5f + RightEnd));\r\n\r\n\t/// <summary>\r\n\t/// Distance of the left edge from the centreline at a curve distance, looking along the segment.\r\n\t/// </summary>\r\n\tpublic float LeftAt(float _Distance)\r\n\t{\r\n\t\tfloat left = Base * 0.5f + Blend(LeftStart, LeftEnd, _Distance);\r\n\t\tfloat right = Base * 0.5f + Blend(RightStart, RightEnd, _Distance);\r\n\r\n\t\treturn left + right < MinWidth ? left + (MinWidth - left - right) * 0.5f : left;\r\n\t}\r\n\r\n\tpublic float RightAt(float _Distance)\r\n\t{\r\n\t\tfloat left = Base * 0.5f + Blend(LeftStart, LeftEnd, _Distance);\r\n\t\tfloat right = Base * 0.5f + Blend(RightStart, RightEnd, _Distance);\r\n\r\n\t\treturn left + right < MinWidth ? right + (MinWidth - left - right) * 0.5f : right;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// The road's full width at a curve distance.\r\n\t/// </summary>\r\n\tpublic float WidthAt(float _Distance)\r\n\t{\r\n\t\treturn LeftAt(_Distance) + RightAt(_Distance);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// How much wider than the base half width the left edge is at a curve distance (negative when narrower).\r\n\t/// </summary>\r\n\tpublic float LeftExtraAt(float _Distance)\r\n\t{\r\n\t\treturn LeftAt(_Distance) - Base * 0.5f;\r\n\t}\r\n\r\n\tpublic float RightExtraAt(float _Distance)\r\n\t{\r\n\t\treturn RightAt(_Distance) - Base * 0.5f;\r\n\t}\r\n\r\n\r\n\r\n\tprivate float Blend(float _Start, float _End, float _Distance)\r\n\t{\r\n\t\tif (_Start == _End)\r\n\t\t\treturn _Start;\r\n\r\n\t\tfloat t = Math.Clamp(_Distance / Length, 0.0f, 1.0f);\r\n\r\n\t\t// Eased, so the edge leaves and arrives without a kink.\r\n\t\tt = t * t * (3.0f - 2.0f * t);\r\n\r\n\t\treturn _Start + (_End - _Start) * t;\r\n\t}\r\n\r\n\r\n\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(Base);\r\n\t\t_Hash.Add(LeftStart);\r\n\t\t_Hash.Add(LeftEnd);\r\n\t\t_Hash.Add(RightStart);\r\n\t\t_Hash.Add(RightEnd);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Code/Utility/StableHash.cs",
            "FileName": "StableHash.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// 64-bit FNV-1a hash that is stable across editor sessions, unlike <see cref=\"HashCode\"/> which is seeded per\r\n/// process. Used to stamp generated meshes with the inputs they were built from, so a scene reopened days later\r\n/// still knows which chunks are current.\r\n/// </summary>\r\npublic struct StableHash\r\n{\r\n\tprivate const ulong OffsetBasis = 14695981039346656037UL;\r\n\tprivate const ulong Prime = 1099511628211UL;\r\n\r\n\tprivate ulong m_Hash;\r\n\r\n\tpublic long Value => unchecked((long)m_Hash);\r\n\r\n\r\n\r\n\tpublic static StableHash Create()\r\n\t{\r\n\t\treturn new StableHash { m_Hash = OffsetBasis };\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(int _Value)\r\n\t{\r\n\t\tunchecked\r\n\t\t{\r\n\t\t\tfor (int i = 0; i < 4; i++)\r\n\t\t\t{\r\n\t\t\t\tm_Hash ^= (byte)(_Value >> (i * 8));\r\n\t\t\t\tm_Hash *= Prime;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(long _Value)\r\n\t{\r\n\t\tAdd(unchecked((int)_Value));\r\n\t\tAdd(unchecked((int)(_Value >> 32)));\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(float _Value)\r\n\t{\r\n\t\tAdd(BitConverter.SingleToInt32Bits(_Value));\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(Vector3 _Value)\r\n\t{\r\n\t\tAdd(_Value.x);\r\n\t\tAdd(_Value.y);\r\n\t\tAdd(_Value.z);\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(string _Value)\r\n\t{\r\n\t\tif (_Value is not null)\r\n\t\t{\r\n\t\t\tforeach (char c in _Value)\r\n\t\t\t\tAdd((int)c);\r\n\t\t}\r\n\r\n\t\t// Terminator, so \"ab\" + \"c\" never collides with \"a\" + \"bc\".\r\n\t\tAdd(-1);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Network/RoadChunk.cs",
            "FileName": "RoadChunk.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\npublic enum RoadChunkKind\r\n{\r\n\t/// <summary>\r\n\t/// The road surface itself.\r\n\t/// </summary>\r\n\tSurface,\r\n\r\n\t/// <summary>\r\n\t/// What runs along the road's sides: sidewalks, verges.\r\n\t/// </summary>\r\n\tBorder,\r\n\r\n\t/// <summary>\r\n\t/// The painted lines on the surface: lane lines, centre line, edge lines.\r\n\t/// </summary>\r\n\tMarking,\r\n\r\n\t/// <summary>\r\n\t/// The bridge under a road: deck, sides, supports.\r\n\t/// </summary>\r\n\tBridge,\r\n\r\n\t/// <summary>\r\n\t/// The prefabs standing along a road and before its junctions.\r\n\t/// </summary>\r\n\tProps,\r\n\r\n\t/// <summary>\r\n\t/// The rounded end of a road that stops: surface and border together, in one piece per end.\r\n\t/// </summary>\r\n\tCap\r\n}\r\n\r\n/// <summary>\r\n/// Marks a generated mesh child of a <see cref=\"RoadNetwork\"/> and remembers what it covers: a stretch of a segment,\r\n/// or the junction at a node, and whether it is the surface or the border of it. The network uses it to replace\r\n/// exactly the pieces that went stale and leave the rest alone.\r\n/// </summary>\r\n[Icon(\"view_week\")]\r\npublic sealed class RoadChunk : Component\r\n{\r\n\t/// <summary>\r\n\t/// The segment this chunk is a stretch of, or -1 for a junction piece.\r\n\t/// </summary>\r\n\t[Property, ReadOnly] public int SegmentId { get; set; } = -1;\r\n\r\n\t/// <summary>\r\n\t/// The node this chunk is the junction of, or -1 for a road stretch.\r\n\t/// </summary>\r\n\t[Property, ReadOnly] public int NodeId { get; set; } = -1;\r\n\r\n\t/// <summary>\r\n\t/// The parking lot this chunk is a piece of, or -1 for anything else.\r\n\t/// </summary>\r\n\t[Property, ReadOnly] public int LotId { get; set; } = -1;\r\n\r\n\t[Property, ReadOnly] public RoadChunkKind Kind { get; set; } = RoadChunkKind.Surface;\r\n\r\n\t[Property, ReadOnly] public int ChunkIndex { get; set; }\r\n\r\n\t[Property, ReadOnly] public int ChunkCount { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// A locked chunk is never regenerated, so its mesh can be edited by hand. It keeps its geometry even if the road\r\n\t/// under it changes; unlock it to have it rebuilt.\r\n\t/// </summary>\r\n\t[Property]\r\n\tpublic bool Locked\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\t// Unlocking is a request to catch up with the graph.\r\n\t\t\tGameObject?.Parent?.Components.Get<RoadNetwork>()?.MarkDirty();\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Hash of everything this chunk was generated from. When it no longer matches what the graph says it should be\r\n\t/// the chunk is stale.\r\n\t/// </summary>\r\n\t[Property, Hide] public long Signature { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// The rows a border piece was built from, when the network's debug overlay is on. Not saved.\r\n\t/// </summary>\r\n\tpublic BorderDebug Debug { get; set; }\r\n\r\n\tpublic bool IsJunction => NodeId >= 0;\r\n\r\n\tpublic bool IsLot => LotId >= 0;\r\n\r\n\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tif (Debug is null)\r\n\t\t\treturn;\r\n\r\n\t\tvar network = GameObject?.Parent?.Components.Get<RoadNetwork>();\r\n\r\n\t\tif (network is null || !network.DebugBorders)\r\n\t\t\treturn;\r\n\r\n\t\tGizmo.Draw.IgnoreDepth = true;\r\n\t\tGizmo.Draw.LineThickness = 2.0f;\r\n\r\n\t\tforeach (var line in Debug.Lines)\r\n\t\t{\r\n\t\t\tGizmo.Draw.Color = line.Color;\r\n\t\t\tGizmo.Draw.Line(line.From, line.To);\r\n\t\t}\r\n\r\n\t\tforeach (var marker in Debug.Markers)\r\n\t\t{\r\n\t\t\tGizmo.Draw.Color = marker.Color;\r\n\t\t\tGizmo.Draw.SolidSphere(marker.Position, 2.0f);\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Network/RoadNetwork.Rebuild.cs",
            "FileName": "RoadNetwork.Rebuild.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\npublic sealed partial class RoadNetwork\r\n{\r\n\t/// <summary>\r\n\t/// Bump whenever the generated geometry changes (UV layout, sampling, profile...), so pieces built by an older\r\n\t/// generator are replaced on the next reconcile instead of lingering until their road is edited.\r\n\t/// </summary>\r\n\tprivate const int GeneratorVersion = 57;\r\n\r\n\t/// <summary>\r\n\t/// A stretch of road shorter than this is not worth a chunk (it happens when a junction swallows a whole chunk).\r\n\t/// </summary>\r\n\tprivate const float MinChunkLength = 1.0f;\r\n\r\n\t/// <summary>\r\n\t/// Where a segment's painted lines may run: between <see cref=\"Start\"/> and <see cref=\"End\"/>, skipping the\r\n\t/// gap between <see cref=\"GapStart\"/> and <see cref=\"GapEnd\"/> when there is one (a crossing in the middle of\r\n\t/// a short road between two junctions).\r\n\t/// </summary>\r\n\tprivate readonly record struct LineRange(float Start, float End, float GapStart, float GapEnd)\r\n\t{\r\n\t\tpublic bool HasGap => GapEnd > GapStart;\r\n\r\n\t\tpublic void AddToHash(ref StableHash _Hash)\r\n\t\t{\r\n\t\t\t_Hash.Add(Start);\r\n\t\t\t_Hash.Add(End);\r\n\t\t\t_Hash.Add(GapStart);\r\n\t\t\t_Hash.Add(GapEnd);\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// A segment's pedestrian crossings: at either end, or one shared crossing halfway along a short road between\r\n\t/// two junctions (recorded on the start end). Centres are curve distances from the segment's start.\r\n\t/// </summary>\r\n\tprivate readonly record struct SegmentCrossings(bool AtStart, float StartCentre, bool AtEnd, float EndCentre);\r\n\r\n\t/// <summary>\r\n\t/// One piece of a segment to build. <see cref=\"Lines\"/> bounds the painted lines; <see cref=\"Taper\"/> is the\r\n\t/// border's run-out into a different road type at either end; <see cref=\"Bridge\"/> is the whole built range\r\n\t/// and the ends of a bridge piece; the other kinds ignore them.\r\n\t/// </summary>\r\n\tprivate readonly record struct ChunkPlan(int Index, int Count, float Start, float End, RoadChunkKind Kind, BorderCaps Caps, RoadBorderBuilder.SegmentBorderBlends Blends, long Signature, LineRange Lines = default, SegmentBorderTapers Taper = default, RoadBridgeBuilder.BridgeSpan Bridge = default, RoadPropBuilder.PropSpan Props = default, RoadMarkingBuilder.DashPhase Dash = default, SegmentCrossings Crossings = default);\r\n\r\n\tprivate readonly record struct PendingGroup(long Signature, int Remaining);\r\n\r\n\t/// <summary>\r\n\t/// One piece to (re)build: a chunk of a segment, or, when <see cref=\"NodeId\"/> is set, a piece of a junction.\r\n\t/// <see cref=\"ArmBlends\"/> and <see cref=\"ArmTapers\"/> carry, for a junction's border piece, what each arm's\r\n\t/// chunks were built with, keyed by segment.\r\n\t/// </summary>\r\n\tprivate readonly record struct RebuildJob(int SegmentId, int NodeId, RoadChunkKind Kind, long GroupSignature, ChunkPlan Plan, RoadCurveTable Table, RoadWidth Width, RoadProfile Profile, RoadJunctionLayout Junction, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> ArmBlends, Dictionary<int, SegmentBorderTapers> ArmTapers, RoadLot Lot);\r\n\r\n\tprivate bool m_IsDirty;\r\n\tprivate readonly Queue<RebuildJob> m_Jobs = new();\r\n\tprivate readonly Dictionary<int, PendingGroup> m_PendingSegments = new();\r\n\tprivate readonly Dictionary<(int NodeId, RoadChunkKind Kind), PendingGroup> m_PendingJunctions = new();\r\n\r\n\t/// <summary>\r\n\t/// Junction layouts keyed by node, each with a hash of what it was computed from. Finding corners on curved\r\n\t/// edges is the one expensive step of a reconcile, and while a node is dragged only its own junction and its\r\n\t/// neighbours' change.\r\n\t/// </summary>\r\n\tprivate readonly Dictionary<int, (long Key, RoadJunctionLayout Layout)> m_JunctionCache = new();\r\n\r\n\t/// <summary>\r\n\t/// True while pieces are still queued for (re)generation.\r\n\t/// </summary>\r\n\tpublic bool IsRebuilding => m_Jobs.Count > 0;\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Asks for a reconcile pass on the next editor frame: the graph is compared against the pieces that exist and\r\n\t/// only what differs gets queued for regeneration.\r\n\t/// </summary>\r\n\tpublic void MarkDirty()\r\n\t{\r\n\t\tm_IsDirty = true;\r\n\r\n\t\t// Undo, reloads and the inspector all land here with a graph nobody told us about piece by piece.\r\n\t\tInvalidateIndex();\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Asks the network in the editor's scene for a reconcile. Editing a road type, a bridge, a prop set or a\r\n\t/// parking layout changes roads that hold no copy of it, so the asset tells the network rather than the other\r\n\t/// way round.\r\n\t/// </summary>\r\n\tpublic static void Editor_MarkDirty()\r\n\t{\r\n\t\tvar scene = Application.Editor?.Scene;\r\n\r\n\t\tif (!scene.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tvar network = scene.Get<RoadNetwork>();\r\n\r\n\t\tif (!network.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tnetwork.MarkDirty();\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// One reconcile pass, then as much of the queued work as this frame's budget allows. Driven from the\r\n\t/// component's own update in RoadNetwork.cs.\r\n\t/// </summary>\r\n\tprivate void Tick()\r\n\t{\r\n\t\tif (m_IsDirty)\r\n\t\t{\r\n\t\t\tm_IsDirty = false;\r\n\t\t\tReconcile();\r\n\t\t}\r\n\r\n\t\tProcessJobs();\r\n\t}\r\n\r\n\r\n\r\n\tpublic IEnumerable<RoadChunk> GetChunks()\r\n\t{\r\n\t\tforeach (var child in GameObject.Children)\r\n\t\t{\r\n\t\t\tif (!child.IsValid() || child.IsDestroyed)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar chunk = child.Components.Get<RoadChunk>();\r\n\r\n\t\t\tif (chunk is not null)\r\n\t\t\t\tyield return chunk;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate RoadChunk FindChunk(int _SegmentId, int _ChunkIndex, RoadChunkKind _Kind)\r\n\t{\r\n\t\treturn GetChunks().FirstOrDefault(chunk => !chunk.IsJunction && chunk.SegmentId == _SegmentId && chunk.ChunkIndex == _ChunkIndex && chunk.Kind == _Kind);\r\n\t}\r\n\r\n\r\n\r\n\tprivate RoadChunk FindJunctionChunk(int _NodeId, RoadChunkKind _Kind)\r\n\t{\r\n\t\treturn GetChunks().FirstOrDefault(chunk => chunk.IsJunction && chunk.NodeId == _NodeId && chunk.Kind == _Kind);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Compares the graph with the pieces under this GameObject and queues work for everything that is missing,\r\n\t/// stale or built from different settings. Stateless on purpose: it copes with undo, reloads, hand deleted\r\n\t/// pieces and hot reloads alike, because the pieces themselves carry what they were built from.\r\n\t/// </summary>\r\n\tprivate void Reconcile()\r\n\t{\r\n\t\tPruneCurveTables();\r\n\r\n\t\t// A lot's entrances belong to its edge, wherever the lot has been moved to since.\r\n\t\tSnapAllOpenings();\r\n\r\n\t\tvar segmentChunks = new Dictionary<int, List<RoadChunk>>();\r\n\t\tvar junctionChunks = new Dictionary<(int NodeId, RoadChunkKind Kind), RoadChunk>();\r\n\t\tvar lotChunks = new Dictionary<(int LotId, RoadChunkKind Kind), RoadChunk>();\r\n\r\n\t\tforeach (var chunk in GetChunks())\r\n\t\t{\r\n\t\t\tif (chunk.IsLot)\r\n\t\t\t{\r\n\t\t\t\tif (lotChunks.TryGetValue((chunk.LotId, chunk.Kind), out var duplicate))\r\n\t\t\t\t\tduplicate.GameObject.Destroy();\r\n\r\n\t\t\t\tlotChunks[(chunk.LotId, chunk.Kind)] = chunk;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (chunk.IsJunction)\r\n\t\t\t{\r\n\t\t\t\t// Two pieces for one junction can only be leftovers; keep one, the reconcile sorts out whether it is current.\r\n\t\t\t\tif (junctionChunks.TryGetValue((chunk.NodeId, chunk.Kind), out var duplicate))\r\n\t\t\t\t\tduplicate.GameObject.Destroy();\r\n\r\n\t\t\t\tjunctionChunks[(chunk.NodeId, chunk.Kind)] = chunk;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (!segmentChunks.TryGetValue(chunk.SegmentId, out var list))\r\n\t\t\t{\r\n\t\t\t\tlist = new List<RoadChunk>();\r\n\t\t\t\tsegmentChunks[chunk.SegmentId] = list;\r\n\t\t\t}\r\n\r\n\t\t\tlist.Add(chunk);\r\n\t\t}\r\n\r\n\t\tvar layouts = ComputeJunctionLayouts();\r\n\t\tvar dashPhases = ComputeDashPhases();\r\n\t\tvar liveSegments = new HashSet<int>();\r\n\t\tvar segmentBlends = new Dictionary<int, RoadBorderBuilder.SegmentBorderBlends>();\r\n\t\tvar segmentTapers = new Dictionary<int, SegmentBorderTapers>();\r\n\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\tvar table = GetCurveTable(segment);\r\n\r\n\t\t\tif (table is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tliveSegments.Add(segment.Id);\r\n\r\n\t\t\tvar profile = ProfileOf(segment);\r\n\t\t\t// A road opening into a parking lot starts where the lot's surface stops covering it.\r\n\t\t\tfloat trimStart = MathF.Max(layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id)?.Trim ?? 0.0f, LotTrimAt(segment.StartNodeId));\r\n\t\t\tfloat trimEnd = MathF.Max(layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id)?.Trim ?? 0.0f, LotTrimAt(segment.EndNodeId));\r\n\t\t\tvar caps = ComputeBorderCaps(segment, layouts);\r\n\t\t\tvar blends = ComputeBorderBlends(segment, table, layouts);\r\n\t\t\tvar taper = ComputeBorderTaper(segment, table, layouts, trimStart, trimEnd);\r\n\t\t\tvar crossings = ComputeCrossings(segment, table, layouts, trimStart, trimEnd);\r\n\t\t\tvar lines = ComputeLineRange(table, profile, trimStart, trimEnd, crossings);\r\n\t\t\tvar dash = dashPhases.GetValueOrDefault(segment.Id);\r\n\r\n\t\t\tvar plans = PlanChunks(segment, table, trimStart, trimEnd, profile, caps, blends, taper, ComputeSurfaceSignature(segment, table.Curve, profile), lines, dash, crossings);\r\n\t\t\tlong groupSignature = CombineSignatures(plans);\r\n\r\n\t\t\tsegmentBlends[segment.Id] = blends;\r\n\t\t\tsegmentTapers[segment.Id] = taper;\r\n\t\t\tsegmentChunks.TryGetValue(segment.Id, out var existing);\r\n\r\n\t\t\tif (IsSegmentUpToDate(existing, plans))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Already queued with these exact inputs: let those jobs finish rather than doubling them.\r\n\t\t\tif (m_PendingSegments.TryGetValue(segment.Id, out var pending) && pending.Signature == groupSignature)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tEnqueueSegment(segment, table, profile, plans, groupSignature, existing);\r\n\t\t}\r\n\r\n\t\tforeach (var (nodeId, layout) in layouts)\r\n\t\t{\r\n\t\t\tReconcileJunctionPiece(nodeId, layout, RoadChunkKind.Surface, ComputeJunctionSurfaceSignature(layout), junctionChunks, null);\r\n\r\n\t\t\tbool anyBorder = layout.Arms.Any(arm => ProfileOf(arm.Segment).HasBorder);\r\n\r\n\t\t\tif (anyBorder)\r\n\t\t\t{\r\n\t\t\t\t// The junction's border runs continue the arms' chunks, so they take the same lifts and tapers those\r\n\t\t\t\t// were built with.\r\n\t\t\t\tvar armBlends = new Dictionary<int, RoadBorderBuilder.SegmentBorderBlends>();\r\n\t\t\t\tvar armTapers = new Dictionary<int, SegmentBorderTapers>();\r\n\r\n\t\t\t\tforeach (var arm in layout.Arms)\r\n\t\t\t\t{\r\n\t\t\t\t\tarmBlends[arm.Segment.Id] = segmentBlends.GetValueOrDefault(arm.Segment.Id);\r\n\t\t\t\t\tarmTapers[arm.Segment.Id] = segmentTapers.GetValueOrDefault(arm.Segment.Id);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tReconcileJunctionPiece(nodeId, layout, RoadChunkKind.Border, ComputeJunctionBorderSignature(layout, armBlends, armTapers), junctionChunks, armBlends, armTapers);\r\n\t\t\t}\r\n\t\t\telse if (junctionChunks.TryGetValue((nodeId, RoadChunkKind.Border), out var border))\r\n\t\t\t{\r\n\t\t\t\tborder.GameObject.Destroy();\r\n\t\t\t}\r\n\r\n\t\t\t// Crossings used to be decals hung on the junction; the roads paint their own now, so anything left\r\n\t\t\t// here from then goes.\r\n\t\t\tif (junctionChunks.TryGetValue((nodeId, RoadChunkKind.Marking), out var marking))\r\n\t\t\t\tmarking.GameObject.Destroy();\r\n\t\t}\r\n\r\n\t\tReconcileLots(lotChunks);\r\n\r\n\t\t// Whatever no longer belongs to anything in the graph goes, locked or not.\r\n\t\tforeach (var (segmentId, chunks) in segmentChunks)\r\n\t\t{\r\n\t\t\tif (liveSegments.Contains(segmentId))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tforeach (var chunk in chunks)\r\n\t\t\t\tchunk.GameObject.Destroy();\r\n\r\n\t\t\tm_PendingSegments.Remove(segmentId);\r\n\t\t}\r\n\r\n\t\tforeach (var (key, chunk) in junctionChunks)\r\n\t\t{\r\n\t\t\tif (layouts.ContainsKey(key.NodeId))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tchunk.GameObject.Destroy();\r\n\t\t\tm_PendingJunctions.Remove(key);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void ReconcileJunctionPiece(int _NodeId, RoadJunctionLayout _Layout, RoadChunkKind _Kind, long _Signature, Dictionary<(int NodeId, RoadChunkKind Kind), RoadChunk> _Existing, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> _ArmBlends, Dictionary<int, SegmentBorderTapers> _ArmTapers = null)\r\n\t{\r\n\t\tvar key = (_NodeId, _Kind);\r\n\r\n\t\tif (_Existing.TryGetValue(key, out var existing) && (existing.Locked || (existing.Signature == _Signature && !LacksDebugRecord(existing))))\r\n\t\t\treturn;\r\n\r\n\t\tif (m_PendingJunctions.TryGetValue(key, out var pending) && pending.Signature == _Signature)\r\n\t\t\treturn;\r\n\r\n\t\tm_Jobs.Enqueue(new RebuildJob(-1, _NodeId, _Kind, _Signature, default, null, RoadWidth.Uniform(0.0f), null, _Layout, _ArmBlends, _ArmTapers, null));\r\n\t\tm_PendingJunctions[key] = new PendingGroup(_Signature, 1);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where every segment sits in its run: the stretch of road carried on through pass-through nodes until a\r\n\t/// junction, a road end or a loop. Each run is walked once, from the lowest-numbered segment in it backwards\r\n\t/// to its head and then forwards to its tail, in that segment's direction, so every segment of the run agrees\r\n\t/// on where the run's distance starts and the dashes carry across the nodes.\r\n\t/// </summary>\r\n\tprivate Dictionary<int, RoadMarkingBuilder.DashPhase> ComputeDashPhases()\r\n\t{\r\n\t\tvar phases = new Dictionary<int, RoadMarkingBuilder.DashPhase>();\r\n\r\n\t\tforeach (var seed in Segments.OrderBy(segment => segment.Id))\r\n\t\t{\r\n\t\t\tif (phases.ContainsKey(seed.Id))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar seen = new HashSet<int> { seed.Id };\r\n\t\t\tvar run = new List<(RoadSegment Segment, bool Forward)>();\r\n\r\n\t\t\t// Back to the head: a road carrying on through the node runs with the run when it ends at that node.\r\n\t\t\tvar segment = seed;\r\n\t\t\tint nodeId = seed.StartNodeId;\r\n\r\n\t\t\twhile (RunContinuation(segment, nodeId) is { } previous && seen.Add(previous.Id))\r\n\t\t\t{\r\n\t\t\t\tbool forward = previous.EndNodeId == nodeId;\r\n\r\n\t\t\t\trun.Add((previous, forward));\r\n\t\t\t\tsegment = previous;\r\n\t\t\t\tnodeId = forward ? previous.StartNodeId : previous.EndNodeId;\r\n\t\t\t}\r\n\r\n\t\t\trun.Reverse();\r\n\t\t\trun.Add((seed, true));\r\n\r\n\t\t\t// On to the tail.\r\n\t\t\tsegment = seed;\r\n\t\t\tnodeId = seed.EndNodeId;\r\n\r\n\t\t\twhile (RunContinuation(segment, nodeId) is { } next && seen.Add(next.Id))\r\n\t\t\t{\r\n\t\t\t\tbool forward = next.StartNodeId == nodeId;\r\n\r\n\t\t\t\trun.Add((next, forward));\r\n\t\t\t\tsegment = next;\r\n\t\t\t\tnodeId = forward ? next.EndNodeId : next.StartNodeId;\r\n\t\t\t}\r\n\r\n\t\t\tfloat offset = 0.0f;\r\n\r\n\t\t\tforeach (var (member, forward) in run)\r\n\t\t\t{\r\n\t\t\t\tphases[member.Id] = new RoadMarkingBuilder.DashPhase(offset, !forward);\r\n\t\t\t\toffset += GetCurveTable(member)?.Length ?? 0.0f;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn phases;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The road a segment carries straight on into at a pass-through node, or null at anything else.\r\n\t/// </summary>\r\n\tprivate RoadSegment RunContinuation(RoadSegment _Segment, int _NodeId)\r\n\t{\r\n\t\tif (GetDegree(_NodeId) != 2)\r\n\t\t\treturn null;\r\n\r\n\t\treturn GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// How a segment's border runs out at each end where it meets another road type's: over the profile's transition\r\n\t/// length, towards the cross-section the two share. Without the junction layouts only pass-through nodes are\r\n\t/// known; the reconcile passes them, so junction corners taper too.\r\n\t/// </summary>\r\n\tpublic SegmentBorderTapers GetBorderTaper(RoadSegment _Segment)\r\n\t{\r\n\t\tvar table = GetCurveTable(_Segment);\r\n\r\n\t\treturn table is null ? default : ComputeBorderTaper(_Segment, table);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// One end of a border's taper: how long it is, the width and share of relief it runs to, whether a country road's\r\n\t/// gravel spills over it, and how far from the node it ends.\r\n\t/// </summary>\r\n\tprivate readonly record struct EndTaper(float Length, float Width, float Relief, bool Spill, float Offset);\r\n\r\n\r\n\r\n\tprivate SegmentBorderTapers ComputeBorderTaper(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts = null, float _TrimStart = 0.0f, float _TrimEnd = 0.0f)\r\n\t{\r\n\t\tvar profile = ProfileOf(_Segment);\r\n\r\n\t\tif (!profile.HasBorder)\r\n\t\t\treturn default;\r\n\r\n\t\tfloat limit = _Table.Length * 0.5f;\r\n\t\tRoadBorderTaper left = default;\r\n\t\tRoadBorderTaper right = default;\r\n\r\n\t\t// A transition ends at its trim, so the two ends share the stretch BETWEEN the trims and each gets half of\r\n\t\t// it; however far a junction reaches along the road (a ramp meeting a highway can trim most of it), the two\r\n\t\t// never run into each other and neither is squeezed out.\r\n\t\tfloat transitionLimit = MathF.Max(0.0f, _Table.Length - _TrimStart - _TrimEnd) * 0.5f;\r\n\r\n\t\tif (profile.TransitionLength > 0.0f)\r\n\t\t{\r\n\t\t\tSideTapers(_Segment, profile, _Segment.StartNodeId, true, transitionLimit, _Layouts, ref left, ref right);\r\n\t\t\tSideTapers(_Segment, profile, _Segment.EndNodeId, false, transitionLimit, _Layouts, ref left, ref right);\r\n\t\t}\r\n\r\n\t\t// At a nose, the border on the side facing the road this one parts from grows out of nothing instead.\r\n\t\tif (GoreLength(_Segment, _Table, _Segment.StartNodeId, true, limit) is { } startLeft)\r\n\t\t\tleft = left with { StartLength = startLeft, StartWidth = 0.0f, StartRelief = 0.0f, StartSpill = false, StartGore = true, StartOffset = 0.0f };\r\n\r\n\t\tif (GoreLength(_Segment, _Table, _Segment.StartNodeId, false, limit) is { } startRight)\r\n\t\t\tright = right with { StartLength = startRight, StartWidth = 0.0f, StartRelief = 0.0f, StartSpill = false, StartGore = true, StartOffset = 0.0f };\r\n\r\n\t\tif (GoreLength(_Segment, _Table, _Segment.EndNodeId, true, limit) is { } endLeft)\r\n\t\t\tleft = left with { EndLength = endLeft, EndWidth = 0.0f, EndRelief = 0.0f, EndSpill = false, EndGore = true, EndOffset = 0.0f };\r\n\r\n\t\tif (GoreLength(_Segment, _Table, _Segment.EndNodeId, false, limit) is { } endRight)\r\n\t\t\tright = right with { EndLength = endRight, EndWidth = 0.0f, EndRelief = 0.0f, EndSpill = false, EndGore = true, EndOffset = 0.0f };\r\n\r\n\t\treturn new SegmentBorderTapers(left, right);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The tapers at one end of a segment, one per side, each towards the border across from it there. At a plain\r\n\t/// pass-through node that is the other road on both sides. At a junction it is the road on the far side of each\r\n\t/// corner, and the taper ends at the arm's trim, where the corner starts: the corner itself is then built in the\r\n\t/// one cross-section both its roads have run to, so it is a clean curve rather than one border turning into\r\n\t/// the other round the arc.\r\n\t/// </summary>\r\n\tprivate void SideTapers(RoadSegment _Segment, RoadProfile _Profile, int _NodeId, bool _AtStart, float _Limit, Dictionary<int, RoadJunctionLayout> _Layouts, ref RoadBorderTaper _Left, ref RoadBorderTaper _Right)\r\n\t{\r\n\t\tint degree = GetDegree(_NodeId);\r\n\r\n\t\tif (degree < 2)\r\n\t\t\treturn;\r\n\r\n\t\tRoadSegment leftNeighbour = null;\r\n\t\tRoadSegment rightNeighbour = null;\r\n\t\tfloat trim = 0.0f;\r\n\r\n\t\tif (_Layouts is not null && _Layouts.TryGetValue(_NodeId, out var layout) && layout.FindArm(_Segment.Id) is { } arm)\r\n\t\t{\r\n\t\t\ttrim = arm.Trim;\r\n\r\n\t\t\t// A corner starting on this arm runs on its left looking out of the node, one ending on it on its right;\r\n\t\t\t// looking along a segment that ends at the node swaps the two.\r\n\t\t\tforeach (var path in layout.CornerPaths)\r\n\t\t\t{\r\n\t\t\t\tif (path.ArmA.Segment.Id == _Segment.Id)\r\n\t\t\t\t{\r\n\t\t\t\t\tif (_AtStart)\r\n\t\t\t\t\t\tleftNeighbour = path.ArmB.Segment;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\trightNeighbour = path.ArmB.Segment;\r\n\t\t\t\t}\r\n\t\t\t\telse if (path.ArmB.Segment.Id == _Segment.Id)\r\n\t\t\t\t{\r\n\t\t\t\t\tif (_AtStart)\r\n\t\t\t\t\t\trightNeighbour = path.ArmA.Segment;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tleftNeighbour = path.ArmA.Segment;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if (degree == 2)\r\n\t\t{\r\n\t\t\tleftNeighbour = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);\r\n\t\t\trightNeighbour = leftNeighbour;\r\n\t\t}\r\n\r\n\t\tif (SideTaper(_Profile, leftNeighbour, degree, trim, _Limit, out var leftEnd))\r\n\t\t\t_Left = WithEnd(_Left, _AtStart, leftEnd);\r\n\r\n\t\tif (SideTaper(_Profile, rightNeighbour, degree, trim, _Limit, out var rightEnd))\r\n\t\t\t_Right = WithEnd(_Right, _AtStart, rightEnd);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The taper one side of a border takes towards the border across from it at a node, or false when it takes none:\r\n\t/// nothing across, a border that is already the same size, or \u2014 at a junction \u2014 a border of another kind, which\r\n\t/// a junction still closes off against this one.\r\n\t/// </summary>\r\n\tprivate bool SideTaper(RoadProfile _Profile, RoadSegment _Neighbour, int _Degree, float _Trim, float _Limit, out EndTaper _Taper)\r\n\t{\r\n\t\t_Taper = default;\r\n\r\n\t\tif (_Neighbour is null)\r\n\t\t\treturn false;\r\n\r\n\t\tvar other = ProfileOf(_Neighbour);\r\n\t\tbool sameKind = other.Border == _Profile.Border;\r\n\r\n\t\tif (_Degree > 2 && !sameKind)\r\n\t\t\treturn false;\r\n\r\n\t\tif (sameKind && MathF.Abs(other.FlatBorderWidth - _Profile.FlatBorderWidth) < 0.01f && MathF.Abs(other.ReliefHeight - _Profile.ReliefHeight) < 0.01f)\r\n\t\t\treturn false;\r\n\r\n\t\t// Held to its half of the stretch between the trims, so it never runs into the taper at the far end.\r\n\t\tfloat length = MathF.Min(_Profile.TransitionLength, _Limit);\r\n\r\n\t\tif (length <= 0.01f)\r\n\t\t\treturn false;\r\n\r\n\t\t_Taper = new EndTaper(length, RoadProfile.MeetingWidth(_Profile, other), RoadProfile.MeetingRelief(_Profile, other), !sameKind && other.Border == RoadBorderStyle.CountrySide, _Trim);\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate static RoadBorderTaper WithEnd(in RoadBorderTaper _Taper, bool _AtStart, in EndTaper _End)\r\n\t{\r\n\t\treturn _AtStart\r\n\t\t\t? _Taper with { StartLength = _End.Length, StartWidth = _End.Width, StartRelief = _End.Relief, StartSpill = _End.Spill, StartOffset = _End.Offset }\r\n\t\t\t: _Taper with { EndLength = _End.Length, EndWidth = _End.Width, EndRelief = _End.Relief, EndSpill = _End.Spill, EndOffset = _End.Offset };\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// How far from a node the gap between this segment's edge on one side (its own left or right) and the edge of\r\n\t/// another road parting from the same point has opened enough for both roads' borders to fit side by side; null\r\n\t/// when no road parts from that edge there. Sampled along both roads at equal distances from the node, which\r\n\t/// is how two roads leaving a common tangent separate.\r\n\t/// </summary>\r\n\tprivate float? GoreLength(RoadSegment _Segment, RoadCurveTable _Table, int _NodeId, bool _Left, float _Limit)\r\n\t{\r\n\t\tconst float step = 25.0f;\r\n\r\n\t\tif (GetDegree(_NodeId) < 3)\r\n\t\t\treturn null;\r\n\r\n\t\tbool atStart = _Segment.StartNodeId == _NodeId;\r\n\t\tfloat own = ProfileOf(_Segment).FlatBorderWidth;\r\n\t\tvar width = new RoadWidth(_Segment, _Table.Length);\r\n\r\n\t\t// This segment's edge point at a distance from the node, on the side asked for.\r\n\t\tVector3 OwnEdge(float _FromNode)\r\n\t\t{\r\n\t\t\tfloat distance = atStart ? _FromNode : _Table.Length - _FromNode;\r\n\r\n\t\t\tRoadSurfaceBuilder.CrossSection(_Table, distance, width, 0.0f, out Vector3 leftEdge, out Vector3 rightEdge);\r\n\r\n\t\t\treturn _Left ? leftEdge : rightEdge;\r\n\t\t}\r\n\r\n\t\tVector3 start = OwnEdge(0.0f);\r\n\r\n\t\tforeach (var other in GetSegmentsAt(_NodeId))\r\n\t\t{\r\n\t\t\tif (other.Id == _Segment.Id)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar otherTable = GetCurveTable(other);\r\n\r\n\t\t\tif (otherTable is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tbool otherAtStart = other.StartNodeId == _NodeId;\r\n\t\t\tvar otherWidth = new RoadWidth(other, otherTable.Length);\r\n\r\n\t\t\tVector3 OtherEdge(float _FromNode, bool _OtherLeft)\r\n\t\t\t{\r\n\t\t\t\tfloat distance = otherAtStart ? _FromNode : otherTable.Length - _FromNode;\r\n\r\n\t\t\t\tRoadSurfaceBuilder.CrossSection(otherTable, distance, otherWidth, 0.0f, out Vector3 leftEdge, out Vector3 rightEdge);\r\n\r\n\t\t\t\treturn _OtherLeft ? leftEdge : rightEdge;\r\n\t\t\t}\r\n\r\n\t\t\t// The other road's edge that starts on ours, if either does.\r\n\t\t\tbool otherLeft;\r\n\r\n\t\t\tif (OtherEdge(0.0f, true).Distance(start) < 1.0f)\r\n\t\t\t\totherLeft = true;\r\n\t\t\telse if (OtherEdge(0.0f, false).Distance(start) < 1.0f)\r\n\t\t\t\totherLeft = false;\r\n\t\t\telse\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Parting means leaving the node the same way; a road turning off at an angle is a corner, not a gore.\r\n\t\t\tVector3 ownWay = OwnEdge(step) - start;\r\n\t\t\tVector3 otherWay = OtherEdge(step, otherLeft) - start;\r\n\r\n\t\t\tif (ownWay.IsNearZeroLength || otherWay.IsNearZeroLength || Vector3.Dot(ownWay.Normal, otherWay.Normal) < 0.7f)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tfloat needed = own + ProfileOf(other).FlatBorderWidth;\r\n\t\t\tfloat reach = MathF.Min(_Limit, MathF.Min(_Table.Length, otherTable.Length));\r\n\r\n\t\t\tfor (float fromNode = step; fromNode <= reach; fromNode += step)\r\n\t\t\t{\r\n\t\t\t\tif (OwnEdge(fromNode).Distance(OtherEdge(fromNode, otherLeft)) >= needed)\r\n\t\t\t\t\treturn MathF.Max(step, fromNode);\r\n\t\t\t}\r\n\r\n\t\t\treturn MathF.Max(step, reach);\r\n\t\t}\r\n\r\n\t\treturn null;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Whether two road types' borders come to one cross-section where they meet, so nothing needs closing off\r\n\t/// between them: of a kind, and both tapering into the width and height they share.\r\n\t/// </summary>\r\n\tprivate static bool MeetsFlush(RoadProfile _A, RoadProfile _B)\r\n\t{\r\n\t\treturn _A.HasBorder && _A.Border == _B.Border && _A.TransitionLength > 0.0f && _B.TransitionLength > 0.0f;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The filled area of every junction, in network space (lifted to the road surface): its centre and its\r\n\t/// outline, which the junction surface fans between, plus how far a pavement reaches out from the outline\r\n\t/// round it and how far above the outline that pavement's top sits (nothing when no arm has a sidewalk). For\r\n\t/// tools that need to know where road is.\r\n\t/// </summary>\r\n\tpublic IEnumerable<(Vector3 Centre, IReadOnlyList<Vector3> Polygon, float PavementReach, float PavementTop)> GetJunctionPolygons()\r\n\t{\r\n\t\t// A parking lot is a junction as far as the ground under it is concerned.\r\n\t\tforeach (var lot in Lots)\r\n\t\t{\r\n\t\t\tvar profile = ProfileOf(lot);\r\n\t\t\tbool pavement = profile.Border == RoadBorderStyle.Sidewalk;\r\n\r\n\t\t\tyield return (lot.Centre, RoadLotOutline.Build(lot, OpeningsOf(lot)).Points, pavement ? profile.BorderWidth : 0.0f, pavement ? profile.CurbHeight : 0.0f);\r\n\t\t}\r\n\r\n\t\t// A rounded dead end reaches past its node, where no road piece covers the ground.\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\tvar table = GetCurveTable(segment);\r\n\r\n\t\t\tif (table is null || segment.Bridge is not null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar profile = ProfileOf(segment);\r\n\t\t\tbool pavement = profile.Border == RoadBorderStyle.Sidewalk;\r\n\t\t\tvar width = new RoadWidth(segment, table.Length);\r\n\r\n\t\t\tforeach (bool atStart in new[] { true, false })\r\n\t\t\t{\r\n\t\t\t\tint nodeId = atStart ? segment.StartNodeId : segment.EndNodeId;\r\n\r\n\t\t\t\tif (GetDegree(nodeId) != 1 || LotAtOpening(nodeId) is not null)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar outline = RoadCapBuilder.Outline(table, atStart ? 0.0f : table.Length, atStart, width, profile, SurfaceOffset, out Vector3 centre);\r\n\r\n\t\t\t\tif (outline.Count >= 3)\r\n\t\t\t\t\tyield return (centre, outline, pavement ? profile.BorderWidth : 0.0f, pavement ? profile.CurbHeight : 0.0f);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach (var layout in ComputeJunctionLayouts().Values)\r\n\t\t{\r\n\t\t\tif (layout.Polygon.Count < 3)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tfloat reach = 0.0f;\r\n\t\t\tfloat top = 0.0f;\r\n\r\n\t\t\tforeach (var arm in layout.Arms)\r\n\t\t\t{\r\n\t\t\t\tvar profile = ProfileOf(arm.Segment);\r\n\r\n\t\t\t\tif (profile.Border != RoadBorderStyle.Sidewalk)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\treach = MathF.Max(reach, profile.BorderWidth);\r\n\t\t\t\ttop = MathF.Max(top, profile.CurbHeight);\r\n\t\t\t}\r\n\r\n\t\t\tyield return (layout.Centre, layout.Polygon, reach, top);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Lays out every junction. A road too short for the fillets at both its ends gets those fillets shrunk until\r\n\t/// their tangent points fit on it (a trim past the tangent point would send the corner geometry back over the\r\n\t/// road); only corners that cannot fit even sharp are clamped. Then the junction polygons are built.\r\n\t/// </summary>\r\n\tprivate Dictionary<int, RoadJunctionLayout> ComputeJunctionLayouts()\r\n\t{\r\n\t\tvar radii = Nodes.ToDictionary(node => node.Id, node => MathF.Max(0.0f, node.CornerRadius));\r\n\t\tvar layouts = BuildJunctionLayouts(radii);\r\n\t\tvar shrink = new Dictionary<int, float>();\r\n\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\tvar table = GetCurveTable(segment);\r\n\r\n\t\t\tif (table is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar startArm = layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id);\r\n\t\t\tvar endArm = layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id);\r\n\t\t\tfloat sharp = (startArm?.SharpTrim ?? 0.0f) + (endArm?.SharpTrim ?? 0.0f);\r\n\t\t\tfloat fillet = (startArm is null ? 0.0f : startArm.RawTrim - startArm.SharpTrim) + (endArm is null ? 0.0f : endArm.RawTrim - endArm.SharpTrim);\r\n\t\t\tfloat available = MathF.Max(0.0f, table.Length - MinChunkLength);\r\n\r\n\t\t\tif (fillet <= 0.0f || sharp + fillet <= available)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// The fillet's share of a trim is proportional to the radius, so this factor makes it fit exactly.\r\n\t\t\tfloat factor = Math.Clamp((available - sharp) / fillet, 0.0f, 1.0f);\r\n\r\n\t\t\tforeach (int nodeId in new[] { segment.StartNodeId, segment.EndNodeId })\r\n\t\t\t{\r\n\t\t\t\tif (layouts.ContainsKey(nodeId))\r\n\t\t\t\t\tshrink[nodeId] = MathF.Min(shrink.GetValueOrDefault(nodeId, 1.0f), factor);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (shrink.Count > 0)\r\n\t\t{\r\n\t\t\tforeach (var (nodeId, factor) in shrink)\r\n\t\t\t\tradii[nodeId] *= factor;\r\n\r\n\t\t\tlayouts = BuildJunctionLayouts(radii);\r\n\t\t}\r\n\r\n\t\t// Last resort, for corners that do not fit even when sharp: share what road there is.\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\tvar table = GetCurveTable(segment);\r\n\r\n\t\t\tif (table is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar startArm = layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id);\r\n\t\t\tvar endArm = layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id);\r\n\t\t\tfloat total = (startArm?.Trim ?? 0.0f) + (endArm?.Trim ?? 0.0f);\r\n\t\t\tfloat available = MathF.Max(0.0f, table.Length - MinChunkLength);\r\n\r\n\t\t\tif (total <= available || total <= 0.0f)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tfloat scale = available / total;\r\n\r\n\t\t\tif (startArm is not null)\r\n\t\t\t\tstartArm.Trim *= scale;\r\n\r\n\t\t\tif (endArm is not null)\r\n\t\t\t\tendArm.Trim *= scale;\r\n\t\t}\r\n\r\n\t\tforeach (var layout in layouts.Values)\r\n\t\t\tlayout.BuildPolygon();\r\n\r\n\t\treturn layouts;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The corner fold on one side of a segment at one of its nodes, and whether the segment is that fold's first\r\n\t/// arm (its left edge) or second (its right edge). Looking out of the node, a segment starting there has its\r\n\t/// left on the arm's left; one ending there has it on the arm's right.\r\n\t/// </summary>\r\n\tprivate static RoadJunctionLayout.CornerFold FindFold(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts, out bool _SideA)\r\n\t{\r\n\t\t_SideA = _AtStart == _Left;\r\n\r\n\t\tif (!_Layouts.TryGetValue(_NodeId, out var layout))\r\n\t\t\treturn null;\r\n\r\n\t\tvar path = _SideA\r\n\t\t\t? layout.CornerPaths.FirstOrDefault(candidate => candidate.ArmA.Segment.Id == _Segment.Id)\r\n\t\t\t: layout.CornerPaths.FirstOrDefault(candidate => candidate.ArmB.Segment.Id == _Segment.Id);\r\n\r\n\t\treturn path?.Fold;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The junction layout of every node for the given fillet radii, from the cache where the inputs match.\r\n\t/// </summary>\r\n\tprivate Dictionary<int, RoadJunctionLayout> BuildJunctionLayouts(Dictionary<int, float> _Radii)\r\n\t{\r\n\t\tvar layouts = new Dictionary<int, RoadJunctionLayout>();\r\n\t\tvar liveNodes = new HashSet<int>();\r\n\r\n\t\tforeach (var node in Nodes)\r\n\t\t{\r\n\t\t\tliveNodes.Add(node.Id);\r\n\r\n\t\t\tfloat radius = _Radii[node.Id];\r\n\t\t\tlong key = ComputeJunctionKey(node, radius);\r\n\t\t\tRoadJunctionLayout layout;\r\n\r\n\t\t\t// A cached layout is only as good as its references: undo and reloads swap the graph's objects.\r\n\t\t\tif (m_JunctionCache.TryGetValue(node.Id, out var cached) && cached.Key == key && (cached.Layout is null || cached.Layout.Refresh(this)))\r\n\t\t\t{\r\n\t\t\t\tlayout = cached.Layout;\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tlayout = RoadJunctionLayout.Create(this, node, SurfaceOffset, radius);\r\n\t\t\t\tm_JunctionCache[node.Id] = (key, layout);\r\n\t\t\t}\r\n\r\n\t\t\tif (layout is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tlayout.ResetTrims();\r\n\t\t\tlayouts[node.Id] = layout;\r\n\t\t}\r\n\r\n\t\tforeach (int nodeId in m_JunctionCache.Keys.Where(id => !liveNodes.Contains(id)).ToList())\r\n\t\t\tm_JunctionCache.Remove(nodeId);\r\n\r\n\t\treturn layouts;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Whether an end of a road asks for a pedestrian crossing: the road type has them, the road has not turned\r\n\t/// this end's off, and the end is a real junction (three roads or more; a bend has a layout too, but nobody\r\n\t/// crosses at a bend) that trims the road.\r\n\t/// </summary>\r\n\tprivate bool WantsCrossing(RoadSegment _Segment, int _NodeId, Dictionary<int, RoadJunctionLayout> _Layouts, float _Trim)\r\n\t{\r\n\t\tif (!ProfileOf(_Segment).Crossings || !_Segment.CrossingAt(_NodeId))\r\n\t\t\treturn false;\r\n\r\n\t\treturn _Layouts.TryGetValue(_NodeId, out var layout) && layout.Arms.Count >= 3 && _Trim > 0.01f;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Places a road's crossings on the stretch left between its junctions. Two crossings need room for both and\r\n\t/// a gap between; a shorter road between two junctions gets one crossing shared by both, halfway along; a road\r\n\t/// shorter than a crossing gets none.\r\n\t/// </summary>\r\n\tprivate SegmentCrossings ComputeCrossings(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts, float _TrimStart, float _TrimEnd)\r\n\t{\r\n\t\tvar profile = ProfileOf(_Segment);\r\n\t\tfloat depth = profile.CrossingDepth;\r\n\t\tfloat clear = _Table.Length - _TrimStart - _TrimEnd;\r\n\t\tbool wantStart = WantsCrossing(_Segment, _Segment.StartNodeId, _Layouts, _TrimStart);\r\n\t\tbool wantEnd = WantsCrossing(_Segment, _Segment.EndNodeId, _Layouts, _TrimEnd);\r\n\r\n\t\tif (clear < depth || (!wantStart && !wantEnd))\r\n\t\t\treturn default;\r\n\r\n\t\t// A ladder of stripes is laid on the road outside the junction; two lines mark the walk across the\r\n\t\t// junction's own mouth, so that kind is pulled in far enough to put its outer line on the pavement's\r\n\t\t// corner instead of a crossing's depth short of it.\r\n\t\tbool inside = profile.CrossingStyle == RoadCrossingStyle.Lines;\r\n\t\tfloat startCentre = inside\r\n\t\t\t? MathF.Max(depth * 0.5f, _TrimStart - depth * 0.5f)\r\n\t\t\t: _TrimStart + depth * 0.5f;\r\n\r\n\t\tfloat endCentre = inside\r\n\t\t\t? MathF.Min(_Table.Length - depth * 0.5f, _Table.Length - _TrimEnd + depth * 0.5f)\r\n\t\t\t: _Table.Length - _TrimEnd - depth * 0.5f;\r\n\r\n\t\tif (wantStart && wantEnd)\r\n\t\t{\r\n\t\t\tif (clear >= 2.0f * depth + profile.CrossingClearance)\r\n\t\t\t\treturn new SegmentCrossings(true, startCentre, true, endCentre);\r\n\r\n\t\t\treturn new SegmentCrossings(true, _TrimStart + clear * 0.5f, false, 0.0f);\r\n\t\t}\r\n\r\n\t\treturn wantStart\r\n\t\t\t? new SegmentCrossings(true, startCentre, false, 0.0f)\r\n\t\t\t: new SegmentCrossings(false, 0.0f, true, endCentre);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where the painted lines run: the stretch between the junctions, kept clear of the crossings.\r\n\t/// </summary>\r\n\tprivate static LineRange ComputeLineRange(RoadCurveTable _Table, RoadProfile _Profile, float _TrimStart, float _TrimEnd, in SegmentCrossings _Crossings)\r\n\t{\r\n\t\tfloat reach = _Profile.CrossingDepth * 0.5f + _Profile.CrossingClearance;\r\n\t\tfloat start = _TrimStart;\r\n\t\tfloat end = _Table.Length - _TrimEnd;\r\n\t\tfloat gapStart = 0.0f;\r\n\t\tfloat gapEnd = 0.0f;\r\n\r\n\t\tif (_Crossings.AtEnd)\r\n\t\t\tend = MathF.Min(end, _Crossings.EndCentre - reach);\r\n\r\n\t\tif (_Crossings.AtStart)\r\n\t\t{\r\n\t\t\tfloat before = _Crossings.StartCentre - reach;\r\n\t\t\tfloat after = _Crossings.StartCentre + reach;\r\n\r\n\t\t\t// A crossing at the very start just pushes the lines back; one further along cuts a gap in them.\r\n\t\t\tif (before <= start + 1.0f)\r\n\t\t\t{\r\n\t\t\t\tstart = MathF.Max(start, after);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tgapStart = before;\r\n\t\t\t\tgapEnd = after;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn new LineRange(start, end, gapStart, gapEnd);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Which ends of a segment's borders nothing continues: a road end, a side of a junction with no corner on it,\r\n\t/// or a pass-through node where the road changes type or width.\r\n\t/// </summary>\r\n\tprivate BorderCaps ComputeBorderCaps(RoadSegment _Segment, Dictionary<int, RoadJunctionLayout> _Layouts)\r\n\t{\r\n\t\tvar caps = BorderCaps.None;\r\n\r\n\t\tif (IsBorderOpen(_Segment, _Segment.StartNodeId, true, _Layouts, out bool startLeft, out bool startRight))\r\n\t\t{\r\n\t\t\tcaps |= startLeft ? BorderCaps.StartLeft : BorderCaps.None;\r\n\t\t\tcaps |= startRight ? BorderCaps.StartRight : BorderCaps.None;\r\n\t\t}\r\n\r\n\t\tif (IsBorderOpen(_Segment, _Segment.EndNodeId, false, _Layouts, out bool endLeft, out bool endRight))\r\n\t\t{\r\n\t\t\tcaps |= endLeft ? BorderCaps.EndLeft : BorderCaps.None;\r\n\t\t\tcaps |= endRight ? BorderCaps.EndRight : BorderCaps.None;\r\n\t\t}\r\n\r\n\t\treturn caps;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// A bridge segment's built range and ends. An end is closed off unless it is a pass-through node onto\r\n\t/// another bridge of the same style, width and border, where the two decks meet flush; there, only one of the\r\n\t/// two segments provides the node's railing post: the one leaving the node, or the lower id when both leave\r\n\t/// or both arrive.\r\n\t/// </summary>\r\n\tprivate RoadBridgeBuilder.BridgeSpan ComputeBridgeSpan(RoadSegment _Segment, float _RangeStart, float _RangeEnd)\r\n\t{\r\n\t\tvar startNeighbour = DeckContinuation(_Segment, _Segment.StartNodeId);\r\n\t\tvar endNeighbour = DeckContinuation(_Segment, _Segment.EndNodeId);\r\n\r\n\t\tbool postStart = startNeighbour is null || startNeighbour.EndNodeId == _Segment.StartNodeId || _Segment.Id < startNeighbour.Id;\r\n\t\tbool postEnd = endNeighbour is null || (endNeighbour.EndNodeId == _Segment.EndNodeId && _Segment.Id < endNeighbour.Id);\r\n\r\n\t\treturn new RoadBridgeBuilder.BridgeSpan(_RangeStart, _RangeEnd, startNeighbour is null, postStart, endNeighbour is null, postEnd);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The bridge segment the deck carries straight on to at a node, or null where the deck ends.\r\n\t/// </summary>\r\n\tprivate RoadSegment DeckContinuation(RoadSegment _Segment, int _NodeId)\r\n\t{\r\n\t\tif (GetDegree(_NodeId) != 2)\r\n\t\t\treturn null;\r\n\r\n\t\tvar other = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);\r\n\r\n\t\tif (other?.Bridge is null || other.Bridge.ResourcePath != _Segment.Bridge.ResourcePath || !SameEdgesAt(_Segment, other, _NodeId))\r\n\t\t\treturn null;\r\n\r\n\t\tbool sameExtent = MathF.Abs(RoadBridgeBuilder.DeckExtent(ProfileOf(other), other.Bridge) - RoadBridgeBuilder.DeckExtent(ProfileOf(_Segment), _Segment.Bridge)) < 0.01f;\r\n\r\n\t\treturn sameExtent ? other : null;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The lift lines of a segment's two borders: at each end, the corner's height difference on that side (or\r\n\t/// nothing), run linearly along the road between the two.\r\n\t/// </summary>\r\n\tprivate RoadBorderBuilder.SegmentBorderBlends ComputeBorderBlends(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts)\r\n\t{\r\n\t\treturn new RoadBorderBuilder.SegmentBorderBlends(SideBlendFor(_Segment, _Table, true, _Layouts), SideBlendFor(_Segment, _Table, false, _Layouts));\r\n\t}\r\n\r\n\r\n\r\n\tprivate RoadBorderBuilder.SideBlend SideBlendFor(RoadSegment _Segment, RoadCurveTable _Table, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts)\r\n\t{\r\n\t\tbool hasStart = CornerLift(_Segment, _Segment.StartNodeId, true, _Left, _Layouts, out float startDistance, out float startDelta);\r\n\t\tbool hasEnd = CornerLift(_Segment, _Segment.EndNodeId, false, _Left, _Layouts, out float endDistance, out float endDelta);\r\n\r\n\t\tif (!hasStart && !hasEnd)\r\n\t\t\treturn default;\r\n\r\n\t\t// An end with no corner on this side holds the border at road height from where the border actually starts,\r\n\t\t// which is past the trim: a road opening into a lot stops at the lot's flares, and a blend still running\r\n\t\t// there hands the lot a pavement tilted by whatever it had left to shed while the lot's own is flat.\r\n\t\tif (!hasStart)\r\n\t\t{\r\n\t\t\tstartDistance = LotTrimAt(_Segment.StartNodeId);\r\n\t\t\tstartDelta = 0.0f;\r\n\t\t}\r\n\r\n\t\tif (!hasEnd)\r\n\t\t{\r\n\t\t\tendDistance = _Table.Length - LotTrimAt(_Segment.EndNodeId);\r\n\t\t\tendDelta = 0.0f;\r\n\t\t}\r\n\r\n\t\treturn new RoadBorderBuilder.SideBlend(true, startDistance, startDelta, endDistance, endDelta);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where along the segment the corner's tangent point on this side is, and how much higher the corner sits\r\n\t/// than the road edge there; false when there is no inside corner on that side at that end.\r\n\t/// </summary>\r\n\tprivate static bool CornerLift(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts, out float _Distance, out float _Delta)\r\n\t{\r\n\t\t_Distance = 0.0f;\r\n\t\t_Delta = 0.0f;\r\n\r\n\t\tvar fold = FindFold(_Segment, _NodeId, _AtStart, _Left, _Layouts, out bool sideA);\r\n\r\n\t\tif (fold is null || !_Layouts.TryGetValue(_NodeId, out var layout))\r\n\t\t\treturn false;\r\n\r\n\t\tvar arm = layout.FindArm(_Segment.Id);\r\n\r\n\t\tif (arm is null)\r\n\t\t\treturn false;\r\n\r\n\t\t_Distance = arm.CurveDistance(sideA ? fold.TangentParamA : fold.TangentParamB);\r\n\t\t_Delta = fold.ApexHeight - (sideA ? fold.TangentHeightA : fold.TangentHeightB);\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsBorderOpen(RoadSegment _Segment, int _NodeId, bool _AtStart, Dictionary<int, RoadJunctionLayout> _Layouts, out bool _Left, out bool _Right)\r\n\t{\r\n\t\tint degree = GetDegree(_NodeId);\r\n\r\n\t\t// A road opening into a lot runs into the lot's own kerb, which closes it off; two caps in one place\r\n\t\t// would only fight each other.\r\n\t\tif (LotAtOpening(_NodeId) is { } lot && ProfileOf(lot).Border == ProfileOf(_Segment).Border)\r\n\t\t{\r\n\t\t\t_Left = false;\r\n\t\t\t_Right = false;\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif (degree <= 1)\r\n\t\t{\r\n\t\t\t// A road rounded off where it stops carries its border round the end; a cap across the road as well\r\n\t\t\t// would sit inside that.\r\n\t\t\tbool rounded = _Segment.Bridge is null && ProfileOf(_Segment).RoundedEnds;\r\n\r\n\t\t\t_Left = !rounded;\r\n\t\t\t_Right = !rounded;\r\n\t\t\treturn !rounded;\r\n\t\t}\r\n\r\n\t\tif (_Layouts.TryGetValue(_NodeId, out var layout))\r\n\t\t{\r\n\t\t\t// A corner path starting on this arm continues its left border (looking out of the node); one ending on\r\n\t\t\t// it continues its right. Looking out of the node along a segment that ends there swaps the sides.\r\n\t\t\tbool armLeftContinued = layout.CornerPaths.Any(path => path.ArmA.Segment.Id == _Segment.Id);\r\n\t\t\tbool armRightContinued = layout.CornerPaths.Any(path => path.ArmB.Segment.Id == _Segment.Id);\r\n\r\n\t\t\t_Left = !(_AtStart ? armLeftContinued : armRightContinued);\r\n\t\t\t_Right = !(_AtStart ? armRightContinued : armLeftContinued);\r\n\t\t\treturn _Left || _Right;\r\n\t\t}\r\n\r\n\t\tif (degree == 2)\r\n\t\t{\r\n\t\t\tvar other = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);\r\n\t\t\tbool continuous = other is not null && SameEdgesAt(_Segment, other, _NodeId)\r\n\t\t\t\t&& (RoadBorderBuilder.SameProfile(ProfileOf(other), ProfileOf(_Segment)) || MeetsFlush(ProfileOf(other), ProfileOf(_Segment)));\r\n\r\n\t\t\t_Left = !continuous;\r\n\t\t\t_Right = !continuous;\r\n\t\t\treturn !continuous;\r\n\t\t}\r\n\r\n\t\t_Left = true;\r\n\t\t_Right = true;\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Splits a segment into chunks at fixed distances from its start, clipped by the junction trims at each end,\r\n\t/// with a border chunk over each stretch when the road type has borders. Anchoring the boundaries to the\r\n\t/// segment rather than to the trimmed length means a junction change at one end only touches the chunk next\r\n\t/// to it.\r\n\t/// </summary>\r\n\tprivate List<ChunkPlan> PlanChunks(RoadSegment _Segment, RoadCurveTable _Table, float _TrimStart, float _TrimEnd, RoadProfile _Profile, BorderCaps _Caps, in RoadBorderBuilder.SegmentBorderBlends _Blends, in SegmentBorderTapers _Taper, long _SurfaceSignature, in LineRange _Lines, in RoadMarkingBuilder.DashPhase _Dash, in SegmentCrossings _Crossings)\r\n\t{\r\n\t\tfloat length = _Table.Length;\r\n\t\tint count = Math.Max(1, (int)MathF.Ceiling(length / ChunkLength));\r\n\t\tfloat usableEnd = length - _TrimEnd;\r\n\t\tvar ranges = new List<(int Index, float Start, float End)>(count);\r\n\r\n\t\tfor (int i = 0; i < count; i++)\r\n\t\t{\r\n\t\t\tfloat start = MathF.Max(length * i / count, _TrimStart);\r\n\t\t\tfloat end = MathF.Min(length * (i + 1) / count, usableEnd);\r\n\r\n\t\t\tif (end - start >= MinChunkLength)\r\n\t\t\t\tranges.Add((i, start, end));\r\n\t\t}\r\n\r\n\t\tvar plans = new List<ChunkPlan>(ranges.Count * 4);\r\n\t\tvar borderHash = StableHash.Create();\r\n\t\tborderHash.Add(_SurfaceSignature);\r\n\t\t_Profile.AddBorderToHash(ref borderHash);\r\n\t\t_Blends.AddToHash(ref borderHash);\r\n\t\t_Taper.AddToHash(ref borderHash);\r\n\t\tborderHash.Add(DebugBorders ? 1 : 0);\r\n\t\tlong borderSignature = borderHash.Value;\r\n\r\n\t\t// The deck follows the road and stands off its border, so it is stale when either changes; its posts and\r\n\t\t// supports are spread over the whole built range, so every piece is stale when an end moves.\r\n\t\tvar bridge = _Segment.Bridge;\r\n\t\tvar bridgeSpan = bridge is null ? default : ComputeBridgeSpan(_Segment, _TrimStart, usableEnd);\r\n\t\tlong bridgeSignature = 0;\r\n\r\n\t\tif (bridge is not null)\r\n\t\t{\r\n\t\t\tvar bridgeHash = StableHash.Create();\r\n\t\t\tbridgeHash.Add(_SurfaceSignature);\r\n\t\t\tbridgeHash.Add(bridge.ResourcePath);\r\n\t\t\t_Profile.AddBorderToHash(ref bridgeHash);\r\n\t\t\tbridge.AddToHash(ref bridgeHash);\r\n\t\t\tbridgeSpan.AddToHash(ref bridgeHash);\r\n\t\t\tbridgeSignature = bridgeHash.Value;\r\n\t\t}\r\n\r\n\t\t// Props are spread over the whole built range too, and stand on the border, so they follow its shape.\r\n\t\tvar props = _Segment.Props;\r\n\t\tvar propSpan = props is null ? default : new RoadPropBuilder.PropSpan(_TrimStart, usableEnd, GetDegree(_Segment.StartNodeId) >= 3, GetDegree(_Segment.EndNodeId) >= 3);\r\n\t\tlong propSignature = 0;\r\n\r\n\t\tif (props is not null && !props.IsEmpty)\r\n\t\t{\r\n\t\t\tvar propHash = StableHash.Create();\r\n\t\t\tpropHash.Add(_SurfaceSignature);\r\n\t\t\tpropHash.Add(props.ResourcePath);\r\n\t\t\tpropHash.Add(bridge is null ? 0 : 1);\r\n\t\t\t_Profile.AddBorderToHash(ref propHash);\r\n\t\t\tprops.AddToHash(ref propHash);\r\n\t\t\tpropSpan.AddToHash(ref propHash);\r\n\t\t\tpropSignature = propHash.Value;\r\n\t\t}\r\n\r\n\t\t// A road with no lines of its own still needs the piece when it has a crossing to paint.\r\n\t\tbool markings = RoadMarkingBuilder.HasMarkings(_Segment, _Profile) || _Crossings.AtStart || _Crossings.AtEnd;\r\n\t\tvar markingHash = StableHash.Create();\r\n\t\tmarkingHash.Add(_SurfaceSignature);\r\n\t\tmarkingHash.Add(_Segment.LaneCount);\r\n\t\tmarkingHash.Add(_Segment.ForwardLanes);\r\n\t\t_Lines.AddToHash(ref markingHash);\r\n\t\t_Dash.AddToHash(ref markingHash);\r\n\t\t_Profile.AddMarkingsToHash(ref markingHash);\r\n\t\t_Profile.AddCrossingsToHash(ref markingHash);\r\n\t\tmarkingHash.Add(_Crossings.AtStart ? _Crossings.StartCentre : -1.0f);\r\n\t\tmarkingHash.Add(_Crossings.AtEnd ? _Crossings.EndCentre : -1.0f);\r\n\t\tlong markingSignature = markingHash.Value;\r\n\r\n\t\tfor (int r = 0; r < ranges.Count; r++)\r\n\t\t{\r\n\t\t\tvar (index, start, end) = ranges[r];\r\n\r\n\t\t\tvar hash = StableHash.Create();\r\n\t\t\thash.Add(_SurfaceSignature);\r\n\t\t\thash.Add(index);\r\n\t\t\thash.Add(count);\r\n\t\t\thash.Add(start);\r\n\t\t\thash.Add(end);\r\n\r\n\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Surface, BorderCaps.None, default, hash.Value));\r\n\r\n\t\t\tif (markings)\r\n\t\t\t{\r\n\t\t\t\thash = StableHash.Create();\r\n\t\t\t\t// A crossing belongs to one piece: the outermost, since its band may lie clear of every piece's\r\n\t\t\t\t// own stretch of road.\r\n\t\t\t\tvar owned = new SegmentCrossings(_Crossings.AtStart && r == 0, _Crossings.StartCentre,\r\n\t\t\t\t\t_Crossings.AtEnd && r == ranges.Count - 1, _Crossings.EndCentre);\r\n\r\n\t\t\t\thash.Add(markingSignature);\r\n\t\t\t\thash.Add(index);\r\n\t\t\t\thash.Add(count);\r\n\t\t\t\thash.Add(start);\r\n\t\t\t\thash.Add(end);\r\n\t\t\t\thash.Add(owned.AtStart ? 1 : 0);\r\n\t\t\t\thash.Add(owned.AtEnd ? 1 : 0);\r\n\r\n\t\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Marking, BorderCaps.None, default, hash.Value, _Lines, Dash: _Dash, Crossings: owned));\r\n\t\t\t}\r\n\r\n\t\t\tif (bridge is not null)\r\n\t\t\t{\r\n\t\t\t\t// The piece works out for itself whether it is an outermost one, from its stretch against the span.\r\n\t\t\t\thash = StableHash.Create();\r\n\t\t\t\thash.Add(bridgeSignature);\r\n\t\t\t\thash.Add(index);\r\n\t\t\t\thash.Add(count);\r\n\t\t\t\thash.Add(start);\r\n\t\t\t\thash.Add(end);\r\n\r\n\t\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Bridge, BorderCaps.None, default, hash.Value, Bridge: bridgeSpan));\r\n\t\t\t}\r\n\r\n\t\t\tif (propSignature != 0)\r\n\t\t\t{\r\n\t\t\t\thash = StableHash.Create();\r\n\t\t\t\thash.Add(propSignature);\r\n\t\t\t\thash.Add(index);\r\n\t\t\t\thash.Add(count);\r\n\t\t\t\thash.Add(start);\r\n\t\t\t\thash.Add(end);\r\n\r\n\t\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Props, BorderCaps.None, default, hash.Value, Props: propSpan));\r\n\t\t\t}\r\n\r\n\t\t\tif (!_Profile.HasBorder)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Only the outermost stretches have open ends to close.\r\n\t\t\tvar caps = BorderCaps.None;\r\n\r\n\t\t\tif (r == 0)\r\n\t\t\t\tcaps |= _Caps & (BorderCaps.StartLeft | BorderCaps.StartRight);\r\n\r\n\t\t\tif (r == ranges.Count - 1)\r\n\t\t\t\tcaps |= _Caps & (BorderCaps.EndLeft | BorderCaps.EndRight);\r\n\r\n\t\t\thash = StableHash.Create();\r\n\t\t\thash.Add(borderSignature);\r\n\t\t\thash.Add(index);\r\n\t\t\thash.Add(count);\r\n\t\t\thash.Add(start);\r\n\t\t\thash.Add(end);\r\n\t\t\thash.Add((int)caps);\r\n\r\n\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Border, caps, _Blends, hash.Value, default, _Taper));\r\n\t\t}\r\n\r\n\t\t// A road that simply stops is rounded off there, in a piece of its own beyond the node.\r\n\t\tAddCapPlan(plans, _Segment, _Table, _Profile, _SurfaceSignature, true);\r\n\t\tAddCapPlan(plans, _Segment, _Table, _Profile, _SurfaceSignature, false);\r\n\r\n\t\treturn plans;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The rounded end of a road, where it has one: at a node with nothing else at it, which is not a way into a\r\n\t/// parking lot (the lot's own kerb closes that), and on a road no bridge carries (a deck ends where it ends).\r\n\t/// The plan's start is the end's own distance along the curve and its end the depth the cap reaches past it.\r\n\t/// </summary>\r\n\tprivate void AddCapPlan(List<ChunkPlan> _Plans, RoadSegment _Segment, RoadCurveTable _Table, RoadProfile _Profile, long _SurfaceSignature, bool _AtStart)\r\n\t{\r\n\t\tint nodeId = _AtStart ? _Segment.StartNodeId : _Segment.EndNodeId;\r\n\r\n\t\tif (_Segment.Bridge is not null || GetDegree(nodeId) != 1 || LotAtOpening(nodeId) is not null)\r\n\t\t\treturn;\r\n\r\n\t\tfloat distance = _AtStart ? 0.0f : _Table.Length;\r\n\t\tfloat depth = RoadCapBuilder.Depth(_Profile, new RoadWidth(_Segment, _Table.Length), distance);\r\n\r\n\t\tif (depth <= 0.01f)\r\n\t\t\treturn;\r\n\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\thash.Add(_SurfaceSignature);\r\n\t\thash.Add(_AtStart ? 1 : 0);\r\n\t\thash.Add(depth);\r\n\t\t_Profile.AddBorderToHash(ref hash);\r\n\r\n\t\t_Plans.Add(new ChunkPlan(_AtStart ? 0 : 1, 2, distance, depth, RoadChunkKind.Cap, BorderCaps.None, default, hash.Value));\r\n\t}\r\n\r\n\r\n\r\n\tprivate static long CombineSignatures(List<ChunkPlan> _Plans)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\t\thash.Add(_Plans.Count);\r\n\r\n\t\tforeach (var plan in _Plans)\r\n\t\t\thash.Add(plan.Signature);\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsSegmentUpToDate(List<RoadChunk> _Existing, List<ChunkPlan> _Plans)\r\n\t{\r\n\t\tint have = _Existing?.Count ?? 0;\r\n\r\n\t\tif (have != _Plans.Count)\r\n\t\t\treturn false;\r\n\r\n\t\tforeach (var plan in _Plans)\r\n\t\t{\r\n\t\t\tvar chunk = _Existing.FirstOrDefault(candidate => candidate.ChunkIndex == plan.Index && candidate.Kind == plan.Kind);\r\n\r\n\t\t\tif (chunk is null)\r\n\t\t\t\treturn false;\r\n\r\n\t\t\t// A locked chunk is exempt: it is deliberately stale.\r\n\t\t\tif (!chunk.Locked && (chunk.Signature != plan.Signature || LacksDebugRecord(chunk)))\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// While the border debug overlay is on, a border piece without its record (built before the flag went on, or\r\n\t/// before a hot reload, which drops the records) is rebuilt to get one.\r\n\t/// </summary>\r\n\tprivate bool LacksDebugRecord(RoadChunk _Chunk)\r\n\t{\r\n\t\treturn DebugBorders && _Chunk.Kind == RoadChunkKind.Border && (_Chunk.Debug is null || _Chunk.Debug.Text.Length == 0);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Throws away every unlocked piece's signature so the next reconcile rebuilds all of them, junction layouts\r\n\t/// included. Toggling the GameObject does not do this: pieces whose inputs have not changed are kept as they\r\n\t/// are, so after a generator change this is the way to see it everywhere.\r\n\t/// </summary>\r\n\t[Button(\"Regenerate All\", \"refresh\")]\r\n\tpublic void RegenerateAll()\r\n\t{\r\n\t\tforeach (var chunk in GetChunks())\r\n\t\t{\r\n\t\t\tif (!chunk.Locked)\r\n\t\t\t\tchunk.Signature = 0;\r\n\t\t}\r\n\r\n\t\tm_JunctionCache.Clear();\r\n\t\tMarkDirty();\r\n\t}\r\n\r\n\r\n\r\n\tprivate long ComputeSurfaceSignature(RoadSegment _Segment, in RoadCurve _Curve, RoadProfile _Profile)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\thash.Add(GeneratorVersion);\r\n\t\thash.Add(_Curve.P0);\r\n\t\thash.Add(_Curve.P1);\r\n\t\thash.Add(_Curve.P2);\r\n\t\thash.Add(_Curve.P3);\r\n\t\tnew RoadWidth(_Segment, 1.0f).AddToHash(ref hash);\r\n\t\thash.Add(SampleSpacing);\r\n\t\thash.Add(ChunkLength);\r\n\t\thash.Add(SurfaceOffset);\r\n\t\t_Profile.AddSurfaceToHash(ref hash);\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Everything a node's junction layout depends on: the node itself and the curve and width of each road at it.\r\n\t/// </summary>\r\n\tprivate long ComputeJunctionKey(RoadNode _Node, float _Radius)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\thash.Add(GeneratorVersion);\r\n\t\thash.Add(_Node.Position);\r\n\t\thash.Add(_Radius);\r\n\t\thash.Add(SurfaceOffset);\r\n\r\n\t\tforeach (var segment in GetSegmentsAt(_Node.Id).OrderBy(segment => segment.Id))\r\n\t\t{\r\n\t\t\thash.Add(segment.Id);\r\n\t\t\tnew RoadWidth(segment, 1.0f).AddToHash(ref hash);\r\n\t\t\thash.Add(segment.Ramp ? 1 : 0);\r\n\t\t\thash.Add(segment.StartNodeId == _Node.Id ? 1 : 0);\r\n\r\n\t\t\tif (!TryGetCurve(segment, out var curve))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\thash.Add(curve.P0);\r\n\t\t\thash.Add(curve.P1);\r\n\t\t\thash.Add(curve.P2);\r\n\t\t\thash.Add(curve.P3);\r\n\t\t}\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\tprivate static void AddPolygonToHash(RoadJunctionLayout _Layout, ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(GeneratorVersion);\r\n\t\t_Hash.Add(_Layout.Centre);\r\n\t\t_Hash.Add(_Layout.Polygon.Count);\r\n\r\n\t\tforeach (var vertex in _Layout.Polygon)\r\n\t\t\t_Hash.Add(vertex);\r\n\t}\r\n\r\n\r\n\r\n\tprivate long ComputeJunctionSurfaceSignature(RoadJunctionLayout _Layout)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\tAddPolygonToHash(_Layout, ref hash);\r\n\t\tJunctionSurfaceProfile(_Layout).AddSurfaceToHash(ref hash);\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\tprivate long ComputeJunctionBorderSignature(RoadJunctionLayout _Layout, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> _ArmBlends, Dictionary<int, SegmentBorderTapers> _ArmTapers)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\tAddPolygonToHash(_Layout, ref hash);\r\n\t\thash.Add(DebugBorders ? 1 : 0);\r\n\r\n\t\tforeach (var arm in _Layout.Arms)\r\n\t\t{\r\n\t\t\tvar profile = ProfileOf(arm.Segment);\r\n\r\n\t\t\thash.Add(arm.Segment.Id);\r\n\t\t\thash.Add(arm.AtStart ? 1 : 0);\r\n\t\t\thash.Add(arm.Length);\r\n\t\t\tprofile.AddSurfaceToHash(ref hash);\r\n\t\t\tprofile.AddBorderToHash(ref hash);\r\n\t\t\t_ArmBlends.GetValueOrDefault(arm.Segment.Id).AddToHash(ref hash);\r\n\t\t\t_ArmTapers.GetValueOrDefault(arm.Segment.Id).AddToHash(ref hash);\r\n\t\t}\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// A junction's surface takes after its main road: the widest arm.\r\n\t/// </summary>\r\n\tprivate RoadProfile JunctionSurfaceProfile(RoadJunctionLayout _Layout)\r\n\t{\r\n\t\tvar main = _Layout.Arms.OrderByDescending(arm => arm.Widths.Base).ThenBy(arm => arm.Segment.Id).FirstOrDefault();\r\n\r\n\t\treturn ProfileOf(main?.Segment);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnqueueSegment(RoadSegment _Segment, RoadCurveTable _Table, RoadProfile _Profile, List<ChunkPlan> _Plans, long _GroupSignature, List<RoadChunk> _Existing)\r\n\t{\r\n\t\t// Chunks with no plan (swallowed by a junction, past a shorter road's end, or a border the road no longer\r\n\t\t// has) have no job coming to replace them.\r\n\t\tif (_Existing is not null)\r\n\t\t{\r\n\t\t\tforeach (var chunk in _Existing)\r\n\t\t\t{\r\n\t\t\t\tif (!chunk.Locked && _Plans.All(plan => plan.Index != chunk.ChunkIndex || plan.Kind != chunk.Kind))\r\n\t\t\t\t\tchunk.GameObject.Destroy();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (_Plans.Count == 0)\r\n\t\t{\r\n\t\t\tm_PendingSegments.Remove(_Segment.Id);\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tforeach (var plan in _Plans)\r\n\t\t\tm_Jobs.Enqueue(new RebuildJob(_Segment.Id, -1, plan.Kind, _GroupSignature, plan, _Table, new RoadWidth(_Segment, _Table.Length), _Profile, null, null, null, null));\r\n\r\n\t\tm_PendingSegments[_Segment.Id] = new PendingGroup(_GroupSignature, _Plans.Count);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void ProcessJobs()\r\n\t{\r\n\t\tint budget = Math.Max(1, RebuildBudget);\r\n\r\n\t\twhile (budget > 0 && m_Jobs.Count > 0)\r\n\t\t{\r\n\t\t\tvar job = m_Jobs.Dequeue();\r\n\r\n\t\t\tif (job.Lot is not null)\r\n\t\t\t{\r\n\t\t\t\tvar lotKey = (job.Lot.Id, job.Kind);\r\n\r\n\t\t\t\t// Superseded by a newer version of the same lot, or the lot is gone.\r\n\t\t\t\tif (!m_PendingLots.TryGetValue(lotKey, out var pendingLot) || pendingLot.Signature != job.GroupSignature)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tBuildLotPiece(job);\r\n\t\t\t\tbudget--;\r\n\r\n\t\t\t\tm_PendingLots.Remove(lotKey);\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (job.NodeId >= 0)\r\n\t\t\t{\r\n\t\t\t\tvar key = (job.NodeId, job.Kind);\r\n\r\n\t\t\t\t// Superseded by a newer layout of the same junction, or the junction is gone.\r\n\t\t\t\tif (!m_PendingJunctions.TryGetValue(key, out var pending) || pending.Signature != job.GroupSignature)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tBuildJunctionPiece(job);\r\n\t\t\t\tbudget--;\r\n\r\n\t\t\t\tm_PendingJunctions.Remove(key);\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// Superseded by a newer rebuild of the same segment, or the segment is gone.\r\n\t\t\tif (!m_PendingSegments.TryGetValue(job.SegmentId, out var pendingSegment) || pendingSegment.Signature != job.GroupSignature)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif (BuildChunk(job))\r\n\t\t\t\tbudget--;\r\n\r\n\t\t\tif (pendingSegment.Remaining <= 1)\r\n\t\t\t\tm_PendingSegments.Remove(job.SegmentId);\r\n\t\t\telse\r\n\t\t\t\tm_PendingSegments[job.SegmentId] = pendingSegment with { Remaining = pendingSegment.Remaining - 1 };\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Returns false when there was nothing to do: the chunk in place was already built from these inputs (a\r\n\t/// junction change at the far end of the road requeues the whole road, but only the chunk next to it differs).\r\n\t/// </summary>\r\n\tprivate bool BuildChunk(in RebuildJob _Job)\r\n\t{\r\n\t\tvar plan = _Job.Plan;\r\n\t\tvar existing = FindChunk(_Job.SegmentId, plan.Index, plan.Kind);\r\n\r\n\t\tif (existing is not null && (existing.Locked || (existing.Signature == plan.Signature && !LacksDebugRecord(existing))))\r\n\t\t\treturn false;\r\n\r\n\t\tPolygonMesh mesh;\r\n\t\tstring name;\r\n\t\tBorderDebug debug = null;\r\n\t\tvar placements = new List<(GameObject Prefab, Transform Transform)>();\r\n\r\n\t\tif (plan.Kind == RoadChunkKind.Border)\r\n\t\t{\r\n\t\t\tdebug = DebugBorders ? new BorderDebug() : null;\r\n\t\t\tmesh = RoadBorderBuilder.BuildSegment(_Job.Table, plan.Start, plan.End, _Job.Width, _Job.Profile, SampleSpacing, SurfaceOffset, plan.Caps, plan.Blends, plan.Taper, debug);\r\n\t\t\tname = $\"Road {_Job.SegmentId} border [{plan.Index + 1}/{plan.Count}]\";\r\n\t\t}\r\n\t\telse if (plan.Kind == RoadChunkKind.Bridge)\r\n\t\t{\r\n\t\t\tvar segment = FindSegment(_Job.SegmentId);\r\n\r\n\t\t\tif (segment?.Bridge is null)\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tvar parts = new List<RoadBridgeBuilder.Placement>();\r\n\t\t\tmesh = RoadBridgeBuilder.Build(_Job.Table, plan.Start, plan.End, plan.Bridge, _Job.Width, _Job.Profile, segment.Bridge, SampleSpacing, SurfaceOffset, GroundBelow, parts);\r\n\t\t\tname = $\"Road {_Job.SegmentId} bridge [{plan.Index + 1}/{plan.Count}]\";\r\n\r\n\t\t\tforeach (var part in parts)\r\n\t\t\t\tplacements.Add((part.Prefab, new Transform(part.Position, part.Rotation)));\r\n\t\t}\r\n\t\telse if (plan.Kind == RoadChunkKind.Props)\r\n\t\t{\r\n\t\t\tvar segment = FindSegment(_Job.SegmentId);\r\n\r\n\t\t\tif (segment?.Props is null)\r\n\t\t\t\treturn false;\r\n\r\n\t\t\t// Nothing to mesh: the piece is only the parent of its prefabs.\r\n\t\t\tvar props = new List<RoadPropBuilder.Placement>();\r\n\t\t\tvar context = new RoadPropBuilder.Context(_Job.Table, plan.Start, plan.End, plan.Props, _Job.SegmentId, _Job.Width, _Job.Profile, segment.Bridge is not null, SurfaceOffset, GroundAround);\r\n\r\n\t\t\tRoadPropBuilder.Build(context, segment.Props, props);\r\n\t\t\tmesh = null;\r\n\t\t\tname = $\"Road {_Job.SegmentId} props [{plan.Index + 1}/{plan.Count}]\";\r\n\r\n\t\t\tforeach (var prop in props)\r\n\t\t\t\tplacements.Add((prop.Prefab, new Transform(prop.Position, prop.Rotation, prop.Scale)));\r\n\t\t}\r\n\t\telse if (plan.Kind == RoadChunkKind.Cap)\r\n\t\t{\r\n\t\t\t// Surface and border in one piece: the whole end is small, and nothing else shares it.\r\n\t\t\tmesh = RoadCapBuilder.Build(_Job.Table, plan.Start, plan.Index == 0, _Job.Width, _Job.Profile, SurfaceOffset, plan.End);\r\n\t\t\tname = $\"Road {_Job.SegmentId} {(plan.Index == 0 ? \"start\" : \"end\")} cap\";\r\n\t\t}\r\n\t\telse if (plan.Kind == RoadChunkKind.Marking)\r\n\t\t{\r\n\t\t\tvar segment = FindSegment(_Job.SegmentId);\r\n\r\n\t\t\tif (segment is null)\r\n\t\t\t\treturn false;\r\n\r\n\t\t\t// Lines only where the chunk overlaps the stretches left clear by the crossings, laid on the rows the\r\n\t\t\t// road surface itself was built on.\r\n\t\t\tmesh = new PolygonMesh();\r\n\r\n\t\t\tvar roadRows = RoadRows.Build(_Job.Table, plan.Start, plan.End, SampleSpacing, _Job.Width, _Job.Profile.Simplification);\r\n\r\n\t\t\tif (plan.Lines.HasGap)\r\n\t\t\t{\r\n\t\t\t\tRoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.Start), MathF.Min(plan.End, plan.Lines.GapStart), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);\r\n\t\t\t\tRoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.GapEnd), MathF.Min(plan.End, plan.Lines.End), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tRoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.Start), MathF.Min(plan.End, plan.Lines.End), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);\r\n\t\t\t}\r\n\r\n\t\t\t// The crossings this piece owns, laid on the same rows as the lines and the road itself.\r\n\t\t\tif (plan.Crossings.AtStart)\r\n\t\t\t\tRoadMarkingBuilder.BuildCrossing(mesh, _Job.Table, roadRows, plan.Crossings.StartCentre, _Job.Width, _Job.Profile, SurfaceOffset);\r\n\r\n\t\t\tif (plan.Crossings.AtEnd)\r\n\t\t\t\tRoadMarkingBuilder.BuildCrossing(mesh, _Job.Table, roadRows, plan.Crossings.EndCentre, _Job.Width, _Job.Profile, SurfaceOffset);\r\n\r\n\t\t\tname = $\"Road {_Job.SegmentId} markings [{plan.Index + 1}/{plan.Count}]\";\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tmesh = RoadSurfaceBuilder.Build(_Job.Table, plan.Start, plan.End, _Job.Width, SampleSpacing, _Job.Profile.Simplification, _Job.Profile.RoadTextureRepeat, SurfaceOffset, _Job.Profile.ResolvedRoadMaterial);\r\n\t\t\tname = $\"Road {_Job.SegmentId} [{plan.Index + 1}/{plan.Count}]\";\r\n\t\t}\r\n\r\n\t\tvar piece = CreateChunkObject(name, _Job.SegmentId, -1, plan.Kind, plan.Index, plan.Count, plan.Signature, mesh, debug);\r\n\r\n\t\tforeach (var (prefab, transform) in placements)\r\n\t\t\tprefab.Clone(transform, piece);\r\n\r\n\t\t// The new piece is in place before the old one goes, so the road never flickers.\r\n\t\texisting?.GameObject.Destroy();\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The height of whatever lies under a point of the network (its own pieces excepted), in network space, for\r\n\t/// bridge supports to land on: the search starts just below the point, so the deck itself is never it. Null\r\n\t/// over nothing.\r\n\t/// </summary>\r\n\tprivate float? GroundBelow(Vector3 _Local)\r\n\t{\r\n\t\treturn TraceGround(_Local, -1.0f);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The height of the ground at a point of the network, for props to stand on: the search starts well above\r\n\t/// the point, so ground rising beside a road is found as well as ground falling away from it.\r\n\t/// </summary>\r\n\tprivate float? GroundAround(Vector3 _Local)\r\n\t{\r\n\t\treturn TraceGround(_Local, 3000.0f);\r\n\t}\r\n\r\n\r\n\r\n\tprivate float? TraceGround(Vector3 _Local, float _StartAbove)\r\n\t{\r\n\t\tconst float reach = 50000.0f;\r\n\r\n\t\tvar transform = WorldTransform;\r\n\t\tVector3 from = transform.PointToWorld(_Local) + Vector3.Up * _StartAbove;\r\n\t\tvar trace = Scene.Trace.Ray(from, from + Vector3.Down * reach).WithoutTags(ChunkTag).Run();\r\n\r\n\t\tif (!trace.Hit)\r\n\t\t\treturn null;\r\n\r\n\t\treturn transform.PointToLocal(trace.EndPosition).z;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildJunctionPiece(in RebuildJob _Job)\r\n\t{\r\n\t\tvar existing = FindJunctionChunk(_Job.NodeId, _Job.Kind);\r\n\r\n\t\tif (existing is not null && existing.Locked)\r\n\t\t\treturn;\r\n\r\n\t\tPolygonMesh mesh;\r\n\t\tstring name;\r\n\t\tBorderDebug debug = null;\r\n\r\n\t\tif (_Job.Kind == RoadChunkKind.Border)\r\n\t\t{\r\n\t\t\tvar armBlends = _Job.ArmBlends;\r\n\t\t\tvar armTapers = _Job.ArmTapers;\r\n\r\n\t\t\tdebug = DebugBorders ? new BorderDebug() : null;\r\n\t\t\tmesh = RoadBorderBuilder.BuildJunction(_Job.Junction, ProfileOf,\r\n\t\t\t\tsegment => armBlends is not null ? armBlends.GetValueOrDefault(segment.Id) : default,\r\n\t\t\t\tsegment => armTapers is not null ? armTapers.GetValueOrDefault(segment.Id) : default,\r\n\t\t\t\tdebug);\r\n\t\t\tname = $\"Junction {_Job.NodeId} border\";\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar profile = JunctionSurfaceProfile(_Job.Junction);\r\n\r\n\t\t\tmesh = RoadJunctionBuilder.Build(_Job.Junction, profile.RoadTextureRepeat, profile.ResolvedRoadMaterial);\r\n\t\t\tname = $\"Junction {_Job.NodeId}\";\r\n\t\t}\r\n\r\n\t\tCreateChunkObject(name, -1, _Job.NodeId, _Job.Kind, 0, 1, _Job.GroupSignature, mesh, debug);\r\n\r\n\t\texisting?.GameObject.Destroy();\r\n\t}\r\n\r\n\r\n\r\n\tprivate GameObject CreateChunkObject(string _Name, int _SegmentId, int _NodeId, RoadChunkKind _Kind, int _Index, int _Count, long _Signature, PolygonMesh _Mesh, BorderDebug _Debug, int _LotId = -1)\r\n\t{\r\n\t\tvar child = new GameObject(GameObject, true, _Name);\r\n\t\tchild.Tags.Add(ChunkTag);\r\n\r\n\t\tvar chunk = child.AddComponent<RoadChunk>();\r\n\t\tchunk.SegmentId = _SegmentId;\r\n\t\tchunk.NodeId = _NodeId;\r\n\t\tchunk.LotId = _LotId;\r\n\t\tchunk.Kind = _Kind;\r\n\t\tchunk.ChunkIndex = _Index;\r\n\t\tchunk.ChunkCount = _Count;\r\n\t\tchunk.Signature = _Signature;\r\n\t\tchunk.Debug = _Debug;\r\n\r\n\t\tif (_Debug is not null)\r\n\t\t\tDebugRevision++;\r\n\r\n\t\t// A piece of props has no geometry of its own; it is the parent of its prefabs.\r\n\t\tif (_Mesh is null)\r\n\t\t\treturn child;\r\n\r\n\t\tvar meshComponent = child.AddComponent<MeshComponent>();\r\n\t\tmeshComponent.Mesh = _Mesh;\r\n\t\tmeshComponent.SmoothingAngle = 40.0f;\r\n\r\n\t\t// Paint lies flat on the road: nothing to collide with, and its shadow would be a sliver hidden under it,\r\n\t\t// at the cost of a shadow pass.\r\n\t\tif (_Kind == RoadChunkKind.Marking)\r\n\t\t{\r\n\t\t\tmeshComponent.Collision = MeshComponent.CollisionType.None;\r\n\t\t\tmeshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;\r\n\t\t}\r\n\r\n\t\treturn child;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Editor/RoadGizmos.cs",
            "FileName": "RoadGizmos.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2.Editor;\r\n\r\n/// <summary>\r\n/// Gizmo drawing shared by the road tools. Curves are in network-local space and drawn through the network's\r\n/// transform, in whatever colour and thickness the caller set up.\r\n/// </summary>\r\ninternal static class RoadGizmos\r\n{\r\n\tpublic static void DrawCurve(in RoadCurve _Curve, in Transform _Transform, int _Steps = 24)\r\n\t{\r\n\t\tVector3 previous = _Transform.PointToWorld(_Curve.P0);\r\n\r\n\t\tfor (int i = 1; i <= _Steps; i++)\r\n\t\t{\r\n\t\t\tVector3 point = _Transform.PointToWorld(_Curve.Evaluate((float)i / _Steps));\r\n\r\n\t\t\tGizmo.Draw.Line(previous, point);\r\n\t\t\tprevious = point;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Draws the two edges of a road of the given width along the curve, capped at both ends.\r\n\t/// </summary>\r\n\tpublic static void DrawRoadOutline(in RoadCurve _Curve, float _Width, in Transform _Transform, int _Steps = 24)\r\n\t{\r\n\t\tDrawRoadOutline(_Curve, RoadWidth.Uniform(_Width), _Transform, _Steps);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Draws the two edges of a road along the curve, each its own distance out (the curve's parameter standing in\r\n\t/// for its distance), capped at both ends.\r\n\t/// </summary>\r\n\tpublic static void DrawRoadOutline(in RoadCurve _Curve, in RoadWidth _Width, in Transform _Transform, int _Steps = 24)\r\n\t{\r\n\t\tVector3 previousLeft = default;\r\n\t\tVector3 previousRight = default;\r\n\r\n\t\tfor (int i = 0; i <= _Steps; i++)\r\n\t\t{\r\n\t\t\tfloat t = (float)i / _Steps;\r\n\t\t\tfloat distance = t * _Width.Length;\r\n\t\t\tVector3 point = _Curve.Evaluate(t);\r\n\t\t\tVector3 right = RoadCurve.RightOf(_Curve.Tangent(t));\r\n\r\n\t\t\tVector3 left = _Transform.PointToWorld(point - right * _Width.LeftAt(distance));\r\n\t\t\tVector3 rightEdge = _Transform.PointToWorld(point + right * _Width.RightAt(distance));\r\n\r\n\t\t\tif (i == 0 || i == _Steps)\r\n\t\t\t\tGizmo.Draw.Line(left, rightEdge);\r\n\r\n\t\t\tif (i > 0)\r\n\t\t\t{\r\n\t\t\t\tGizmo.Draw.Line(previousLeft, left);\r\n\t\t\t\tGizmo.Draw.Line(previousRight, rightEdge);\r\n\t\t\t}\r\n\r\n\t\t\tpreviousLeft = left;\r\n\t\t\tpreviousRight = rightEdge;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// A node marker: a flat ring on the ground, filled when it is the one being worked on.\r\n\t/// </summary>\r\n\tpublic static void DrawNode(Vector3 _WorldPosition, bool _Filled)\r\n\t{\r\n\t\tfloat radius = RoadPicker.NodeRadius(_WorldPosition);\r\n\r\n\t\tif (_Filled)\r\n\t\t\tGizmo.Draw.SolidSphere(_WorldPosition, radius * 0.6f);\r\n\r\n\t\tGizmo.Draw.LineCircle(_WorldPosition, Vector3.Up, radius);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Code/Geometry/RoadCurve.cs",
            "FileName": "RoadCurve.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// The cubic Bezier of one road segment, in network-local space. P1 and P2 are the tangent handles hanging off the\r\n/// two end nodes.\r\n/// </summary>\r\npublic readonly struct RoadCurve : IEquatable<RoadCurve>\r\n{\r\n\tpublic readonly Vector3 P0;\r\n\tpublic readonly Vector3 P1;\r\n\tpublic readonly Vector3 P2;\r\n\tpublic readonly Vector3 P3;\r\n\r\n\r\n\r\n\tpublic RoadCurve(Vector3 _P0, Vector3 _P1, Vector3 _P2, Vector3 _P3)\r\n\t{\r\n\t\tP0 = _P0;\r\n\t\tP1 = _P1;\r\n\t\tP2 = _P2;\r\n\t\tP3 = _P3;\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 Evaluate(float _T)\r\n\t{\r\n\t\tfloat u = 1.0f - _T;\r\n\t\tfloat uu = u * u;\r\n\t\tfloat tt = _T * _T;\r\n\r\n\t\treturn P0 * (uu * u) + P1 * (3.0f * uu * _T) + P2 * (3.0f * u * tt) + P3 * (tt * _T);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Unit direction of travel at <paramref name=\"_T\"/>. Falls back to the chord when the derivative vanishes (a\r\n\t/// handle sitting on its node) so callers always get a usable frame.\r\n\t/// </summary>\r\n\tpublic Vector3 Tangent(float _T)\r\n\t{\r\n\t\tfloat u = 1.0f - _T;\r\n\t\tVector3 derivative = (P1 - P0) * (3.0f * u * u) + (P2 - P1) * (6.0f * u * _T) + (P3 - P2) * (3.0f * _T * _T);\r\n\r\n\t\tif (derivative.IsNearZeroLength)\r\n\t\t\tderivative = P3 - P0;\r\n\r\n\t\tif (derivative.IsNearZeroLength)\r\n\t\t\treturn Vector3.Forward;\r\n\r\n\t\treturn derivative.Normal;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Chord-summed length, good enough to size sampling tables and count chunks.\r\n\t/// </summary>\r\n\tpublic float ApproximateLength(int _Steps = 32)\r\n\t{\r\n\t\tfloat length = 0.0f;\r\n\t\tVector3 previous = P0;\r\n\r\n\t\tfor (int i = 1; i <= _Steps; i++)\r\n\t\t{\r\n\t\t\tVector3 point = Evaluate((float)i / _Steps);\r\n\t\t\tlength += Vector3.DistanceBetween(previous, point);\r\n\t\t\tprevious = point;\r\n\t\t}\r\n\r\n\t\treturn length;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// De Casteljau subdivision: the two halves trace exactly the same path as the whole, so a road can be split\r\n\t/// without moving.\r\n\t/// </summary>\r\n\tpublic void Split(float _T, out RoadCurve _Left, out RoadCurve _Right)\r\n\t{\r\n\t\tVector3 q0 = Vector3.Lerp(P0, P1, _T);\r\n\t\tVector3 q1 = Vector3.Lerp(P1, P2, _T);\r\n\t\tVector3 q2 = Vector3.Lerp(P2, P3, _T);\r\n\t\tVector3 r0 = Vector3.Lerp(q0, q1, _T);\r\n\t\tVector3 r1 = Vector3.Lerp(q1, q2, _T);\r\n\t\tVector3 s = Vector3.Lerp(r0, r1, _T);\r\n\r\n\t\t_Left = new RoadCurve(P0, q0, r0, s);\r\n\t\t_Right = new RoadCurve(s, r1, q2, P3);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The road's right-hand direction for a direction of travel: flat, perpendicular to the tangent, and\r\n\t/// consistent with <see cref=\"Rotation.Right\"/> for a rotation looking down the tangent with world up.\r\n\t/// </summary>\r\n\tpublic static Vector3 RightOf(Vector3 _Tangent)\r\n\t{\r\n\t\tVector3 right = Vector3.Cross(_Tangent, Vector3.Up);\r\n\r\n\t\t// Vertical tangent (a ramp seen from nowhere useful): any horizontal axis will do.\r\n\t\tif (right.IsNearZeroLength)\r\n\t\t\treturn Vector3.Right;\r\n\r\n\t\treturn right.Normal;\r\n\t}\r\n\r\n\r\n\r\n\tpublic bool Equals(RoadCurve _Other)\r\n\t{\r\n\t\treturn P0 == _Other.P0 && P1 == _Other.P1 && P2 == _Other.P2 && P3 == _Other.P3;\r\n\t}\r\n\r\n\r\n\r\n\tpublic override bool Equals(object _Obj)\r\n\t{\r\n\t\treturn _Obj is RoadCurve other && Equals(other);\r\n\t}\r\n\r\n\r\n\r\n\tpublic override int GetHashCode()\r\n\t{\r\n\t\treturn HashCode.Combine(P0, P1, P2, P3);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Code/Network/RoadCapBuilder.cs",
            "FileName": "RoadCapBuilder.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Generates the rounded end of a road that stops: a half disc of surface carried on past the last row, with the\r\n/// border swept round it, so a dead end reads as a turning head rather than a road sliced through. The piece is\r\n/// built beyond the road's own end, on the end row itself, so its seam with the last chunk is exact and nothing\r\n/// about the road's length changes.\r\n/// </summary>\r\ninternal static class RoadCapBuilder\r\n{\r\n\t/// <summary>\r\n\t/// The arc is drawn in steps of about this many degrees.\r\n\t/// </summary>\r\n\tprivate const float ArcStepDegrees = 9.0f;\r\n\r\n\t/// <summary>\r\n\t/// How deep a road's rounded end reaches past it, as a share of the road's half width. 1 is a half circle.\r\n\t/// </summary>\r\n\tpublic const float DepthFactor = 1.0f;\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// How far the rounded end of a road reaches past the node, or 0 where the road type does not round its ends.\r\n\t/// </summary>\r\n\tpublic static float Depth(RoadProfile _Profile, in RoadWidth _Width, float _Distance)\r\n\t{\r\n\t\tif (_Profile is null || !_Profile.RoundedEnds)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\treturn (_Width.LeftAt(_Distance) + _Width.RightAt(_Distance)) * 0.5f * DepthFactor;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The rounded end at one end of a road. <paramref name=\"_Distance\"/> is the end's own distance along the curve\r\n\t/// (0 or the table's length) and <paramref name=\"_AtStart\"/> which end it is; the cap reaches out from there.\r\n\t/// </summary>\r\n\tpublic static PolygonMesh Build(RoadCurveTable _Table, float _Distance, bool _AtStart, in RoadWidth _Width, RoadProfile _Profile, float _Lift, float _Depth)\r\n\t{\r\n\t\tvar mesh = new PolygonMesh();\r\n\r\n\t\tif (_Depth <= 0.01f)\r\n\t\t\treturn mesh;\r\n\r\n\t\t_Table.Frame(_Distance, out Vector3 centre, out Vector3 tangent);\r\n\r\n\t\tcentre += Vector3.Up * _Lift;\r\n\r\n\t\t// Out of the road, and the road's own right at that end. The arc keeps the road's grade because the\r\n\t\t// direction it reaches along carries the curve's climb with it.\r\n\t\tVector3 forward = (_AtStart ? -tangent : tangent).Normal;\r\n\t\tVector3 right = RoadCurve.RightOf(tangent);\r\n\t\tfloat leftHalf = _Width.LeftAt(_Distance);\r\n\t\tfloat rightHalf = _Width.RightAt(_Distance);\r\n\r\n\t\tif (leftHalf + rightHalf <= 0.01f)\r\n\t\t\treturn mesh;\r\n\r\n\t\tint steps = Math.Max(4, (int)MathF.Ceiling(180.0f / ArcStepDegrees));\r\n\t\tvar outline = new List<Vector3>(steps + 1);\r\n\t\tvar outwards = new List<Vector3>(steps + 1);\r\n\t\tvar offsets = new List<float>(steps + 1);\r\n\t\tvar aheads = new List<float>(steps + 1);\r\n\r\n\t\tArc(centre, forward, right, leftHalf, rightHalf, _Depth, steps, outline, outwards, offsets, aheads);\r\n\r\n\t\tAddSurface(mesh, centre, outline, offsets, aheads, _Distance, _AtStart, _Profile);\r\n\t\tAddBorder(mesh, outline, outwards, _Profile);\r\n\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The outline of a road's rounded end in network space, lifted onto the road surface, for the tools that need\r\n\t/// to know where road is (the ground under it is held down and brought up to its kerb like a junction's).\r\n\t/// Empty where the road does not round that end.\r\n\t/// </summary>\r\n\tpublic static List<Vector3> Outline(RoadCurveTable _Table, float _Distance, bool _AtStart, in RoadWidth _Width, RoadProfile _Profile, float _Lift, out Vector3 _Centre)\r\n\t{\r\n\t\tvar outline = new List<Vector3>();\r\n\t\tfloat depth = Depth(_Profile, _Width, _Distance);\r\n\r\n\t\t_Table.Frame(_Distance, out _Centre, out Vector3 tangent);\r\n\r\n\t\t_Centre += Vector3.Up * _Lift;\r\n\r\n\t\tfloat leftHalf = _Width.LeftAt(_Distance);\r\n\t\tfloat rightHalf = _Width.RightAt(_Distance);\r\n\r\n\t\tif (depth <= 0.01f || leftHalf + rightHalf <= 0.01f)\r\n\t\t\treturn outline;\r\n\r\n\t\tint steps = Math.Max(4, (int)MathF.Ceiling(180.0f / ArcStepDegrees));\r\n\r\n\t\tArc(_Centre, (_AtStart ? -tangent : tangent).Normal, RoadCurve.RightOf(tangent), leftHalf, rightHalf, depth, steps, outline, null, null, null);\r\n\r\n\t\treturn outline;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The half ellipse from the road's left edge, round the tip, to its right edge: where each point is, which way\r\n\t/// is out of the road there, and how far across and ahead of the end row it sits.\r\n\t/// </summary>\r\n\tprivate static void Arc(Vector3 _Centre, Vector3 _Forward, Vector3 _Right, float _LeftHalf, float _RightHalf, float _Depth, int _Steps,\r\n\t\tList<Vector3> _Points, List<Vector3> _Outwards, List<float> _Offsets, List<float> _Aheads)\r\n\t{\r\n\t\tfor (int i = 0; i <= _Steps; i++)\r\n\t\t{\r\n\t\t\tfloat angle = MathF.PI * (i / (float)_Steps - 0.5f);\r\n\t\t\tfloat sin = MathF.Sin(angle);\r\n\t\t\tfloat cos = MathF.Cos(angle);\r\n\t\t\tfloat offset = sin * (sin >= 0.0f ? _RightHalf : _LeftHalf);\r\n\t\t\tfloat ahead = _Depth * cos;\r\n\r\n\t\t\t_Points.Add(_Centre + _Right * offset + _Forward * ahead);\r\n\t\t\t_Outwards?.Add((_Right * sin + _Forward * cos).Normal);\r\n\t\t\t_Offsets?.Add(offset);\r\n\t\t\t_Aheads?.Add(ahead);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The half disc of road surface: a fan from the middle of the end row out to the arc, so every triangle is\r\n\t/// planar and the row itself is the fan's own base.\r\n\t/// </summary>\r\n\tprivate static void AddSurface(PolygonMesh _Mesh, Vector3 _Centre, List<Vector3> _Outline, List<float> _Offsets, List<float> _Aheads,\r\n\t\tfloat _Distance, bool _AtStart, RoadProfile _Profile)\r\n\t{\r\n\t\tfloat repeat = MathF.Max(1.0f, _Profile.RoadTextureRepeat);\r\n\t\tvar material = _Profile.ResolvedRoadMaterial;\r\n\t\tvar centre = _Mesh.AddVertices(_Centre)[0];\r\n\t\tvar ring = new HalfEdgeMesh.VertexHandle[_Outline.Count];\r\n\r\n\t\tfor (int i = 0; i < _Outline.Count; i++)\r\n\t\t\tring[i] = _Mesh.AddVertices(_Outline[i])[0];\r\n\r\n\t\t// U runs across the road from its centreline and V along it, the same as the road's own surface, so the\r\n\t\t// asphalt carries on through the seam instead of restarting at it.\r\n\t\tVector2 centreUv = new(0.0f, _Distance / repeat);\r\n\r\n\t\t// Which way round the fan closes, so it faces up like the road it carries on from.\r\n\t\tVector3 first = _Outline[0] - _Centre;\r\n\t\tVector3 second = _Outline[1] - _Centre;\r\n\t\tbool flip = Vector3.Cross(first, second).z < 0.0f;\r\n\r\n\t\tfor (int i = 0; i < _Outline.Count - 1; i++)\r\n\t\t{\r\n\t\t\tvar uvA = new Vector2(_Offsets[i] / repeat, (_Distance + (_AtStart ? -_Aheads[i] : _Aheads[i])) / repeat);\r\n\t\t\tvar uvB = new Vector2(_Offsets[i + 1] / repeat, (_Distance + (_AtStart ? -_Aheads[i + 1] : _Aheads[i + 1])) / repeat);\r\n\r\n\t\t\tif (flip)\r\n\t\t\t\tMeshUtility.AddTexturedTriangle(_Mesh, material, centre, ring[i + 1], ring[i], centreUv, uvB, uvA);\r\n\t\t\telse\r\n\t\t\t\tMeshUtility.AddTexturedTriangle(_Mesh, material, centre, ring[i], ring[i + 1], centreUv, uvA, uvB);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The border round the arc, open at both ends: what closes it off is the road's own border, which runs into\r\n\t/// it at the end row.\r\n\t/// </summary>\r\n\tprivate static void AddBorder(PolygonMesh _Mesh, List<Vector3> _Outline, List<Vector3> _Outwards, RoadProfile _Profile)\r\n\t{\r\n\t\tif (!_Profile.HasBorder)\r\n\t\t\treturn;\r\n\r\n\t\tvar path = new List<RoadBorderBuilder.PathPoint>(_Outline.Count);\r\n\t\tfloat along = 0.0f;\r\n\r\n\t\tfor (int i = 0; i < _Outline.Count; i++)\r\n\t\t{\r\n\t\t\tif (i > 0)\r\n\t\t\t\talong += _Outline[i].Distance(_Outline[i - 1]);\r\n\r\n\t\t\tpath.Add(new RoadBorderBuilder.PathPoint(_Outline[i], _Outwards[i], along));\r\n\t\t}\r\n\r\n\t\tRoadBorderBuilder.BuildPath(_Mesh, _Profile, path, false, false);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Code/Network/RoadJunctionBuilder.cs",
            "FileName": "RoadJunctionBuilder.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Generates the surface of a junction: the layout's polygon fanned out from the node. Every triangle shares the\r\n/// centre, so each one is planar and none can be dropped by the mesh builder, whatever the arms' heights do.\r\n/// </summary>\r\ninternal static class RoadJunctionBuilder\r\n{\r\n\tpublic static PolygonMesh Build(RoadJunctionLayout _Layout, float _TextureRepeat, Material _Material)\r\n\t{\r\n\t\tvar mesh = new PolygonMesh();\r\n\t\tvar polygon = _Layout.Polygon;\r\n\r\n\t\tif (polygon.Count < 3)\r\n\t\t\treturn mesh;\r\n\r\n\t\tvar centre = mesh.AddVertices(_Layout.Centre)[0];\r\n\t\tvar ring = mesh.AddVertices(polygon.ToArray());\r\n\t\tVector2 centreUv = PlanarUv(_Layout.Centre, _TextureRepeat);\r\n\r\n\t\tfor (int i = 0; i < polygon.Count; i++)\r\n\t\t{\r\n\t\t\tint next = (i + 1) % polygon.Count;\r\n\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, centre, ring[i], ring[next],\r\n\t\t\t\tcentreUv, PlanarUv(polygon[i], _TextureRepeat), PlanarUv(polygon[next], _TextureRepeat));\r\n\t\t}\r\n\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Junctions have no single direction to run a texture along, so they use a top-down projection.\r\n\t/// </summary>\r\n\tprivate static Vector2 PlanarUv(Vector3 _Position, float _TextureRepeat)\r\n\t{\r\n\t\treturn new Vector2(_Position.x, _Position.y) / _TextureRepeat;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Code/Profiles/ParkingStyle.cs",
            "FileName": "ParkingStyle.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System.Text.Json.Serialization;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// How a parking lot is laid out inside: the bays round its edges and, on a lot with room for them, the rows down\r\n/// its middle. Lots reference a style, so every car park built from it changes together when the asset is edited.\r\n/// </summary>\r\n[AssetType(Name = \"Parking Style\", Extension = \"park\", Category = \"Road Tool 2\")]\r\npublic sealed class ParkingStyle : GameResource\r\n{\r\n\t/// <summary>\r\n\t/// How wide one bay is, measured along the edge it stands on.\r\n\t/// </summary>\r\n\t[Group(\"Bays\"), Range(80.0f, 400.0f)] public float BayWidth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 150.0f;\r\n\r\n\t/// <summary>\r\n\t/// How far a bay reaches in from the edge.\r\n\t/// </summary>\r\n\t[Group(\"Bays\"), Range(100.0f, 800.0f)] public float BayDepth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 260.0f;\r\n\r\n\t/// <summary>\r\n\t/// Bays turned this far from square to the edge they stand on, so cars drive into them at an angle. The bays\r\n\t/// on the far side of an aisle lean the other way, as they do in a real car park.\r\n\t/// </summary>\r\n\t[Group(\"Bays\"), Range(0.0f, 60.0f)] public float BayAngle { get; set { field = value.Clamp(0.0f, 80.0f); } } = 0.0f;\r\n\r\n\t/// <summary>\r\n\t/// Bays round the inside of the lot's edges.\r\n\t/// </summary>\r\n\t[Group(\"Bays\")] public bool EdgeBays { get; set; } = true;\r\n\r\n\t/// <summary>\r\n\t/// Back to back rows of bays down the middle of the lot, as many as fit with an aisle between each of them.\r\n\t/// </summary>\r\n\t[Group(\"Rows\")] public bool CentreRows { get; set; } = true;\r\n\r\n\t/// <summary>\r\n\t/// How much room is left for driving between one row of bays and the next.\r\n\t/// </summary>\r\n\t[Group(\"Rows\"), Range(300.0f, 1500.0f)] public float AisleWidth { get; set { field = value.Clamp(50.0f, 100000.0f); } } = 600.0f;\r\n\r\n\t/// <summary>\r\n\t/// How the bay lines are painted. Empty paints them like a road's lane lines.\r\n\t/// </summary>\r\n\t[Group(\"Markings\")] public RoadLineStyle Line { get; set; } = new() { Width = 8.0f };\r\n\r\n\t/// <summary>\r\n\t/// A line along the back of each row, closing its bays off.\r\n\t/// </summary>\r\n\t[Group(\"Markings\")] public bool BackLine { get; set; } = false;\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedLineMaterial => Line?.Material ?? Material.Load(\"roadtool2/materials/linewhite.vmat\");\r\n\r\n\t/// <summary>\r\n\t/// How far apart two bay lines sit along the edge, which is wider than a bay once the bays lean.\r\n\t/// </summary>\r\n\t[Hide, JsonIgnore] public float Pitch => BayWidth / float.Cos(BayAngle.DegreeToRadian());\r\n\r\n\r\n\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(BayWidth);\r\n\t\t_Hash.Add(BayDepth);\r\n\t\t_Hash.Add(BayAngle);\r\n\t\t_Hash.Add(EdgeBays ? 1 : 0);\r\n\t\t_Hash.Add(CentreRows ? 1 : 0);\r\n\t\t_Hash.Add(AisleWidth);\r\n\t\t_Hash.Add(BackLine ? 1 : 0);\r\n\t\t_Hash.Add(Line is null ? 0 : 1);\r\n\t\tLine?.AddToHash(ref _Hash);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The asset was saved: every network with a lot in this style has stale pieces now.\r\n\t/// </summary>\r\n\tprotected override void PostReload()\r\n\t{\r\n\t\tbase.PostReload();\r\n\r\n\t\tRoadNetwork.Editor_MarkDirty();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)\r\n\t{\r\n\t\treturn CreateSimpleAssetTypeIcon(\"local_parking\", _Width, _Height, \"#8fd3ff\", \"black\");\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Network/BorderDebug.cs",
            "FileName": "BorderDebug.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System.Collections.Generic;\r\nusing System.Text;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// What a border piece was built from, kept on the piece while the network's debug overlay is on: one line per row\r\n/// from the kerb to the outer edge, coloured by the run it belongs to, plus a plain-text record of every number\r\n/// that went into the rows, corners and patches (the editor writes those out to a file). Diagnostic only.\r\n/// </summary>\r\npublic sealed class BorderDebug\r\n{\r\n\tpublic readonly record struct DebugLine(Vector3 From, Vector3 To, Color Color);\r\n\r\n\tpublic readonly record struct DebugMarker(Vector3 Position, Color Color);\r\n\r\n\tpublic static readonly Color ArmColor = Color.Green;\r\n\tpublic static readonly Color FirstHalfColor = Color.Red;\r\n\tpublic static readonly Color SecondHalfColor = Color.Cyan;\r\n\tpublic static readonly Color PatchColor = Color.Yellow;\r\n\tpublic static readonly Color CapColor = Color.Magenta;\r\n\r\n\tpublic List<DebugLine> Lines { get; } = new();\r\n\r\n\tpublic List<DebugMarker> Markers { get; } = new();\r\n\r\n\t/// <summary>\r\n\t/// The record, one entry per line.\r\n\t/// </summary>\r\n\tpublic StringBuilder Text { get; } = new();\r\n\r\n\r\n\r\n\tpublic void Line(Vector3 _From, Vector3 _To, Color _Color)\r\n\t{\r\n\t\tLines.Add(new DebugLine(_From, _To, _Color));\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Marker(Vector3 _Position, Color _Color)\r\n\t{\r\n\t\tMarkers.Add(new DebugMarker(_Position, _Color));\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Note(string _Text)\r\n\t{\r\n\t\tText.AppendLine(_Text);\r\n\t}\r\n\r\n\r\n\r\n\tpublic static string Format(Vector3 _Vector)\r\n\t{\r\n\t\treturn $\"({_Vector.x:0.###} {_Vector.y:0.###} {_Vector.z:0.###})\";\r\n\t}\r\n\r\n\r\n\r\n\tpublic static string Format(Vector2 _Vector)\r\n\t{\r\n\t\treturn $\"({_Vector.x:0.###} {_Vector.y:0.###})\";\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "redsnail.roadtool2",
            "Path": "Network/RoadNetwork.Debug.cs",
            "FileName": "RoadNetwork.Debug.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 426010,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Linq;\r\nusing System.Text;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\npublic sealed partial class RoadNetwork\r\n{\r\n\t/// <summary>\r\n\t/// The whole border debug record as text: the graph and generation settings, then what every border piece was\r\n\t/// built from. Only pieces built while <see cref=\"DebugBorders\"/> was on carry a record.\r\n\t/// </summary>\r\n\tpublic string BuildDebugRecord()\r\n\t{\r\n\t\tvar text = new StringBuilder();\r\n\r\n\t\ttext.AppendLine($\"ROADTOOL2 BORDER DEBUG generator={GeneratorVersion} revision={DebugRevision} time={DateTime.Now:HH:mm:ss}\");\r\n\t\ttext.AppendLine($\"NETWORK name=\\\"{GameObject.Name}\\\" surfaceOffset={SurfaceOffset} spacing={SampleSpacing} chunkLength={ChunkLength} defaultProfile={DefaultProfile?.ResourceName ?? \"(none)\"}\");\r\n\r\n\t\tforeach (var node in Nodes)\r\n\t\t\ttext.AppendLine($\"NODE id={node.Id} position={BorderDebug.Format(node.Position)} cornerRadius={node.CornerRadius} autoTangents={(node.AutoTangents ? 1 : 0)} degree={GetDegree(node.Id)}\");\r\n\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\ttext.AppendLine($\"SEGMENT id={segment.Id} start={segment.StartNodeId} end={segment.EndNodeId} startTangent={BorderDebug.Format(segment.StartTangent)} endTangent={BorderDebug.Format(segment.EndTangent)} width={segment.Width} profile={ProfileOf(segment).ResourceName}\");\r\n\r\n\t\t\tif (TryGetCurve(segment, out var curve))\r\n\t\t\t\ttext.AppendLine($\"CURVE segment={segment.Id} p0={BorderDebug.Format(curve.P0)} p1={BorderDebug.Format(curve.P1)} p2={BorderDebug.Format(curve.P2)} p3={BorderDebug.Format(curve.P3)} length={GetCurveTable(segment)?.Length ?? 0.0f:0.###}\");\r\n\t\t}\r\n\r\n\t\tforeach (var chunk in GetChunks().OrderBy(chunk => chunk.IsJunction ? 1 : 0).ThenBy(chunk => chunk.IsJunction ? chunk.NodeId : chunk.SegmentId).ThenBy(chunk => chunk.ChunkIndex))\r\n\t\t{\r\n\t\t\tif (chunk.Kind != RoadChunkKind.Border)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\ttext.AppendLine();\r\n\t\t\ttext.AppendLine($\"PIECE name=\\\"{chunk.GameObject.Name}\\\" segment={chunk.SegmentId} node={chunk.NodeId} index={chunk.ChunkIndex} count={chunk.ChunkCount} record={(chunk.Debug is not null ? 1 : 0)}\");\r\n\r\n\t\t\tif (chunk.Debug is not null)\r\n\t\t\t\ttext.Append(chunk.Debug.Text);\r\n\t\t}\r\n\r\n\t\treturn text.ToString();\r\n\t}\r\n}\r\n"
        }
    ]
}